aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/0x.js/CHANGELOG.md4
-rw-r--r--packages/0x.js/README.md3
-rw-r--r--packages/0x.js/package.json13
-rw-r--r--packages/0x.js/src/0x.ts2
-rw-r--r--packages/0x.js/src/contract.ts2
-rw-r--r--packages/0x.js/src/contract_wrappers/exchange_wrapper.ts2
-rw-r--r--packages/0x.js/src/contract_wrappers/token_wrapper.ts2
-rw-r--r--packages/0x.js/src/order_watcher/order_state_watcher.ts2
-rw-r--r--packages/0x.js/src/types.ts1
-rw-r--r--packages/0x.js/src/utils/abi_decoder.ts2
-rw-r--r--packages/0x.js/src/utils/assert.ts7
-rw-r--r--packages/0x.js/src/utils/order_state_utils.ts13
-rw-r--r--packages/0x.js/test/order_state_watcher_test.ts184
-rw-r--r--packages/0x.js/test/token_registry_wrapper_test.ts2
-rw-r--r--packages/0x.js/tslint.json2
-rw-r--r--packages/assert/CHANGELOG.md6
-rw-r--r--packages/assert/README.md9
-rw-r--r--packages/assert/package.json9
-rw-r--r--packages/assert/src/index.ts5
-rw-r--r--packages/assert/test/assert_test.ts2
-rw-r--r--packages/assert/tslint.json2
-rw-r--r--packages/connect/CHANGELOG.md4
-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--packages/json-schemas/CHANGELOG.md5
-rw-r--r--packages/json-schemas/README.md24
-rw-r--r--packages/json-schemas/package.json46
-rw-r--r--packages/json-schemas/schemas/basic_type_schemas.ts11
-rw-r--r--packages/json-schemas/schemas/ec_signature_schema.ts20
-rw-r--r--packages/json-schemas/schemas/index_filter_values_schema.ts11
-rw-r--r--packages/json-schemas/schemas/order_cancel_schema.ts12
-rw-r--r--packages/json-schemas/schemas/order_fill_or_kill_requests_schema.ts12
-rw-r--r--packages/json-schemas/schemas/order_fill_requests_schema.ts12
-rw-r--r--packages/json-schemas/schemas/order_hash_schema.ts5
-rw-r--r--packages/json-schemas/schemas/order_schemas.ts35
-rw-r--r--packages/json-schemas/schemas/relayer_api_error_response_schema.ts21
-rw-r--r--packages/json-schemas/schemas/relayer_api_fees_payload_schema.ts19
-rw-r--r--packages/json-schemas/schemas/relayer_api_fees_response_schema.ts10
-rw-r--r--packages/json-schemas/schemas/relayer_api_orberbook_channel_subscribe_schema.ts22
-rw-r--r--packages/json-schemas/schemas/relayer_api_orderbook_channel_snapshot_schema.ts21
-rw-r--r--packages/json-schemas/schemas/relayer_api_orderbook_channel_update_response_schema.ts11
-rw-r--r--packages/json-schemas/schemas/relayer_api_orderbook_response_schema.ts9
-rw-r--r--packages/json-schemas/schemas/relayer_api_token_pairs_response_schema.ts24
-rw-r--r--packages/json-schemas/schemas/signed_orders_schema.ts5
-rw-r--r--packages/json-schemas/schemas/subscription_opts_schema.ts20
-rw-r--r--packages/json-schemas/schemas/token_schema.ts11
-rw-r--r--packages/json-schemas/schemas/tx_data_schema.ts42
-rw-r--r--packages/json-schemas/src/globals.d.ts7
-rw-r--r--packages/json-schemas/src/index.ts4
-rw-r--r--packages/json-schemas/src/schema_validator.ts28
-rw-r--r--packages/json-schemas/src/schemas.ts99
-rw-r--r--packages/json-schemas/test/schema_test.ts972
-rw-r--r--packages/json-schemas/tsconfig.json17
-rw-r--r--packages/json-schemas/tslint.json5
-rw-r--r--packages/tslint-config/CHANGELOG.md6
-rw-r--r--packages/tslint-config/README.md10
-rw-r--r--packages/tslint-config/package.json38
-rw-r--r--packages/tslint-config/tslint.json53
88 files changed, 2994 insertions, 100 deletions
diff --git a/packages/0x.js/CHANGELOG.md b/packages/0x.js/CHANGELOG.md
index d5e965ed7..0112ca33f 100644
--- a/packages/0x.js/CHANGELOG.md
+++ b/packages/0x.js/CHANGELOG.md
@@ -3,10 +3,10 @@
vx.x.x
------------------------
* Remove support for Async callback types when used in Subscribe functions
-
-v0.24.0 - _November 13, 2017_
+v0.25.1 - _November 13, 2017_
------------------------
* Standardise on Cancelled over Canceled
+ * Add missing `DecodedLogEvent` type to exported types
v0.23.0 - _November 12, 2017_
------------------------
diff --git a/packages/0x.js/README.md b/packages/0x.js/README.md
index 4b6cc8df4..6cdcbde70 100644
--- a/packages/0x.js/README.md
+++ b/packages/0x.js/README.md
@@ -1,3 +1,6 @@
+0x.js
+-----
+
## Installation
0x.js ships as both a [UMD](https://github.com/umdjs/umd) module and a [CommonJS](https://en.wikipedia.org/wiki/CommonJS) package.
diff --git a/packages/0x.js/package.json b/packages/0x.js/package.json
index 6839e6513..0204d723f 100644
--- a/packages/0x.js/package.json
+++ b/packages/0x.js/package.json
@@ -1,6 +1,6 @@
{
"name": "0x.js",
- "version": "0.23.0",
+ "version": "0.25.1",
"description": "A javascript library for interacting with the 0x protocol",
"keywords": [
"0x.js",
@@ -13,7 +13,7 @@
"types": "lib/src/index.d.ts",
"scripts": {
"prebuild": "npm run clean",
- "build": "run-p build:umd:prod build:commonjs",
+ "build": "run-p build:umd:prod build:commonjs; exit 0;",
"prepublishOnly": "run-p build",
"postpublish": "run-s release docs:json upload_docs_json",
"release": "publish-release --assets _bundles/index.js,_bundles/index.min.js --tag $(git describe --tags) --owner 0xProject --repo 0x.js",
@@ -49,6 +49,7 @@
"node": ">=6.0.0"
},
"devDependencies": {
+ "@0xproject/tslint-config": "^0.1.0",
"@types/jsonschema": "^1.1.1",
"@types/lodash": "^4.14.64",
"@types/mocha": "^2.2.41",
@@ -76,8 +77,7 @@
"sinon": "^4.0.0",
"source-map-support": "^0.5.0",
"truffle-hdwallet-provider": "^0.0.3",
- "tslint": "~5.5.0",
- "tslint-config-0xproject": "^0.0.2",
+ "tslint": "5.8.0",
"typedoc": "~0.8.0",
"types-bn": "^0.0.1",
"types-ethereumjs-util": "0xProject/types-ethereumjs-util",
@@ -87,9 +87,10 @@
"webpack": "^3.1.0"
},
"dependencies": {
- "@0xproject/assert": "0.0.3",
- "0x-json-schemas": "^0.6.1",
+ "@0xproject/assert": "^0.0.4",
+ "@0xproject/json-schemas": "^0.6.7",
"bignumber.js": "~4.1.0",
+ "bn.js": "4.11.8",
"compare-versions": "^3.0.1",
"es6-promisify": "^5.0.0",
"ethereumjs-abi": "^0.6.4",
diff --git a/packages/0x.js/src/0x.ts b/packages/0x.js/src/0x.ts
index fe765bbbe..85c2b7724 100644
--- a/packages/0x.js/src/0x.ts
+++ b/packages/0x.js/src/0x.ts
@@ -1,6 +1,6 @@
import * as _ from 'lodash';
import BigNumber from 'bignumber.js';
-import {SchemaValidator, schemas} from '0x-json-schemas';
+import {SchemaValidator, schemas} from '@0xproject/json-schemas';
import {bigNumberConfigs} from './bignumber_config';
import * as ethUtil from 'ethereumjs-util';
import {Web3Wrapper} from './web3_wrapper';
diff --git a/packages/0x.js/src/contract.ts b/packages/0x.js/src/contract.ts
index 1aacc65dc..7ccd336d6 100644
--- a/packages/0x.js/src/contract.ts
+++ b/packages/0x.js/src/contract.ts
@@ -1,7 +1,7 @@
import * as Web3 from 'web3';
import * as _ from 'lodash';
import promisify = require('es6-promisify');
-import {SchemaValidator, schemas} from '0x-json-schemas';
+import {SchemaValidator, schemas} from '@0xproject/json-schemas';
import {AbiType} from './types';
export class Contract implements Web3.ContractInstance {
diff --git a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts
index 5acfd3cc9..3e631b73e 100644
--- a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts
+++ b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts
@@ -1,7 +1,7 @@
import * as _ from 'lodash';
import * as Web3 from 'web3';
import BigNumber from 'bignumber.js';
-import {schemas} from '0x-json-schemas';
+import {schemas} from '@0xproject/json-schemas';
import {Web3Wrapper} from '../web3_wrapper';
import {
ECSignature,
diff --git a/packages/0x.js/src/contract_wrappers/token_wrapper.ts b/packages/0x.js/src/contract_wrappers/token_wrapper.ts
index 614ac19d4..4b89d3cfc 100644
--- a/packages/0x.js/src/contract_wrappers/token_wrapper.ts
+++ b/packages/0x.js/src/contract_wrappers/token_wrapper.ts
@@ -1,6 +1,6 @@
import * as _ from 'lodash';
import BigNumber from 'bignumber.js';
-import {schemas} from '0x-json-schemas';
+import {schemas} from '@0xproject/json-schemas';
import {Web3Wrapper} from '../web3_wrapper';
import {assert} from '../utils/assert';
import {constants} from '../utils/constants';
diff --git a/packages/0x.js/src/order_watcher/order_state_watcher.ts b/packages/0x.js/src/order_watcher/order_state_watcher.ts
index 80f323017..1b410dad1 100644
--- a/packages/0x.js/src/order_watcher/order_state_watcher.ts
+++ b/packages/0x.js/src/order_watcher/order_state_watcher.ts
@@ -1,5 +1,5 @@
import * as _ from 'lodash';
-import {schemas} from '0x-json-schemas';
+import {schemas} from '@0xproject/json-schemas';
import {ZeroEx} from '../0x';
import {EventWatcher} from './event_watcher';
import {assert} from '../utils/assert';
diff --git a/packages/0x.js/src/types.ts b/packages/0x.js/src/types.ts
index 91e3c0f20..39e5fa9f2 100644
--- a/packages/0x.js/src/types.ts
+++ b/packages/0x.js/src/types.ts
@@ -485,6 +485,7 @@ export interface OrderRelevantState {
filledTakerTokenAmount: BigNumber;
cancelledTakerTokenAmount: BigNumber;
remainingFillableMakerTokenAmount: BigNumber;
+ remainingFillableTakerTokenAmount: BigNumber;
}
export interface OrderStateValid {
diff --git a/packages/0x.js/src/utils/abi_decoder.ts b/packages/0x.js/src/utils/abi_decoder.ts
index 840ad9be0..df0fb2d6f 100644
--- a/packages/0x.js/src/utils/abi_decoder.ts
+++ b/packages/0x.js/src/utils/abi_decoder.ts
@@ -34,7 +34,7 @@ export class AbiDecoder {
value = this.padZeros(new BigNumber(value).toString(16));
} else if (param.type === SolidityTypes.Uint256 ||
param.type === SolidityTypes.Uint8 ||
- param.type === SolidityTypes.Uint ) {
+ param.type === SolidityTypes.Uint) {
value = new BigNumber(value);
}
decodedParams[param.name] = value;
diff --git a/packages/0x.js/src/utils/assert.ts b/packages/0x.js/src/utils/assert.ts
index 4aa83ef17..55912525c 100644
--- a/packages/0x.js/src/utils/assert.ts
+++ b/packages/0x.js/src/utils/assert.ts
@@ -1,7 +1,7 @@
import * as _ from 'lodash';
import * as Web3 from 'web3';
import BigNumber from 'bignumber.js';
-import {SchemaValidator, Schema} from '0x-json-schemas';
+import {SchemaValidator, Schema} from '@0xproject/json-schemas';
import {assert as sharedAssert} from '@0xproject/assert';
import {Web3Wrapper} from '../web3_wrapper';
import {signatureUtils} from '../utils/signature_utils';
@@ -9,7 +9,8 @@ import {ECSignature} from '../types';
const HEX_REGEX = /^0x[0-9A-F]*$/i;
-export const assert = _.extend({}, sharedAssert, {
+export const assert = {
+ ...sharedAssert,
isValidSignature(orderHash: string, ecSignature: ECSignature, signerAddress: string) {
const isValidSignature = signatureUtils.isValidSignature(orderHash, ecSignature, signerAddress);
this.assert(isValidSignature, `Expected order with hash '${orderHash}' to have a valid signature`);
@@ -26,4 +27,4 @@ export const assert = _.extend({}, sharedAssert, {
const availableAddresses = await web3Wrapper.getAvailableAddressesAsync();
this.assert(!_.isEmpty(availableAddresses), 'No addresses were available on the provided web3 provider');
},
-});
+};
diff --git a/packages/0x.js/src/utils/order_state_utils.ts b/packages/0x.js/src/utils/order_state_utils.ts
index a1ee7577d..123584f90 100644
--- a/packages/0x.js/src/utils/order_state_utils.ts
+++ b/packages/0x.js/src/utils/order_state_utils.ts
@@ -18,6 +18,8 @@ import {constants} from '../utils/constants';
import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store';
import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
+const ACCEPTABLE_RELATIVE_ROUNDING_ERROR = 0.0001;
+
export class OrderStateUtils {
private balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore;
private orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore;
@@ -78,6 +80,9 @@ export class OrderStateUtils {
.dividedToIntegerBy(totalTakerTokenAmount);
const fillableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]);
const remainingFillableMakerTokenAmount = BigNumber.min(fillableMakerTokenAmount, remainingMakerTokenAmount);
+ const remainingFillableTakerTokenAmount = remainingFillableMakerTokenAmount
+ .times(totalTakerTokenAmount)
+ .dividedToIntegerBy(totalMakerTokenAmount);
// TODO: Handle edge case where maker token is ZRX with fee
const orderRelevantState = {
makerBalance,
@@ -87,6 +92,7 @@ export class OrderStateUtils {
filledTakerTokenAmount,
cancelledTakerTokenAmount,
remainingFillableMakerTokenAmount,
+ remainingFillableTakerTokenAmount,
};
return orderRelevantState;
}
@@ -113,6 +119,13 @@ export class OrderStateUtils {
throw new Error(ExchangeContractErrs.InsufficientMakerFeeAllowance);
}
}
+ const minFillableTakerTokenAmountWithinNoRoundingErrorRange = signedOrder.takerTokenAmount
+ .dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR)
+ .dividedBy(signedOrder.makerTokenAmount);
+ if (orderRelevantState.remainingFillableTakerTokenAmount
+ .lessThan(minFillableTakerTokenAmountWithinNoRoundingErrorRange)) {
+ throw new Error(ExchangeContractErrs.OrderFillRoundingError);
+ }
// TODO Add linear function solver when maker token is ZRX #badass
// Return the max amount that's fillable
}
diff --git a/packages/0x.js/test/order_state_watcher_test.ts b/packages/0x.js/test/order_state_watcher_test.ts
index 32f493ee6..a112bac1d 100644
--- a/packages/0x.js/test/order_state_watcher_test.ts
+++ b/packages/0x.js/test/order_state_watcher_test.ts
@@ -47,7 +47,7 @@ describe('OrderStateWatcher', () => {
let taker: string;
let web3Wrapper: Web3Wrapper;
let signedOrder: SignedOrder;
- const fillableAmount = new BigNumber(5);
+ const fillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(5), 18);
before(async () => {
web3 = web3Factory.create();
zeroEx = new ZeroEx(web3.currentProvider);
@@ -61,6 +61,12 @@ describe('OrderStateWatcher', () => {
[makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
web3Wrapper = (zeroEx as any)._web3Wrapper;
});
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
describe('#removeOrder', async () => {
it('should successfully remove existing order', async () => {
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
@@ -215,6 +221,8 @@ describe('OrderStateWatcher', () => {
const remainingFillable = fillableAmount.minus(fillAmountInBaseUnits);
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
remainingFillable);
+ expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
+ remainingFillable);
expect(orderRelevantState.makerBalance).to.be.bignumber.equal(remainingMakerBalance);
if (eventCount === 2) {
done();
@@ -227,84 +235,92 @@ describe('OrderStateWatcher', () => {
);
})().catch(done);
});
- describe('remainingFillableMakerTokenAmount', () => {
- it('should calculate correct remaining fillable', (done: DoneCallback) => {
- (async () => {
- const takerFillableAmount = new BigNumber(10);
- const makerFillableAmount = new BigNumber(20);
- signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
- makerToken.address, takerToken.address, maker, taker, makerFillableAmount, takerFillableAmount);
- const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
- const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
- const fillAmountInBaseUnits = new BigNumber(2);
- const orderHash = ZeroEx.getOrderHashHex(signedOrder);
- zeroEx.orderStateWatcher.addOrder(signedOrder);
- let eventCount = 0;
- const callback = reportCallbackErrors(done)((orderState: OrderState) => {
- eventCount++;
- expect(orderState.isValid).to.be.true();
- const validOrderState = orderState as OrderStateValid;
- expect(validOrderState.orderHash).to.be.equal(orderHash);
- const orderRelevantState = validOrderState.orderRelevantState;
- expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
- new BigNumber(16));
- if (eventCount === 2) {
- done();
- }
- });
- zeroEx.orderStateWatcher.subscribe(callback);
- const shouldThrowOnInsufficientBalanceOrAllowance = true;
- await zeroEx.exchange.fillOrderAsync(
- signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker,
- );
- })().catch(done);
- });
- it('should equal approved amount when approved amount is lowest', (done: DoneCallback) => {
- (async () => {
- signedOrder = await fillScenarios.createFillableSignedOrderAsync(
- makerToken.address, takerToken.address, maker, taker, fillableAmount,
- );
+ describe('remainingFillable(M|T)akerTokenAmount', () => {
+ it('should calculate correct remaining fillable', (done: DoneCallback) => {
+ (async () => {
+ const takerFillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(10), 18);
+ const makerFillableAmount = ZeroEx.toBaseUnitAmount(new BigNumber(20), 18);
+ signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, makerFillableAmount,
+ takerFillableAmount,
+ );
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+ const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
+ const fillAmountInBaseUnits = ZeroEx.toBaseUnitAmount(new BigNumber(2), 18);
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+ let eventCount = 0;
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ eventCount++;
+ expect(orderState.isValid).to.be.true();
+ const validOrderState = orderState as OrderStateValid;
+ expect(validOrderState.orderHash).to.be.equal(orderHash);
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ ZeroEx.toBaseUnitAmount(new BigNumber(16), 18));
+ expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
+ ZeroEx.toBaseUnitAmount(new BigNumber(8), 18));
+ if (eventCount === 2) {
+ done();
+ }
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ const shouldThrowOnInsufficientBalanceOrAllowance = true;
+ await zeroEx.exchange.fillOrderAsync(
+ signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker,
+ );
+ })().catch(done);
+ });
+ it('should equal approved amount when approved amount is lowest', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
- const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
- const changedMakerApprovalAmount = new BigNumber(3);
- zeroEx.orderStateWatcher.addOrder(signedOrder);
+ const changedMakerApprovalAmount = ZeroEx.toBaseUnitAmount(new BigNumber(3), 18);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
- const callback = reportCallbackErrors(done)((orderState: OrderState) => {
- const validOrderState = orderState as OrderStateValid;
- const orderRelevantState = validOrderState.orderRelevantState;
- expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
- changedMakerApprovalAmount);
- done();
- });
- zeroEx.orderStateWatcher.subscribe(callback);
- await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, changedMakerApprovalAmount);
- })().catch(done);
- });
- it('should equal balance amount when balance amount is lowest', (done: DoneCallback) => {
- (async () => {
- signedOrder = await fillScenarios.createFillableSignedOrderAsync(
- makerToken.address, takerToken.address, maker, taker, fillableAmount,
- );
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ const validOrderState = orderState as OrderStateValid;
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ changedMakerApprovalAmount);
+ expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
+ changedMakerApprovalAmount);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, changedMakerApprovalAmount);
+ })().catch(done);
+ });
+ it('should equal balance amount when balance amount is lowest', (done: DoneCallback) => {
+ (async () => {
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
- const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
+ const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
- const remainingAmount = new BigNumber(1);
- const transferAmount = makerBalance.sub(remainingAmount);
- zeroEx.orderStateWatcher.addOrder(signedOrder);
+ const remainingAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), 18);
+ const transferAmount = makerBalance.sub(remainingAmount);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
- const callback = reportCallbackErrors(done)((orderState: OrderState) => {
- const validOrderState = orderState as OrderStateValid;
- const orderRelevantState = validOrderState.orderRelevantState;
- expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
- remainingAmount);
- done();
- });
- zeroEx.orderStateWatcher.subscribe(callback);
- await zeroEx.token.transferAsync(
- makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferAmount);
- })().catch(done);
- });
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ const validOrderState = orderState as OrderStateValid;
+ const orderRelevantState = validOrderState.orderRelevantState;
+ expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
+ remainingAmount);
+ expect(orderRelevantState.remainingFillableTakerTokenAmount).to.be.bignumber.equal(
+ remainingAmount);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ await zeroEx.token.transferAsync(
+ makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferAmount);
+ })().catch(done);
+ });
});
it('should emit orderStateInvalid when watched order cancelled', (done: DoneCallback) => {
(async () => {
@@ -327,6 +343,28 @@ describe('OrderStateWatcher', () => {
await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
})().catch(done);
});
+ it('should emit orderStateInvalid when within rounding error range', (done: DoneCallback) => {
+ (async () => {
+ const remainingFillableAmountInBaseUnits = new BigNumber(100);
+ signedOrder = await fillScenarios.createFillableSignedOrderAsync(
+ makerToken.address, takerToken.address, maker, taker, fillableAmount,
+ );
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ zeroEx.orderStateWatcher.addOrder(signedOrder);
+
+ const callback = reportCallbackErrors(done)((orderState: OrderState) => {
+ expect(orderState.isValid).to.be.false();
+ const invalidOrderState = orderState as OrderStateInvalid;
+ expect(invalidOrderState.orderHash).to.be.equal(orderHash);
+ expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderFillRoundingError);
+ done();
+ });
+ zeroEx.orderStateWatcher.subscribe(callback);
+ await zeroEx.exchange.cancelOrderAsync(
+ signedOrder, fillableAmount.minus(remainingFillableAmountInBaseUnits),
+ );
+ })().catch(done);
+ });
it('should emit orderStateValid when watched order partially cancelled', (done: DoneCallback) => {
(async () => {
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
diff --git a/packages/0x.js/test/token_registry_wrapper_test.ts b/packages/0x.js/test/token_registry_wrapper_test.ts
index 6b5dd517e..d3497451b 100644
--- a/packages/0x.js/test/token_registry_wrapper_test.ts
+++ b/packages/0x.js/test/token_registry_wrapper_test.ts
@@ -1,7 +1,7 @@
import * as _ from 'lodash';
import 'mocha';
import * as chai from 'chai';
-import {SchemaValidator, schemas} from '0x-json-schemas';
+import {SchemaValidator, schemas} from '@0xproject/json-schemas';
import {chaiSetup} from './utils/chai_setup';
import {web3Factory} from './utils/web3_factory';
import {ZeroEx, Token} from '../src';
diff --git a/packages/0x.js/tslint.json b/packages/0x.js/tslint.json
index 5842a872a..a07795151 100644
--- a/packages/0x.js/tslint.json
+++ b/packages/0x.js/tslint.json
@@ -1,5 +1,5 @@
{
"extends": [
- "tslint-config-0xproject"
+ "@0xproject/tslint-config"
]
}
diff --git a/packages/assert/CHANGELOG.md b/packages/assert/CHANGELOG.md
new file mode 100644
index 000000000..fd6bec3f4
--- /dev/null
+++ b/packages/assert/CHANGELOG.md
@@ -0,0 +1,6 @@
+# CHANGELOG
+
+v0.0.4 - _Nov. 14, 2017_
+------------------------
+ * Re-publish Assert previously published under NPM package @0xproject/0x-assert
+ * Added assertion isValidBaseUnitAmount which checks both that the value is a valid bigNumber and that it does not contain decimals.
diff --git a/packages/assert/README.md b/packages/assert/README.md
index 0c60671ea..b0dc9a451 100644
--- a/packages/assert/README.md
+++ b/packages/assert/README.md
@@ -1 +1,10 @@
+assert
+------
+
Standard type and schema assertions to be used across all 0x projects and packages
+
+## Install
+
+```bash
+npm install @0xproject/assert --save
+```
diff --git a/packages/assert/package.json b/packages/assert/package.json
index 3ce67c97b..ed1d2a98b 100644
--- a/packages/assert/package.json
+++ b/packages/assert/package.json
@@ -1,7 +1,6 @@
{
- "private": true,
"name": "@0xproject/assert",
- "version": "0.0.3",
+ "version": "0.0.4",
"description": "Provides a standard way of performing type and schema validation across 0x projects",
"main": "lib/src/index.js",
"types": "lib/src/index.d.ts",
@@ -24,6 +23,7 @@
},
"homepage": "https://github.com/0xProject/0x.js/packages/assert/README.md",
"devDependencies": {
+ "@0xproject/tslint-config": "^0.1.0",
"@types/lodash": "^4.14.78",
"@types/mocha": "^2.2.42",
"@types/valid-url": "^1.0.2",
@@ -33,12 +33,11 @@
"mocha": "^4.0.1",
"npm-run-all": "^4.1.1",
"shx": "^0.2.2",
- "tslint": "~5.5.0",
- "tslint-config-0xproject": "^0.0.2",
+ "tslint": "5.8.0",
"typescript": "^2.4.2"
},
"dependencies": {
- "0x-json-schemas": "^0.6.5",
+ "@0xproject/json-schemas": "^0.6.7",
"bignumber.js": "~4.1.0",
"ethereum-address": "^0.0.4",
"lodash": "^4.17.4",
diff --git a/packages/assert/src/index.ts b/packages/assert/src/index.ts
index 5a9a7cc43..eb224223f 100644
--- a/packages/assert/src/index.ts
+++ b/packages/assert/src/index.ts
@@ -2,7 +2,10 @@ import BigNumber from 'bignumber.js';
import * as ethereum_address from 'ethereum-address';
import * as _ from 'lodash';
import * as validUrl from 'valid-url';
-import {SchemaValidator, Schema} from '0x-json-schemas';
+import {
+ SchemaValidator,
+ Schema,
+} from '@0xproject/json-schemas';
const HEX_REGEX = /^0x[0-9A-F]*$/i;
diff --git a/packages/assert/test/assert_test.ts b/packages/assert/test/assert_test.ts
index 0e35f7f50..66fa4eb54 100644
--- a/packages/assert/test/assert_test.ts
+++ b/packages/assert/test/assert_test.ts
@@ -2,7 +2,7 @@ import 'mocha';
import * as dirtyChai from 'dirty-chai';
import * as chai from 'chai';
import {BigNumber} from 'bignumber.js';
-import {schemas} from '0x-json-schemas';
+import {schemas} from '@0xproject/json-schemas';
import {assert} from '../src/index';
chai.config.includeStack = true;
diff --git a/packages/assert/tslint.json b/packages/assert/tslint.json
index 5842a872a..a07795151 100644
--- a/packages/assert/tslint.json
+++ b/packages/assert/tslint.json
@@ -1,5 +1,5 @@
{
"extends": [
- "tslint-config-0xproject"
+ "@0xproject/tslint-config"
]
}
diff --git a/packages/connect/CHANGELOG.md b/packages/connect/CHANGELOG.md
new file mode 100644
index 000000000..f3367f4ce
--- /dev/null
+++ b/packages/connect/CHANGELOG.md
@@ -0,0 +1,4 @@
+# CHANGELOG
+
+v0.0.0 - _Nov. 15, 2017_
+------------------------
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/packages/json-schemas/CHANGELOG.md b/packages/json-schemas/CHANGELOG.md
new file mode 100644
index 000000000..9f080adeb
--- /dev/null
+++ b/packages/json-schemas/CHANGELOG.md
@@ -0,0 +1,5 @@
+# CHANGELOG
+
+v0.6.7 - _Nov. 14, 2017_
+------------------------
+ * Re-publish JSON-schema previously published under NPM package 0x-json-schemas
diff --git a/packages/json-schemas/README.md b/packages/json-schemas/README.md
new file mode 100644
index 000000000..d89f57a5e
--- /dev/null
+++ b/packages/json-schemas/README.md
@@ -0,0 +1,24 @@
+json-schemas
+------------
+
+Contains 0x-related json schemas
+
+## Install:
+
+```bash
+npm install @0xproject/json-schemas --save
+```
+
+## Usage:
+```
+import {SchemaValidator, ValidatorResult, schemas} from '@0xproject/json-schemas';
+
+const {orderSchema} = schemas;
+const validator = new SchemaValidator();
+
+const order = {
+ ...
+};
+const validatorResult: ValidatorResult = validator.validate(order, orderSchema); // Contains all errors
+const isValid: boolean = validator.isValid(order, orderSchema); // Only returns boolean
+```
diff --git a/packages/json-schemas/package.json b/packages/json-schemas/package.json
new file mode 100644
index 000000000..07ed20551
--- /dev/null
+++ b/packages/json-schemas/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@0xproject/json-schemas",
+ "version": "0.6.7",
+ "description": "0x-related json schemas",
+ "main": "lib/src/index.js",
+ "types": "lib/src/index.d.ts",
+ "scripts": {
+ "lint": "tslint src/*.ts test/*.ts",
+ "test": "run-s clean build run_mocha",
+ "test:circleci": "yarn test",
+ "run_mocha": "mocha lib/test/**/*_test.js",
+ "clean": "shx rm -rf _bundles lib test_temp",
+ "build": "tsc"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/0xProject/0x.js.git"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/0xProject/0x.js/issues"
+ },
+ "homepage": "https://github.com/0xProject/0x.js/packages/json-schemas/README.md",
+ "dependencies": {
+ "es6-promisify": "^5.0.0",
+ "jsonschema": "^1.2.0",
+ "lodash.values": "^4.3.0"
+ },
+ "devDependencies": {
+ "@0xproject/tslint-config": "^0.1.0",
+ "@types/lodash.foreach": "^4.5.3",
+ "@types/lodash.values": "^4.3.3",
+ "@types/mocha": "^2.2.42",
+ "bignumber.js": "^4.0.2",
+ "chai": "^4.1.1",
+ "chai-typescript-typings": "^0.0.1",
+ "dirty-chai": "^2.0.1",
+ "lodash.foreach": "^4.5.0",
+ "mocha": "^4.0.1",
+ "npm-run-all": "^4.1.1",
+ "shx": "^0.2.2",
+ "tslint": "5.8.0",
+ "typescript": "~2.6.1"
+ }
+}
diff --git a/packages/json-schemas/schemas/basic_type_schemas.ts b/packages/json-schemas/schemas/basic_type_schemas.ts
new file mode 100644
index 000000000..9d81ff333
--- /dev/null
+++ b/packages/json-schemas/schemas/basic_type_schemas.ts
@@ -0,0 +1,11 @@
+export const addressSchema = {
+ id: '/Address',
+ type: 'string',
+ pattern: '^0x[0-9a-f]{40}$',
+};
+
+export const numberSchema = {
+ id: '/Number',
+ type: 'string',
+ pattern: '^\\d+(\\.\\d+)?$',
+};
diff --git a/packages/json-schemas/schemas/ec_signature_schema.ts b/packages/json-schemas/schemas/ec_signature_schema.ts
new file mode 100644
index 000000000..2b769f3b6
--- /dev/null
+++ b/packages/json-schemas/schemas/ec_signature_schema.ts
@@ -0,0 +1,20 @@
+export const ecSignatureParameterSchema = {
+ id: '/ECSignatureParameter',
+ type: 'string',
+ pattern: '^0[xX][0-9A-Fa-f]{64}$',
+};
+
+export const ecSignatureSchema = {
+ id: '/ECSignature',
+ properties: {
+ v: {
+ type: 'number',
+ minimum: 27,
+ maximum: 28,
+ },
+ r: {$ref: '/ECSignatureParameter'},
+ s: {$ref: '/ECSignatureParameter'},
+ },
+ required: ['v', 'r', 's'],
+ type: 'object',
+};
diff --git a/packages/json-schemas/schemas/index_filter_values_schema.ts b/packages/json-schemas/schemas/index_filter_values_schema.ts
new file mode 100644
index 000000000..f7e323e45
--- /dev/null
+++ b/packages/json-schemas/schemas/index_filter_values_schema.ts
@@ -0,0 +1,11 @@
+export const indexFilterValuesSchema = {
+ id: '/IndexFilterValues',
+ additionalProperties: {
+ oneOf: [
+ {$ref: '/Number'},
+ {$ref: '/Address'},
+ {$ref: '/OrderHashSchema'},
+ ],
+ },
+ type: 'object',
+};
diff --git a/packages/json-schemas/schemas/order_cancel_schema.ts b/packages/json-schemas/schemas/order_cancel_schema.ts
new file mode 100644
index 000000000..ac7d2ee20
--- /dev/null
+++ b/packages/json-schemas/schemas/order_cancel_schema.ts
@@ -0,0 +1,12 @@
+export const orderCancellationRequestsSchema = {
+ id: '/OrderCancellationRequests',
+ type: 'array',
+ items: {
+ properties: {
+ order: {$ref: '/Order'},
+ takerTokenCancelAmount: {$ref: '/Number'},
+ },
+ required: ['order', 'takerTokenCancelAmount'],
+ type: 'object',
+ },
+};
diff --git a/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.ts b/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.ts
new file mode 100644
index 000000000..4ef7b069a
--- /dev/null
+++ b/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.ts
@@ -0,0 +1,12 @@
+export const orderFillOrKillRequestsSchema = {
+ id: '/OrderFillOrKillRequests',
+ type: 'array',
+ items: {
+ properties: {
+ signedOrder: {$ref: '/SignedOrder'},
+ fillTakerAmount: {$ref: '/Number'},
+ },
+ required: ['signedOrder', 'fillTakerAmount'],
+ type: 'object',
+ },
+};
diff --git a/packages/json-schemas/schemas/order_fill_requests_schema.ts b/packages/json-schemas/schemas/order_fill_requests_schema.ts
new file mode 100644
index 000000000..ec19dd9f8
--- /dev/null
+++ b/packages/json-schemas/schemas/order_fill_requests_schema.ts
@@ -0,0 +1,12 @@
+export const orderFillRequestsSchema = {
+ id: '/OrderFillRequests',
+ type: 'array',
+ items: {
+ properties: {
+ signedOrder: {$ref: '/SignedOrder'},
+ takerTokenFillAmount: {$ref: '/Number'},
+ },
+ required: ['signedOrder', 'takerTokenFillAmount'],
+ type: 'object',
+ },
+};
diff --git a/packages/json-schemas/schemas/order_hash_schema.ts b/packages/json-schemas/schemas/order_hash_schema.ts
new file mode 100644
index 000000000..6af06927f
--- /dev/null
+++ b/packages/json-schemas/schemas/order_hash_schema.ts
@@ -0,0 +1,5 @@
+export const orderHashSchema = {
+ id: '/OrderHashSchema',
+ type: 'string',
+ pattern: '^0x[0-9a-fA-F]{64}$',
+};
diff --git a/packages/json-schemas/schemas/order_schemas.ts b/packages/json-schemas/schemas/order_schemas.ts
new file mode 100644
index 000000000..3cce49351
--- /dev/null
+++ b/packages/json-schemas/schemas/order_schemas.ts
@@ -0,0 +1,35 @@
+export const orderSchema = {
+ id: '/Order',
+ properties: {
+ maker: {$ref: '/Address'},
+ taker: {$ref: '/Address'},
+ makerFee: {$ref: '/Number'},
+ takerFee: {$ref: '/Number'},
+ makerTokenAmount: {$ref: '/Number'},
+ takerTokenAmount: {$ref: '/Number'},
+ makerTokenAddress: {$ref: '/Address'},
+ takerTokenAddress: {$ref: '/Address'},
+ salt: {$ref: '/Number'},
+ feeRecipient: {$ref: '/Address'},
+ expirationUnixTimestampSec: {$ref: '/Number'},
+ exchangeContractAddress: {$ref: '/Address'},
+ },
+ required: [
+ 'maker', 'taker', 'makerFee', 'takerFee', 'makerTokenAmount', 'takerTokenAmount',
+ 'salt', 'feeRecipient', 'expirationUnixTimestampSec', 'exchangeContractAddress',
+ ],
+ type: 'object',
+};
+
+export const signedOrderSchema = {
+ id: '/SignedOrder',
+ allOf: [
+ { $ref: '/Order' },
+ {
+ properties: {
+ ecSignature: {$ref: '/ECSignature'},
+ },
+ required: ['ecSignature'],
+ },
+ ],
+};
diff --git a/packages/json-schemas/schemas/relayer_api_error_response_schema.ts b/packages/json-schemas/schemas/relayer_api_error_response_schema.ts
new file mode 100644
index 000000000..eacbb2bce
--- /dev/null
+++ b/packages/json-schemas/schemas/relayer_api_error_response_schema.ts
@@ -0,0 +1,21 @@
+export const relayerApiErrorResponseSchema = {
+ id: '/RelayerApiErrorResponse',
+ type: 'object',
+ properties: {
+ code: {type: 'number'},
+ reason: {type: 'string'},
+ validationErrors: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ field: {type: 'string'},
+ code: {type: 'number'},
+ reason: {type: 'string'},
+ },
+ required: ['field', 'code', 'reason'],
+ },
+ },
+ },
+ required: ['code', 'reason'],
+};
diff --git a/packages/json-schemas/schemas/relayer_api_fees_payload_schema.ts b/packages/json-schemas/schemas/relayer_api_fees_payload_schema.ts
new file mode 100644
index 000000000..645660844
--- /dev/null
+++ b/packages/json-schemas/schemas/relayer_api_fees_payload_schema.ts
@@ -0,0 +1,19 @@
+export const relayerApiFeesPayloadSchema = {
+ id: '/RelayerApiFeesPayload',
+ type: 'object',
+ properties: {
+ exchangeContractAddress: {$ref: '/Address'},
+ maker: {$ref: '/Address'},
+ taker: {$ref: '/Address'},
+ makerTokenAddress: {$ref: '/Address'},
+ takerTokenAddress: {$ref: '/Address'},
+ makerTokenAmount: {$ref: '/Number'},
+ takerTokenAmount: {$ref: '/Number'},
+ expirationUnixTimestampSec: {$ref: '/Number'},
+ salt: {$ref: '/Number'},
+ },
+ required: [
+ 'exchangeContractAddress', 'maker', 'taker', 'makerTokenAddress', 'takerTokenAddress',
+ 'expirationUnixTimestampSec', 'salt',
+ ],
+};
diff --git a/packages/json-schemas/schemas/relayer_api_fees_response_schema.ts b/packages/json-schemas/schemas/relayer_api_fees_response_schema.ts
new file mode 100644
index 000000000..86e51feb0
--- /dev/null
+++ b/packages/json-schemas/schemas/relayer_api_fees_response_schema.ts
@@ -0,0 +1,10 @@
+export const relayerApiFeesResponseSchema = {
+ id: '/RelayerApiFeesResponse',
+ type: 'object',
+ properties: {
+ makerFee: {$ref: '/Number'},
+ takerFee: {$ref: '/Number'},
+ feeRecipient: {$ref: '/Address'},
+ },
+ required: ['makerFee', 'takerFee', 'feeRecipient'],
+};
diff --git a/packages/json-schemas/schemas/relayer_api_orberbook_channel_subscribe_schema.ts b/packages/json-schemas/schemas/relayer_api_orberbook_channel_subscribe_schema.ts
new file mode 100644
index 000000000..8ded9adb0
--- /dev/null
+++ b/packages/json-schemas/schemas/relayer_api_orberbook_channel_subscribe_schema.ts
@@ -0,0 +1,22 @@
+export const relayerApiOrderbookChannelSubscribeSchema = {
+ id: '/RelayerApiOrderbookChannelSubscribe',
+ type: 'object',
+ properties: {
+ type: {enum: ['subscribe']},
+ channel: {enum: ['orderbook']},
+ payload: {$ref: '/RelayerApiOrderbookChannelSubscribePayload'},
+ },
+ required: ['type', 'channel', 'payload'],
+};
+
+export const relayerApiOrderbookChannelSubscribePayload = {
+ id: '/RelayerApiOrderbookChannelSubscribePayload',
+ type: 'object',
+ properties: {
+ baseTokenAddress: {$ref: '/Address'},
+ quoteTokenAddress: {$ref: '/Address'},
+ snapshot: {type: 'boolean'},
+ limit: {type: 'number'},
+ },
+ required: ['baseTokenAddress', 'quoteTokenAddress'],
+};
diff --git a/packages/json-schemas/schemas/relayer_api_orderbook_channel_snapshot_schema.ts b/packages/json-schemas/schemas/relayer_api_orderbook_channel_snapshot_schema.ts
new file mode 100644
index 000000000..cfc0ddc8f
--- /dev/null
+++ b/packages/json-schemas/schemas/relayer_api_orderbook_channel_snapshot_schema.ts
@@ -0,0 +1,21 @@
+export const relayerApiOrderbookChannelSnapshotSchema = {
+ id: '/RelayerApiOrderbookChannelSnapshot',
+ type: 'object',
+ properties: {
+ type: {enum: ['snapshot']},
+ channel: {enum: ['orderbook']},
+ channelId: {type: 'number'},
+ payload: {$ref: '/RelayerApiOrderbookChannelSnapshotPayload'},
+ },
+ required: ['type', 'channel', 'channelId', 'payload'],
+};
+
+export const relayerApiOrderbookChannelSnapshotPayload = {
+ id: '/RelayerApiOrderbookChannelSnapshotPayload',
+ type: 'object',
+ properties: {
+ bids: {$ref: '/signedOrdersSchema'},
+ asks: {$ref: '/signedOrdersSchema'},
+ },
+ required: ['bids', 'asks'],
+};
diff --git a/packages/json-schemas/schemas/relayer_api_orderbook_channel_update_response_schema.ts b/packages/json-schemas/schemas/relayer_api_orderbook_channel_update_response_schema.ts
new file mode 100644
index 000000000..51308ed49
--- /dev/null
+++ b/packages/json-schemas/schemas/relayer_api_orderbook_channel_update_response_schema.ts
@@ -0,0 +1,11 @@
+export const relayerApiOrderbookChannelUpdateSchema = {
+ id: '/RelayerApiOrderbookChannelUpdate',
+ type: 'object',
+ properties: {
+ type: {enum: ['update']},
+ channel: {enum: ['orderbook']},
+ channelId: {type: 'number'},
+ payload: {$ref: '/SignedOrder'},
+ },
+ required: ['type', 'channel', 'channelId', 'payload'],
+};
diff --git a/packages/json-schemas/schemas/relayer_api_orderbook_response_schema.ts b/packages/json-schemas/schemas/relayer_api_orderbook_response_schema.ts
new file mode 100644
index 000000000..b592d4f8e
--- /dev/null
+++ b/packages/json-schemas/schemas/relayer_api_orderbook_response_schema.ts
@@ -0,0 +1,9 @@
+export const relayerApiOrderBookResponseSchema = {
+ id: '/RelayerApiOrderBookResponse',
+ type: 'object',
+ properties: {
+ bids: {$ref: '/signedOrdersSchema'},
+ asks: {$ref: '/signedOrdersSchema'},
+ },
+ required: ['bids', 'asks'],
+};
diff --git a/packages/json-schemas/schemas/relayer_api_token_pairs_response_schema.ts b/packages/json-schemas/schemas/relayer_api_token_pairs_response_schema.ts
new file mode 100644
index 000000000..8ecab1424
--- /dev/null
+++ b/packages/json-schemas/schemas/relayer_api_token_pairs_response_schema.ts
@@ -0,0 +1,24 @@
+export const relayerApiTokenPairsResponseSchema = {
+ id: '/RelayerApiTokenPairsResponse',
+ type: 'array',
+ items: {
+ properties: {
+ tokenA: {$ref: '/RelayerApiTokenTradeInfo'},
+ tokenB: {$ref: '/RelayerApiTokenTradeInfo'},
+ },
+ required: ['tokenA', 'tokenB'],
+ type: 'object',
+ },
+};
+
+export const relayerApiTokenTradeInfoSchema = {
+ id: '/RelayerApiTokenTradeInfo',
+ type: 'object',
+ properties: {
+ address: {$ref: '/Address'},
+ minAmount: {$ref: '/Number'},
+ maxAmount: {$ref: '/Number'},
+ precision: {type: 'number'},
+ },
+ required: ['address'],
+};
diff --git a/packages/json-schemas/schemas/signed_orders_schema.ts b/packages/json-schemas/schemas/signed_orders_schema.ts
new file mode 100644
index 000000000..c4c4a68ac
--- /dev/null
+++ b/packages/json-schemas/schemas/signed_orders_schema.ts
@@ -0,0 +1,5 @@
+export const signedOrdersSchema = {
+ id: '/signedOrdersSchema',
+ type: 'array',
+ items: {$ref: '/SignedOrder'},
+};
diff --git a/packages/json-schemas/schemas/subscription_opts_schema.ts b/packages/json-schemas/schemas/subscription_opts_schema.ts
new file mode 100644
index 000000000..a476e6963
--- /dev/null
+++ b/packages/json-schemas/schemas/subscription_opts_schema.ts
@@ -0,0 +1,20 @@
+export const blockParamSchema = {
+ id: '/BlockParam',
+ oneOf: [
+ {
+ type: 'number',
+ },
+ {
+ enum: ['latest', 'earliest', 'pending'],
+ },
+ ],
+};
+
+export const subscriptionOptsSchema = {
+ id: '/SubscriptionOpts',
+ properties: {
+ fromBlock: {$ref: '/BlockParam'},
+ toBlock: {$ref: '/BlockParam'},
+ },
+ type: 'object',
+};
diff --git a/packages/json-schemas/schemas/token_schema.ts b/packages/json-schemas/schemas/token_schema.ts
new file mode 100644
index 000000000..aca4d4ad2
--- /dev/null
+++ b/packages/json-schemas/schemas/token_schema.ts
@@ -0,0 +1,11 @@
+export const tokenSchema = {
+ id: '/Token',
+ properties: {
+ name: {type: 'string'},
+ symbol: {type: 'string'},
+ decimals: {type: 'number'},
+ address: {$ref: '/Address'},
+ },
+ required: ['name', 'symbol', 'decimals', 'address'],
+ type: 'object',
+};
diff --git a/packages/json-schemas/schemas/tx_data_schema.ts b/packages/json-schemas/schemas/tx_data_schema.ts
new file mode 100644
index 000000000..41eaadd3c
--- /dev/null
+++ b/packages/json-schemas/schemas/tx_data_schema.ts
@@ -0,0 +1,42 @@
+export const jsNumber = {
+ id: '/JsNumber',
+ type: 'number',
+ minimum: 0,
+};
+
+export const txDataSchema = {
+ id: '/TxData',
+ properties: {
+ from: {$ref: '/Address'},
+ to: {$ref: '/Address'},
+ value: {
+ oneOf: [
+ {$ref: '/Number'},
+ {$ref: '/JsNumber'},
+ ],
+ },
+ gas: {
+ oneOf: [
+ {$ref: '/Number'},
+ {$ref: '/JsNumber'},
+ ],
+ },
+ gasPrice: {
+ oneOf: [
+ {$ref: '/Number'},
+ {$ref: '/JsNumber'},
+ ],
+ },
+ data: {
+ type: 'string',
+ pattern: '^0x[0-9a-f]*$',
+ },
+ nonce: {
+ type: 'number',
+ minimum: 0,
+ },
+ },
+ required: ['from'],
+ type: 'object',
+ additionalProperties: false,
+};
diff --git a/packages/json-schemas/src/globals.d.ts b/packages/json-schemas/src/globals.d.ts
new file mode 100644
index 000000000..157705f57
--- /dev/null
+++ b/packages/json-schemas/src/globals.d.ts
@@ -0,0 +1,7 @@
+declare module 'dirty-chai';
+
+// es6-promisify declarations
+declare function promisify(original: any, settings?: any): ((...arg: any[]) => Promise<any>);
+declare module 'es6-promisify' {
+ export = promisify;
+}
diff --git a/packages/json-schemas/src/index.ts b/packages/json-schemas/src/index.ts
new file mode 100644
index 000000000..b7cae277e
--- /dev/null
+++ b/packages/json-schemas/src/index.ts
@@ -0,0 +1,4 @@
+export {ValidatorResult, Schema} from 'jsonschema';
+
+export {SchemaValidator} from './schema_validator';
+export {schemas} from './schemas';
diff --git a/packages/json-schemas/src/schema_validator.ts b/packages/json-schemas/src/schema_validator.ts
new file mode 100644
index 000000000..0bc88cc45
--- /dev/null
+++ b/packages/json-schemas/src/schema_validator.ts
@@ -0,0 +1,28 @@
+import values = require('lodash.values');
+import {Validator, ValidatorResult, Schema} from 'jsonschema';
+import {schemas} from './schemas';
+
+export class SchemaValidator {
+ private validator: Validator;
+ constructor() {
+ this.validator = new Validator();
+ for (const schema of values(schemas)) {
+ this.validator.addSchema(schema, schema.id);
+ }
+ }
+ public addSchema(schema: Schema) {
+ this.validator.addSchema(schema, schema.id);
+ }
+ // In order to validate a complex JS object using jsonschema, we must replace any complex
+ // sub-types (e.g BigNumber) with a simpler string representation. Since BigNumber and other
+ // complex types implement the `toString` method, we can stringify the object and
+ // then parse it. The resultant object can then be checked using jsonschema.
+ public validate(instance: any, schema: Schema): ValidatorResult {
+ const jsonSchemaCompatibleObject = JSON.parse(JSON.stringify(instance));
+ return this.validator.validate(jsonSchemaCompatibleObject, schema);
+ }
+ public isValid(instance: any, schema: Schema): boolean {
+ const isValid = this.validate(instance, schema).errors.length === 0;
+ return isValid;
+ }
+}
diff --git a/packages/json-schemas/src/schemas.ts b/packages/json-schemas/src/schemas.ts
new file mode 100644
index 000000000..a8e5ecbcb
--- /dev/null
+++ b/packages/json-schemas/src/schemas.ts
@@ -0,0 +1,99 @@
+import {
+ numberSchema,
+ addressSchema,
+} from '../schemas/basic_type_schemas';
+import {
+ ecSignatureSchema,
+ ecSignatureParameterSchema,
+} from '../schemas/ec_signature_schema';
+import {
+ indexFilterValuesSchema,
+} from '../schemas/index_filter_values_schema';
+import {
+ orderCancellationRequestsSchema,
+} from '../schemas/order_cancel_schema';
+import {
+ orderFillOrKillRequestsSchema,
+} from '../schemas/order_fill_or_kill_requests_schema';
+import {
+ orderFillRequestsSchema,
+} from '../schemas/order_fill_requests_schema';
+import {
+ orderHashSchema,
+} from '../schemas/order_hash_schema';
+import {
+ orderSchema,
+ signedOrderSchema,
+} from '../schemas/order_schemas';
+import {
+ blockParamSchema,
+ subscriptionOptsSchema,
+} from '../schemas/subscription_opts_schema';
+import {
+ tokenSchema,
+} from '../schemas/token_schema';
+import {
+ signedOrdersSchema,
+} from '../schemas/signed_orders_schema';
+import {
+ relayerApiErrorResponseSchema,
+} from '../schemas/relayer_api_error_response_schema';
+import {
+ relayerApiFeesResponseSchema,
+} from '../schemas/relayer_api_fees_response_schema';
+import {
+ relayerApiFeesPayloadSchema,
+} from '../schemas/relayer_api_fees_payload_schema';
+import {
+ relayerApiOrderBookResponseSchema,
+} from '../schemas/relayer_api_orderbook_response_schema';
+import {
+ relayerApiTokenPairsResponseSchema,
+ relayerApiTokenTradeInfoSchema,
+} from '../schemas/relayer_api_token_pairs_response_schema';
+import {
+ jsNumber,
+ txDataSchema,
+} from '../schemas/tx_data_schema';
+import {
+ relayerApiOrderbookChannelSubscribeSchema,
+ relayerApiOrderbookChannelSubscribePayload,
+} from '../schemas/relayer_api_orberbook_channel_subscribe_schema';
+import {
+ relayerApiOrderbookChannelUpdateSchema,
+} from '../schemas/relayer_api_orderbook_channel_update_response_schema';
+import {
+ relayerApiOrderbookChannelSnapshotSchema,
+ relayerApiOrderbookChannelSnapshotPayload,
+} from '../schemas/relayer_api_orderbook_channel_snapshot_schema';
+
+export const schemas = {
+ numberSchema,
+ addressSchema,
+ ecSignatureSchema,
+ ecSignatureParameterSchema,
+ indexFilterValuesSchema,
+ orderCancellationRequestsSchema,
+ orderFillOrKillRequestsSchema,
+ orderFillRequestsSchema,
+ orderHashSchema,
+ orderSchema,
+ signedOrderSchema,
+ signedOrdersSchema,
+ blockParamSchema,
+ subscriptionOptsSchema,
+ tokenSchema,
+ jsNumber,
+ txDataSchema,
+ relayerApiErrorResponseSchema,
+ relayerApiFeesPayloadSchema,
+ relayerApiFeesResponseSchema,
+ relayerApiOrderBookResponseSchema,
+ relayerApiTokenPairsResponseSchema,
+ relayerApiTokenTradeInfoSchema,
+ relayerApiOrderbookChannelSubscribeSchema,
+ relayerApiOrderbookChannelSubscribePayload,
+ relayerApiOrderbookChannelUpdateSchema,
+ relayerApiOrderbookChannelSnapshotSchema,
+ relayerApiOrderbookChannelSnapshotPayload,
+};
diff --git a/packages/json-schemas/test/schema_test.ts b/packages/json-schemas/test/schema_test.ts
new file mode 100644
index 000000000..0ff456dec
--- /dev/null
+++ b/packages/json-schemas/test/schema_test.ts
@@ -0,0 +1,972 @@
+import 'mocha';
+import forEach = require('lodash.foreach');
+import * as dirtyChai from 'dirty-chai';
+import * as chai from 'chai';
+import BigNumber from 'bignumber.js';
+import promisify = require('es6-promisify');
+import {SchemaValidator, schemas} from '../src/index';
+
+chai.config.includeStack = true;
+chai.use(dirtyChai);
+const expect = chai.expect;
+const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
+const {
+ numberSchema,
+ addressSchema,
+ ecSignatureSchema,
+ ecSignatureParameterSchema,
+ indexFilterValuesSchema,
+ orderCancellationRequestsSchema,
+ orderFillOrKillRequestsSchema,
+ orderFillRequestsSchema,
+ orderHashSchema,
+ orderSchema,
+ signedOrderSchema,
+ signedOrdersSchema,
+ blockParamSchema,
+ subscriptionOptsSchema,
+ tokenSchema,
+ jsNumber,
+ txDataSchema,
+ relayerApiErrorResponseSchema,
+ relayerApiOrderBookResponseSchema,
+ relayerApiTokenPairsResponseSchema,
+ relayerApiFeesPayloadSchema,
+ relayerApiFeesResponseSchema,
+ relayerApiOrderbookChannelSubscribeSchema,
+ relayerApiOrderbookChannelUpdateSchema,
+ relayerApiOrderbookChannelSnapshotSchema,
+} = schemas;
+
+describe('Schema', () => {
+ const validator = new SchemaValidator();
+ const validateAgainstSchema = (testCases: any[], schema: any, shouldFail = false) => {
+ forEach(testCases, (testCase: any) => {
+ const validationResult = validator.validate(testCase, schema);
+ const hasErrors = validationResult.errors.length !== 0;
+ if (shouldFail) {
+ if (!hasErrors) {
+ throw new Error(
+ `Expected testCase: ${JSON.stringify(testCase, null, '\t')} to fail and it didn't.`,
+ );
+ }
+ } else {
+ if (hasErrors) {
+ throw new Error(JSON.stringify(validationResult.errors, null, '\t'));
+ }
+ }
+ });
+ };
+ describe('#numberSchema', () => {
+ it('should validate valid numbers', () => {
+ const testCases = ['42', '0', '1.3', '0.2', '00.00'];
+ validateAgainstSchema(testCases, numberSchema);
+ });
+ it('should fail for invalid numbers', () => {
+ const testCases = ['.3', '1.', 'abacaba', 'и', '1..0'];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, numberSchema, shouldFail);
+ });
+ });
+ describe('#addressSchema', () => {
+ it('should validate valid addresses', () => {
+ const testCases = ['0x8b0292b11a196601ed2ce54b665cafeca0347d42', NULL_ADDRESS];
+ validateAgainstSchema(testCases, addressSchema);
+ });
+ it('should fail for invalid addresses', () => {
+ const testCases = [
+ '0x',
+ '0',
+ '0x00',
+ '0xzzzzzzB11a196601eD2ce54B665CaFEca0347D42',
+ '0x8b0292B11a196601eD2ce54B665CaFEca0347D42',
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, addressSchema, shouldFail);
+ });
+ });
+ describe('#ecSignatureParameterSchema', () => {
+ it('should validate valid parameters', () => {
+ const testCases = [
+ '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
+ '0X40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
+ ];
+ validateAgainstSchema(testCases, ecSignatureParameterSchema);
+ });
+ it('should fail for invalid parameters', () => {
+ const testCases = [
+ '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3', // shorter
+ '0xzzzz9190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', // invalid characters
+ '40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', // no 0x
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, ecSignatureParameterSchema, shouldFail);
+ });
+ });
+ describe('#ecSignatureSchema', () => {
+ it('should validate valid signature', () => {
+ const signature = {
+ v: 27,
+ r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
+ s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
+ };
+ const testCases = [
+ signature,
+ {
+ ...signature,
+ v: 28,
+ },
+ ];
+ validateAgainstSchema(testCases, ecSignatureSchema);
+ });
+ it('should fail for invalid signature', () => {
+ const v = 27;
+ const r = '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33';
+ const s = '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254';
+ const testCases = [
+ {},
+ {v},
+ {r, s, v: 31},
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, ecSignatureSchema, shouldFail);
+ });
+ });
+ describe('#orderHashSchema', () => {
+ it('should validate valid order hash', () => {
+ const testCases = [
+ '0x61a3ed31B43c8780e905a260a35faefEc527be7516aa11c0256729b5b351bc33',
+ '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
+ ];
+ validateAgainstSchema(testCases, orderHashSchema);
+ });
+ it('should fail for invalid order hash', () => {
+ const testCases = [
+ {},
+ '0x',
+ '0x8b0292B11a196601eD2ce54B665CaFEca0347D42',
+ '61a3ed31B43c8780e905a260a35faefEc527be7516aa11c0256729b5b351bc33',
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, orderHashSchema, shouldFail);
+ });
+ });
+ describe('#blockParamSchema', () => {
+ it('should validate valid block param', () => {
+ const testCases = [
+ 42,
+ 'latest',
+ 'pending',
+ 'earliest',
+ ];
+ validateAgainstSchema(testCases, blockParamSchema);
+ });
+ it('should fail for invalid block param', () => {
+ const testCases = [
+ {},
+ '42',
+ 'pemding',
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, blockParamSchema, shouldFail);
+ });
+ });
+ describe('#subscriptionOptsSchema', () => {
+ it('should validate valid subscription opts', () => {
+ const testCases = [
+ {fromBlock: 42, toBlock: 'latest'},
+ {fromBlock: 42},
+ {},
+ ];
+ validateAgainstSchema(testCases, subscriptionOptsSchema);
+ });
+ it('should fail for invalid subscription opts', () => {
+ const testCases = [
+ {fromBlock: '42'},
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, subscriptionOptsSchema, shouldFail);
+ });
+ });
+ describe('#tokenSchema', () => {
+ const token = {
+ name: 'Zero Ex',
+ symbol: 'ZRX',
+ decimals: 100500,
+ address: '0x8b0292b11a196601ed2ce54b665cafeca0347d42',
+ url: 'https://0xproject.com',
+ };
+ it('should validate valid token', () => {
+ const testCases = [
+ token,
+ ];
+ validateAgainstSchema(testCases, tokenSchema);
+ });
+ it('should fail for invalid token', () => {
+ const testCases = [
+ {
+ ...token,
+ address: null,
+ },
+ {
+ ...token,
+ decimals: undefined,
+ },
+ [],
+ 4,
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, tokenSchema, shouldFail);
+ });
+ });
+ describe('order including schemas', () => {
+ const order = {
+ maker: NULL_ADDRESS,
+ taker: NULL_ADDRESS,
+ makerFee: '1',
+ takerFee: '2',
+ makerTokenAmount: '1',
+ takerTokenAmount: '2',
+ makerTokenAddress: NULL_ADDRESS,
+ takerTokenAddress: NULL_ADDRESS,
+ salt: '67006738228878699843088602623665307406148487219438534730168799356281242528500',
+ feeRecipient: NULL_ADDRESS,
+ exchangeContractAddress: NULL_ADDRESS,
+ expirationUnixTimestampSec: '42',
+ };
+ describe('#orderSchema', () => {
+ it('should validate valid order', () => {
+ const testCases = [
+ order,
+ ];
+ validateAgainstSchema(testCases, orderSchema);
+ });
+ it('should fail for invalid order', () => {
+ const testCases = [
+ {
+ ...order,
+ salt: undefined,
+ },
+ {
+ ...order,
+ salt: 'salt',
+ },
+ 'order',
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, orderSchema, shouldFail);
+ });
+ });
+ describe('signed order including schemas', () => {
+ const signedOrder = {
+ ...order,
+ ecSignature: {
+ v: 27,
+ r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
+ s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
+ },
+ };
+ describe('#signedOrdersSchema', () => {
+ it('should validate valid signed orders', () => {
+ const testCases = [
+ [signedOrder],
+ [],
+ ];
+ validateAgainstSchema(testCases, signedOrdersSchema);
+ });
+ it('should fail for invalid signed orders', () => {
+ const testCases = [
+ [
+ signedOrder,
+ 1,
+ ],
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, signedOrdersSchema, shouldFail);
+ });
+ });
+ describe('#signedOrderSchema', () => {
+ it('should validate valid signed order', () => {
+ const testCases = [
+ signedOrder,
+ ];
+ validateAgainstSchema(testCases, signedOrderSchema);
+ });
+ it('should fail for invalid signed order', () => {
+ const testCases = [
+ {
+ ...signedOrder,
+ ecSignature: undefined,
+ },
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, signedOrderSchema, shouldFail);
+ });
+ });
+ describe('#orderFillOrKillRequestsSchema', () => {
+ const orderFillOrKillRequests = [
+ {
+ signedOrder,
+ fillTakerAmount: '5',
+ },
+ ];
+ it('should validate valid order fill or kill requests', () => {
+ const testCases = [
+ orderFillOrKillRequests,
+ ];
+ validateAgainstSchema(testCases, orderFillOrKillRequestsSchema);
+ });
+ it('should fail for invalid order fill or kill requests', () => {
+ const testCases = [
+ [
+ {
+ ...orderFillOrKillRequests[0],
+ fillTakerAmount: undefined,
+ },
+ ],
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, orderFillOrKillRequestsSchema, shouldFail);
+ });
+ });
+ describe('#orderCancellationRequestsSchema', () => {
+ const orderCancellationRequests = [
+ {
+ order,
+ takerTokenCancelAmount: '5',
+ },
+ ];
+ it('should validate valid order cancellation requests', () => {
+ const testCases = [
+ orderCancellationRequests,
+ ];
+ validateAgainstSchema(testCases, orderCancellationRequestsSchema);
+ });
+ it('should fail for invalid order cancellation requests', () => {
+ const testCases = [
+ [
+ {
+ ...orderCancellationRequests[0],
+ takerTokenCancelAmount: undefined,
+ },
+ ],
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, orderCancellationRequestsSchema, shouldFail);
+ });
+ });
+ describe('#orderFillRequestsSchema', () => {
+ const orderFillRequests = [
+ {
+ signedOrder,
+ takerTokenFillAmount: '5',
+ },
+ ];
+ it('should validate valid order fill requests', () => {
+ const testCases = [
+ orderFillRequests,
+ ];
+ validateAgainstSchema(testCases, orderFillRequestsSchema);
+ });
+ it('should fail for invalid order fill requests', () => {
+ const testCases = [
+ [
+ {
+ ...orderFillRequests[0],
+ takerTokenFillAmount: undefined,
+ },
+ ],
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, orderFillRequestsSchema, shouldFail);
+ });
+ });
+ describe('#relayerApiOrderBookResponseSchema', () => {
+ it('should validate valid order book responses', () => {
+ const testCases = [
+ {
+ bids: [],
+ asks: [],
+ },
+ {
+ bids: [signedOrder, signedOrder],
+ asks: [],
+ },
+ {
+ bids: [],
+ asks: [signedOrder, signedOrder],
+ },
+ {
+ bids: [signedOrder],
+ asks: [signedOrder, signedOrder],
+ },
+ ];
+ validateAgainstSchema(testCases, relayerApiOrderBookResponseSchema);
+ });
+ it('should fail for invalid order fill requests', () => {
+ const testCases = [
+ {},
+ {
+ bids: [signedOrder, signedOrder],
+ },
+ {
+ asks: [signedOrder, signedOrder],
+ },
+ {
+ bids: signedOrder,
+ asks: [signedOrder, signedOrder],
+ },
+ {
+ bids: [signedOrder],
+ asks: signedOrder,
+ },
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, relayerApiOrderBookResponseSchema, shouldFail);
+ });
+ });
+ describe('#relayerApiOrderbookChannelSubscribeSchema', () => {
+ it('should validate valid orderbook channel websocket subscribe message', () => {
+ const testCases = [
+ {
+ type: 'subscribe',
+ channel: 'orderbook',
+ payload: {
+ baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ snapshot: true,
+ limit: 100,
+ },
+ },
+ {
+ type: 'subscribe',
+ channel: 'orderbook',
+ payload: {
+ baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ },
+ },
+ ];
+ validateAgainstSchema(testCases, relayerApiOrderbookChannelSubscribeSchema);
+ });
+ it('should fail for invalid orderbook channel websocket subscribe message', () => {
+ const checksummedAddress = '0xA2b31daCf30a9C50ca473337c01d8A201ae33e32';
+ const testCases = [
+ {
+ type: 'foo',
+ channel: 'orderbook',
+ payload: {
+ baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ },
+ },
+ {
+ type: 'subscribe',
+ channel: 'bar',
+ payload: {
+ baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ },
+ },
+ {
+ type: 'subscribe',
+ channel: 'orderbook',
+ payload: {
+ baseTokenAddress: checksummedAddress,
+ quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ },
+ },
+ {
+ type: 'subscribe',
+ channel: 'orderbook',
+ payload: {
+ baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ quoteTokenAddress: checksummedAddress,
+ },
+ },
+ {
+ type: 'subscribe',
+ channel: 'orderbook',
+ payload: {
+ quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ },
+ },
+ {
+ type: 'subscribe',
+ channel: 'orderbook',
+ payload: {
+ baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ },
+ },
+ {
+ type: 'subscribe',
+ channel: 'orderbook',
+ payload: {
+ baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ snapshot: 'true',
+ limit: 100,
+ },
+ },
+ {
+ type: 'subscribe',
+ channel: 'orderbook',
+ payload: {
+ baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ quoteTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ snapshot: true,
+ limit: '100',
+ },
+ },
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, relayerApiOrderbookChannelSubscribeSchema, shouldFail);
+ });
+ });
+ describe('#relayerApiOrderbookChannelSnapshotSchema', () => {
+ it('should validate valid orderbook channel websocket snapshot message', () => {
+ const testCases = [
+ {
+ type: 'snapshot',
+ channel: 'orderbook',
+ channelId: 2,
+ payload: {
+ bids: [],
+ asks: [],
+ },
+ },
+ {
+ type: 'snapshot',
+ channel: 'orderbook',
+ channelId: 2,
+ payload: {
+ bids: [
+ signedOrder,
+ ],
+ asks: [
+ signedOrder,
+ ],
+ },
+ },
+ ];
+ validateAgainstSchema(testCases, relayerApiOrderbookChannelSnapshotSchema);
+ });
+ it('should fail for invalid orderbook channel websocket snapshot message', () => {
+ const testCases = [
+ {
+ type: 'foo',
+ channel: 'orderbook',
+ channelId: 2,
+ payload: {
+ bids: [
+ signedOrder,
+ ],
+ asks: [
+ signedOrder,
+ ],
+ },
+ },
+ {
+ type: 'snapshot',
+ channel: 'bar',
+ channelId: 2,
+ payload: {
+ bids: [
+ signedOrder,
+ ],
+ asks: [
+ signedOrder,
+ ],
+ },
+ },
+ {
+ type: 'snapshot',
+ channel: 'orderbook',
+ payload: {
+ bids: [
+ signedOrder,
+ ],
+ asks: [
+ signedOrder,
+ ],
+ },
+ },
+ {
+ type: 'snapshot',
+ channel: 'orderbook',
+ channelId: '2',
+ payload: {
+ bids: [
+ signedOrder,
+ ],
+ asks: [
+ signedOrder,
+ ],
+ },
+ },
+ {
+ type: 'snapshot',
+ channel: 'orderbook',
+ channelId: 2,
+ payload: {
+ bids: [
+ signedOrder,
+ ],
+ },
+ },
+ {
+ type: 'snapshot',
+ channel: 'orderbook',
+ channelId: 2,
+ payload: {
+ asks: [
+ signedOrder,
+ ],
+ },
+ },
+ {
+ type: 'snapshot',
+ channel: 'orderbook',
+ channelId: 2,
+ payload: {
+ bids: [
+ signedOrder,
+ ],
+ asks: [
+ {},
+ ],
+ },
+ },
+ {
+ type: 'snapshot',
+ channel: 'orderbook',
+ channelId: 2,
+ payload: {
+ bids: [
+ {},
+ ],
+ asks: [
+ signedOrder,
+ ],
+ },
+ },
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, relayerApiOrderbookChannelSnapshotSchema, shouldFail);
+ });
+ });
+ describe('#relayerApiOrderbookChannelUpdateSchema', () => {
+ it('should validate valid orderbook channel websocket update message', () => {
+ const testCases = [
+ {
+ type: 'update',
+ channel: 'orderbook',
+ channelId: 2,
+ payload: signedOrder,
+ },
+ ];
+ validateAgainstSchema(testCases, relayerApiOrderbookChannelUpdateSchema);
+ });
+ it('should fail for invalid orderbook channel websocket update message', () => {
+ const testCases = [
+ {
+ type: 'foo',
+ channel: 'orderbook',
+ payload: signedOrder,
+ },
+ {
+ type: 'update',
+ channel: 'bar',
+ payload: signedOrder,
+ },
+ {
+ type: 'update',
+ channel: 'orderbook',
+ payload: {},
+ },
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, relayerApiOrderbookChannelUpdateSchema, shouldFail);
+ });
+ });
+ });
+ });
+ describe('BigNumber serialization', () => {
+ it('should correctly serialize BigNumbers', () => {
+ const testCases = {
+ '42': '42',
+ '0': '0',
+ '1.3': '1.3',
+ '0.2': '0.2',
+ '00.00': '0',
+ '.3': '0.3',
+ };
+ forEach(testCases, (serialized: string, input: string) => {
+ expect(JSON.parse(JSON.stringify(new BigNumber(input)))).to.be.equal(serialized);
+ });
+ });
+ });
+ describe('#relayerApiErrorResponseSchema', () => {
+ it('should validate valid errorResponse', () => {
+ const testCases = [
+ {
+ code: 102,
+ reason: 'Order submission disabled',
+ },
+ {
+ code: 101,
+ reason: 'Validation failed',
+ validationErrors: [
+ {
+ field: 'maker',
+ code: 1002,
+ reason: 'Invalid address',
+ },
+ ],
+ },
+ ];
+ validateAgainstSchema(testCases, relayerApiErrorResponseSchema);
+ });
+ it('should fail for invalid error responses', () => {
+ const testCases = [
+ {},
+ {
+ code: 102,
+ },
+ {
+ code: '102',
+ reason: 'Order submission disabled',
+ },
+ {
+ reason: 'Order submission disabled',
+ },
+ {
+ code: 101,
+ reason: 'Validation failed',
+ validationErrors: [
+ {
+ field: 'maker',
+ reason: 'Invalid address',
+ },
+ ],
+ },
+ {
+ code: 101,
+ reason: 'Validation failed',
+ validationErrors: [
+ {
+ field: 'maker',
+ code: '1002',
+ reason: 'Invalid address',
+ },
+ ],
+ },
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, relayerApiErrorResponseSchema, shouldFail);
+ });
+ });
+ describe('#relayerApiFeesPayloadSchema', () => {
+ it('should validate valid fees payloads', () => {
+ const testCases = [
+ {
+ exchangeContractAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ maker: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ taker: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990',
+ makerTokenAmount: '10000000000000000000',
+ takerTokenAmount: '30000000000000000000',
+ expirationUnixTimestampSec: '42',
+ salt: '67006738228878699843088602623665307406148487219438534730168799356281242528500',
+ },
+ ];
+ validateAgainstSchema(testCases, relayerApiFeesPayloadSchema);
+ });
+ it('should fail for invalid fees payloads', () => {
+ const checksummedAddress = '0xA2b31daCf30a9C50ca473337c01d8A201ae33e32';
+ const testCases = [
+ {},
+ {
+ takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990',
+ makerTokenAmount: '10000000000000000000',
+ takerTokenAmount: '30000000000000000000',
+ },
+ {
+ taker: checksummedAddress,
+ makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990',
+ makerTokenAmount: '10000000000000000000',
+ takerTokenAmount: '30000000000000000000',
+ },
+ {
+ makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990',
+ makerTokenAmount: 10000000000000000000,
+ takerTokenAmount: 30000000000000000000,
+ },
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, relayerApiFeesPayloadSchema, shouldFail);
+ });
+ });
+ describe('#relayerApiFeesResponseSchema', () => {
+ it('should validate valid fees responses', () => {
+ const testCases = [
+ {
+ makerFee: '10000000000000000',
+ takerFee: '30000000000000000',
+ feeRecipient: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ },
+ ];
+ validateAgainstSchema(testCases, relayerApiFeesResponseSchema);
+ });
+ it('should fail for invalid fees responses', () => {
+ const checksummedAddress = '0xA2b31daCf30a9C50ca473337c01d8A201ae33e32';
+ const testCases = [
+ {},
+ {
+ makerFee: 10000000000000000,
+ takerFee: 30000000000000000,
+ },
+ {
+ feeRecipient: checksummedAddress,
+ takerToSpecify: checksummedAddress,
+ makerFee: '10000000000000000',
+ takerFee: '30000000000000000',
+ },
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, relayerApiFeesResponseSchema, shouldFail);
+ });
+ });
+ describe('#relayerApiTokenPairsResponseSchema', () => {
+ it('should validate valid tokenPairs response', () => {
+ const testCases = [
+ [],
+ [
+ {
+ tokenA: {
+ address: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ minAmount: '0',
+ maxAmount: '10000000000000000000',
+ precision: 5,
+ },
+ tokenB: {
+ address: '0xef7fff64389b814a946f3e92105513705ca6b990',
+ minAmount: '0',
+ maxAmount: '50000000000000000000',
+ precision: 5,
+ },
+ },
+ ],
+ [
+ {
+ tokenA: {
+ address: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ },
+ tokenB: {
+ address: '0xef7fff64389b814a946f3e92105513705ca6b990',
+ },
+ },
+ ],
+ ];
+ validateAgainstSchema(testCases, relayerApiTokenPairsResponseSchema);
+ });
+ it('should fail for invalid tokenPairs responses', () => {
+ const checksummedAddress = '0xA2b31daCf30a9C50ca473337c01d8A201ae33e32';
+ const testCases = [
+ [
+ {
+ tokenA: {
+ address: checksummedAddress,
+ },
+ tokenB: {
+ address: checksummedAddress,
+ },
+ },
+ ],
+ [
+ {
+ tokenA: {
+ address: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ minAmount: 0,
+ maxAmount: 10000000000000000000,
+ },
+ tokenB: {
+ address: '0xef7fff64389b814a946f3e92105513705ca6b990',
+ minAmount: 0,
+ maxAmount: 50000000000000000000,
+ },
+ },
+ ],
+ [
+ {
+ tokenA: {
+ address: '0x323b5d4c32345ced77393b3530b1eed0f346429d',
+ precision: '5',
+ },
+ tokenB: {
+ address: '0xef7fff64389b814a946f3e92105513705ca6b990',
+ precision: '5',
+ },
+ },
+ ],
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, relayerApiTokenPairsResponseSchema, shouldFail);
+ });
+ });
+ describe('#jsNumberSchema', () => {
+ it('should validate valid js number', () => {
+ const testCases = [
+ 1,
+ 42,
+ ];
+ validateAgainstSchema(testCases, jsNumber);
+ });
+ it('should fail for invalid js number', () => {
+ const testCases = [
+ NaN,
+ -1,
+ new BigNumber(1),
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, jsNumber, shouldFail);
+ });
+ });
+ describe('#txDataSchema', () => {
+ it('should validate valid txData', () => {
+ const testCases = [
+ {
+ from: NULL_ADDRESS,
+ },
+ {
+ from: NULL_ADDRESS,
+ gas: new BigNumber(42),
+ },
+ {
+ from: NULL_ADDRESS,
+ gas: 42,
+ },
+ ];
+ validateAgainstSchema(testCases, txDataSchema);
+ });
+ it('should fail for invalid txData', () => {
+ const testCases = [
+ {
+ gas: new BigNumber(42),
+ },
+ {
+ from: NULL_ADDRESS,
+ unknownProp: 'here',
+ },
+ {},
+ [],
+ new BigNumber(1),
+ ];
+ const shouldFail = true;
+ validateAgainstSchema(testCases, txDataSchema, shouldFail);
+ });
+ });
+});
diff --git a/packages/json-schemas/tsconfig.json b/packages/json-schemas/tsconfig.json
new file mode 100644
index 000000000..40c2f0c8c
--- /dev/null
+++ b/packages/json-schemas/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "es5",
+ "lib": [ "es2017", "dom"],
+ "outDir": "lib",
+ "sourceMap": true,
+ "declaration": true,
+ "noImplicitAny": true,
+ "strictNullChecks": true
+ },
+ "include": [
+ "./src/**/*",
+ "./test/**/*",
+ "../../node_modules/chai-typescript-typings/index.d.ts"
+ ]
+}
diff --git a/packages/json-schemas/tslint.json b/packages/json-schemas/tslint.json
new file mode 100644
index 000000000..a07795151
--- /dev/null
+++ b/packages/json-schemas/tslint.json
@@ -0,0 +1,5 @@
+{
+ "extends": [
+ "@0xproject/tslint-config"
+ ]
+}
diff --git a/packages/tslint-config/CHANGELOG.md b/packages/tslint-config/CHANGELOG.md
new file mode 100644
index 000000000..7a6ba41c0
--- /dev/null
+++ b/packages/tslint-config/CHANGELOG.md
@@ -0,0 +1,6 @@
+# CHANGELOG
+
+v0.1.0 - _Nov. 14, 2017_
+------------------------
+ * Re-published TsLintConfig previously published under NPM package `tslint-config-0xproject`
+ * Updated to TSLint v5.8.0, requiring several rule additions to keep our conventions aligned.
diff --git a/packages/tslint-config/README.md b/packages/tslint-config/README.md
new file mode 100644
index 000000000..38a6bce45
--- /dev/null
+++ b/packages/tslint-config/README.md
@@ -0,0 +1,10 @@
+tslint-config
+-------------
+
+Lint rules related to 0xProject for TSLint.
+
+## Install:
+
+```bash
+npm install @0xproject/tslint-config --save-dev
+```
diff --git a/packages/tslint-config/package.json b/packages/tslint-config/package.json
new file mode 100644
index 000000000..ca46d63fc
--- /dev/null
+++ b/packages/tslint-config/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@0xproject/tslint-config",
+ "version": "0.1.0",
+ "description": "Lint rules related to 0xProject for TSLint",
+ "main": "tslint.json",
+ "files": [
+ "tslint.js",
+ "README.md",
+ "LICENSE"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/0xProject/0x.js.git"
+ },
+ "keywords": [
+ "tslint",
+ "config",
+ "0xProject",
+ "typescript",
+ "ts"
+ ],
+ "author": {
+ "name": "Fabio Berger",
+ "email": "fabio@0xproject.com"
+ },
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/0xProject/0x.js/issues"
+ },
+ "homepage": "https://github.com/0xProject/0x.js/packages/tslint-config/README.md",
+ "devDependencies": {
+ "tslint": "5.8.0",
+ "typescript": "2.6.1"
+ },
+ "dependencies": {
+ "tslint-react": "^3.2.0"
+ }
+}
diff --git a/packages/tslint-config/tslint.json b/packages/tslint-config/tslint.json
new file mode 100644
index 000000000..8b839f25a
--- /dev/null
+++ b/packages/tslint-config/tslint.json
@@ -0,0 +1,53 @@
+{
+ "extends": [
+ "tslint:latest",
+ "tslint-react"
+ ],
+ "rules": {
+ "arrow-parens": [true, "ban-single-arg-parens"],
+ "ordered-imports": false,
+ "quotemark": [true, "single", "avoid-escape", "jsx-double"],
+ "callable-types": true,
+ "interface-name": false,
+ "interface-over-type-literal": true,
+ "object-literal-sort-keys": false,
+ "max-classes-per-file": false,
+ "max-line-length": [true, 120],
+ "member-ordering": [true,
+ "public-before-private",
+ "static-before-instance",
+ "variables-before-functions"
+ ],
+ "no-angle-bracket-type-assertion": true,
+ "no-default-export": true,
+ "no-empty-interface": false,
+ "no-string-throw": true,
+ "no-submodule-imports": false,
+ "no-implicit-dependencies": [true, "dev"],
+ "prefer-const": true,
+ "variable-name": [true,
+ "ban-keywords",
+ "allow-pascal-case"
+ ],
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-separator",
+ "check-rest-spread",
+ "check-type",
+ "check-typecast",
+ "check-preblock"
+ ],
+ "jsx-alignment": true,
+ "jsx-boolean-value": true,
+ "jsx-curly-spacing": [true, "never"],
+ "jsx-no-lambda": true,
+ "jsx-no-multiline-js": false,
+ "jsx-no-string-ref": true,
+ "jsx-self-close": true,
+ "jsx-wrap-multiline": false,
+ "jsx-no-bind": false
+ }
+}