From 655b0636facc110e9192cc7c3190f4b16f212be9 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Tue, 14 Nov 2017 18:26:36 -0500 Subject: Add connect to monorepo --- packages/connect/README.md | 1 + packages/connect/package.json | 68 ++++++++ packages/connect/src/globals.d.ts | 6 + packages/connect/src/http_client.ts | 171 +++++++++++++++++++++ packages/connect/src/index.ts | 15 ++ .../src/schemas/relayer_fees_request_schema.ts | 8 + .../schemas/relayer_orderbook_request_schema.ts | 8 + .../src/schemas/relayer_orders_request_schema.ts | 16 ++ .../schemas/relayer_token_pairs_request_schema.ts | 8 + packages/connect/src/schemas/schemas.ts | 15 ++ packages/connect/src/types.ts | 120 +++++++++++++++ .../src/utils/orderbook_channel_message_parsers.ts | 43 ++++++ packages/connect/src/utils/type_converters.ts | 31 ++++ packages/connect/src/ws_orderbook_channel.ts | 127 +++++++++++++++ .../test/fixtures/standard_relayer_api/fees.json | 5 + .../test/fixtures/standard_relayer_api/fees.ts | 8 + ...77fa9ac94a50f016026fd13f42990861238897721f.json | 19 +++ ...f977fa9ac94a50f016026fd13f42990861238897721f.ts | 21 +++ .../fixtures/standard_relayer_api/orderbook.json | 44 ++++++ .../fixtures/standard_relayer_api/orderbook.ts | 46 ++++++ .../test/fixtures/standard_relayer_api/orders.json | 21 +++ .../test/fixtures/standard_relayer_api/orders.ts | 23 +++ .../snapshot_orderbook_channel_message.ts | 17 ++ .../fixtures/standard_relayer_api/token_pairs.json | 16 ++ .../fixtures/standard_relayer_api/token_pairs.ts | 19 +++ .../unknown_orderbook_channel_message.ts | 10 ++ .../update_orderbook_channel_message.ts | 17 ++ packages/connect/test/http_client_test.ts | 130 ++++++++++++++++ .../test/orderbook_channel_message_parsers_test.ts | 66 ++++++++ packages/connect/test/ws_orderbook_channel_test.ts | 46 ++++++ packages/connect/tsconfig.json | 19 +++ packages/connect/tslint.json | 5 + yarn.lock | 67 +++++++- 33 files changed, 1231 insertions(+), 5 deletions(-) create mode 100644 packages/connect/README.md create mode 100644 packages/connect/package.json create mode 100644 packages/connect/src/globals.d.ts create mode 100644 packages/connect/src/http_client.ts create mode 100644 packages/connect/src/index.ts create mode 100644 packages/connect/src/schemas/relayer_fees_request_schema.ts create mode 100644 packages/connect/src/schemas/relayer_orderbook_request_schema.ts create mode 100644 packages/connect/src/schemas/relayer_orders_request_schema.ts create mode 100644 packages/connect/src/schemas/relayer_token_pairs_request_schema.ts create mode 100644 packages/connect/src/schemas/schemas.ts create mode 100644 packages/connect/src/types.ts create mode 100644 packages/connect/src/utils/orderbook_channel_message_parsers.ts create mode 100644 packages/connect/src/utils/type_converters.ts create mode 100644 packages/connect/src/ws_orderbook_channel.ts create mode 100644 packages/connect/test/fixtures/standard_relayer_api/fees.json create mode 100644 packages/connect/test/fixtures/standard_relayer_api/fees.ts create mode 100644 packages/connect/test/fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.json create mode 100644 packages/connect/test/fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.ts create mode 100644 packages/connect/test/fixtures/standard_relayer_api/orderbook.json create mode 100644 packages/connect/test/fixtures/standard_relayer_api/orderbook.ts create mode 100644 packages/connect/test/fixtures/standard_relayer_api/orders.json create mode 100644 packages/connect/test/fixtures/standard_relayer_api/orders.ts create mode 100644 packages/connect/test/fixtures/standard_relayer_api/snapshot_orderbook_channel_message.ts create mode 100644 packages/connect/test/fixtures/standard_relayer_api/token_pairs.json create mode 100644 packages/connect/test/fixtures/standard_relayer_api/token_pairs.ts create mode 100644 packages/connect/test/fixtures/standard_relayer_api/unknown_orderbook_channel_message.ts create mode 100644 packages/connect/test/fixtures/standard_relayer_api/update_orderbook_channel_message.ts create mode 100644 packages/connect/test/http_client_test.ts create mode 100644 packages/connect/test/orderbook_channel_message_parsers_test.ts create mode 100644 packages/connect/test/ws_orderbook_channel_test.ts create mode 100644 packages/connect/tsconfig.json create mode 100644 packages/connect/tslint.json diff --git a/packages/connect/README.md b/packages/connect/README.md new file mode 100644 index 000000000..900045526 --- /dev/null +++ b/packages/connect/README.md @@ -0,0 +1 @@ +This repository contains a Javascript library that makes it easy to interact with Relayers that conform to the [Standard Relayer API](https://github.com/0xProject/standard-relayer-api) diff --git a/packages/connect/package.json b/packages/connect/package.json new file mode 100644 index 000000000..d26594b5d --- /dev/null +++ b/packages/connect/package.json @@ -0,0 +1,68 @@ +{ + "name": "@0xproject/connect", + "version": "0.0.0", + "description": "A javascript library for interacting with the standard relayer api", + "keywords": [ + "0x-connect", + "0xproject", + "ethereum", + "tokens", + "exchange" + ], + "main": "lib/src/index.js", + "types": "lib/src/index.d.ts", + "scripts": { + "build": "tsc", + "clean": "shx rm -rf _bundles lib test_temp", + "copy_test_fixtures": "copyfiles -u 2 './test/fixtures/**/*.json' ./lib/test/fixtures", + "lint": "tslint src/**/*.ts test/**/*.ts", + "prepublishOnly": "run-p build", + "run_mocha": "mocha lib/test/**/*_test.js", + "test": "run-s clean build copy_test_fixtures run_mocha", + "test:circleci": "yarn test" + }, + "repository": { + "type": "git", + "url": "https://github.com/0xProject/0x.js.git" + }, + "author": "Brandon Millman", + "license": "Apache-2.0", + "engines": { + "node": ">=6.0.0" + }, + "bugs": { + "url": "https://github.com/0xProject/0x.js/issues" + }, + "homepage": "https://github.com/0xProject/0x.js/packages/connect/README.md", + "dependencies": { + "@0xproject/assert": "0.0.4", + "@0xproject/json-schemas": "0.6.7", + "0x.js": "~0.25.1", + "bignumber.js": "~4.1.0", + "isomorphic-fetch": "^2.2.1", + "lodash": "^4.17.4", + "query-string": "^5.0.1", + "websocket": "^1.0.25" + }, + "devDependencies": { + "@0xproject/tslint-config": "0.1.0", + "@types/fetch-mock": "^5.12.1", + "@types/lodash": "^4.14.77", + "@types/mocha": "^2.2.42", + "@types/query-string": "^5.0.1", + "@types/websocket": "^0.0.34", + "chai": "^4.0.1", + "chai-as-promised": "^7.1.0", + "chai-as-promised-typescript-typings": "0.0.3", + "chai-typescript-typings": "^0.0.1", + "copyfiles": "^1.2.0", + "dirty-chai": "^2.0.1", + "fetch-mock": "^5.13.1", + "mocha": "^4.0.0", + "npm-run-all": "^4.0.2", + "shx": "^0.2.2", + "tslint": "5.8.0", + "typescript": "~2.6.1", + "web3-typescript-typings": "^0.7.1" + } +} diff --git a/packages/connect/src/globals.d.ts b/packages/connect/src/globals.d.ts new file mode 100644 index 000000000..078e189cd --- /dev/null +++ b/packages/connect/src/globals.d.ts @@ -0,0 +1,6 @@ +declare module 'dirty-chai'; + +declare module '*.json' { + const value: any; + export default value; +} diff --git a/packages/connect/src/http_client.ts b/packages/connect/src/http_client.ts new file mode 100644 index 000000000..ab8c6bfa1 --- /dev/null +++ b/packages/connect/src/http_client.ts @@ -0,0 +1,171 @@ +import 'isomorphic-fetch'; +import * as _ from 'lodash'; +import {BigNumber} from 'bignumber.js'; +import * as queryString from 'query-string'; +import {assert} from '@0xproject/assert'; +import {schemas} from '@0xproject/json-schemas'; +import {SignedOrder} from '0x.js'; +import { + Client, + FeesRequest, + FeesResponse, + OrderbookRequest, + OrderbookResponse, + OrdersRequest, + TokenPairsItem, + TokenPairsRequest, +} from './types'; +import {schemas as clientSchemas} from './schemas/schemas'; +import {typeConverters} from './utils/type_converters'; + +interface RequestOptions { + params?: object; + payload?: object; +} + +enum RequestType { + Get = 'GET', + Post = 'POST', +} + +/** + * This class includes all the functionality related to interacting with a set of HTTP endpoints + * that implement the standard relayer API v0 + */ +export class HttpClient implements Client { + private apiEndpointUrl: string; + /** + * Instantiates a new HttpClient instance + * @param url The base url for making API calls + * @return An instance of HttpClient + */ + constructor(url: string) { + assert.isHttpUrl('url', url); + this.apiEndpointUrl = url; + } + /** + * Retrieve token pair info from the API + * @param request A TokenPairsRequest instance describing specific token information + * to retrieve + * @return The resulting TokenPairsItems that match the request + */ + public async getTokenPairsAsync(request?: TokenPairsRequest): Promise { + if (!_.isUndefined(request)) { + assert.doesConformToSchema('request', request, clientSchemas.relayerTokenPairsRequestSchema); + } + const requestOpts = { + params: request, + }; + const tokenPairs = await this._requestAsync('/token_pairs', RequestType.Get, requestOpts); + assert.doesConformToSchema( + 'tokenPairs', tokenPairs, schemas.relayerApiTokenPairsResponseSchema); + _.each(tokenPairs, (tokenPair: object) => { + typeConverters.convertStringsFieldsToBigNumbers(tokenPair, [ + 'tokenA.minAmount', + 'tokenA.maxAmount', + 'tokenB.minAmount', + 'tokenB.maxAmount', + ]); + }); + return tokenPairs; + } + /** + * Retrieve orders from the API + * @param request An OrdersRequest instance describing specific orders to retrieve + * @return The resulting SignedOrders that match the request + */ + public async getOrdersAsync(request?: OrdersRequest): Promise { + if (!_.isUndefined(request)) { + assert.doesConformToSchema('request', request, clientSchemas.relayerOrdersRequestSchema); + } + const requestOpts = { + params: request, + }; + const orders = await this._requestAsync(`/orders`, RequestType.Get, requestOpts); + assert.doesConformToSchema('orders', orders, schemas.signedOrdersSchema); + _.each(orders, (order: object) => typeConverters.convertOrderStringFieldsToBigNumber(order)); + return orders; + } + /** + * Retrieve a specific order from the API + * @param orderHash An orderHash generated from the desired order + * @return The SignedOrder that matches the supplied orderHash + */ + public async getOrderAsync(orderHash: string): Promise { + assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); + const order = await this._requestAsync(`/order/${orderHash}`, RequestType.Get); + assert.doesConformToSchema('order', order, schemas.signedOrderSchema); + typeConverters.convertOrderStringFieldsToBigNumber(order); + return order; + } + /** + * Retrieve an orderbook from the API + * @param request An OrderbookRequest instance describing the specific orderbook to retrieve + * @return The resulting OrderbookResponse that matches the request + */ + public async getOrderbookAsync(request: OrderbookRequest): Promise { + assert.doesConformToSchema('request', request, clientSchemas.relayerOrderBookRequestSchema); + const requestOpts = { + params: request, + }; + const orderBook = await this._requestAsync('/orderbook', RequestType.Get, requestOpts); + assert.doesConformToSchema('orderBook', orderBook, schemas.relayerApiOrderBookResponseSchema); + typeConverters.convertOrderbookStringFieldsToBigNumber(orderBook); + return orderBook; + } + /** + * Retrieve fee information from the API + * @param request A FeesRequest instance describing the specific fees to retrieve + * @return The resulting FeesResponse that matches the request + */ + public async getFeesAsync(request: FeesRequest): Promise { + assert.doesConformToSchema('request', request, schemas.relayerApiFeesPayloadSchema); + typeConverters.convertBigNumberFieldsToStrings(request, [ + 'makerTokenAmount', + 'takerTokenAmount', + 'expirationUnixTimestampSec', + 'salt', + ]); + const requestOpts = { + payload: request, + }; + const fees = await this._requestAsync('/fees', RequestType.Post, requestOpts); + assert.doesConformToSchema('fees', fees, schemas.relayerApiFeesResponseSchema); + typeConverters.convertStringsFieldsToBigNumbers(fees, ['makerFee', 'takerFee']); + return fees; + } + /** + * Submit a signed order to the API + * @param signedOrder A SignedOrder instance to submit + */ + public async submitOrderAsync(signedOrder: SignedOrder): Promise { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + const requestOpts = { + payload: signedOrder, + }; + await this._requestAsync('/order', RequestType.Post, requestOpts); + } + private async _requestAsync(path: string, requestType: RequestType, requestOptions?: RequestOptions): Promise { + const params = _.get(requestOptions, 'params'); + const payload = _.get(requestOptions, 'payload'); + let query = ''; + if (!_.isUndefined(params) && !_.isEmpty(params)) { + const stringifiedParams = queryString.stringify(params); + query = `?${stringifiedParams}`; + } + const url = `${this.apiEndpointUrl}/v0${path}${query}`; + const headers = new Headers({ + 'content-type': 'application/json', + }); + const response = await fetch(url, { + method: requestType, + body: payload, + headers, + }); + if (!response.ok) { + throw Error(response.statusText); + } + const json = await response.json(); + return json; + } +} diff --git a/packages/connect/src/index.ts b/packages/connect/src/index.ts new file mode 100644 index 000000000..5e97f4f26 --- /dev/null +++ b/packages/connect/src/index.ts @@ -0,0 +1,15 @@ +export {HttpClient} from './http_client'; +export {WebSocketOrderbookChannel} from './ws_orderbook_channel'; +export { + Client, + FeesRequest, + FeesResponse, + OrderbookChannel, + OrderbookChannelHandler, + OrderbookChannelSubscriptionOpts, + OrderbookRequest, + OrderbookResponse, + OrdersRequest, + TokenPairsItem, + TokenPairsRequest, +} from './types'; diff --git a/packages/connect/src/schemas/relayer_fees_request_schema.ts b/packages/connect/src/schemas/relayer_fees_request_schema.ts new file mode 100644 index 000000000..9408c94a0 --- /dev/null +++ b/packages/connect/src/schemas/relayer_fees_request_schema.ts @@ -0,0 +1,8 @@ +export const relayerOrderBookRequestSchema = { + id: '/RelayerOrderBookRequest', + type: 'object', + properties: { + baseTokenAddress: {$ref: '/Address'}, + quoteTokenAddress: {$ref: '/Address'}, + }, +}; diff --git a/packages/connect/src/schemas/relayer_orderbook_request_schema.ts b/packages/connect/src/schemas/relayer_orderbook_request_schema.ts new file mode 100644 index 000000000..9408c94a0 --- /dev/null +++ b/packages/connect/src/schemas/relayer_orderbook_request_schema.ts @@ -0,0 +1,8 @@ +export const relayerOrderBookRequestSchema = { + id: '/RelayerOrderBookRequest', + type: 'object', + properties: { + baseTokenAddress: {$ref: '/Address'}, + quoteTokenAddress: {$ref: '/Address'}, + }, +}; diff --git a/packages/connect/src/schemas/relayer_orders_request_schema.ts b/packages/connect/src/schemas/relayer_orders_request_schema.ts new file mode 100644 index 000000000..c11bc77be --- /dev/null +++ b/packages/connect/src/schemas/relayer_orders_request_schema.ts @@ -0,0 +1,16 @@ +export const relayerOrdersRequestSchema = { + id: '/RelayerOrdersRequest', + type: 'object', + properties: { + exchangeContractAddress: {$ref: '/Address'}, + tokenAddress: {$ref: '/Address'}, + makerTokenAddress: {$ref: '/Address'}, + takerTokenAddress: {$ref: '/Address'}, + tokenA: {$ref: '/Address'}, + tokenB: {$ref: '/Address'}, + maker: {$ref: '/Address'}, + taker: {$ref: '/Address'}, + trader: {$ref: '/Address'}, + feeRecipient: {$ref: '/Address'}, + }, +}; diff --git a/packages/connect/src/schemas/relayer_token_pairs_request_schema.ts b/packages/connect/src/schemas/relayer_token_pairs_request_schema.ts new file mode 100644 index 000000000..8013e1454 --- /dev/null +++ b/packages/connect/src/schemas/relayer_token_pairs_request_schema.ts @@ -0,0 +1,8 @@ +export const relayerTokenPairsRequestSchema = { + id: '/RelayerTokenPairsRequest', + type: 'object', + properties: { + tokenA: {$ref: '/Address'}, + tokenB: {$ref: '/Address'}, + }, +}; diff --git a/packages/connect/src/schemas/schemas.ts b/packages/connect/src/schemas/schemas.ts new file mode 100644 index 000000000..97ac672bf --- /dev/null +++ b/packages/connect/src/schemas/schemas.ts @@ -0,0 +1,15 @@ +import { + relayerOrderBookRequestSchema, +} from './relayer_orderbook_request_schema'; +import { + relayerOrdersRequestSchema, +} from './relayer_orders_request_schema'; +import { + relayerTokenPairsRequestSchema, +} from './relayer_token_pairs_request_schema'; + +export const schemas = { + relayerOrderBookRequestSchema, + relayerOrdersRequestSchema, + relayerTokenPairsRequestSchema, +}; diff --git a/packages/connect/src/types.ts b/packages/connect/src/types.ts new file mode 100644 index 000000000..75b6b8020 --- /dev/null +++ b/packages/connect/src/types.ts @@ -0,0 +1,120 @@ +import {SignedOrder} from '0x.js'; +import {BigNumber} from 'bignumber.js'; + +export interface Client { + getTokenPairsAsync: (request?: TokenPairsRequest) => Promise; + getOrdersAsync: (request?: OrdersRequest) => Promise; + getOrderAsync: (orderHash: string) => Promise; + getOrderbookAsync: (request: OrderbookRequest) => Promise; + getFeesAsync: (request: FeesRequest) => Promise; + submitOrderAsync: (signedOrder: SignedOrder) => Promise; +} + +export interface OrderbookChannel { + subscribe: (subscriptionOpts: OrderbookChannelSubscriptionOpts, handler: OrderbookChannelHandler) => void; + close: () => void; +} + +export interface OrderbookChannelHandler { + onSnapshot: (channel: OrderbookChannel, snapshot: OrderbookResponse) => void; + onUpdate: (channel: OrderbookChannel, order: SignedOrder) => void; + onError: (channel: OrderbookChannel, err: Error) => void; + onClose: (channel: OrderbookChannel) => void; +} + +export type OrderbookChannelMessage = + SnapshotOrderbookChannelMessage | + UpdateOrderbookChannelMessage | + UnknownOrderbookChannelMessage; + +export enum OrderbookChannelMessageTypes { + Snapshot = 'snapshot', + Update = 'update', + Unknown = 'unknown', +} + +export interface SnapshotOrderbookChannelMessage { + type: OrderbookChannelMessageTypes.Snapshot; + payload: OrderbookResponse; +} + +export interface UpdateOrderbookChannelMessage { + type: OrderbookChannelMessageTypes.Update; + payload: SignedOrder; +} + +export interface UnknownOrderbookChannelMessage { + type: OrderbookChannelMessageTypes.Unknown; + payload: undefined; +} + +/* + * baseTokenAddress: The address of token designated as the baseToken in the currency pair calculation of price + * quoteTokenAddress: The address of token designated as the quoteToken in the currency pair calculation of price + * snapshot: If true, a snapshot of the orderbook will be sent before the updates to the orderbook + * limit: Maximum number of bids and asks in orderbook snapshot + */ +export interface OrderbookChannelSubscriptionOpts { + baseTokenAddress: string; + quoteTokenAddress: string; + snapshot: boolean; + limit: number; +} + +export interface TokenPairsRequest { + tokenA?: string; + tokenB?: string; +} + +export interface TokenPairsItem { + tokenA: TokenTradeInfo; + tokenB: TokenTradeInfo; +} + +export interface TokenTradeInfo { + address: string; + minAmount: BigNumber; + maxAmount: BigNumber; + precision: number; +} + +export interface OrdersRequest { + exchangeContractAddress?: string; + tokenAddress?: string; + makerTokenAddress?: string; + takerTokenAddress?: string; + tokenA?: string; + tokenB?: string; + maker?: string; + taker?: string; + trader?: string; + feeRecipient?: string; +} + +export interface OrderbookRequest { + baseTokenAddress: string; + quoteTokenAddress: string; +} + +export interface OrderbookResponse { + bids: SignedOrder[]; + asks: SignedOrder[]; +} + +export interface FeesRequest { + exchangeContractAddress: string; + maker: string; + taker: string; + makerTokenAddress: string; + takerTokenAddress: string; + makerTokenAmount: BigNumber; + takerTokenAmount: BigNumber; + expirationUnixTimestampSec: BigNumber; + salt: BigNumber; +} + +export interface FeesResponse { + feeRecipient: string; + makerFee: BigNumber; + takerFee: BigNumber; +} diff --git a/packages/connect/src/utils/orderbook_channel_message_parsers.ts b/packages/connect/src/utils/orderbook_channel_message_parsers.ts new file mode 100644 index 000000000..b590b189b --- /dev/null +++ b/packages/connect/src/utils/orderbook_channel_message_parsers.ts @@ -0,0 +1,43 @@ +import * as _ from 'lodash'; +import {SignedOrder} from '0x.js'; +import {assert} from '@0xproject/assert'; +import {schemas} from '@0xproject/json-schemas'; +import { + OrderbookChannelMessage, + OrderbookChannelMessageTypes, +} from '../types'; +import {typeConverters} from './type_converters'; + +export const orderbookChannelMessageParsers = { + parser(utf8Data: string): OrderbookChannelMessage { + const messageObj = JSON.parse(utf8Data); + const type: string = _.get(messageObj, 'type'); + assert.assert(!_.isUndefined(type), `Message is missing a type parameter: ${utf8Data}`); + switch (type) { + case (OrderbookChannelMessageTypes.Snapshot): { + assert.doesConformToSchema('message', messageObj, schemas.relayerApiOrderbookChannelSnapshotSchema); + const orderbook = messageObj.payload; + typeConverters.convertOrderbookStringFieldsToBigNumber(orderbook); + return { + type, + payload: orderbook, + }; + } + case (OrderbookChannelMessageTypes.Update): { + assert.doesConformToSchema('message', messageObj, schemas.relayerApiOrderbookChannelUpdateSchema); + const order = messageObj.payload; + typeConverters.convertOrderStringFieldsToBigNumber(order); + return { + type, + payload: order, + }; + } + default: { + return { + type: OrderbookChannelMessageTypes.Unknown, + payload: undefined, + }; + } + } + }, +}; diff --git a/packages/connect/src/utils/type_converters.ts b/packages/connect/src/utils/type_converters.ts new file mode 100644 index 000000000..bf17a5629 --- /dev/null +++ b/packages/connect/src/utils/type_converters.ts @@ -0,0 +1,31 @@ +import * as _ from 'lodash'; +import {BigNumber} from 'bignumber.js'; + +// TODO: convert all of these to non-mutating, pure functions +export const typeConverters = { + convertOrderbookStringFieldsToBigNumber(orderbook: object): void { + _.each(orderbook, (orders: object[]) => { + _.each(orders, (order: object) => this.convertOrderStringFieldsToBigNumber(order)); + }); + }, + convertOrderStringFieldsToBigNumber(order: object): void { + this.convertStringsFieldsToBigNumbers(order, [ + 'makerTokenAmount', + 'takerTokenAmount', + 'makerFee', + 'takerFee', + 'expirationUnixTimestampSec', + 'salt', + ]); + }, + convertBigNumberFieldsToStrings(obj: object, fields: string[]): void { + _.each(fields, field => { + _.update(obj, field, (value: BigNumber) => value.toString()); + }); + }, + convertStringsFieldsToBigNumbers(obj: object, fields: string[]): void { + _.each(fields, field => { + _.update(obj, field, (value: string) => new BigNumber(value)); + }); + }, +}; diff --git a/packages/connect/src/ws_orderbook_channel.ts b/packages/connect/src/ws_orderbook_channel.ts new file mode 100644 index 000000000..78b823dbe --- /dev/null +++ b/packages/connect/src/ws_orderbook_channel.ts @@ -0,0 +1,127 @@ +import * as _ from 'lodash'; +import * as WebSocket from 'websocket'; +import {assert} from '@0xproject/assert'; +import {schemas} from '@0xproject/json-schemas'; +import {SignedOrder} from '0x.js'; +import { + OrderbookChannel, + OrderbookChannelHandler, + OrderbookChannelMessageTypes, + OrderbookChannelSubscriptionOpts, +} from './types'; +import {orderbookChannelMessageParsers} from './utils/orderbook_channel_message_parsers'; + +enum ConnectionEventType { + Close = 'close', + Error = 'error', + Message = 'message', +} + +enum ClientEventType { + Connect = 'connect', + ConnectFailed = 'connectFailed', +} + +/** + * This class includes all the functionality related to interacting with a websocket endpoint + * that implements the standard relayer API v0 + */ +export class WebSocketOrderbookChannel implements OrderbookChannel { + private apiEndpointUrl: string; + private client: WebSocket.client; + private connectionIfExists?: WebSocket.connection; + /** + * Instantiates a new WebSocketOrderbookChannel instance + * @param url The base url for making API calls + * @return An instance of WebSocketOrderbookChannel + */ + constructor(url: string) { + assert.isUri('url', url); + this.apiEndpointUrl = url; + this.client = new WebSocket.client(); + } + /** + * Subscribe to orderbook snapshots and updates from the websocket + * @param subscriptionOpts An OrderbookChannelSubscriptionOpts instance describing which + * token pair to subscribe to + * @param handler An OrderbookChannelHandler instance that responds to various + * channel updates + */ + public subscribe(subscriptionOpts: OrderbookChannelSubscriptionOpts, handler: OrderbookChannelHandler): void { + assert.doesConformToSchema( + 'subscriptionOpts', subscriptionOpts, schemas.relayerApiOrderbookChannelSubscribePayload); + assert.isFunction('handler.onSnapshot', _.get(handler, 'onSnapshot')); + assert.isFunction('handler.onUpdate', _.get(handler, 'onUpdate')); + assert.isFunction('handler.onError', _.get(handler, 'onError')); + assert.isFunction('handler.onClose', _.get(handler, 'onClose')); + const subscribeMessage = { + type: 'subscribe', + channel: 'orderbook', + payload: subscriptionOpts, + }; + this._getConnection((error, connection) => { + if (!_.isUndefined(error)) { + handler.onError(this, error); + } else if (!_.isUndefined(connection) && connection.connected) { + connection.on(ConnectionEventType.Error, wsError => { + handler.onError(this, wsError); + }); + connection.on(ConnectionEventType.Close, () => { + handler.onClose(this); + }); + connection.on(ConnectionEventType.Message, message => { + this._handleWebSocketMessage(message, handler); + }); + connection.sendUTF(JSON.stringify(subscribeMessage)); + } + }); + } + /** + * Close the websocket and stop receiving updates + */ + public close() { + if (!_.isUndefined(this.connectionIfExists)) { + this.connectionIfExists.close(); + } + } + private _getConnection(callback: (error?: Error, connection?: WebSocket.connection) => void) { + if (!_.isUndefined(this.connectionIfExists) && this.connectionIfExists.connected) { + callback(undefined, this.connectionIfExists); + } else { + this.client.on(ClientEventType.Connect, connection => { + this.connectionIfExists = connection; + callback(undefined, this.connectionIfExists); + }); + this.client.on(ClientEventType.ConnectFailed, error => { + callback(error, undefined); + }); + this.client.connect(this.apiEndpointUrl); + } + } + private _handleWebSocketMessage(message: WebSocket.IMessage, handler: OrderbookChannelHandler): void { + if (!_.isUndefined(message.utf8Data)) { + try { + const utf8Data = message.utf8Data; + const parserResult = orderbookChannelMessageParsers.parser(utf8Data); + const type = parserResult.type; + switch (parserResult.type) { + case (OrderbookChannelMessageTypes.Snapshot): { + handler.onSnapshot(this, parserResult.payload); + break; + } + case (OrderbookChannelMessageTypes.Update): { + handler.onUpdate(this, parserResult.payload); + break; + } + default: { + handler.onError(this, new Error(`Message has missing a type parameter: ${utf8Data}`)); + } + } + } catch (error) { + handler.onError(this, error); + } + } else { + handler.onError(this, new Error(`Message does not contain utf8Data`)); + } + } +} diff --git a/packages/connect/test/fixtures/standard_relayer_api/fees.json b/packages/connect/test/fixtures/standard_relayer_api/fees.json new file mode 100644 index 000000000..483a74254 --- /dev/null +++ b/packages/connect/test/fixtures/standard_relayer_api/fees.json @@ -0,0 +1,5 @@ +{ + "feeRecipient": "0x323b5d4c32345ced77393b3530b1eed0f346429d", + "makerFee": "10000000000000000", + "takerFee": "30000000000000000" +} diff --git a/packages/connect/test/fixtures/standard_relayer_api/fees.ts b/packages/connect/test/fixtures/standard_relayer_api/fees.ts new file mode 100644 index 000000000..c57b42717 --- /dev/null +++ b/packages/connect/test/fixtures/standard_relayer_api/fees.ts @@ -0,0 +1,8 @@ +import {BigNumber} from 'bignumber.js'; +import {FeesResponse} from '../../../src/types'; + +export const feesResponse: FeesResponse = { + feeRecipient: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + makerFee: new BigNumber('10000000000000000'), + takerFee: new BigNumber('30000000000000000'), +}; diff --git a/packages/connect/test/fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.json b/packages/connect/test/fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.json new file mode 100644 index 000000000..e84954b0d --- /dev/null +++ b/packages/connect/test/fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.json @@ -0,0 +1,19 @@ +{ + "maker": "0x9e56625509c2f60af937f23b7b532600390e8c8b", + "taker": "0xa2b31dacf30a9c50ca473337c01d8a201ae33e32", + "makerFee": "100000000000000", + "takerFee": "200000000000000", + "makerTokenAmount": "10000000000000000", + "takerTokenAmount": "20000000000000000", + "makerTokenAddress": "0x323b5d4c32345ced77393b3530b1eed0f346429d", + "takerTokenAddress": "0xef7fff64389b814a946f3e92105513705ca6b990", + "salt": "256", + "feeRecipient": "0xb046140686d052fff581f63f8136cce132e857da", + "exchangeContractAddress": "0x12459c951127e0c374ff9105dda097662a027093", + "expirationUnixTimestampSec": "42", + "ecSignature": { + "v": 27, + "r": "0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33", + "s": "0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254" + } +} diff --git a/packages/connect/test/fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.ts b/packages/connect/test/fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.ts new file mode 100644 index 000000000..9df45065c --- /dev/null +++ b/packages/connect/test/fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.ts @@ -0,0 +1,21 @@ +import {BigNumber} from 'bignumber.js'; + +export const orderResponse = { + maker: '0x9e56625509c2f60af937f23b7b532600390e8c8b', + taker: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32', + makerFee: new BigNumber('100000000000000'), + takerFee: new BigNumber('200000000000000'), + makerTokenAmount: new BigNumber('10000000000000000'), + takerTokenAmount: new BigNumber('20000000000000000'), + makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990', + salt: new BigNumber('256'), + feeRecipient: '0xb046140686d052fff581f63f8136cce132e857da', + exchangeContractAddress: '0x12459c951127e0c374ff9105dda097662a027093', + expirationUnixTimestampSec: new BigNumber('42'), + ecSignature: { + v: 27, + r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', + s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', + }, +}; diff --git a/packages/connect/test/fixtures/standard_relayer_api/orderbook.json b/packages/connect/test/fixtures/standard_relayer_api/orderbook.json new file mode 100644 index 000000000..bd6e10e4c --- /dev/null +++ b/packages/connect/test/fixtures/standard_relayer_api/orderbook.json @@ -0,0 +1,44 @@ +{ + "bids": [ + { + "maker": "0x9e56625509c2f60af937f23b7b532600390e8c8b", + "taker": "0xa2b31dacf30a9c50ca473337c01d8a201ae33e32", + "makerFee": "100000000000000", + "takerFee": "200000000000000", + "makerTokenAmount": "10000000000000000", + "takerTokenAmount": "20000000000000000", + "makerTokenAddress": "0x323b5d4c32345ced77393b3530b1eed0f346429d", + "takerTokenAddress": "0xef7fff64389b814a946f3e92105513705ca6b990", + "salt": "256", + "feeRecipient": "0xb046140686d052fff581f63f8136cce132e857da", + "exchangeContractAddress": "0x12459c951127e0c374ff9105dda097662a027093", + "expirationUnixTimestampSec": "42", + "ecSignature": { + "v": 27, + "r": "0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33", + "s": "0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254" + } + } + ], + "asks": [ + { + "maker": "0x9e56625509c2f60af937f23b7b532600390e8c8b", + "taker": "0xa2b31dacf30a9c50ca473337c01d8a201ae33e32", + "makerFee": "100000000000000", + "takerFee": "200000000000000", + "makerTokenAmount": "10000000000000000", + "takerTokenAmount": "20000000000000000", + "makerTokenAddress": "0x323b5d4c32345ced77393b3530b1eed0f346429d", + "takerTokenAddress": "0xef7fff64389b814a946f3e92105513705ca6b990", + "salt": "256", + "feeRecipient": "0xb046140686d052fff581f63f8136cce132e857da", + "exchangeContractAddress": "0x12459c951127e0c374ff9105dda097662a027093", + "expirationUnixTimestampSec": "42", + "ecSignature": { + "v": 27, + "r": "0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33", + "s": "0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254" + } + } + ] +} \ No newline at end of file diff --git a/packages/connect/test/fixtures/standard_relayer_api/orderbook.ts b/packages/connect/test/fixtures/standard_relayer_api/orderbook.ts new file mode 100644 index 000000000..529d2b450 --- /dev/null +++ b/packages/connect/test/fixtures/standard_relayer_api/orderbook.ts @@ -0,0 +1,46 @@ +import {BigNumber} from 'bignumber.js'; + +export const orderbookResponse = { + bids: [ + { + maker: '0x9e56625509c2f60af937f23b7b532600390e8c8b', + taker: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32', + makerFee: new BigNumber('100000000000000'), + takerFee: new BigNumber('200000000000000'), + makerTokenAmount: new BigNumber('10000000000000000'), + takerTokenAmount: new BigNumber('20000000000000000'), + makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990', + salt: new BigNumber('256'), + feeRecipient: '0xb046140686d052fff581f63f8136cce132e857da', + exchangeContractAddress: '0x12459c951127e0c374ff9105dda097662a027093', + expirationUnixTimestampSec: new BigNumber('42'), + ecSignature: { + v: 27, + r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', + s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', + }, + }, + ], + asks: [ + { + maker: '0x9e56625509c2f60af937f23b7b532600390e8c8b', + taker: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32', + makerFee: new BigNumber('100000000000000'), + takerFee: new BigNumber('200000000000000'), + makerTokenAmount: new BigNumber('10000000000000000'), + takerTokenAmount: new BigNumber('20000000000000000'), + makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990', + salt: new BigNumber('256'), + feeRecipient: '0xb046140686d052fff581f63f8136cce132e857da', + exchangeContractAddress: '0x12459c951127e0c374ff9105dda097662a027093', + expirationUnixTimestampSec: new BigNumber('42'), + ecSignature: { + v: 27, + r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', + s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', + }, + }, + ], +}; diff --git a/packages/connect/test/fixtures/standard_relayer_api/orders.json b/packages/connect/test/fixtures/standard_relayer_api/orders.json new file mode 100644 index 000000000..cfa780dc4 --- /dev/null +++ b/packages/connect/test/fixtures/standard_relayer_api/orders.json @@ -0,0 +1,21 @@ +[ + { + "maker": "0x9e56625509c2f60af937f23b7b532600390e8c8b", + "taker": "0xa2b31dacf30a9c50ca473337c01d8a201ae33e32", + "makerFee": "100000000000000", + "takerFee": "200000000000000", + "makerTokenAmount": "10000000000000000", + "takerTokenAmount": "20000000000000000", + "makerTokenAddress": "0x323b5d4c32345ced77393b3530b1eed0f346429d", + "takerTokenAddress": "0xef7fff64389b814a946f3e92105513705ca6b990", + "salt": "256", + "feeRecipient": "0x9e56625509c2f60af937f23b7b532600390e8c8b", + "exchangeContractAddress": "0x9e56625509c2f60af937f23b7b532600390e8c8b", + "expirationUnixTimestampSec": "42", + "ecSignature": { + "v": 27, + "r": "0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33", + "s": "0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254" + } + } +] diff --git a/packages/connect/test/fixtures/standard_relayer_api/orders.ts b/packages/connect/test/fixtures/standard_relayer_api/orders.ts new file mode 100644 index 000000000..54c8a150d --- /dev/null +++ b/packages/connect/test/fixtures/standard_relayer_api/orders.ts @@ -0,0 +1,23 @@ +import {BigNumber} from 'bignumber.js'; + +export const ordersResponse = [ + { + maker: '0x9e56625509c2f60af937f23b7b532600390e8c8b', + taker: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32', + makerFee: new BigNumber('100000000000000'), + takerFee: new BigNumber('200000000000000'), + makerTokenAmount: new BigNumber('10000000000000000'), + takerTokenAmount: new BigNumber('20000000000000000'), + makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990', + salt: new BigNumber('256'), + feeRecipient: '0x9e56625509c2f60af937f23b7b532600390e8c8b', + exchangeContractAddress: '0x9e56625509c2f60af937f23b7b532600390e8c8b', + expirationUnixTimestampSec: new BigNumber('42'), + ecSignature: { + v: 27, + r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', + s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', + }, + }, +]; diff --git a/packages/connect/test/fixtures/standard_relayer_api/snapshot_orderbook_channel_message.ts b/packages/connect/test/fixtures/standard_relayer_api/snapshot_orderbook_channel_message.ts new file mode 100644 index 000000000..3cedafb20 --- /dev/null +++ b/packages/connect/test/fixtures/standard_relayer_api/snapshot_orderbook_channel_message.ts @@ -0,0 +1,17 @@ +import * as orderbookJSON from './orderbook.json'; + +const orderbookJsonString = JSON.stringify(orderbookJSON); + +export const snapshotOrderbookChannelMessage = `{ + "type": "snapshot", + "channel": "orderbook", + "channelId": 1, + "payload": ${orderbookJsonString} +}`; + +export const malformedSnapshotOrderbookChannelMessage = `{ + "type": "snapshot", + "channel": "orderbook", + "channelId": 1, + "payload": {} +}`; diff --git a/packages/connect/test/fixtures/standard_relayer_api/token_pairs.json b/packages/connect/test/fixtures/standard_relayer_api/token_pairs.json new file mode 100644 index 000000000..90f57a974 --- /dev/null +++ b/packages/connect/test/fixtures/standard_relayer_api/token_pairs.json @@ -0,0 +1,16 @@ +[ + { + "tokenA": { + "address": "0x323b5d4c32345ced77393b3530b1eed0f346429d", + "minAmount": "0", + "maxAmount": "10000000000000000000", + "precision": 5 + }, + "tokenB": { + "address": "0xef7fff64389b814a946f3e92105513705ca6b990", + "minAmount": "0", + "maxAmount": "50000000000000000000", + "precision": 5 + } + } +] diff --git a/packages/connect/test/fixtures/standard_relayer_api/token_pairs.ts b/packages/connect/test/fixtures/standard_relayer_api/token_pairs.ts new file mode 100644 index 000000000..250277436 --- /dev/null +++ b/packages/connect/test/fixtures/standard_relayer_api/token_pairs.ts @@ -0,0 +1,19 @@ +import {BigNumber} from 'bignumber.js'; +import {TokenPairsItem} from '../../../src/types'; + +export const tokenPairsResponse: TokenPairsItem[] = [ + { + tokenA: { + address: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + minAmount: new BigNumber(0), + maxAmount: new BigNumber('10000000000000000000'), + precision: 5, + }, + tokenB: { + address: '0xef7fff64389b814a946f3e92105513705ca6b990', + minAmount: new BigNumber(0), + maxAmount: new BigNumber('50000000000000000000'), + precision: 5, + }, + }, +]; diff --git a/packages/connect/test/fixtures/standard_relayer_api/unknown_orderbook_channel_message.ts b/packages/connect/test/fixtures/standard_relayer_api/unknown_orderbook_channel_message.ts new file mode 100644 index 000000000..842738d99 --- /dev/null +++ b/packages/connect/test/fixtures/standard_relayer_api/unknown_orderbook_channel_message.ts @@ -0,0 +1,10 @@ +import * as orderResponseJSON from './order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.json'; + +const orderJSONString = JSON.stringify(orderResponseJSON); + +export const unknownOrderbookChannelMessage = `{ + "type": "superGoodUpdate", + "channel": "orderbook", + "channelId": 1, + "payload": ${orderJSONString} +}`; diff --git a/packages/connect/test/fixtures/standard_relayer_api/update_orderbook_channel_message.ts b/packages/connect/test/fixtures/standard_relayer_api/update_orderbook_channel_message.ts new file mode 100644 index 000000000..bc83854c6 --- /dev/null +++ b/packages/connect/test/fixtures/standard_relayer_api/update_orderbook_channel_message.ts @@ -0,0 +1,17 @@ +import * as orderResponseJSON from './order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.json'; + +const orderJSONString = JSON.stringify(orderResponseJSON); + +export const updateOrderbookChannelMessage = `{ + "type": "update", + "channel": "orderbook", + "channelId": 1, + "payload": ${orderJSONString} +}`; + +export const malformedUpdateOrderbookChannelMessage = `{ + "type": "update", + "channel": "orderbook", + "channelId": 1, + "payload": {} +}`; diff --git a/packages/connect/test/http_client_test.ts b/packages/connect/test/http_client_test.ts new file mode 100644 index 000000000..4ac93df76 --- /dev/null +++ b/packages/connect/test/http_client_test.ts @@ -0,0 +1,130 @@ +import 'mocha'; +import * as dirtyChai from 'dirty-chai'; +import * as chai from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as fetchMock from 'fetch-mock'; +import {BigNumber} from 'bignumber.js'; +import {HttpClient} from '../src/index'; +import {feesResponse} from './fixtures/standard_relayer_api/fees'; +import { + orderResponse, +} from './fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f'; +import {ordersResponse} from './fixtures/standard_relayer_api/orders'; +import {tokenPairsResponse} from './fixtures/standard_relayer_api/token_pairs'; +import {orderbookResponse} from './fixtures/standard_relayer_api/orderbook'; +import * as feesResponseJSON from './fixtures/standard_relayer_api/fees.json'; +// tslint:disable-next-line:max-line-length +import * as orderResponseJSON from './fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.json'; +import * as ordersResponseJSON from './fixtures/standard_relayer_api/orders.json'; +import * as tokenPairsResponseJSON from './fixtures/standard_relayer_api/token_pairs.json'; +import * as orderbookJSON from './fixtures/standard_relayer_api/orderbook.json'; + +chai.config.includeStack = true; +chai.use(dirtyChai); +chai.use(chaiAsPromised); +const expect = chai.expect; + +describe('HttpClient', () => { + const relayUrl = 'https://example.com'; + const relayerClient = new HttpClient(relayUrl); + afterEach(() => { + fetchMock.restore(); + }); + describe('#getTokenPairsAsync', () => { + const url = `${relayUrl}/v0/token_pairs`; + it('gets token pairs', async () => { + fetchMock.get(url, tokenPairsResponseJSON); + const tokenPairs = await relayerClient.getTokenPairsAsync(); + expect(tokenPairs).to.be.deep.equal(tokenPairsResponse); + }); + it('gets specfic token pairs for request', async () => { + const tokenAddress = '0x323b5d4c32345ced77393b3530b1eed0f346429d'; + const tokenPairsRequest = { + tokenA: tokenAddress, + }; + const urlWithQuery = `${url}?tokenA=${tokenAddress}`; + fetchMock.get(urlWithQuery, tokenPairsResponseJSON); + const tokenPairs = await relayerClient.getTokenPairsAsync(tokenPairsRequest); + expect(tokenPairs).to.be.deep.equal(tokenPairsResponse); + }); + it('throws an error for invalid JSON response', async () => { + fetchMock.get(url, {test: 'dummy'}); + expect(relayerClient.getTokenPairsAsync()).to.be.rejected(); + }); + }); + describe('#getOrdersAsync', () => { + const url = `${relayUrl}/v0/orders`; + it('gets orders', async () => { + fetchMock.get(url, ordersResponseJSON); + const orders = await relayerClient.getOrdersAsync(); + expect(orders).to.be.deep.equal(ordersResponse); + }); + it('gets specfic orders for request', async () => { + const tokenAddress = '0x323b5d4c32345ced77393b3530b1eed0f346429d'; + const ordersRequest = { + tokenA: tokenAddress, + }; + const urlWithQuery = `${url}?tokenA=${tokenAddress}`; + fetchMock.get(urlWithQuery, ordersResponseJSON); + const orders = await relayerClient.getOrdersAsync(ordersRequest); + expect(orders).to.be.deep.equal(ordersResponse); + }); + it('throws an error for invalid JSON response', async () => { + fetchMock.get(url, {test: 'dummy'}); + expect(relayerClient.getOrdersAsync()).to.be.rejected(); + }); + }); + describe('#getOrderAsync', () => { + const orderHash = '0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f'; + const url = `${relayUrl}/v0/order/${orderHash}`; + it('gets order', async () => { + fetchMock.get(url, orderResponseJSON); + const order = await relayerClient.getOrderAsync(orderHash); + expect(order).to.be.deep.equal(orderResponse); + }); + it('throws an error for invalid JSON response', async () => { + fetchMock.get(url, {test: 'dummy'}); + expect(relayerClient.getOrderAsync(orderHash)).to.be.rejected(); + }); + }); + describe('#getOrderBookAsync', () => { + const request = { + baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + quoteTokenAddress: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32', + }; + // tslint:disable-next-line:max-line-length + const url = `${relayUrl}/v0/orderbook?baseTokenAddress=${request.baseTokenAddress}"eTokenAddress=${request.quoteTokenAddress}`; + it('gets order book', async () => { + fetchMock.get(url, orderbookJSON); + const orderbook = await relayerClient.getOrderbookAsync(request); + expect(orderbook).to.be.deep.equal(orderbookResponse); + }); + it('throws an error for invalid JSON response', async () => { + fetchMock.get(url, {test: 'dummy'}); + expect(relayerClient.getOrderbookAsync(request)).to.be.rejected(); + }); + }); + describe('#getFeesAsync', () => { + const request = { + exchangeContractAddress: '0x12459c951127e0c374ff9105dda097662a027093', + maker: '0x9e56625509c2f60af937f23b7b532600390e8c8b', + taker: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32', + makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990', + makerTokenAmount: new BigNumber('10000000000000000000'), + takerTokenAmount: new BigNumber('30000000000000000000'), + salt: new BigNumber('256'), + expirationUnixTimestampSec: new BigNumber('42'), + }; + const url = `${relayUrl}/v0/fees`; + it('gets fees', async () => { + fetchMock.post(url, feesResponseJSON); + const fees = await relayerClient.getFeesAsync(request); + expect(fees).to.be.deep.equal(feesResponse); + }); + it('throws an error for invalid JSON response', async () => { + fetchMock.post(url, {test: 'dummy'}); + expect(relayerClient.getFeesAsync(request)).to.be.rejected(); + }); + }); +}); diff --git a/packages/connect/test/orderbook_channel_message_parsers_test.ts b/packages/connect/test/orderbook_channel_message_parsers_test.ts new file mode 100644 index 000000000..8efc5e500 --- /dev/null +++ b/packages/connect/test/orderbook_channel_message_parsers_test.ts @@ -0,0 +1,66 @@ +import 'mocha'; +import * as dirtyChai from 'dirty-chai'; +import * as chai from 'chai'; +import {orderbookChannelMessageParsers} from '../src/utils/orderbook_channel_message_parsers'; +import { + snapshotOrderbookChannelMessage, + malformedSnapshotOrderbookChannelMessage, +} from './fixtures/standard_relayer_api/snapshot_orderbook_channel_message'; +import { + updateOrderbookChannelMessage, + malformedUpdateOrderbookChannelMessage, +} from './fixtures/standard_relayer_api/update_orderbook_channel_message'; +import {unknownOrderbookChannelMessage} from './fixtures/standard_relayer_api/unknown_orderbook_channel_message'; +import {orderbookResponse} from './fixtures/standard_relayer_api/orderbook'; +// tslint:disable-next-line:max-line-length +import {orderResponse} from './fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f'; + +chai.config.includeStack = true; +chai.use(dirtyChai); +const expect = chai.expect; + +describe('orderbookChannelMessageParsers', () => { + describe('#parser', () => { + it('parses snapshot messages', () => { + const snapshotMessage = orderbookChannelMessageParsers.parser(snapshotOrderbookChannelMessage); + expect(snapshotMessage.type).to.be.equal('snapshot'); + expect(snapshotMessage.payload).to.be.deep.equal(orderbookResponse); + }); + it('parses update messages', () => { + const updateMessage = orderbookChannelMessageParsers.parser(updateOrderbookChannelMessage); + expect(updateMessage.type).to.be.equal('update'); + expect(updateMessage.payload).to.be.deep.equal(orderResponse); + }); + it('returns unknown message for messages with unsupported types', () => { + const unknownMessage = orderbookChannelMessageParsers.parser(unknownOrderbookChannelMessage); + expect(unknownMessage.type).to.be.equal('unknown'); + expect(unknownMessage.payload).to.be.undefined(); + }); + it('throws when message does not include a type', () => { + const typelessMessage = `{ + "channel": "orderbook", + "channelId": 1, + "payload": {} + }`; + const badCall = () => orderbookChannelMessageParsers.parser(typelessMessage); + expect(badCall).throws(`Message is missing a type parameter: ${typelessMessage}`); + }); + it('throws when snapshot message has malformed payload', () => { + const badCall = () => + orderbookChannelMessageParsers.parser(malformedSnapshotOrderbookChannelMessage); + // tslint:disable-next-line:max-line-length + const errMsg = 'Validation errors: instance.payload requires property "bids", instance.payload requires property "asks"'; + expect(badCall).throws(errMsg); + }); + it('throws when update message has malformed payload', () => { + const badCall = () => + orderbookChannelMessageParsers.parser(malformedUpdateOrderbookChannelMessage); + expect(badCall).throws(/^Expected message to conform to schema/); + }); + it('throws when input message is not valid JSON', () => { + const nonJsonString = 'h93b{sdfs9fsd f'; + const badCall = () => orderbookChannelMessageParsers.parser(nonJsonString); + expect(badCall).throws('Unexpected token h in JSON at position 0'); + }); + }); +}); diff --git a/packages/connect/test/ws_orderbook_channel_test.ts b/packages/connect/test/ws_orderbook_channel_test.ts new file mode 100644 index 000000000..6752950d9 --- /dev/null +++ b/packages/connect/test/ws_orderbook_channel_test.ts @@ -0,0 +1,46 @@ +import 'mocha'; +import * as dirtyChai from 'dirty-chai'; +import * as chai from 'chai'; +import { + WebSocketOrderbookChannel, +} from '../src/index'; + +chai.config.includeStack = true; +chai.use(dirtyChai); +const expect = chai.expect; + +describe('WebSocketOrderbookChannel', () => { + const websocketUrl = 'ws://localhost:8080'; + const orderbookChannel = new WebSocketOrderbookChannel(websocketUrl); + const subscriptionOpts = { + baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + quoteTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990', + snapshot: true, + limit: 100, + }; + const emptyOrderbookChannelHandler = { + onSnapshot: () => { return; }, + onUpdate: () => { return; }, + onError: () => { return; }, + onClose: () => { return; }, + }; + describe('#subscribe', () => { + it('throws when subscriptionOpts does not conform to schema', () => { + const badSubscribeCall = orderbookChannel.subscribe.bind( + orderbookChannel, {}, emptyOrderbookChannelHandler); + // tslint:disable-next-line:max-line-length + expect(badSubscribeCall) + .throws('Expected subscriptionOpts to conform to schema /RelayerApiOrderbookChannelSubscribePayload\nEncountered: {}\nValidation errors: instance requires property "baseTokenAddress", instance requires property "quoteTokenAddress"'); + }); + it('throws when handler has the incorrect members', () => { + const badSubscribeCall = orderbookChannel.subscribe.bind(orderbookChannel, subscriptionOpts, {}); + expect(badSubscribeCall) + .throws('Expected handler.onSnapshot to be of type function, encountered: undefined'); + }); + it('does not throw when inputs are of correct types', () => { + const goodSubscribeCall = orderbookChannel.subscribe.bind( + orderbookChannel, subscriptionOpts, emptyOrderbookChannelHandler); + expect(goodSubscribeCall).to.not.throw(); + }); + }); +}); diff --git a/packages/connect/tsconfig.json b/packages/connect/tsconfig.json new file mode 100644 index 000000000..a6c8277f8 --- /dev/null +++ b/packages/connect/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": [ "es2015", "dom" ], + "outDir": "lib", + "sourceMap": true, + "declaration": true, + "noImplicitAny": true, + "strictNullChecks": true + }, + "include": [ + "./src/**/*", + "./test/**/*", + "../../node_modules/chai-as-promised-typescript-typings/index.d.ts", + "../../node_modules/chai-typescript-typings/index.d.ts", + "../../node_modules/web3-typescript-typings/index.d.ts" + ] + } diff --git a/packages/connect/tslint.json b/packages/connect/tslint.json new file mode 100644 index 000000000..a07795151 --- /dev/null +++ b/packages/connect/tslint.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "@0xproject/tslint-config" + ] +} diff --git a/yarn.lock b/yarn.lock index b7be3fc06..d10468fed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,10 @@ # yarn lockfile v1 +"@types/fetch-mock@^5.12.1": + version "5.12.2" + resolved "https://registry.yarnpkg.com/@types/fetch-mock/-/fetch-mock-5.12.2.tgz#8c96517ff74303031c65c5da2d99858e34c844d2" + "@types/fs-extra@^4.0.0": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.4.tgz#72947e108f2cbeda5ab288a927399fdf6d02bd42" @@ -41,7 +45,7 @@ dependencies: "@types/lodash" "*" -"@types/lodash@*", "@types/lodash@^4.14.37", "@types/lodash@^4.14.64", "@types/lodash@^4.14.78": +"@types/lodash@*", "@types/lodash@^4.14.37", "@types/lodash@^4.14.64", "@types/lodash@^4.14.77", "@types/lodash@^4.14.78": version "4.14.85" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.85.tgz#a16fbf942422f6eca5622b6910492c496c35069b" @@ -61,6 +65,10 @@ version "8.0.51" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" +"@types/query-string@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/query-string/-/query-string-5.0.1.tgz#6cb41c724cb1644d56c2d1dae7c7b204e706b39e" + "@types/shelljs@^0.7.0": version "0.7.5" resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.5.tgz#5834fb7385d1137bd2be5842f2c278ac36a117f4" @@ -82,6 +90,12 @@ version "1.0.2" resolved "https://registry.yarnpkg.com/@types/valid-url/-/valid-url-1.0.2.tgz#60fa435ce24bfd5ba107b8d2a80796aeaf3a8f45" +"@types/websocket@^0.0.34": + version "0.0.34" + resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-0.0.34.tgz#25596764cec885eda070fdb6d19cd76fe582747c" + dependencies: + "@types/node" "*" + JSONStream@^1.0.4: version "1.3.1" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" @@ -2281,6 +2295,14 @@ fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" +fetch-mock@^5.13.1: + version "5.13.1" + resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-5.13.1.tgz#955794a77f3d972f1644b9ace65a0fdfd60f1df7" + dependencies: + glob-to-regexp "^0.3.0" + node-fetch "^1.3.3" + path-to-regexp "^1.7.0" + fetch-ponyfill@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/fetch-ponyfill/-/fetch-ponyfill-4.1.0.tgz#ae3ce5f732c645eab87e4ae8793414709b239893" @@ -2599,6 +2621,10 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" +glob-to-regexp@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" + glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@~7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" @@ -3140,7 +3166,7 @@ is-text-path@^1.0.0: dependencies: text-extensions "^1.0.0" -is-typedarray@~1.0.0: +is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -3174,7 +3200,7 @@ isobject@^3.0.0, isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" -isomorphic-fetch@^2.2.0: +isomorphic-fetch@^2.2.0, isomorphic-fetch@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" dependencies: @@ -3881,7 +3907,7 @@ mute-stream@0.0.7, mute-stream@~0.0.4: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -nan@^2.0.5, nan@^2.0.8, nan@^2.2.1, nan@^2.3.0: +nan@^2.0.5, nan@^2.0.8, nan@^2.2.1, nan@^2.3.0, nan@^2.3.3: version "2.7.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" @@ -3917,7 +3943,7 @@ node-abi@^2.1.1: dependencies: semver "^5.4.1" -node-fetch@^1.0.1, node-fetch@~1.7.1: +node-fetch@^1.0.1, node-fetch@^1.3.3, node-fetch@~1.7.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" dependencies: @@ -4519,6 +4545,14 @@ qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" +query-string@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.0.1.tgz#6e2b86fe0e08aef682ecbe86e85834765402bd88" + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -5279,6 +5313,10 @@ stream-http@^2.3.1: to-arraybuffer "^1.0.0" xtend "^4.0.0" +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + string-editor@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/string-editor/-/string-editor-0.1.2.tgz#f5ff1b5ac4aed7ac6c2fb8de236d1551b20f61d0" @@ -5667,6 +5705,12 @@ type-detect@^4.0.0: version "4.0.5" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.5.tgz#d70e5bc81db6de2a381bcaca0c6e0cbdc7635de2" +typedarray-to-buffer@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.2.tgz#1017b32d984ff556eba100f501589aba1ace2e04" + dependencies: + is-typedarray "^1.0.0" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -5978,6 +6022,15 @@ webpack@^3.0.0, webpack@^3.1.0: webpack-sources "^1.0.1" yargs "^8.0.2" +websocket@^1.0.25: + version "1.0.25" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.25.tgz#998ec790f0a3eacb8b08b50a4350026692a11958" + dependencies: + debug "^2.2.0" + nan "^2.3.3" + typedarray-to-buffer "^3.1.2" + yaeti "^0.0.6" + whatwg-fetch@>=0.10.0: version "2.0.3" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" @@ -6094,6 +6147,10 @@ y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" +yaeti@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" -- cgit v1.2.3