diff options
-rw-r--r-- | CHANGELOG.md | 4 | ||||
-rw-r--r-- | circle.yml | 2 | ||||
-rw-r--r-- | package-lock.json | 2 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/0x.ts | 14 | ||||
-rw-r--r-- | src/order_watcher/event_watcher.ts | 48 | ||||
-rw-r--r-- | src/order_watcher/order_state_watcher.ts | 46 | ||||
-rw-r--r-- | src/types.ts | 7 | ||||
-rw-r--r-- | test/event_watcher_test.ts | 12 | ||||
-rw-r--r-- | test/order_state_watcher_test.ts | 6 | ||||
-rw-r--r-- | test/token_wrapper_test.ts | 4 |
11 files changed, 96 insertions, 51 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 156fb1222..029144b5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +v0.22.6 - _November 10, 2017_ +------------------------ + * Add a timeout parameter to transaction awaiting (#206) + v0.22.5 - _November 7, 2017_ ------------------------ * Re-publish v0.22.4 to fix publishing issue diff --git a/circle.yml b/circle.yml index 5a8d340f0..3dc00bd03 100644 --- a/circle.yml +++ b/circle.yml @@ -2,7 +2,7 @@ machine: node: version: 6.5.0 environment: - CONTRACTS_COMMIT_HASH: '35053f9' + CONTRACTS_COMMIT_HASH: '78fe8dd' PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" dependencies: diff --git a/package-lock.json b/package-lock.json index c3c4962e7..6b4a97d87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "0x.js", - "version": "0.22.5", + "version": "0.22.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4b23bf6e1..e7e21bdce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "0x.js", - "version": "0.22.5", + "version": "0.22.6", "description": "A javascript library for interacting with the 0x protocol", "keywords": [ "0x.js", @@ -285,13 +285,24 @@ export class ZeroEx { * Waits for a transaction to be mined and returns the transaction receipt. * @param txHash Transaction hash * @param pollingIntervalMs How often (in ms) should we check if the transaction is mined. + * @param timeoutMs How long (in ms) to poll for transaction mined until aborting. * @return Transaction receipt with decoded log args. */ public async awaitTransactionMinedAsync( - txHash: string, pollingIntervalMs: number = 1000): Promise<TransactionReceiptWithDecodedLogs> { + txHash: string, pollingIntervalMs = 1000, timeoutMs?: number): Promise<TransactionReceiptWithDecodedLogs> { + let timeoutExceeded = false; + if (timeoutMs) { + setTimeout(() => timeoutExceeded = true, timeoutMs); + } + const txReceiptPromise = new Promise( (resolve: (receipt: TransactionReceiptWithDecodedLogs) => void, reject) => { const intervalId = intervalUtils.setAsyncExcludingInterval(async () => { + if (timeoutExceeded) { + intervalUtils.clearAsyncExcludingInterval(intervalId); + return reject(ZeroExError.TransactionMiningTimeout); + } + const transactionReceipt = await this._web3Wrapper.getTransactionReceiptAsync(txHash); if (!_.isNull(transactionReceipt)) { intervalUtils.clearAsyncExcludingInterval(intervalId); @@ -307,6 +318,7 @@ export class ZeroEx { } }, pollingIntervalMs); }); + return txReceiptPromise; } /* diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index 2a1b6dacf..c9e72281c 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -1,42 +1,58 @@ import * as Web3 from 'web3'; import * as _ from 'lodash'; import {Web3Wrapper} from '../web3_wrapper'; -import {BlockParamLiteral, EventCallback, EventWatcherCallback} from '../types'; +import { + BlockParamLiteral, + EventCallback, + EventWatcherCallback, + ZeroExError, +} from '../types'; import {AbiDecoder} from '../utils/abi_decoder'; import {intervalUtils} from '../utils/interval_utils'; import {assert} from '../utils/assert'; +import {utils} from '../utils/utils'; const DEFAULT_EVENT_POLLING_INTERVAL = 200; +enum LogEventState { + Removed, + Added, +} + +/* + * The EventWatcher watches for blockchain events at the specified block confirmation + * depth. + */ export class EventWatcher { private _web3Wrapper: Web3Wrapper; private _pollingIntervalMs: number; private _intervalIdIfExists?: NodeJS.Timer; private _lastEvents: Web3.LogEntry[] = []; - private _callbackIfExistsAsync?: EventWatcherCallback; private _numConfirmations: number; constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number, numConfirmations: number) { this._web3Wrapper = web3Wrapper; this._numConfirmations = numConfirmations; this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? - DEFAULT_EVENT_POLLING_INTERVAL : - pollingIntervalMs; + DEFAULT_EVENT_POLLING_INTERVAL : + pollingIntervalMs; } public subscribe(callback: EventWatcherCallback): void { assert.isFunction('callback', callback); - this._callbackIfExistsAsync = callback; + if (!_.isUndefined(this._intervalIdIfExists)) { + throw new Error(ZeroExError.SubscriptionAlreadyPresent); + } this._intervalIdIfExists = intervalUtils.setAsyncExcludingInterval( - this._pollForMempoolEventsAsync.bind(this), this._pollingIntervalMs, + this._pollForBlockchainEventsAsync.bind(this, callback), this._pollingIntervalMs, ); } public unsubscribe(): void { - delete this._callbackIfExistsAsync; this._lastEvents = []; if (!_.isUndefined(this._intervalIdIfExists)) { intervalUtils.clearAsyncExcludingInterval(this._intervalIdIfExists); + delete this._intervalIdIfExists; } } - private async _pollForMempoolEventsAsync(): Promise<void> { + private async _pollForBlockchainEventsAsync(callback: EventWatcherCallback): Promise<void> { const pendingEvents = await this._getEventsAsync(); if (pendingEvents.length === 0) { // HACK: Sometimes when node rebuilds the pending block we get back the empty result. @@ -46,10 +62,8 @@ export class EventWatcher { } const removedEvents = _.differenceBy(this._lastEvents, pendingEvents, JSON.stringify); const newEvents = _.differenceBy(pendingEvents, this._lastEvents, JSON.stringify); - let isRemoved = true; - await this._emitDifferencesAsync(removedEvents, isRemoved); - isRemoved = false; - await this._emitDifferencesAsync(newEvents, isRemoved); + await this._emitDifferencesAsync(removedEvents, LogEventState.Removed, callback); + await this._emitDifferencesAsync(newEvents, LogEventState.Added, callback); this._lastEvents = pendingEvents; } private async _getEventsAsync(): Promise<Web3.LogEntry[]> { @@ -67,14 +81,16 @@ export class EventWatcher { const events = await this._web3Wrapper.getLogsAsync(eventFilter); return events; } - private async _emitDifferencesAsync(logs: Web3.LogEntry[], isRemoved: boolean): Promise<void> { + private async _emitDifferencesAsync( + logs: Web3.LogEntry[], logEventState: LogEventState, callback: EventWatcherCallback, + ): Promise<void> { for (const log of logs) { const logEvent = { - removed: isRemoved, + removed: logEventState === LogEventState.Removed, ...log, }; - if (!_.isUndefined(this._callbackIfExistsAsync)) { - await this._callbackIfExistsAsync(logEvent); + if (!_.isUndefined(this._intervalIdIfExists)) { + await callback(logEvent); } } } diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 303ec8bd3..4866f8409 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -35,9 +35,15 @@ interface OrderByOrderHash { [orderHash: string]: SignedOrder; } +/** + * This class includes all the functionality related to watching a set of orders + * for potential changes in order validity/fillability. The orderWatcher notifies + * the subscriber of these changes so that a final decison can be made on whether + * the order should be deemed invalid. + */ export class OrderStateWatcher { - private _orders: OrderByOrderHash; - private _dependentOrderHashes: DependentOrderHashes; + private _orderByOrderHash: OrderByOrderHash = {}; + private _dependentOrderHashes: DependentOrderHashes = {}; private _web3Wrapper: Web3Wrapper; private _callbackIfExistsAsync?: OnOrderStateChangeCallback; private _eventWatcher: EventWatcher; @@ -49,8 +55,6 @@ export class OrderStateWatcher { config?: OrderStateWatcherConfig, ) { this._web3Wrapper = web3Wrapper; - this._orders = {}; - this._dependentOrderHashes = {}; const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.pollingIntervalMs; this._numConfirmations = _.isUndefined(config) ? DEFAULT_NUM_CONFIRMATIONS @@ -62,14 +66,15 @@ export class OrderStateWatcher { this._orderStateUtils = orderStateUtils; } /** - * Add an order to the orderStateWatcher + * 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 { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); const orderHash = ZeroEx.getOrderHashHex(signedOrder); assert.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker); - this._orders[orderHash] = signedOrder; + this._orderByOrderHash[orderHash] = signedOrder; this.addToDependentOrderHashes(signedOrder, orderHash); } /** @@ -78,22 +83,18 @@ export class OrderStateWatcher { */ public removeOrder(orderHash: string): void { assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); - const signedOrder = this._orders[orderHash]; + const signedOrder = this._orderByOrderHash[orderHash]; if (_.isUndefined(signedOrder)) { return; // noop } - delete this._orders[orderHash]; - this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress].delete(orderHash); - // We currently do not remove the maker/makerToken keys from the mapping when all orderHashes removed + delete this._orderByOrderHash[orderHash]; + this.removeFromDependentOrderHashes(signedOrder.maker, signedOrder.makerTokenAddress, orderHash); } /** * Starts an orderStateWatcher subscription. The callback will be called every time a watched order's * backing blockchain state has changed. This is a call-to-action for the caller to re-validate the order. * @param callback Receives the orderHash of the order that should be re-validated, together * with all the order-relevant blockchain state needed to re-validate the order. - * @param numConfirmations Number of confirmed blocks deeps you want to run the orderWatcher from. Passing - * is 0 will watch the backing node's mempool, 3 will emit events when blockchain - * state relevant to a watched order changed 3 blocks ago. */ public subscribe(callback: OnOrderStateChangeCallback): void { assert.isFunction('callback', callback); @@ -112,10 +113,12 @@ export class OrderStateWatcher { } private async _onEventWatcherCallbackAsync(log: LogEvent): Promise<void> { const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log); - const isDecodedLog = !_.isUndefined((maybeDecodedLog as LogWithDecodedArgs<any>).event); - if (!isDecodedLog) { + const isLogDecoded = !_.isUndefined((maybeDecodedLog as LogWithDecodedArgs<any>).event); + if (!isLogDecoded) { return; // noop } + // Unfortunately blockNumber is returned as a hex-encoded string, so we + // convert it to a number here. const blockNumberBuff = ethUtil.toBuffer(maybeDecodedLog.blockNumber); const blockNumber = ethUtil.bufferToInt(blockNumberBuff); @@ -147,7 +150,7 @@ export class OrderStateWatcher { case ExchangeEvents.LogFill: case ExchangeEvents.LogCancel: const orderHash = decodedLog.args.orderHash; - const isOrderWatched = !_.isUndefined(this._orders[orderHash]); + const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); if (isOrderWatched) { await this._emitRevalidateOrdersAsync([orderHash], blockNumber); } @@ -169,7 +172,7 @@ export class OrderStateWatcher { }; for (const orderHash of orderHashes) { - const signedOrder = this._orders[orderHash] as SignedOrder; + const signedOrder = this._orderByOrderHash[orderHash] as SignedOrder; const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder, methodOpts); if (!_.isUndefined(this._callbackIfExistsAsync)) { await this._callbackIfExistsAsync(orderState); @@ -187,4 +190,13 @@ export class OrderStateWatcher { } this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress].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]; + } + if (_.isEmpty(this._dependentOrderHashes[makerAddress])) { + delete this._dependentOrderHashes[makerAddress]; + } + } } diff --git a/src/types.ts b/src/types.ts index 13867dac8..160b71fda 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,7 @@ export enum ZeroExError { NoNetworkId = 'NO_NETWORK_ID', SubscriptionNotFound = 'SUBSCRIPTION_NOT_FOUND', SubscriptionAlreadyPresent = 'SUBSCRIPTION_ALREADY_PRESENT', + TransactionMiningTimeout = 'TRANSACTION_MINING_TIMEOUT', } export enum InternalZeroExError { @@ -508,6 +509,6 @@ export interface OrderStateInvalid { export type OrderState = OrderStateValid|OrderStateInvalid; -export type OnOrderStateChangeCallback = ( - orderState: OrderState, -) => void; +export type OnOrderStateChangeCallbackSync = (orderState: OrderState) => void; +export type OnOrderStateChangeCallbackAsync = (orderState: OrderState) => Promise<void>; +export type OnOrderStateChangeCallback = OnOrderStateChangeCallbackAsync|OnOrderStateChangeCallbackSync; diff --git a/test/event_watcher_test.ts b/test/event_watcher_test.ts index 36153c207..98dab93b5 100644 --- a/test/event_watcher_test.ts +++ b/test/event_watcher_test.ts @@ -24,7 +24,7 @@ describe('EventWatcher', () => { let eventWatcher: EventWatcher; let web3Wrapper: Web3Wrapper; const numConfirmations = 0; - const logA = { + const logA: Web3.LogEntry = { address: '0x71d271f8b14adef568f8f28f1587ce7271ac4ca5', blockHash: null, blockNumber: null, @@ -32,9 +32,9 @@ describe('EventWatcher', () => { logIndex: null, topics: [], transactionHash: '0x004881d38cd4a8f72f1a0d68c8b9b8124504706041ff37019c1d1ed6bfda8e17', - transactionIndex: null, + transactionIndex: 0, }; - const logB = { + const logB: Web3.LogEntry = { address: '0x8d12a197cb00d4747a1fe03395095ce2a5cc6819', blockHash: null, blockNumber: null, @@ -42,9 +42,9 @@ describe('EventWatcher', () => { logIndex: null, topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ], transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25', - transactionIndex: null, + transactionIndex: 0, }; - const logC = { + const logC: Web3.LogEntry = { address: '0x1d271f8b174adef58f1587ce68f8f27271ac4ca5', blockHash: null, blockNumber: null, @@ -52,7 +52,7 @@ describe('EventWatcher', () => { logIndex: null, topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ], transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25', - transactionIndex: null, + transactionIndex: 0, }; before(async () => { web3 = web3Factory.create(); diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 269956400..d8ac0af49 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -67,17 +67,17 @@ describe('OrderStateWatcher', () => { ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); zeroEx.orderStateWatcher.addOrder(signedOrder); - expect((zeroEx.orderStateWatcher as any)._orders).to.include({ + 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); - expect((zeroEx.orderStateWatcher as any)._orders).to.not.include({ + expect((zeroEx.orderStateWatcher as any)._orderByOrderHash).to.not.include({ [orderHash]: signedOrder, }); dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes; - expect(dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress]).to.not.have.keys(orderHash); + expect(dependentOrderHashes[signedOrder.maker]).to.be.undefined(); }); it('should no-op when removing a non-existing order', async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( diff --git a/test/token_wrapper_test.ts b/test/token_wrapper_test.ts index 2f6f126c1..23020c47a 100644 --- a/test/token_wrapper_test.ts +++ b/test/token_wrapper_test.ts @@ -162,7 +162,7 @@ describe('TokenWrapper', () => { const token = tokens[0]; const ownerAddress = coinbase; const balance = await zeroEx.token.getBalanceAsync(token.address, ownerAddress); - const expectedBalance = new BigNumber('100000000000000000000000000'); + const expectedBalance = new BigNumber('1000000000000000000000000000'); return expect(balance).to.be.bignumber.equal(expectedBalance); }); it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => { @@ -190,7 +190,7 @@ describe('TokenWrapper', () => { const token = tokens[0]; const ownerAddress = coinbase; const balance = await zeroExWithoutAccounts.token.getBalanceAsync(token.address, ownerAddress); - const expectedBalance = new BigNumber('100000000000000000000000000'); + const expectedBalance = new BigNumber('1000000000000000000000000000'); return expect(balance).to.be.bignumber.equal(expectedBalance); }); }); |