diff options
author | Jacob Evans <dekz@dekz.net> | 2017-11-16 07:25:09 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-11-16 07:25:09 +0800 |
commit | d7e5c6f9b5bbe4e9216d3a91ade71bfab7785cdd (patch) | |
tree | 4e1d4197d52203b648493301fe74e577e1d99e3e /packages | |
parent | 85c3b2996d7097f8d390f4b8f6199b983928defb (diff) | |
parent | 557faba31b149ab52f6cc4b1930f457205b00229 (diff) | |
download | dexon-sol-tools-d7e5c6f9b5bbe4e9216d3a91ade71bfab7785cdd.tar dexon-sol-tools-d7e5c6f9b5bbe4e9216d3a91ade71bfab7785cdd.tar.gz dexon-sol-tools-d7e5c6f9b5bbe4e9216d3a91ade71bfab7785cdd.tar.bz2 dexon-sol-tools-d7e5c6f9b5bbe4e9216d3a91ade71bfab7785cdd.tar.lz dexon-sol-tools-d7e5c6f9b5bbe4e9216d3a91ade71bfab7785cdd.tar.xz dexon-sol-tools-d7e5c6f9b5bbe4e9216d3a91ade71bfab7785cdd.tar.zst dexon-sol-tools-d7e5c6f9b5bbe4e9216d3a91ade71bfab7785cdd.zip |
Merge branch 'development' into syncSubscribe
Diffstat (limited to 'packages')
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}"eTokenAddress=${request.quoteTokenAddress}`; + it('gets order book', async () => { + fetchMock.get(url, orderbookJSON); + const orderbook = await relayerClient.getOrderbookAsync(request); + expect(orderbook).to.be.deep.equal(orderbookResponse); + }); + it('throws an error for invalid JSON response', async () => { + fetchMock.get(url, {test: 'dummy'}); + expect(relayerClient.getOrderbookAsync(request)).to.be.rejected(); + }); + }); + describe('#getFeesAsync', () => { + const request = { + exchangeContractAddress: '0x12459c951127e0c374ff9105dda097662a027093', + maker: '0x9e56625509c2f60af937f23b7b532600390e8c8b', + taker: '0xa2b31dacf30a9c50ca473337c01d8a201ae33e32', + makerTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', + takerTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990', + makerTokenAmount: new BigNumber('10000000000000000000'), + takerTokenAmount: new BigNumber('30000000000000000000'), + salt: new BigNumber('256'), + expirationUnixTimestampSec: new BigNumber('42'), + }; + const url = `${relayUrl}/v0/fees`; + it('gets fees', async () => { + fetchMock.post(url, feesResponseJSON); + const fees = await relayerClient.getFeesAsync(request); + expect(fees).to.be.deep.equal(feesResponse); + }); + it('throws an error for invalid JSON response', async () => { + fetchMock.post(url, {test: 'dummy'}); + expect(relayerClient.getFeesAsync(request)).to.be.rejected(); + }); + }); +}); diff --git a/packages/connect/test/orderbook_channel_message_parsers_test.ts b/packages/connect/test/orderbook_channel_message_parsers_test.ts new file mode 100644 index 000000000..8efc5e500 --- /dev/null +++ b/packages/connect/test/orderbook_channel_message_parsers_test.ts @@ -0,0 +1,66 @@ +import 'mocha'; +import * as dirtyChai from 'dirty-chai'; +import * as chai from 'chai'; +import {orderbookChannelMessageParsers} from '../src/utils/orderbook_channel_message_parsers'; +import { + snapshotOrderbookChannelMessage, + malformedSnapshotOrderbookChannelMessage, +} from './fixtures/standard_relayer_api/snapshot_orderbook_channel_message'; +import { + updateOrderbookChannelMessage, + malformedUpdateOrderbookChannelMessage, +} from './fixtures/standard_relayer_api/update_orderbook_channel_message'; +import {unknownOrderbookChannelMessage} from './fixtures/standard_relayer_api/unknown_orderbook_channel_message'; +import {orderbookResponse} from './fixtures/standard_relayer_api/orderbook'; +// tslint:disable-next-line:max-line-length +import {orderResponse} from './fixtures/standard_relayer_api/order/0xabc67323774bdbd24d94f977fa9ac94a50f016026fd13f42990861238897721f'; + +chai.config.includeStack = true; +chai.use(dirtyChai); +const expect = chai.expect; + +describe('orderbookChannelMessageParsers', () => { + describe('#parser', () => { + it('parses snapshot messages', () => { + const snapshotMessage = orderbookChannelMessageParsers.parser(snapshotOrderbookChannelMessage); + expect(snapshotMessage.type).to.be.equal('snapshot'); + expect(snapshotMessage.payload).to.be.deep.equal(orderbookResponse); + }); + it('parses update messages', () => { + const updateMessage = orderbookChannelMessageParsers.parser(updateOrderbookChannelMessage); + expect(updateMessage.type).to.be.equal('update'); + expect(updateMessage.payload).to.be.deep.equal(orderResponse); + }); + it('returns unknown message for messages with unsupported types', () => { + const unknownMessage = orderbookChannelMessageParsers.parser(unknownOrderbookChannelMessage); + expect(unknownMessage.type).to.be.equal('unknown'); + expect(unknownMessage.payload).to.be.undefined(); + }); + it('throws when message does not include a type', () => { + const typelessMessage = `{ + "channel": "orderbook", + "channelId": 1, + "payload": {} + }`; + const badCall = () => orderbookChannelMessageParsers.parser(typelessMessage); + expect(badCall).throws(`Message is missing a type parameter: ${typelessMessage}`); + }); + it('throws when snapshot message has malformed payload', () => { + const badCall = () => + orderbookChannelMessageParsers.parser(malformedSnapshotOrderbookChannelMessage); + // tslint:disable-next-line:max-line-length + const errMsg = 'Validation errors: instance.payload requires property "bids", instance.payload requires property "asks"'; + expect(badCall).throws(errMsg); + }); + it('throws when update message has malformed payload', () => { + const badCall = () => + orderbookChannelMessageParsers.parser(malformedUpdateOrderbookChannelMessage); + expect(badCall).throws(/^Expected message to conform to schema/); + }); + it('throws when input message is not valid JSON', () => { + const nonJsonString = 'h93b{sdfs9fsd f'; + const badCall = () => orderbookChannelMessageParsers.parser(nonJsonString); + expect(badCall).throws('Unexpected token h in JSON at position 0'); + }); + }); +}); diff --git a/packages/connect/test/ws_orderbook_channel_test.ts b/packages/connect/test/ws_orderbook_channel_test.ts new file mode 100644 index 000000000..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 + } +} |