diff options
32 files changed, 626 insertions, 113 deletions
@@ -23,5 +23,6 @@ This repository contains all the 0x developer tools written in TypeScript. Our h |--------|-------|------------| | [`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/connect`](/packages/connect) | [![npm](https://img.shields.io/npm/v/@0xproject/connect.svg?maxAge=2592000)](https://www.npmjs.com/package/@0xproject/connect) | A Javascript library for interacting with the standard relayer api | | [`@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..091ae1069 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 --registry=https://registry.npmjs.org/" }, "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 892a0863e..235a6eedb 100644 --- a/packages/0x.js/CHANGELOG.md +++ b/packages/0x.js/CHANGELOG.md @@ -1,14 +1,21 @@ # CHANGELOG +v0.26.0 +------------------------ + * Add post-formatter for logs converting `blockNumber`, `logIndex`, `transactionIndex` from hexes to numbers (#231) + * Remove support for Async callback types when used in Subscribe functions (#222) + * 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 + * Standardise on Cancelled over Canceled (#217) + * Add missing `DecodedLogEvent` type to exported types (#205) + * 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. (#200) 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 + * Fixed unhandled promise rejection error in subscribe methods (#209) + * Subscribe callbacks now receive an error object as their first argument v0.22.6 - _November 10, 2017_ ------------------------ diff --git a/packages/0x.js/package.json b/packages/0x.js/package.json index 0204d723f..afca00fe9 100644 --- a/packages/0x.js/package.json +++ b/packages/0x.js/package.json @@ -1,6 +1,6 @@ { "name": "0x.js", - "version": "0.25.1", + "version": "0.26.1", "description": "A javascript library for interacting with the 0x protocol", "keywords": [ "0x.js", @@ -14,20 +14,15 @@ "scripts": { "prebuild": "npm run clean", "build": "run-p build:umd:prod build:commonjs; exit 0;", - "prepublishOnly": "run-p build", - "postpublish": "run-s release docs:json upload_docs_json", - "release": "publish-release --assets _bundles/index.js,_bundles/index.min.js --tag $(git describe --tags) --owner 0xProject --repo 0x.js", - "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", + "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:circleci": "run-s test:coverage report_test_coverage && if [ $CIRCLE_BRANCH = \"development\" ]; then yarn test:umd; fi", "test": "run-s clean test:commonjs", "test:umd": "./scripts/test_umd.sh", "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,7 +44,8 @@ "node": ">=6.0.0" }, "devDependencies": { - "@0xproject/tslint-config": "^0.1.0", + "@0xproject/tslint-config": "^0.1.1", + "@types/bintrees": "^1.0.2", "@types/jsonschema": "^1.1.1", "@types/lodash": "^4.14.64", "@types/mocha": "^2.2.41", @@ -87,9 +83,10 @@ "webpack": "^3.1.0" }, "dependencies": { - "@0xproject/assert": "^0.0.4", - "@0xproject/json-schemas": "^0.6.7", + "@0xproject/assert": "^0.0.5", + "@0xproject/json-schemas": "^0.6.8", "bignumber.js": "~4.1.0", + "bintrees": "^1.0.2", "bn.js": "4.11.8", "compare-versions": "^3.0.1", "es6-promisify": "^5.0.0", @@ -99,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/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 81529a98c..ecbab0cd5 100644 --- a/packages/0x.js/src/order_watcher/event_watcher.ts +++ b/packages/0x.js/src/order_watcher/event_watcher.ts @@ -12,7 +12,7 @@ import {intervalUtils} from '../utils/interval_utils'; import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; -const DEFAULT_EVENT_POLLING_INTERVAL = 200; +const DEFAULT_EVENT_POLLING_INTERVAL_MS = 200; enum LogEventState { Removed, @@ -28,11 +28,11 @@ export class EventWatcher { private _pollingIntervalMs: number; private _intervalIdIfExists?: NodeJS.Timer; private _lastEvents: Web3.LogEntry[] = []; - constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) { + constructor(web3Wrapper: Web3Wrapper, pollingIntervalIfExistsMs: undefined|number) { this._web3Wrapper = web3Wrapper; - this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? - DEFAULT_EVENT_POLLING_INTERVAL : - pollingIntervalMs; + this._pollingIntervalMs = _.isUndefined(pollingIntervalIfExistsMs) ? + DEFAULT_EVENT_POLLING_INTERVAL_MS : + pollingIntervalIfExistsMs; } public subscribe(callback: EventWatcherCallback): void { assert.isFunction('callback', callback); @@ -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/expiration_watcher.ts b/packages/0x.js/src/order_watcher/expiration_watcher.ts new file mode 100644 index 000000000..717edaad7 --- /dev/null +++ b/packages/0x.js/src/order_watcher/expiration_watcher.ts @@ -0,0 +1,76 @@ +import * as _ from 'lodash'; +import {BigNumber} from 'bignumber.js'; +import {RBTree} from 'bintrees'; +import {utils} from '../utils/utils'; +import {intervalUtils} from '../utils/interval_utils'; +import {SignedOrder, ZeroExError} from '../types'; +import {ZeroEx} from '../0x'; + +const DEFAULT_EXPIRATION_MARGIN_MS = 0; +const DEFAULT_ORDER_EXPIRATION_CHECKING_INTERVAL_MS = 50; + +/** + * This class includes the functionality to detect expired orders. + * It stores them in a min heap by expiration time and checks for expired ones every `orderExpirationCheckingIntervalMs` + */ +export class ExpirationWatcher { + private orderHashByExpirationRBTree: RBTree<string>; + private expiration: {[orderHash: string]: BigNumber} = {}; + private orderExpirationCheckingIntervalMs: number; + private expirationMarginMs: number; + private orderExpirationCheckingIntervalIdIfExists?: NodeJS.Timer; + constructor(expirationMarginIfExistsMs?: number, + orderExpirationCheckingIntervalIfExistsMs?: number) { + this.expirationMarginMs = expirationMarginIfExistsMs || + DEFAULT_EXPIRATION_MARGIN_MS; + this.orderExpirationCheckingIntervalMs = expirationMarginIfExistsMs || + DEFAULT_ORDER_EXPIRATION_CHECKING_INTERVAL_MS; + const scoreFunction = (orderHash: string) => this.expiration[orderHash].toNumber(); + const comparator = (lhs: string, rhs: string) => scoreFunction(lhs) - scoreFunction(rhs); + this.orderHashByExpirationRBTree = new RBTree(comparator); + } + public subscribe(callbackAsync: (orderHash: string) => Promise<void>): void { + if (!_.isUndefined(this.orderExpirationCheckingIntervalIdIfExists)) { + throw new Error(ZeroExError.SubscriptionAlreadyPresent); + } + this.orderExpirationCheckingIntervalIdIfExists = intervalUtils.setAsyncExcludingInterval( + this.pruneExpiredOrdersAsync.bind(this, callbackAsync), this.orderExpirationCheckingIntervalMs, + ); + } + public unsubscribe(): void { + if (_.isUndefined(this.orderExpirationCheckingIntervalIdIfExists)) { + throw new Error(ZeroExError.SubscriptionNotFound); + } + intervalUtils.clearAsyncExcludingInterval(this.orderExpirationCheckingIntervalIdIfExists); + delete this.orderExpirationCheckingIntervalIdIfExists; + } + public addOrder(orderHash: string, expirationUnixTimestampMs: BigNumber): void { + this.expiration[orderHash] = expirationUnixTimestampMs; + this.orderHashByExpirationRBTree.insert(orderHash); + } + public removeOrder(orderHash: string): void { + this.orderHashByExpirationRBTree.remove(orderHash); + delete this.expiration[orderHash]; + } + private async pruneExpiredOrdersAsync(callbackAsync: (orderHash: string) => Promise<void>): Promise<void> { + const currentUnixTimestampMs = utils.getCurrentUnixTimestampMs(); + while (true) { + const hasTrakedOrders = this.orderHashByExpirationRBTree.size === 0; + if (hasTrakedOrders) { + break; + } + const nextOrderHashToExpire = this.orderHashByExpirationRBTree.min(); + const hasNoExpiredOrders = this.expiration[nextOrderHashToExpire].greaterThan( + currentUnixTimestampMs.plus(this.expirationMarginMs), + ); + const isSubscriptionActive = _.isUndefined(this.orderExpirationCheckingIntervalIdIfExists); + if (hasNoExpiredOrders || isSubscriptionActive) { + break; + } + const orderHash = this.orderHashByExpirationRBTree.min(); + this.orderHashByExpirationRBTree.remove(orderHash); + delete this.expiration[orderHash]; + await callbackAsync(orderHash); + } + } +} 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 bafd7a994..fd7496699 100644 --- a/packages/0x.js/src/order_watcher/order_state_watcher.ts +++ b/packages/0x.js/src/order_watcher/order_state_watcher.ts @@ -6,6 +6,7 @@ import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; import {artifacts} from '../artifacts'; import {AbiDecoder} from '../utils/abi_decoder'; +import {intervalUtils} from '../utils/interval_utils'; import {OrderStateUtils} from '../utils/order_state_utils'; import { LogEvent, @@ -24,14 +25,14 @@ import { ExchangeEvents, TokenEvents, ZeroExError, + ExchangeContractErrs, } from '../types'; import {Web3Wrapper} from '../web3_wrapper'; import {TokenWrapper} from '../contract_wrappers/token_wrapper'; import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper'; import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store'; import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store'; - -const DEFAULT_NUM_CONFIRMATIONS = 0; +import {ExpirationWatcher} from './expiration_watcher'; interface DependentOrderHashes { [makerAddress: string]: { @@ -52,10 +53,11 @@ 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; + private _expirationWatcher: ExpirationWatcher; private _orderStateUtils: OrderStateUtils; private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; @@ -65,38 +67,53 @@ export class OrderStateWatcher { ) { this._abiDecoder = abiDecoder; this._web3Wrapper = web3Wrapper; - const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; - this._eventWatcher = new EventWatcher(web3Wrapper, eventPollingIntervalMs); + const pollingIntervalIfExistsMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; + this._eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalIfExistsMs); this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore(token); this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore(exchange); this._orderStateUtils = new OrderStateUtils( this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore, ); + const orderExpirationCheckingIntervalMsIfExists = _.isUndefined(config) ? + undefined : + config.orderExpirationCheckingIntervalMs; + const expirationMarginIfExistsMs = _.isUndefined(config) ? + undefined : + config.expirationMarginMs; + this._expirationWatcher = new ExpirationWatcher( + expirationMarginIfExistsMs, orderExpirationCheckingIntervalMsIfExists, + ); } /** * Add an order to the orderStateWatcher. Before the order is added, it's * 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); } /** * 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); } /** * Starts an orderStateWatcher subscription. The callback will be called every time a watched order's @@ -106,23 +123,38 @@ 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._onOrderExpiredAsync.bind(this)); } /** * 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(); + } + private async _onOrderExpiredAsync(orderHash: string): Promise<void> { + const orderState: OrderState = { + isValid: false, + orderHash, + error: ExchangeContractErrs.OrderFillExpired, + }; + if (!_.isUndefined(this._orderByOrderHash[orderHash])) { + await this.removeOrderAsync(orderHash); + if (!_.isUndefined(this._callbackIfExists)) { + this._callbackIfExists(orderState); + } + } } private async _onEventWatcherCallbackAsync(log: LogEvent): Promise<void> { const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log); @@ -204,13 +236,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] = {}; } @@ -218,11 +250,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 71089f9a1..c3aabfd86 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: { @@ -397,10 +392,15 @@ export interface JSONRPCPayload { } /* - * eventPollingIntervalMs: How often to poll the Ethereum node for new events + * orderExpirationCheckingIntervalMs: How often to check for expired orders. Default: 50 + * eventPollingIntervalMs: How often to poll the Ethereum node for new events. Defaults: 200 + * expirationMarginMs: Amount of time before order expiry that you'd like to be notified + * of an orders expiration. Defaults: 0 */ export interface OrderStateWatcherConfig { + orderExpirationCheckingIntervalMs?: number; eventPollingIntervalMs?: number; + expirationMarginMs?: number; } /* @@ -507,9 +507,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/order_validation_utils.ts b/packages/0x.js/src/utils/order_validation_utils.ts index f03703c4e..ed723e3d4 100644 --- a/packages/0x.js/src/utils/order_validation_utils.ts +++ b/packages/0x.js/src/utils/order_validation_utils.ts @@ -102,7 +102,7 @@ export class OrderValidationUtils { if (order.takerTokenAmount.eq(unavailableTakerTokenAmount)) { throw new Error(ExchangeContractErrs.OrderAlreadyCancelledOrFilled); } - const currentUnixTimestampSec = utils.getCurrentUnixTimestamp(); + const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); if (order.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { throw new Error(ExchangeContractErrs.OrderCancelExpired); } @@ -150,7 +150,7 @@ export class OrderValidationUtils { } } private validateOrderNotExpiredOrThrow(expirationUnixTimestampSec: BigNumber) { - const currentUnixTimestampSec = utils.getCurrentUnixTimestamp(); + const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); if (expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { throw new Error(ExchangeContractErrs.OrderFillExpired); } diff --git a/packages/0x.js/src/utils/utils.ts b/packages/0x.js/src/utils/utils.ts index 280f3e979..5370c3b4b 100644 --- a/packages/0x.js/src/utils/utils.ts +++ b/packages/0x.js/src/utils/utils.ts @@ -49,7 +49,10 @@ export const utils = { const hashHex = ethUtil.bufferToHex(hashBuff); return hashHex; }, - getCurrentUnixTimestamp(): BigNumber { - return new BigNumber(Date.now() / 1000); + getCurrentUnixTimestampSec(): BigNumber { + return new BigNumber(Date.now() / 1000).round(); + }, + getCurrentUnixTimestampMs(): BigNumber { + return new BigNumber(Date.now()); }, }; diff --git a/packages/0x.js/src/web3_wrapper.ts b/packages/0x.js/src/web3_wrapper.ts index c937f9288..7bd8ea093 100644 --- a/packages/0x.js/src/web3_wrapper.ts +++ b/packages/0x.js/src/web3_wrapper.ts @@ -5,6 +5,17 @@ import promisify = require('es6-promisify'); import {ZeroExError, Artifact, TransactionReceipt} from './types'; import {Contract} from './contract'; +interface RawLogEntry { + logIndex: string|null; + transactionIndex: string|null; + transactionHash: string; + blockHash: string|null; + blockNumber: string|null; + address: string; + data: string; + topics: string[]; +} + export class Web3Wrapper { private web3: Web3; private defaults: Partial<Web3.TxData>; @@ -39,7 +50,9 @@ export class Web3Wrapper { } public async getTransactionReceiptAsync(txHash: string): Promise<TransactionReceipt> { const transactionReceipt = await promisify(this.web3.eth.getTransactionReceipt)(txHash); - transactionReceipt.status = this.normalizeTxReceiptStatus(transactionReceipt.status); + if (!_.isNull(transactionReceipt)) { + transactionReceipt.status = this.normalizeTxReceiptStatus(transactionReceipt.status); + } return transactionReceipt; } public getCurrentProvider(): Web3.Provider { @@ -137,8 +150,9 @@ export class Web3Wrapper { method: 'eth_getLogs', params: [serializedFilter], }; - const logs = await this.sendRawPayloadAsync(payload); - return logs; + const rawLogs = await this.sendRawPayloadAsync<RawLogEntry[]>(payload); + const formattedLogs = _.map(rawLogs, this.formatLog.bind(this)); + return formattedLogs; } private getContractInstance<A extends Web3.ContractInstance>(abi: Web3.ContractAbi, address: string): A { const web3ContractInstance = this.web3.eth.contract(abi).at(address); @@ -149,7 +163,7 @@ export class Web3Wrapper { const networkId = await promisify(this.web3.version.getNetwork)(); return networkId; } - private async sendRawPayloadAsync(payload: Web3.JSONRPCRequestPayload): Promise<any> { + private async sendRawPayloadAsync<A>(payload: Web3.JSONRPCRequestPayload): Promise<A> { const sendAsync = this.web3.currentProvider.sendAsync.bind(this.web3.currentProvider); const response = await promisify(sendAsync)(payload); const result = response.result; @@ -169,4 +183,20 @@ export class Web3Wrapper { return status; } } + private formatLog(rawLog: RawLogEntry): Web3.LogEntry { + const formattedLog = { + ...rawLog, + logIndex: this.hexToDecimal(rawLog.logIndex), + blockNumber: this.hexToDecimal(rawLog.blockNumber), + transactionIndex: this.hexToDecimal(rawLog.transactionIndex), + }; + return formattedLog; + } + private hexToDecimal(hex: string|null): number|null { + if (_.isNull(hex)) { + return null; + } + const decimal = this.web3.toDecimal(hex); + return decimal; + } } diff --git a/packages/0x.js/test/expiration_watcher_test.ts b/packages/0x.js/test/expiration_watcher_test.ts new file mode 100644 index 000000000..0f2470070 --- /dev/null +++ b/packages/0x.js/test/expiration_watcher_test.ts @@ -0,0 +1,138 @@ +import 'mocha'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +import * as Sinon from 'sinon'; +import * as Web3 from 'web3'; +import BigNumber from 'bignumber.js'; +import {chaiSetup} from './utils/chai_setup'; +import {web3Factory} from './utils/web3_factory'; +import {utils} from '../src/utils/utils'; +import {Web3Wrapper} from '../src/web3_wrapper'; +import {TokenUtils} from './utils/token_utils'; +import {ExpirationWatcher} from '../src/order_watcher/expiration_watcher'; +import {Token, DoneCallback} from '../src/types'; +import {ZeroEx} from '../src/0x'; +import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; +import {FillScenarios} from './utils/fill_scenarios'; +import {reportCallbackErrors} from './utils/report_callback_errors'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(); + +describe('ExpirationWatcher', () => { + let web3: Web3; + let zeroEx: ZeroEx; + let tokenUtils: TokenUtils; + let tokens: Token[]; + let userAddresses: string[]; + let zrxTokenAddress: string; + let fillScenarios: FillScenarios; + let exchangeContractAddress: string; + let makerTokenAddress: string; + let takerTokenAddress: string; + let coinbase: string; + let makerAddress: string; + let takerAddress: string; + let feeRecipient: string; + const fillableAmount = new BigNumber(5); + let currentUnixTimestampSec: BigNumber; + let timer: Sinon.SinonFakeTimers; + let expirationWatcher: ExpirationWatcher; + before(async () => { + web3 = web3Factory.create(); + zeroEx = new ZeroEx(web3.currentProvider); + exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync(); + userAddresses = await zeroEx.getAvailableAddressesAsync(); + tokens = await zeroEx.tokenRegistry.getTokensAsync(); + tokenUtils = new TokenUtils(tokens); + zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address; + fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress); + [coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses; + tokens = await zeroEx.tokenRegistry.getTokensAsync(); + const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens(); + makerTokenAddress = makerToken.address; + takerTokenAddress = takerToken.address; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + const sinonTimerConfig = {shouldAdvanceTime: true} as any; + // This constructor has incorrect types + timer = Sinon.useFakeTimers(sinonTimerConfig); + currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); + expirationWatcher = new ExpirationWatcher(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + timer.restore(); + expirationWatcher.unsubscribe(); + }); + it('correctly emits events when order expires', (done: DoneCallback) => { + (async () => { + const orderLifetimeSec = 60; + const expirationUnixTimestampSec = currentUnixTimestampSec.plus(orderLifetimeSec); + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + expirationUnixTimestampSec, + ); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + expirationWatcher.addOrder(orderHash, signedOrder.expirationUnixTimestampSec.times(1000)); + const callbackAsync = reportCallbackErrors(done)(async (hash: string) => { + expect(hash).to.be.equal(orderHash); + expect(utils.getCurrentUnixTimestampSec()).to.be.bignumber.gte(expirationUnixTimestampSec); + done(); + }); + expirationWatcher.subscribe(callbackAsync); + timer.tick(orderLifetimeSec * 1000); + })().catch(done); + }); + it('doesn\'t emit events before order expires', (done: DoneCallback) => { + (async () => { + const orderLifetimeSec = 60; + const expirationUnixTimestampSec = currentUnixTimestampSec.plus(orderLifetimeSec); + const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + expirationUnixTimestampSec, + ); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + expirationWatcher.addOrder(orderHash, signedOrder.expirationUnixTimestampSec.times(1000)); + const callbackAsync = reportCallbackErrors(done)(async (hash: string) => { + done(new Error('Emitted expiration went before the order actually expired')); + }); + expirationWatcher.subscribe(callbackAsync); + const notEnoughTime = orderLifetimeSec - 1; + timer.tick(notEnoughTime * 1000); + done(); + })().catch(done); + }); + it('emits events in correct order', (done: DoneCallback) => { + (async () => { + const order1Lifetime = 60; + const order2Lifetime = 120; + const order1ExpirationUnixTimestampSec = currentUnixTimestampSec.plus(order1Lifetime); + const order2ExpirationUnixTimestampSec = currentUnixTimestampSec.plus(order2Lifetime); + const signedOrder1 = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + order1ExpirationUnixTimestampSec, + ); + const signedOrder2 = await fillScenarios.createFillableSignedOrderAsync( + makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount, + order2ExpirationUnixTimestampSec, + ); + const orderHash1 = ZeroEx.getOrderHashHex(signedOrder1); + const orderHash2 = ZeroEx.getOrderHashHex(signedOrder2); + expirationWatcher.addOrder(orderHash2, signedOrder2.expirationUnixTimestampSec.times(1000)); + expirationWatcher.addOrder(orderHash1, signedOrder1.expirationUnixTimestampSec.times(1000)); + const expirationOrder = [orderHash1, orderHash2]; + const callbackAsync = reportCallbackErrors(done)(async (hash: string) => { + const orderHash = expirationOrder.shift(); + expect(hash).to.be.equal(orderHash); + if (_.isEmpty(expirationOrder)) { + done(); + } + }); + expirationWatcher.subscribe(callbackAsync); + timer.tick(order2Lifetime * 1000); + })().catch(done); + }); +}); diff --git a/packages/0x.js/test/order_state_watcher_test.ts b/packages/0x.js/test/order_state_watcher_test.ts index e64a69890..ab6174f71 100644 --- a/packages/0x.js/test/order_state_watcher_test.ts +++ b/packages/0x.js/test/order_state_watcher_test.ts @@ -75,13 +75,13 @@ describe('OrderStateWatcher', () => { 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, }); @@ -94,7 +94,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 () => { @@ -111,7 +111,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 () => { @@ -119,7 +119,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; @@ -137,7 +137,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'); }); @@ -158,7 +158,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; @@ -178,7 +178,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) => { @@ -210,7 +210,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) => { @@ -237,6 +237,22 @@ describe('OrderStateWatcher', () => { ); })().catch(done); }); + 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 () => { @@ -250,7 +266,7 @@ describe('OrderStateWatcher', () => { const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker); const fillAmountInBaseUnits = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals); const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.addOrder(signedOrder); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); let eventCount = 0; const callback = reportCallbackErrors(done)((orderState: OrderState) => { eventCount++; @@ -282,7 +298,7 @@ describe('OrderStateWatcher', () => { const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); const changedMakerApprovalAmount = ZeroEx.toBaseUnitAmount(new BigNumber(3), decimals); - zeroEx.orderStateWatcher.addOrder(signedOrder); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); const callback = reportCallbackErrors(done)((orderState: OrderState) => { const validOrderState = orderState as OrderStateValid; @@ -307,7 +323,7 @@ describe('OrderStateWatcher', () => { const remainingAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), decimals); const transferAmount = makerBalance.sub(remainingAmount); - zeroEx.orderStateWatcher.addOrder(signedOrder); + await zeroEx.orderStateWatcher.addOrderAsync(signedOrder); const callback = reportCallbackErrors(done)((orderState: OrderState) => { const validOrderState = orderState as OrderStateValid; @@ -412,7 +428,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(); @@ -434,7 +450,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(); @@ -460,7 +476,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_wrapper_test.ts b/packages/0x.js/test/token_wrapper_test.ts index b30762e8c..1a7cb9e40 100644 --- a/packages/0x.js/test/token_wrapper_test.ts +++ b/packages/0x.js/test/token_wrapper_test.ts @@ -361,6 +361,9 @@ describe('TokenWrapper', () => { (async () => { const callback = (err: Error, logEvent: DecodedLogEvent<TransferContractEventArgs>) => { expect(logEvent).to.not.be.undefined(); + expect(logEvent.logIndex).to.be.equal(0); + expect(logEvent.transactionIndex).to.be.equal(0); + expect(logEvent.blockNumber).to.be.a('number'); const args = logEvent.args; expect(args._from).to.be.equal(coinbase); expect(args._to).to.be.equal(addressWithoutFunds); diff --git a/packages/0x.js/test/utils/report_callback_errors.ts b/packages/0x.js/test/utils/report_callback_errors.ts index d471b2af2..4f9517704 100644 --- a/packages/0x.js/test/utils/report_callback_errors.ts +++ b/packages/0x.js/test/utils/report_callback_errors.ts @@ -1,10 +1,10 @@ import { DoneCallback } from '../../src/types'; export const reportCallbackErrors = (done: DoneCallback) => { - return (f: (...args: any[]) => void) => { - const wrapped = (...args: any[]) => { + return (fAsync: (...args: any[]) => void|Promise<void>) => { + const wrapped = async (...args: any[]) => { try { - f(...args); + await fAsync(...args); } catch (err) { done(err); } diff --git a/packages/assert/package.json b/packages/assert/package.json index ed1d2a98b..d0f40c66e 100644 --- a/packages/assert/package.json +++ b/packages/assert/package.json @@ -1,6 +1,6 @@ { "name": "@0xproject/assert", - "version": "0.0.4", + "version": "0.0.5", "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", @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/0xProject/0x.js/packages/assert/README.md", "devDependencies": { - "@0xproject/tslint-config": "^0.1.0", + "@0xproject/tslint-config": "^0.1.1", "@types/lodash": "^4.14.78", "@types/mocha": "^2.2.42", "@types/valid-url": "^1.0.2", @@ -37,7 +37,7 @@ "typescript": "^2.4.2" }, "dependencies": { - "@0xproject/json-schemas": "^0.6.7", + "@0xproject/json-schemas": "^0.6.8", "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/connect/CHANGELOG.md b/packages/connect/CHANGELOG.md index f3367f4ce..ec6727a12 100644 --- a/packages/connect/CHANGELOG.md +++ b/packages/connect/CHANGELOG.md @@ -1,4 +1,5 @@ # CHANGELOG -v0.0.0 - _Nov. 15, 2017_ +v0.1.0 - _November 22, 2017_ ------------------------ + * Provide a HttpClient class for interacting with standard relayer api compliant HTTP urls diff --git a/packages/connect/package.json b/packages/connect/package.json index d26594b5d..9131eef14 100644 --- a/packages/connect/package.json +++ b/packages/connect/package.json @@ -1,9 +1,9 @@ { "name": "@0xproject/connect", - "version": "0.0.0", + "version": "0.1.0", "description": "A javascript library for interacting with the standard relayer api", "keywords": [ - "0x-connect", + "connect", "0xproject", "ethereum", "tokens", @@ -14,9 +14,10 @@ "scripts": { "build": "tsc", "clean": "shx rm -rf _bundles lib test_temp", + "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", "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" @@ -35,9 +36,9 @@ }, "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", + "0x.js": "^0.26.1", + "@0xproject/assert": "^0.0.5", + "@0xproject/json-schemas": "^0.6.8", "bignumber.js": "~4.1.0", "isomorphic-fetch": "^2.2.1", "lodash": "^4.17.4", @@ -45,7 +46,7 @@ "websocket": "^1.0.25" }, "devDependencies": { - "@0xproject/tslint-config": "0.1.0", + "@0xproject/tslint-config": "^0.1.1", "@types/fetch-mock": "^5.12.1", "@types/lodash": "^4.14.77", "@types/mocha": "^2.2.42", @@ -62,6 +63,7 @@ "npm-run-all": "^4.0.2", "shx": "^0.2.2", "tslint": "5.8.0", + "typedoc": "~0.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..ba0f8507d --- /dev/null +++ b/packages/connect/scripts/postpublish.js @@ -0,0 +1,39 @@ +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://connect-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); + return postpublish_utils.publishReleaseNotes(tag, releaseName); + }) + .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/connect/src/http_client.ts b/packages/connect/src/http_client.ts index ab8c6bfa1..85dc83c61 100644 --- a/packages/connect/src/http_client.ts +++ b/packages/connect/src/http_client.ts @@ -18,6 +18,11 @@ import { import {schemas as clientSchemas} from './schemas/schemas'; import {typeConverters} from './utils/type_converters'; +// TODO: move this and bigNumberConfigs in the 0x.js package into one place +BigNumber.config({ + EXPONENTIAL_AT: 1000, +}); + interface RequestOptions { params?: object; payload?: object; @@ -157,9 +162,10 @@ export class HttpClient implements Client { const headers = new Headers({ 'content-type': 'application/json', }); + const response = await fetch(url, { method: requestType, - body: payload, + body: JSON.stringify(payload), headers, }); if (!response.ok) { diff --git a/packages/connect/src/index.ts b/packages/connect/src/index.ts index 5e97f4f26..c9ebde510 100644 --- a/packages/connect/src/index.ts +++ b/packages/connect/src/index.ts @@ -1,12 +1,8 @@ export {HttpClient} from './http_client'; -export {WebSocketOrderbookChannel} from './ws_orderbook_channel'; export { Client, FeesRequest, FeesResponse, - OrderbookChannel, - OrderbookChannelHandler, - OrderbookChannelSubscriptionOpts, OrderbookRequest, OrderbookResponse, OrdersRequest, diff --git a/packages/connect/test/ws_orderbook_channel_test.ts b/packages/connect/test/ws_orderbook_channel_test.ts index f3dead9ae..e92c6f44a 100644 --- a/packages/connect/test/ws_orderbook_channel_test.ts +++ b/packages/connect/test/ws_orderbook_channel_test.ts @@ -4,7 +4,7 @@ import * as dirtyChai from 'dirty-chai'; import * as chai from 'chai'; import { WebSocketOrderbookChannel, -} from '../src/index'; +} from '../src/ws_orderbook_channel'; chai.config.includeStack = true; chai.use(dirtyChai); diff --git a/packages/json-schemas/package.json b/packages/json-schemas/package.json index 07ed20551..89c0d25f7 100644 --- a/packages/json-schemas/package.json +++ b/packages/json-schemas/package.json @@ -1,6 +1,6 @@ { "name": "@0xproject/json-schemas", - "version": "0.6.7", + "version": "0.6.8", "description": "0x-related json schemas", "main": "lib/src/index.js", "types": "lib/src/index.d.ts", @@ -28,7 +28,7 @@ "lodash.values": "^4.3.0" }, "devDependencies": { - "@0xproject/tslint-config": "^0.1.0", + "@0xproject/tslint-config": "^0.1.1", "@types/lodash.foreach": "^4.5.3", "@types/lodash.values": "^4.3.3", "@types/mocha": "^2.2.42", 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/tslint-config/package.json b/packages/tslint-config/package.json index ca46d63fc..7ee3f1f71 100644 --- a/packages/tslint-config/package.json +++ b/packages/tslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@0xproject/tslint-config", - "version": "0.1.0", + "version": "0.1.1", "description": "Lint rules related to 0xProject for TSLint", "main": "tslint.json", "files": [ 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/scripts/postpublish_utils.js b/scripts/postpublish_utils.js new file mode 100644 index 000000000..4f9798e60 --- /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 tag -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; + }, +}; @@ -6,9 +6,13 @@ 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" "*" @@ -53,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" @@ -70,8 +78,8 @@ 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" "*" @@ -323,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" @@ -740,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: @@ -849,6 +863,10 @@ bindings@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7" +bintrees@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8" + bip39@^2.2.0: version "2.4.0" resolved "https://registry.yarnpkg.com/bip39/-/bip39-2.4.0.tgz#a0b8adbf163f53495f00f05d9ede7c25369ccf13" @@ -4500,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" @@ -4979,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" |