diff options
author | Leonid <logvinov.leon@gmail.com> | 2017-11-21 04:46:53 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-11-21 04:46:53 +0800 |
commit | d39c0bee39a7bc9cbab6810e8b1b42dfe2e6cf09 (patch) | |
tree | a02081e61b11bfb6bb643fcb4e8981541bb8d2bd | |
parent | 5788b90c522b63b53f5e8480f2b357ee24bb08a3 (diff) | |
parent | 037f466e1f80f635b48f3235258402e2ce75fb7b (diff) | |
download | dexon-0x-contracts-d39c0bee39a7bc9cbab6810e8b1b42dfe2e6cf09.tar.gz dexon-0x-contracts-d39c0bee39a7bc9cbab6810e8b1b42dfe2e6cf09.tar.zst dexon-0x-contracts-d39c0bee39a7bc9cbab6810e8b1b42dfe2e6cf09.zip |
Merge branch 'development' into feature/orderExpired
100 files changed, 3342 insertions, 210 deletions
@@ -6,33 +6,22 @@ This repository contains all the 0x developer tools written in TypeScript. Our hope is that these tools make it easy to build Relayers and other DApps that use the 0x protocol. +[website-url]: https://0xproject.com/ +[whitepaper-url]: https://0xproject.com/pdfs/0x_white_paper.pdf + [![CircleCI](https://circleci.com/gh/0xProject/0x.js.svg?style=svg&circle-token=61bf7cd8c9b4e11b132089dfcffdd1be277d1e0c)](https://circleci.com/gh/0xProject/0x.js) -[![npm version](https://badge.fury.io/js/0x.js.svg)](https://badge.fury.io/js/0x.js) [![Coverage Status](https://coveralls.io/repos/github/0xProject/0x.js/badge.svg?branch=master&t=fp0cXD)](https://coveralls.io/github/0xProject/0x.js?branch=master) -[![Slack Status](http://slack.0xProject.com/badge.svg)](http://slack.0xProject.com) +[![Discord](https://img.shields.io/badge/chat-rocket.chat-yellow.svg?style=flat +)](https://chat.0xproject.com) [![Join the chat at https://gitter.im/0xProject/Lobby](https://badges.gitter.im/0xProject/Lobby.svg)](https://gitter.im/0xProject/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Greenkeeper badge](https://badges.greenkeeper.io/0xProject/0x.js.svg?token=7c22e5c72acf39d3ead8d29c5d9bb38f9096df3e643024dcedd53ab732847be1&ts=1496426342666)](https://greenkeeper.io/) -Instructions ------------- - -Make sure you have `yarn@1.x` installed locally. - -### Creating a new sub-package - -1. Make sure the `name` field in the sub-package's `package.json` starts with `@0xproject/` and has a unique name (e.g `@0xproject/assert`). - -2. Run `yarn install` to install all it's dependencies. - -### How to add a sub-package as a dependency to another sub-package: - -1. Add the sub-packages name (declared in it's `package.json`) to your sub-packages `package.json` under `dependencies` or `devDependencies`. - -2. Run `yarn install` from anywhere in the mono repo. - -3. Import the sub-package as: +### Core Packages -``` -import {myPkg} from '@0xproject/myPkg'; -``` +| Package | Version | Description | +|--------|-------|------------| +| [`0x.js`](/packages/0x.js) | [![npm](https://img.shields.io/npm/v/0x.js.svg?maxAge=2592000)](https://www.npmjs.com/package/0x.js) | A Javascript library for interacting with the 0x protocol | +| [`@0xproject/assert`](/packages/assert) | [![npm](https://img.shields.io/npm/v/@0xproject/assert.svg?maxAge=2592000)](https://www.npmjs.com/package/@0xproject/assert) | Standard type and schema assertions | +| [`@0xproject/json-schemas`](/packages/json-schemas) | [![npm](https://img.shields.io/npm/v/@0xproject/json-schemas.svg?maxAge=2592000)](https://www.npmjs.com/package/@0xproject/json-schemas) | 0x-related json schemas | +| [`@0xproject/tslint-config`](/packages/tslint-config) | [![npm](https://img.shields.io/npm/v/@0xproject/tslint-config.svg?maxAge=2592000)](https://www.npmjs.com/package/@0xproject/tslint-config) | Custom 0x project TSLint rules | diff --git a/package.json b/package.json index 49c97eff6..265e0459e 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,17 @@ ], "scripts": { "testrpc": "testrpc -p 8545 --networkId 50 -m \"${npm_package_config_mnemonic}\"", - "lerna:run": "lerna run" + "lerna:run": "lerna run", + "lerna:publish": "lerna run clean; lerna run build; lerna publish" }, "config": { "mnemonic": "concert load couple harbor equip island argue ramp clarify fence smart topic" }, "devDependencies": { - "lerna": "^2.5.1" + "lerna": "^2.5.1", + "async-child-process": "^1.1.1", + "semver-sort": "^0.0.4", + "publish-release": "0xproject/publish-release", + "es6-promisify": "^5.0.0" } } diff --git a/packages/0x.js/CHANGELOG.md b/packages/0x.js/CHANGELOG.md index a9362151c..6245308c3 100644 --- a/packages/0x.js/CHANGELOG.md +++ b/packages/0x.js/CHANGELOG.md @@ -1,13 +1,20 @@ # CHANGELOG -v0.24.0 - _November 13, 2017_ +vx.x.x +------------------------ + * Remove support for Async callback types when used in Subscribe functions + * In OrderWatcher subscribe to ZRX Token Transfer and Approval events when maker token is different (#225) + +v0.25.1 - _November 13, 2017_ ------------------------ * Standardise on Cancelled over Canceled + * Add missing `DecodedLogEvent` type to exported types + * Normalized the transactionReceipt status to be `null|0|1`, 1 meaning transaction execution successful, 0 unsuccessful and `null` if it is a pre-byzantinium transaction. v0.23.0 - _November 12, 2017_ ------------------------ * Fixed unhandled promise rejection error in subscribe methods (#209) - * Subscribe callbacks now receive an error object as their first argument + * Subscribe callbacks now receive an error object as their first argument v0.22.6 - _November 10, 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 539a78f37..4fe11a8dc 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,11 +13,9 @@ "types": "lib/src/index.d.ts", "scripts": { "prebuild": "npm run clean", - "build": "run-p build:umd:prod build:commonjs", - "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", - "upload_docs_json": "aws s3 cp docs/index.json s3://0xjs-docs-jsons/$(git describe --tags).json --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type aplication/json", + "build": "run-p build:umd:prod build:commonjs; exit 0;", + "docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --json $JSON_FILE_PATH $PROJECT_DIR", + "upload_docs_json": "aws s3 cp docs/index.json $S3_URL --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type aplication/json", "lint": "tslint src/**/*.ts test/**/*.ts", "test:circleci": "run-s test:coverage report_test_coverage; if [ $CIRCLE_BRANCH = \"development\" ]; then yarn test:umd; fi", "test": "run-s clean test:commonjs", @@ -25,9 +23,6 @@ "test:coverage": "nyc npm run test --all", "report_test_coverage": "nyc report --reporter=text-lcov | coveralls", "update_contracts": "for i in ${npm_package_config_artifacts}; do copyfiles -u 4 ../contracts/build/contracts/$i.json ../0x.js/src/artifacts; done;", - "docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --json docs/index.json .", - "docs:generate": "typedoc --out docs .", - "docs:open": "opn docs/index.html", "clean": "shx rm -rf _bundles lib test_temp", "build:umd:dev": "webpack", "build:umd:prod": "NODE_ENV=production webpack", @@ -49,6 +44,7 @@ "node": ">=6.0.0" }, "devDependencies": { + "@0xproject/tslint-config": "^0.1.0", "@types/bintrees": "^1.0.2", "@types/jsonschema": "^1.1.1", "@types/lodash": "^4.14.64", @@ -77,8 +73,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", @@ -88,10 +83,11 @@ "webpack": "^3.1.0" }, "dependencies": { - "0x-json-schemas": "^0.6.1", - "@0xproject/assert": "0.0.3", + "@0xproject/assert": "^0.0.4", + "@0xproject/json-schemas": "^0.6.7", "bignumber.js": "~4.1.0", "bintrees": "^1.0.2", + "bn.js": "4.11.8", "compare-versions": "^3.0.1", "es6-promisify": "^5.0.0", "ethereumjs-abi": "^0.6.4", @@ -100,7 +96,6 @@ "find-versions": "^2.0.0", "js-sha3": "^0.6.1", "lodash": "^4.17.4", - "publish-release": "^1.3.3", "uuid": "^3.1.0", "web3": "^0.20.0" } diff --git a/packages/0x.js/scripts/postpublish.js b/packages/0x.js/scripts/postpublish.js new file mode 100644 index 000000000..ffc68afb4 --- /dev/null +++ b/packages/0x.js/scripts/postpublish.js @@ -0,0 +1,43 @@ +const execAsync = require('async-child-process').execAsync; +const postpublish_utils = require('../../../scripts/postpublish_utils'); +const packageJSON = require('../package.json'); + +const cwd = __dirname + '/..'; +const subPackageName = packageJSON.name; +const S3BucketPath = 's3://0xjs-docs-jsons/'; + +let tag; +let version; +postpublish_utils.getLatestTagAndVersionAsync(subPackageName) + .then(function(result) { + tag = result.tag; + version = result.version; + const releaseName = postpublish_utils.getReleaseName(subPackageName, version); + const assets = [ + __dirname + '/../_bundles/index.js', + __dirname + '/../_bundles/index.min.js', + ]; + return postpublish_utils.publishReleaseNotes(tag, releaseName, assets); + }) + .then(function(release) { + console.log('POSTPUBLISH: Release successful, generating docs...'); + return execAsync( + 'JSON_FILE_PATH=' + __dirname + '/../docs/index.json PROJECT_DIR=' + __dirname + '/.. yarn docs:json', + { + cwd, + } + ); + }) + .then(function(result) { + if (result.stderr !== '') { + throw new Error(result.stderr); + } + const fileName = 'v' + version + '.json'; + console.log('POSTPUBLISH: Doc generation successful, uploading docs... as ', fileName); + const s3Url = S3BucketPath + fileName; + return execAsync('S3_URL=' + s3Url + ' yarn upload_docs_json', { + cwd, + }); + }).catch (function(err) { + throw err; + }); diff --git a/packages/0x.js/scripts/test_umd.sh b/packages/0x.js/scripts/test_umd.sh index d200c76d0..e3eba088a 100755 --- a/packages/0x.js/scripts/test_umd.sh +++ b/packages/0x.js/scripts/test_umd.sh @@ -3,5 +3,4 @@ # UMD tests should only be run after building the commonjs because they reuse some of the commonjs build artifacts run-s substitute_umd_bundle run_mocha return_code=$? -npm run clean exit $return_code 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/index.ts b/packages/0x.js/src/index.ts index 1b3e893ba..e529e2858 100644 --- a/packages/0x.js/src/index.ts +++ b/packages/0x.js/src/index.ts @@ -6,8 +6,6 @@ export { ECSignature, ZeroExError, EventCallback, - EventCallbackAsync, - EventCallbackSync, ExchangeContractErrs, ContractEvent, Token, diff --git a/packages/0x.js/src/order_watcher/event_watcher.ts b/packages/0x.js/src/order_watcher/event_watcher.ts index da824bd09..ecbab0cd5 100644 --- a/packages/0x.js/src/order_watcher/event_watcher.ts +++ b/packages/0x.js/src/order_watcher/event_watcher.ts @@ -81,7 +81,7 @@ export class EventWatcher { ...log, }; if (!_.isUndefined(this._intervalIdIfExists)) { - await callback(logEvent); + callback(logEvent); } } } 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 975679f57..579fa388a 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'; @@ -53,7 +53,7 @@ interface OrderByOrderHash { export class OrderStateWatcher { private _orderByOrderHash: OrderByOrderHash = {}; private _dependentOrderHashes: DependentOrderHashes = {}; - private _callbackIfExistsAsync?: OnOrderStateChangeCallback; + private _callbackIfExists?: OnOrderStateChangeCallback; private _eventWatcher: EventWatcher; private _web3Wrapper: Web3Wrapper; private _abiDecoder: AbiDecoder; @@ -89,12 +89,12 @@ export class OrderStateWatcher { * signature is verified. * @param signedOrder The order you wish to start watching. */ - public addOrder(signedOrder: SignedOrder): void { + public async addOrderAsync(signedOrder: SignedOrder): Promise<void> { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); const orderHash = ZeroEx.getOrderHashHex(signedOrder); assert.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker); this._orderByOrderHash[orderHash] = signedOrder; - this.addToDependentOrderHashes(signedOrder, orderHash); + await this.addToDependentOrderHashesAsync(signedOrder, orderHash); const expirationUnixTimestampMs = signedOrder.expirationUnixTimestampSec.times(1000); this._expirationWatcher.addOrder(orderHash, expirationUnixTimestampMs); } @@ -102,13 +102,16 @@ export class OrderStateWatcher { * Removes an order from the orderStateWatcher * @param orderHash The orderHash of the order you wish to stop watching. */ - public removeOrder(orderHash: string): void { + public async removeOrderAsync(orderHash: string): Promise<void> { assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); const signedOrder = this._orderByOrderHash[orderHash]; if (_.isUndefined(signedOrder)) { return; // noop } delete this._orderByOrderHash[orderHash]; + const exchange = (this._orderFilledCancelledLazyStore as any).exchange as ExchangeWrapper; + const zrxTokenAddress = await exchange.getZRXTokenAddressAsync(); + this.removeFromDependentOrderHashes(signedOrder.maker, zrxTokenAddress, orderHash); this.removeFromDependentOrderHashes(signedOrder.maker, signedOrder.makerTokenAddress, orderHash); this._expirationWatcher.removeOrder(orderHash); } @@ -120,10 +123,10 @@ export class OrderStateWatcher { */ public subscribe(callback: OnOrderStateChangeCallback): void { assert.isFunction('callback', callback); - if (!_.isUndefined(this._callbackIfExistsAsync)) { + if (!_.isUndefined(this._callbackIfExists)) { throw new Error(ZeroExError.SubscriptionAlreadyPresent); } - this._callbackIfExistsAsync = callback; + this._callbackIfExists = callback; this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this)); this._expirationWatcher.subscribe(this._onOrderExpired.bind(this)); } @@ -131,12 +134,12 @@ export class OrderStateWatcher { * Ends an orderStateWatcher subscription. */ public unsubscribe(): void { - if (_.isUndefined(this._callbackIfExistsAsync)) { + if (_.isUndefined(this._callbackIfExists)) { throw new Error(ZeroExError.SubscriptionNotFound); } this._balanceAndProxyAllowanceLazyStore.deleteAll(); this._orderFilledCancelledLazyStore.deleteAll(); - delete this._callbackIfExistsAsync; + delete this._callbackIfExists; this._eventWatcher.unsubscribe(); this._expirationWatcher.unsubscribe(); } @@ -232,13 +235,13 @@ export class OrderStateWatcher { // Most of these calls will never reach the network because the data is fetched from stores // and only updated when cache is invalidated const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder); - if (_.isUndefined(this._callbackIfExistsAsync)) { + if (_.isUndefined(this._callbackIfExists)) { break; // Unsubscribe was called } - await this._callbackIfExistsAsync(orderState); + this._callbackIfExists(orderState); } } - private addToDependentOrderHashes(signedOrder: SignedOrder, orderHash: string) { + private async addToDependentOrderHashesAsync(signedOrder: SignedOrder, orderHash: string): Promise<void> { if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker])) { this._dependentOrderHashes[signedOrder.maker] = {}; } @@ -246,11 +249,17 @@ export class OrderStateWatcher { this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress] = new Set(); } this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress].add(orderHash); + const exchange = (this._orderFilledCancelledLazyStore as any).exchange as ExchangeWrapper; + const zrxTokenAddress = await exchange.getZRXTokenAddressAsync(); + if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker][zrxTokenAddress])) { + this._dependentOrderHashes[signedOrder.maker][zrxTokenAddress] = new Set(); + } + this._dependentOrderHashes[signedOrder.maker][zrxTokenAddress].add(orderHash); } - private removeFromDependentOrderHashes(makerAddress: string, makerTokenAddress: string, orderHash: string) { - this._dependentOrderHashes[makerAddress][makerTokenAddress].delete(orderHash); - if (this._dependentOrderHashes[makerAddress][makerTokenAddress].size === 0) { - delete this._dependentOrderHashes[makerAddress][makerTokenAddress]; + private removeFromDependentOrderHashes(makerAddress: string, tokenAddress: string, orderHash: string) { + this._dependentOrderHashes[makerAddress][tokenAddress].delete(orderHash); + if (this._dependentOrderHashes[makerAddress][tokenAddress].size === 0) { + delete this._dependentOrderHashes[makerAddress][tokenAddress]; } if (_.isEmpty(this._dependentOrderHashes[makerAddress])) { delete this._dependentOrderHashes[makerAddress]; diff --git a/packages/0x.js/src/types.ts b/packages/0x.js/src/types.ts index 13a46659d..39c60695e 100644 --- a/packages/0x.js/src/types.ts +++ b/packages/0x.js/src/types.ts @@ -42,13 +42,8 @@ export type OrderValues = [BigNumber, BigNumber, BigNumber, export type LogEvent = Web3.LogEntryEvent; export type DecodedLogEvent<ArgsType> = Web3.DecodedLogEntryEvent<ArgsType>; -export type EventCallbackAsync<ArgsType> = (err: null|Error, log?: DecodedLogEvent<ArgsType>) => Promise<void>; -export type EventCallbackSync<ArgsType> = (err: null|Error, log?: DecodedLogEvent<ArgsType>) => void; -export type EventCallback<ArgsType> = EventCallbackSync<ArgsType>|EventCallbackAsync<ArgsType>; - -export type EventWatcherCallbackSync = (log: LogEvent) => void; -export type EventWatcherCallbackAsync = (log: LogEvent) => Promise<void>; -export type EventWatcherCallback = EventWatcherCallbackSync|EventWatcherCallbackAsync; +export type EventCallback<ArgsType> = (err: null|Error, log?: DecodedLogEvent<ArgsType>) => void; +export type EventWatcherCallback = (log: LogEvent) => void; export interface ExchangeContract extends Web3.ContractInstance { isValidSignature: { @@ -496,6 +491,7 @@ export interface OrderRelevantState { filledTakerTokenAmount: BigNumber; cancelledTakerTokenAmount: BigNumber; remainingFillableMakerTokenAmount: BigNumber; + remainingFillableTakerTokenAmount: BigNumber; } export interface OrderStateValid { @@ -512,9 +508,7 @@ export interface OrderStateInvalid { export type OrderState = OrderStateValid|OrderStateInvalid; -export type OnOrderStateChangeCallbackSync = (orderState: OrderState) => void; -export type OnOrderStateChangeCallbackAsync = (orderState: OrderState) => Promise<void>; -export type OnOrderStateChangeCallback = OnOrderStateChangeCallbackAsync|OnOrderStateChangeCallbackSync; +export type OnOrderStateChangeCallback = (orderState: OrderState) => void; export interface TransactionReceipt { blockHash: string; 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..00b290252 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,19 +61,25 @@ 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( makerToken.address, takerToken.address, maker, taker, fillableAmount, ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.addOrder(signedOrder); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); expect((zeroEx.orderStateWatcher as any)._orderByOrderHash).to.include({ [orderHash]: signedOrder, }); let dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes; expect(dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress]).to.have.keys(orderHash); - zeroEx.orderStateWatcher.removeOrder(orderHash); + await zeroEx.orderStateWatcher.removeOrderAsync(orderHash); expect((zeroEx.orderStateWatcher as any)._orderByOrderHash).to.not.include({ [orderHash]: signedOrder, }); @@ -86,7 +92,7 @@ describe('OrderStateWatcher', () => { ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); const nonExistentOrderHash = `0x${orderHash.substr(2).split('').reverse().join('')}`; - zeroEx.orderStateWatcher.removeOrder(nonExistentOrderHash); + await zeroEx.orderStateWatcher.removeOrderAsync(nonExistentOrderHash); }); }); describe('#subscribe', async () => { @@ -103,7 +109,7 @@ describe('OrderStateWatcher', () => { afterEach(async () => { zeroEx.orderStateWatcher.unsubscribe(); const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.removeOrder(orderHash); + await zeroEx.orderStateWatcher.removeOrderAsync(orderHash); }); it('should emit orderStateInvalid when maker allowance set to 0 for watched order', (done: DoneCallback) => { (async () => { @@ -111,7 +117,7 @@ describe('OrderStateWatcher', () => { makerToken.address, takerToken.address, maker, taker, fillableAmount, ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.addOrder(signedOrder); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); const callback = reportCallbackErrors(done)((orderState: OrderState) => { expect(orderState.isValid).to.be.false(); const invalidOrderState = orderState as OrderStateInvalid; @@ -129,7 +135,7 @@ describe('OrderStateWatcher', () => { makerToken.address, takerToken.address, maker, taker, fillableAmount, ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.addOrder(signedOrder); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); const callback = reportCallbackErrors(done)((orderState: OrderState) => { throw new Error('OrderState callback fired for irrelevant order'); }); @@ -150,7 +156,7 @@ describe('OrderStateWatcher', () => { makerToken.address, takerToken.address, maker, taker, fillableAmount, ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.addOrder(signedOrder); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); const callback = reportCallbackErrors(done)((orderState: OrderState) => { expect(orderState.isValid).to.be.false(); const invalidOrderState = orderState as OrderStateInvalid; @@ -170,7 +176,7 @@ describe('OrderStateWatcher', () => { makerToken.address, takerToken.address, maker, taker, fillableAmount, ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.addOrder(signedOrder); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); let eventCount = 0; const callback = reportCallbackErrors(done)((orderState: OrderState) => { @@ -202,7 +208,7 @@ describe('OrderStateWatcher', () => { const fillAmountInBaseUnits = new BigNumber(2); const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.addOrder(signedOrder); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); let eventCount = 0; const callback = reportCallbackErrors(done)((orderState: OrderState) => { @@ -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,108 @@ 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, - ); + it('should trigger the callback when orders backing ZRX allowance changes', (done: DoneCallback) => { + (async () => { + const makerFee = ZeroEx.toBaseUnitAmount(new BigNumber(2), 18); + const takerFee = ZeroEx.toBaseUnitAmount(new BigNumber(0), 18); + signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync( + makerToken.address, takerToken.address, makerFee, takerFee, maker, taker, fillableAmount, + taker); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); + const callback = reportCallbackErrors(done)((orderState: OrderState) => { + done(); + }); + zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.token.setProxyAllowanceAsync(zrxTokenAddress, maker, new BigNumber(0)); + })().catch(done); + }); + 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); + await zeroEx.orderStateWatcher.addOrderAsync(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); + await zeroEx.orderStateWatcher.addOrderAsync(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); + await zeroEx.orderStateWatcher.addOrderAsync(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 () => { @@ -312,7 +344,7 @@ describe('OrderStateWatcher', () => { makerToken.address, takerToken.address, maker, taker, fillableAmount, ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.addOrder(signedOrder); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); const callback = reportCallbackErrors(done)((orderState: OrderState) => { expect(orderState.isValid).to.be.false(); @@ -327,6 +359,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); + await zeroEx.orderStateWatcher.addOrderAsync(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( @@ -338,7 +392,7 @@ describe('OrderStateWatcher', () => { const cancelAmountInBaseUnits = new BigNumber(2); const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.addOrder(signedOrder); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); const callback = reportCallbackErrors(done)((orderState: OrderState) => { expect(orderState.isValid).to.be.true(); 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/scripts/postpublish.js b/packages/assert/scripts/postpublish.js new file mode 100644 index 000000000..7fa452b08 --- /dev/null +++ b/packages/assert/scripts/postpublish.js @@ -0,0 +1,14 @@ +const postpublish_utils = require('../../../scripts/postpublish_utils'); +const packageJSON = require('../package.json'); + +const subPackageName = packageJSON.name; + +postpublish_utils.getLatestTagAndVersionAsync(subPackageName) + .then(function(result) { + const releaseName = postpublish_utils.getReleaseName(subPackageName, result.version); + const assets = []; + return postpublish_utils.publishReleaseNotes(result.tag, releaseName, assets); + }) + .catch (function(err) { + throw err; + }); 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/scripts/postpublish.js b/packages/connect/scripts/postpublish.js new file mode 100644 index 000000000..7fa452b08 --- /dev/null +++ b/packages/connect/scripts/postpublish.js @@ -0,0 +1,14 @@ +const postpublish_utils = require('../../../scripts/postpublish_utils'); +const packageJSON = require('../package.json'); + +const subPackageName = packageJSON.name; + +postpublish_utils.getLatestTagAndVersionAsync(subPackageName) + .then(function(result) { + const releaseName = postpublish_utils.getReleaseName(subPackageName, result.version); + const assets = []; + return postpublish_utils.publishReleaseNotes(result.tag, releaseName, assets); + }) + .catch (function(err) { + throw err; + }); 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/scripts/postpublish.js b/packages/json-schemas/scripts/postpublish.js new file mode 100644 index 000000000..7fa452b08 --- /dev/null +++ b/packages/json-schemas/scripts/postpublish.js @@ -0,0 +1,14 @@ +const postpublish_utils = require('../../../scripts/postpublish_utils'); +const packageJSON = require('../package.json'); + +const subPackageName = packageJSON.name; + +postpublish_utils.getLatestTagAndVersionAsync(subPackageName) + .then(function(result) { + const releaseName = postpublish_utils.getReleaseName(subPackageName, result.version); + const assets = []; + return postpublish_utils.publishReleaseNotes(result.tag, releaseName, assets); + }) + .catch (function(err) { + throw err; + }); 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/scripts/postpublish.js b/packages/tslint-config/scripts/postpublish.js new file mode 100644 index 000000000..7fa452b08 --- /dev/null +++ b/packages/tslint-config/scripts/postpublish.js @@ -0,0 +1,14 @@ +const postpublish_utils = require('../../../scripts/postpublish_utils'); +const packageJSON = require('../package.json'); + +const subPackageName = packageJSON.name; + +postpublish_utils.getLatestTagAndVersionAsync(subPackageName) + .then(function(result) { + const releaseName = postpublish_utils.getReleaseName(subPackageName, result.version); + const assets = []; + return postpublish_utils.publishReleaseNotes(result.tag, releaseName, assets); + }) + .catch (function(err) { + throw err; + }); 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 + } +} diff --git a/scripts/postpublish_utils.js b/scripts/postpublish_utils.js new file mode 100644 index 000000000..3fb079bad --- /dev/null +++ b/scripts/postpublish_utils.js @@ -0,0 +1,51 @@ +const execAsync = require('async-child-process').execAsync; +const semverSort = require('semver-sort'); +const promisify = require('es6-promisify'); +const publishRelease = require('publish-release'); + +const publishReleaseAsync = promisify(publishRelease); +const githubPersonalAccessToken = process.env.GITHUB_PERSONAL_ACCESS_TOKEN_0X_JS; + +module.exports = { + getLatestTagAndVersionAsync: function(subPackageName) { + const subPackagePrefix = subPackageName + '@'; + const gitTagsCommand = 'git tags -l "' + subPackagePrefix + '*"'; + return execAsync(gitTagsCommand) + .then(function(result) { + if (result.stderr !== '') { + throw new Error(result.stderr); + } + const tags = result.stdout.trim().split('\n'); + const versions = tags.map(function(tag) { + return tag.slice(subPackagePrefix.length); + }); + const sortedVersions = semverSort.desc(versions); + const latestVersion = sortedVersions[0]; + const latestTag = subPackagePrefix + latestVersion; + return { + tag: latestTag, + version: latestVersion + }; + }); + }, + publishReleaseNotes: function(tag, releaseName, assets) { + console.log('POSTPUBLISH: Releasing ', releaseName, '...'); + return publishReleaseAsync({ + token: githubPersonalAccessToken, + owner: '0xProject', + repo: '0x.js', + tag: tag, + name: releaseName, + notes: 'TODO', + draft: false, + prerelease: false, + reuseRelease: true, + reuseDraftOnly: false, + assets: assets, + }); + }, + getReleaseName(subPackageName, version) { + const releaseName = subPackageName + ' v' + version; + return releaseName; + }, +}; @@ -2,20 +2,17 @@ # yarn lockfile v1 -"0x-json-schemas@^0.6.1", "0x-json-schemas@^0.6.5": - version "0.6.6" - resolved "https://registry.yarnpkg.com/0x-json-schemas/-/0x-json-schemas-0.6.6.tgz#3852e639245474a14daa2f8c454ba83ca5df8a9c" - dependencies: - jsonschema "^1.2.0" - lodash.values "^4.3.0" +"@types/fetch-mock@^5.12.1": + version "5.12.2" + resolved "https://registry.yarnpkg.com/@types/fetch-mock/-/fetch-mock-5.12.2.tgz#8c96517ff74303031c65c5da2d99858e34c844d2" "@types/bintrees@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/bintrees/-/bintrees-1.0.2.tgz#0dfdce4eeebdf90427bd35b0e79dc248b3d157a6" "@types/fs-extra@^4.0.0": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.4.tgz#72947e108f2cbeda5ab288a927399fdf6d02bd42" + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.5.tgz#8aa6033c0e87c653b09a6711686916864b48ec9e" dependencies: "@types/node" "*" @@ -40,7 +37,19 @@ dependencies: jsonschema "*" -"@types/lodash@^4.14.37", "@types/lodash@^4.14.64", "@types/lodash@^4.14.78": +"@types/lodash.foreach@^4.5.3": + version "4.5.3" + resolved "https://registry.yarnpkg.com/@types/lodash.foreach/-/lodash.foreach-4.5.3.tgz#87c01a0c5d9d17eec936ca3c28897af79440cdfc" + dependencies: + "@types/lodash" "*" + +"@types/lodash.values@^4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@types/lodash.values/-/lodash.values-4.3.3.tgz#910edc65b391782d65dc4b4d8804a0dabc5370e6" + dependencies: + "@types/lodash" "*" + +"@types/lodash@*", "@types/lodash@^4.14.37", "@types/lodash@^4.14.64", "@types/lodash@^4.14.77", "@types/lodash@^4.14.78": version "4.14.85" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.85.tgz#a16fbf942422f6eca5622b6910492c496c35069b" @@ -48,7 +57,11 @@ version "0.0.28" resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.0.28.tgz#44ba754e9fa51432583e8eb30a7c4dd249b52faa" -"@types/minimatch@*", "@types/minimatch@^2.0.29": +"@types/minimatch@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.1.tgz#b683eb60be358304ef146f5775db4c0e3696a550" + +"@types/minimatch@^2.0.29": version "2.0.29" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-2.0.29.tgz#5002e14f75e2d71e564281df0431c8c1b4a2a36a" @@ -60,9 +73,13 @@ version "8.0.51" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" +"@types/query-string@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/query-string/-/query-string-5.0.1.tgz#6cb41c724cb1644d56c2d1dae7c7b204e706b39e" + "@types/shelljs@^0.7.0": - version "0.7.5" - resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.5.tgz#5834fb7385d1137bd2be5842f2c278ac36a117f4" + version "0.7.6" + resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.6.tgz#4ac7ca01c191ba65b8e2bf50543c5560084d8d27" dependencies: "@types/glob" "*" "@types/node" "*" @@ -81,6 +98,12 @@ version "1.0.2" resolved "https://registry.yarnpkg.com/@types/valid-url/-/valid-url-1.0.2.tgz#60fa435ce24bfd5ba107b8d2a80796aeaf3a8f45" +"@types/websocket@^0.0.34": + version "0.0.34" + resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-0.0.34.tgz#25596764cec885eda070fdb6d19cd76fe582747c" + dependencies: + "@types/node" "*" + JSONStream@^1.0.4: version "1.3.1" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" @@ -308,6 +331,12 @@ assertion-error@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c" +async-child-process@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/async-child-process/-/async-child-process-1.1.1.tgz#27d0a598b5738707f9898c048bd231340583747b" + dependencies: + babel-runtime "^6.11.6" + async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" @@ -725,7 +754,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: +babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: @@ -876,7 +905,7 @@ bn.js@4.11.7: version "4.11.7" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.7.tgz#ddb048e50d9482790094c13eb3fcfc833ce7ab46" -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.10.0, bn.js@^4.11.3, bn.js@^4.11.7, bn.js@^4.4.0, bn.js@^4.8.0: +bn.js@4.11.8, bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.10.0, bn.js@^4.11.3, bn.js@^4.11.7, bn.js@^4.4.0, bn.js@^4.8.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" @@ -1031,7 +1060,7 @@ buffer@^5.0.6: base64-js "^1.0.2" ieee754 "^1.1.4" -builtin-modules@^1.0.0: +builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1127,7 +1156,7 @@ chai-typescript-typings@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/chai-typescript-typings/-/chai-typescript-typings-0.0.1.tgz#433dee303b0b2978ad0dd03129df0a5afb791274" -chai@^4.0.1: +chai@^4.0.1, chai@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/chai/-/chai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c" dependencies: @@ -2284,6 +2313,14 @@ fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" +fetch-mock@^5.13.1: + version "5.13.1" + resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-5.13.1.tgz#955794a77f3d972f1644b9ace65a0fdfd60f1df7" + dependencies: + glob-to-regexp "^0.3.0" + node-fetch "^1.3.3" + path-to-regexp "^1.7.0" + fetch-ponyfill@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/fetch-ponyfill/-/fetch-ponyfill-4.1.0.tgz#ae3ce5f732c645eab87e4ae8793414709b239893" @@ -2602,6 +2639,10 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" +glob-to-regexp@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" + glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@~7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" @@ -3143,7 +3184,7 @@ is-text-path@^1.0.0: dependencies: text-extensions "^1.0.0" -is-typedarray@~1.0.0: +is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -3177,7 +3218,7 @@ isobject@^3.0.0, isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" -isomorphic-fetch@^2.2.0: +isomorphic-fetch@^2.2.0, isomorphic-fetch@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" dependencies: @@ -3555,6 +3596,10 @@ lodash.assign@^4.0.3, lodash.assign@^4.0.6: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" +lodash.foreach@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -3880,7 +3925,7 @@ mute-stream@0.0.7, mute-stream@~0.0.4: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -nan@^2.0.5, nan@^2.0.8, nan@^2.2.1, nan@^2.3.0: +nan@^2.0.5, nan@^2.0.8, nan@^2.2.1, nan@^2.3.0, nan@^2.3.3: version "2.7.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" @@ -3916,7 +3961,7 @@ node-abi@^2.1.1: dependencies: semver "^5.4.1" -node-fetch@^1.0.1, node-fetch@~1.7.1: +node-fetch@^1.0.1, node-fetch@^1.3.3, node-fetch@~1.7.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" dependencies: @@ -4473,9 +4518,9 @@ public-encrypt@^4.0.0: parse-asn1 "^5.0.0" randombytes "^2.0.1" -publish-release@^1.3.3: +publish-release@0xproject/publish-release: version "1.3.3" - resolved "https://registry.yarnpkg.com/publish-release/-/publish-release-1.3.3.tgz#6cd11df835e14c13b0e08a35d3fb992b918bec3c" + resolved "https://codeload.github.com/0xproject/publish-release/tar.gz/c67c546726deecabd0cb35f9873afc912f862bd3" dependencies: async "^0.9.0" ghauth "^2.0.0" @@ -4518,6 +4563,14 @@ qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" +query-string@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.0.1.tgz#6e2b86fe0e08aef682ecbe86e85834765402bd88" + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -4944,7 +4997,14 @@ semver-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-1.0.0.tgz#92a4969065f9c70c694753d55248fc68f8f652c9" -"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@~5.4.1: +semver-sort@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/semver-sort/-/semver-sort-0.0.4.tgz#34fdbddc6a6b2b4161398c3c4dba56243bfeaa8b" + dependencies: + semver "^5.0.3" + semver-regex "^1.0.0" + +"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@~5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" @@ -5278,6 +5338,10 @@ stream-http@^2.3.1: to-arraybuffer "^1.0.0" xtend "^4.0.0" +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + string-editor@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/string-editor/-/string-editor-0.1.2.tgz#f5ff1b5ac4aed7ac6c2fb8de236d1551b20f61d0" @@ -5620,24 +5684,19 @@ tslib@^1.7.1: version "1.8.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.8.0.tgz#dc604ebad64bcbf696d613da6c954aa0e7ea1eb6" -tslint-config-0xproject@^0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/tslint-config-0xproject/-/tslint-config-0xproject-0.0.2.tgz#39901e0c0b3e9388f00092a28b90c015395d5bba" - dependencies: - tslint-react "^3.0.0" - -tslint-react@^3.0.0: +tslint-react@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/tslint-react/-/tslint-react-3.2.0.tgz#851fb505201c63d0343c51726e6364f7e9ad2e99" dependencies: tsutils "^2.8.0" -tslint@~5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.5.0.tgz#10e8dab3e3061fa61e9442e8cee3982acf20a6aa" +tslint@5.8.0: + version "5.8.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.8.0.tgz#1f49ad5b2e77c76c3af4ddcae552ae4e3612eb13" dependencies: babel-code-frame "^6.22.0" - colors "^1.1.2" + builtin-modules "^1.1.1" + chalk "^2.1.0" commander "^2.9.0" diff "^3.2.0" glob "^7.1.1" @@ -5645,9 +5704,9 @@ tslint@~5.5.0: resolve "^1.3.2" semver "^5.3.0" tslib "^1.7.1" - tsutils "^2.5.1" + tsutils "^2.12.1" -tsutils@^2.5.1, tsutils@^2.8.0: +tsutils@^2.12.1, tsutils@^2.8.0: version "2.12.2" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.12.2.tgz#ad58a4865d17ec3ddb6631b6ca53be14a5656ff3" dependencies: @@ -5671,6 +5730,12 @@ type-detect@^4.0.0: version "4.0.5" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.5.tgz#d70e5bc81db6de2a381bcaca0c6e0cbdc7635de2" +typedarray-to-buffer@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.2.tgz#1017b32d984ff556eba100f501589aba1ace2e04" + dependencies: + is-typedarray "^1.0.0" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -5719,7 +5784,7 @@ typescript@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc" -typescript@^2.4.2, typescript@~2.6.1: +typescript@2.6.1, typescript@^2.4.2, typescript@~2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.1.tgz#ef39cdea27abac0b500242d6726ab90e0c846631" @@ -5982,6 +6047,15 @@ webpack@^3.0.0, webpack@^3.1.0: webpack-sources "^1.0.1" yargs "^8.0.2" +websocket@^1.0.25: + version "1.0.25" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.25.tgz#998ec790f0a3eacb8b08b50a4350026692a11958" + dependencies: + debug "^2.2.0" + nan "^2.3.3" + typedarray-to-buffer "^3.1.2" + yaeti "^0.0.6" + whatwg-fetch@>=0.10.0: version "2.0.3" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" @@ -6098,6 +6172,10 @@ y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" +yaeti@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" |