diff options
Diffstat (limited to 'packages/0x.js/src')
-rw-r--r-- | packages/0x.js/src/order_watcher/event_watcher.ts | 10 | ||||
-rw-r--r-- | packages/0x.js/src/order_watcher/expiration_watcher.ts | 76 | ||||
-rw-r--r-- | packages/0x.js/src/order_watcher/order_state_watcher.ts | 37 | ||||
-rw-r--r-- | packages/0x.js/src/types.ts | 7 | ||||
-rw-r--r-- | packages/0x.js/src/utils/order_validation_utils.ts | 4 | ||||
-rw-r--r-- | packages/0x.js/src/utils/utils.ts | 7 | ||||
-rw-r--r-- | packages/0x.js/src/web3_wrapper.ts | 4 |
7 files changed, 130 insertions, 15 deletions
diff --git a/packages/0x.js/src/order_watcher/event_watcher.ts b/packages/0x.js/src/order_watcher/event_watcher.ts index c39431f6d..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); 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 33fa69b1c..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]: { @@ -56,6 +57,7 @@ export class OrderStateWatcher { private _eventWatcher: EventWatcher; private _web3Wrapper: Web3Wrapper; private _abiDecoder: AbiDecoder; + private _expirationWatcher: ExpirationWatcher; private _orderStateUtils: OrderStateUtils; private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; @@ -65,13 +67,22 @@ 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 @@ -84,6 +95,8 @@ export class OrderStateWatcher { assert.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker); this._orderByOrderHash[orderHash] = signedOrder; await this.addToDependentOrderHashesAsync(signedOrder, orderHash); + const expirationUnixTimestampMs = signedOrder.expirationUnixTimestampSec.times(1000); + this._expirationWatcher.addOrder(orderHash, expirationUnixTimestampMs); } /** * Removes an order from the orderStateWatcher @@ -100,6 +113,7 @@ export class OrderStateWatcher { 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 @@ -114,6 +128,7 @@ export class OrderStateWatcher { } this._callbackIfExists = callback; this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this)); + this._expirationWatcher.subscribe(this._onOrderExpiredAsync.bind(this)); } /** * Ends an orderStateWatcher subscription. @@ -126,6 +141,20 @@ export class OrderStateWatcher { this._orderFilledCancelledLazyStore.deleteAll(); 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); diff --git a/packages/0x.js/src/types.ts b/packages/0x.js/src/types.ts index 39e5fa9f2..c3aabfd86 100644 --- a/packages/0x.js/src/types.ts +++ b/packages/0x.js/src/types.ts @@ -392,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; } /* 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 f6b6ca09a..7bd8ea093 100644 --- a/packages/0x.js/src/web3_wrapper.ts +++ b/packages/0x.js/src/web3_wrapper.ts @@ -50,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 { |