aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--packages/connect/README.md1
-rw-r--r--packages/connect/package.json68
-rw-r--r--packages/connect/src/globals.d.ts6
-rw-r--r--packages/connect/src/http_client.ts171
-rw-r--r--packages/connect/src/index.ts15
-rw-r--r--packages/connect/src/schemas/relayer_fees_request_schema.ts8
-rw-r--r--packages/connect/src/schemas/relayer_orderbook_request_schema.ts8
-rw-r--r--packages/connect/src/schemas/relayer_orders_request_schema.ts16
-rw-r--r--packages/connect/src/schemas/relayer_token_pairs_request_schema.ts8
-rw-r--r--packages/connect/src/schemas/schemas.ts15
-rw-r--r--packages/connect/src/types.ts120
-rw-r--r--packages/connect/src/utils/orderbook_channel_message_parsers.ts43
-rw-r--r--packages/connect/src/utils/type_converters.ts31
-rw-r--r--packages/connect/src/ws_orderbook_channel.ts127
-rw-r--r--packages/connect/test/fixtures/standard_relayer_api/fees.json5
-rw-r--r--packages/connect/test/fixtures/standard_relayer_api/fees.ts8
-rw-r--r--packages/connect/test/fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.json19
-rw-r--r--packages/connect/test/fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f.ts21
-rw-r--r--packages/connect/test/fixtures/standard_relayer_api/orderbook.json44
-rw-r--r--packages/connect/test/fixtures/standard_relayer_api/orderbook.ts46
-rw-r--r--packages/connect/test/fixtures/standard_relayer_api/orders.json21
-rw-r--r--packages/connect/test/fixtures/standard_relayer_api/orders.ts23
-rw-r--r--packages/connect/test/fixtures/standard_relayer_api/snapshot_orderbook_channel_message.ts17
-rw-r--r--packages/connect/test/fixtures/standard_relayer_api/token_pairs.json16
-rw-r--r--packages/connect/test/fixtures/standard_relayer_api/token_pairs.ts19
-rw-r--r--packages/connect/test/fixtures/standard_relayer_api/unknown_orderbook_channel_message.ts10
-rw-r--r--packages/connect/test/fixtures/standard_relayer_api/update_orderbook_channel_message.ts17
-rw-r--r--packages/connect/test/http_client_test.ts130
-rw-r--r--packages/connect/test/orderbook_channel_message_parsers_test.ts66
-rw-r--r--packages/connect/test/ws_orderbook_channel_test.ts46
-rw-r--r--packages/connect/tsconfig.json19
-rw-r--r--packages/connect/tslint.json5
-rw-r--r--yarn.lock67
33 files changed, 1231 insertions, 5 deletions
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<TokenPairsItem[]> {
+ 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<SignedOrder[]> {
+ 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<SignedOrder> {
+ 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<OrderbookResponse> {
+ 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<FeesResponse> {
+ 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<void> {
+ 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<any> {
+ 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<TokenPairsItem[]>;
+ getOrdersAsync: (request?: OrdersRequest) => Promise<SignedOrder[]>;
+ getOrderAsync: (orderHash: string) => Promise<SignedOrder>;
+ getOrderbookAsync: (request: OrderbookRequest) => Promise<OrderbookResponse>;
+ getFeesAsync: (request: FeesRequest) => Promise<FeesResponse>;
+ submitOrderAsync: (signedOrder: SignedOrder) => Promise<void>;
+}
+
+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}&quoteTokenAddress=${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..f3dead9ae
--- /dev/null
+++ b/packages/connect/test/ws_orderbook_channel_test.ts
@@ -0,0 +1,46 @@
+import 'mocha';
+import * as _ from 'lodash';
+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: () => { _.noop(); },
+ onUpdate: () => { _.noop(); },
+ onError: () => { _.noop(); },
+ onClose: () => { _.noop(); },
+ };
+ 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"