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 + 32 files changed, 1169 insertions(+) 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 (limited to 'packages/connect') 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" + ] +} -- cgit v1.2.3