diff options
Diffstat (limited to 'packages/0x.js/src/order_watcher')
4 files changed, 558 insertions, 558 deletions
diff --git a/packages/0x.js/src/order_watcher/event_watcher.ts b/packages/0x.js/src/order_watcher/event_watcher.ts index 5d05bfb60..3e3cd978d 100644 --- a/packages/0x.js/src/order_watcher/event_watcher.ts +++ b/packages/0x.js/src/order_watcher/event_watcher.ts @@ -9,8 +9,8 @@ import { assert } from '../utils/assert'; const DEFAULT_EVENT_POLLING_INTERVAL_MS = 200; enum LogEventState { - Removed, - Added, + Removed, + Added, } /* @@ -18,76 +18,76 @@ enum LogEventState { * depth. */ export class EventWatcher { - private _web3Wrapper: Web3Wrapper; - private _pollingIntervalMs: number; - private _intervalIdIfExists?: NodeJS.Timer; - private _lastEvents: Web3.LogEntry[] = []; - constructor(web3Wrapper: Web3Wrapper, pollingIntervalIfExistsMs: undefined | number) { - this._web3Wrapper = web3Wrapper; - this._pollingIntervalMs = _.isUndefined(pollingIntervalIfExistsMs) - ? DEFAULT_EVENT_POLLING_INTERVAL_MS - : pollingIntervalIfExistsMs; - } - public subscribe(callback: EventWatcherCallback): void { - assert.isFunction('callback', callback); - if (!_.isUndefined(this._intervalIdIfExists)) { - throw new Error(ZeroExError.SubscriptionAlreadyPresent); - } - this._intervalIdIfExists = intervalUtils.setAsyncExcludingInterval( - this._pollForBlockchainEventsAsync.bind(this, callback), - this._pollingIntervalMs, - (err: Error) => { - this.unsubscribe(); - callback(err); - }, - ); - } - public unsubscribe(): void { - this._lastEvents = []; - if (!_.isUndefined(this._intervalIdIfExists)) { - intervalUtils.clearAsyncExcludingInterval(this._intervalIdIfExists); - delete this._intervalIdIfExists; - } - } - private async _pollForBlockchainEventsAsync(callback: EventWatcherCallback): Promise<void> { - const pendingEvents = await this._getEventsAsync(); - if (_.isUndefined(pendingEvents)) { - // HACK: This should never happen, but happens frequently on CI due to a ganache bug - return; - } - if (pendingEvents.length === 0) { - // HACK: Sometimes when node rebuilds the pending block we get back the empty result. - // We don't want to emit a lot of removal events and bring them back after a couple of miliseconds, - // that's why we just ignore those cases. - return; - } - const removedEvents = _.differenceBy(this._lastEvents, pendingEvents, JSON.stringify); - const newEvents = _.differenceBy(pendingEvents, this._lastEvents, JSON.stringify); - await this._emitDifferencesAsync(removedEvents, LogEventState.Removed, callback); - await this._emitDifferencesAsync(newEvents, LogEventState.Added, callback); - this._lastEvents = pendingEvents; - } - private async _getEventsAsync(): Promise<Web3.LogEntry[]> { - const eventFilter = { - fromBlock: BlockParamLiteral.Pending, - toBlock: BlockParamLiteral.Pending, - }; - const events = await this._web3Wrapper.getLogsAsync(eventFilter); - return events; - } - private async _emitDifferencesAsync( - logs: Web3.LogEntry[], - logEventState: LogEventState, - callback: EventWatcherCallback, - ): Promise<void> { - for (const log of logs) { - const logEvent = { - removed: logEventState === LogEventState.Removed, - ...log, - }; - if (!_.isUndefined(this._intervalIdIfExists)) { - callback(null, logEvent); - } - } - } + private _web3Wrapper: Web3Wrapper; + private _pollingIntervalMs: number; + private _intervalIdIfExists?: NodeJS.Timer; + private _lastEvents: Web3.LogEntry[] = []; + constructor(web3Wrapper: Web3Wrapper, pollingIntervalIfExistsMs: undefined | number) { + this._web3Wrapper = web3Wrapper; + this._pollingIntervalMs = _.isUndefined(pollingIntervalIfExistsMs) + ? DEFAULT_EVENT_POLLING_INTERVAL_MS + : pollingIntervalIfExistsMs; + } + public subscribe(callback: EventWatcherCallback): void { + assert.isFunction('callback', callback); + if (!_.isUndefined(this._intervalIdIfExists)) { + throw new Error(ZeroExError.SubscriptionAlreadyPresent); + } + this._intervalIdIfExists = intervalUtils.setAsyncExcludingInterval( + this._pollForBlockchainEventsAsync.bind(this, callback), + this._pollingIntervalMs, + (err: Error) => { + this.unsubscribe(); + callback(err); + }, + ); + } + public unsubscribe(): void { + this._lastEvents = []; + if (!_.isUndefined(this._intervalIdIfExists)) { + intervalUtils.clearAsyncExcludingInterval(this._intervalIdIfExists); + delete this._intervalIdIfExists; + } + } + private async _pollForBlockchainEventsAsync(callback: EventWatcherCallback): Promise<void> { + const pendingEvents = await this._getEventsAsync(); + if (_.isUndefined(pendingEvents)) { + // HACK: This should never happen, but happens frequently on CI due to a ganache bug + return; + } + if (pendingEvents.length === 0) { + // HACK: Sometimes when node rebuilds the pending block we get back the empty result. + // We don't want to emit a lot of removal events and bring them back after a couple of miliseconds, + // that's why we just ignore those cases. + return; + } + const removedEvents = _.differenceBy(this._lastEvents, pendingEvents, JSON.stringify); + const newEvents = _.differenceBy(pendingEvents, this._lastEvents, JSON.stringify); + await this._emitDifferencesAsync(removedEvents, LogEventState.Removed, callback); + await this._emitDifferencesAsync(newEvents, LogEventState.Added, callback); + this._lastEvents = pendingEvents; + } + private async _getEventsAsync(): Promise<Web3.LogEntry[]> { + const eventFilter = { + fromBlock: BlockParamLiteral.Pending, + toBlock: BlockParamLiteral.Pending, + }; + const events = await this._web3Wrapper.getLogsAsync(eventFilter); + return events; + } + private async _emitDifferencesAsync( + logs: Web3.LogEntry[], + logEventState: LogEventState, + callback: EventWatcherCallback, + ): Promise<void> { + for (const log of logs) { + const logEvent = { + removed: logEventState === LogEventState.Removed, + ...log, + }; + if (!_.isUndefined(this._intervalIdIfExists)) { + callback(null, logEvent); + } + } + } } diff --git a/packages/0x.js/src/order_watcher/expiration_watcher.ts b/packages/0x.js/src/order_watcher/expiration_watcher.ts index 00b62162d..a08de94c0 100644 --- a/packages/0x.js/src/order_watcher/expiration_watcher.ts +++ b/packages/0x.js/src/order_watcher/expiration_watcher.ts @@ -13,63 +13,63 @@ const DEFAULT_ORDER_EXPIRATION_CHECKING_INTERVAL_MS = 50; * 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(callback: (orderHash: string) => void): void { - if (!_.isUndefined(this._orderExpirationCheckingIntervalIdIfExists)) { - throw new Error(ZeroExError.SubscriptionAlreadyPresent); - } - this._orderExpirationCheckingIntervalIdIfExists = intervalUtils.setInterval( - this._pruneExpiredOrders.bind(this, callback), - this._orderExpirationCheckingIntervalMs, - _.noop, // _pruneExpiredOrders never throws - ); - } - public unsubscribe(): void { - if (_.isUndefined(this._orderExpirationCheckingIntervalIdIfExists)) { - throw new Error(ZeroExError.SubscriptionNotFound); - } - intervalUtils.clearInterval(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 _pruneExpiredOrders(callback: (orderHash: string) => void): 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]; - callback(orderHash); - } - } + 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(callback: (orderHash: string) => void): void { + if (!_.isUndefined(this._orderExpirationCheckingIntervalIdIfExists)) { + throw new Error(ZeroExError.SubscriptionAlreadyPresent); + } + this._orderExpirationCheckingIntervalIdIfExists = intervalUtils.setInterval( + this._pruneExpiredOrders.bind(this, callback), + this._orderExpirationCheckingIntervalMs, + _.noop, // _pruneExpiredOrders never throws + ); + } + public unsubscribe(): void { + if (_.isUndefined(this._orderExpirationCheckingIntervalIdIfExists)) { + throw new Error(ZeroExError.SubscriptionNotFound); + } + intervalUtils.clearInterval(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 _pruneExpiredOrders(callback: (orderHash: string) => void): 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]; + callback(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 12ac60960..2c5da6b57 100644 --- a/packages/0x.js/src/order_watcher/order_state_watcher.ts +++ b/packages/0x.js/src/order_watcher/order_state_watcher.ts @@ -9,25 +9,25 @@ import { TokenWrapper } from '../contract_wrappers/token_wrapper'; import { BalanceAndProxyAllowanceLazyStore } from '../stores/balance_proxy_allowance_lazy_store'; import { OrderFilledCancelledLazyStore } from '../stores/order_filled_cancelled_lazy_store'; import { - ApprovalContractEventArgs, - BlockParamLiteral, - ContractEventArgs, - DepositContractEventArgs, - EtherTokenEvents, - ExchangeContractErrs, - ExchangeEvents, - LogCancelContractEventArgs, - LogEvent, - LogFillContractEventArgs, - LogWithDecodedArgs, - OnOrderStateChangeCallback, - OrderState, - OrderStateWatcherConfig, - SignedOrder, - TokenEvents, - TransferContractEventArgs, - WithdrawalContractEventArgs, - ZeroExError, + ApprovalContractEventArgs, + BlockParamLiteral, + ContractEventArgs, + DepositContractEventArgs, + EtherTokenEvents, + ExchangeContractErrs, + ExchangeEvents, + LogCancelContractEventArgs, + LogEvent, + LogFillContractEventArgs, + LogWithDecodedArgs, + OnOrderStateChangeCallback, + OrderState, + OrderStateWatcherConfig, + SignedOrder, + TokenEvents, + TransferContractEventArgs, + WithdrawalContractEventArgs, + ZeroExError, } from '../types'; import { AbiDecoder } from '../utils/abi_decoder'; import { assert } from '../utils/assert'; @@ -38,17 +38,17 @@ import { EventWatcher } from './event_watcher'; import { ExpirationWatcher } from './expiration_watcher'; interface DependentOrderHashes { - [makerAddress: string]: { - [makerToken: string]: Set<string>; - }; + [makerAddress: string]: { + [makerToken: string]: Set<string>; + }; } interface OrderByOrderHash { - [orderHash: string]: SignedOrder; + [orderHash: string]: SignedOrder; } interface OrderStateByOrderHash { - [orderHash: string]: OrderState; + [orderHash: string]: OrderState; } const DEFAULT_CLEANUP_JOB_INTERVAL_MS = 1000 * 60 * 60; // 1h @@ -60,319 +60,319 @@ const DEFAULT_CLEANUP_JOB_INTERVAL_MS = 1000 * 60 * 60; // 1h * the order should be deemed invalid. */ export class OrderStateWatcher { - private _orderStateByOrderHashCache: OrderStateByOrderHash = {}; - private _orderByOrderHash: OrderByOrderHash = {}; - private _dependentOrderHashes: DependentOrderHashes = {}; - private _callbackIfExists?: OnOrderStateChangeCallback; - private _eventWatcher: EventWatcher; - private _web3Wrapper: Web3Wrapper; - private _abiDecoder: AbiDecoder; - private _expirationWatcher: ExpirationWatcher; - private _orderStateUtils: OrderStateUtils; - private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; - private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; - private _cleanupJobInterval: number; - private _cleanupJobIntervalIdIfExists?: NodeJS.Timer; - constructor( - web3Wrapper: Web3Wrapper, - abiDecoder: AbiDecoder, - token: TokenWrapper, - exchange: ExchangeWrapper, - config?: OrderStateWatcherConfig, - ) { - this._abiDecoder = abiDecoder; - this._web3Wrapper = web3Wrapper; - const pollingIntervalIfExistsMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; - this._eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalIfExistsMs); - this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( - token, - BlockParamLiteral.Pending, - ); - 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, - ); - this._cleanupJobInterval = - _.isUndefined(config) || _.isUndefined(config.cleanupJobIntervalMs) - ? DEFAULT_CLEANUP_JOB_INTERVAL_MS - : config.cleanupJobIntervalMs; - } - /** - * 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._orderByOrderHash[orderHash] = signedOrder; - this._addToDependentOrderHashes(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 { - assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); - const signedOrder = this._orderByOrderHash[orderHash]; - if (_.isUndefined(signedOrder)) { - return; // noop - } - delete this._orderByOrderHash[orderHash]; - delete this._orderStateByOrderHashCache[orderHash]; - const exchange = (this._orderFilledCancelledLazyStore as any)._exchange as ExchangeWrapper; - const zrxTokenAddress = exchange.getZRXTokenAddress(); - 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 - * 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. - */ - public subscribe(callback: OnOrderStateChangeCallback): void { - assert.isFunction('callback', callback); - if (!_.isUndefined(this._callbackIfExists)) { - throw new Error(ZeroExError.SubscriptionAlreadyPresent); - } - this._callbackIfExists = callback; - this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this)); - this._expirationWatcher.subscribe(this._onOrderExpired.bind(this)); - this._cleanupJobIntervalIdIfExists = intervalUtils.setAsyncExcludingInterval( - this._cleanupAsync.bind(this), - this._cleanupJobInterval, - (err: Error) => { - this.unsubscribe(); - callback(err); - }, - ); - } - /** - * Ends an orderStateWatcher subscription. - */ - public unsubscribe(): void { - if (_.isUndefined(this._callbackIfExists) || _.isUndefined(this._cleanupJobIntervalIdIfExists)) { - throw new Error(ZeroExError.SubscriptionNotFound); - } - this._balanceAndProxyAllowanceLazyStore.deleteAll(); - this._orderFilledCancelledLazyStore.deleteAll(); - delete this._callbackIfExists; - this._eventWatcher.unsubscribe(); - this._expirationWatcher.unsubscribe(); - intervalUtils.clearAsyncExcludingInterval(this._cleanupJobIntervalIdIfExists); - } - private async _cleanupAsync(): Promise<void> { - for (const orderHash of _.keys(this._orderByOrderHash)) { - this._cleanupOrderRelatedState(orderHash); - await this._emitRevalidateOrdersAsync([orderHash]); - } - } - private _cleanupOrderRelatedState(orderHash: string): void { - const signedOrder = this._orderByOrderHash[orderHash]; + private _orderStateByOrderHashCache: OrderStateByOrderHash = {}; + private _orderByOrderHash: OrderByOrderHash = {}; + private _dependentOrderHashes: DependentOrderHashes = {}; + private _callbackIfExists?: OnOrderStateChangeCallback; + private _eventWatcher: EventWatcher; + private _web3Wrapper: Web3Wrapper; + private _abiDecoder: AbiDecoder; + private _expirationWatcher: ExpirationWatcher; + private _orderStateUtils: OrderStateUtils; + private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; + private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; + private _cleanupJobInterval: number; + private _cleanupJobIntervalIdIfExists?: NodeJS.Timer; + constructor( + web3Wrapper: Web3Wrapper, + abiDecoder: AbiDecoder, + token: TokenWrapper, + exchange: ExchangeWrapper, + config?: OrderStateWatcherConfig, + ) { + this._abiDecoder = abiDecoder; + this._web3Wrapper = web3Wrapper; + const pollingIntervalIfExistsMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; + this._eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalIfExistsMs); + this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( + token, + BlockParamLiteral.Pending, + ); + 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, + ); + this._cleanupJobInterval = + _.isUndefined(config) || _.isUndefined(config.cleanupJobIntervalMs) + ? DEFAULT_CLEANUP_JOB_INTERVAL_MS + : config.cleanupJobIntervalMs; + } + /** + * 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._orderByOrderHash[orderHash] = signedOrder; + this._addToDependentOrderHashes(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 { + assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); + const signedOrder = this._orderByOrderHash[orderHash]; + if (_.isUndefined(signedOrder)) { + return; // noop + } + delete this._orderByOrderHash[orderHash]; + delete this._orderStateByOrderHashCache[orderHash]; + const exchange = (this._orderFilledCancelledLazyStore as any)._exchange as ExchangeWrapper; + const zrxTokenAddress = exchange.getZRXTokenAddress(); + 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 + * 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. + */ + public subscribe(callback: OnOrderStateChangeCallback): void { + assert.isFunction('callback', callback); + if (!_.isUndefined(this._callbackIfExists)) { + throw new Error(ZeroExError.SubscriptionAlreadyPresent); + } + this._callbackIfExists = callback; + this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this)); + this._expirationWatcher.subscribe(this._onOrderExpired.bind(this)); + this._cleanupJobIntervalIdIfExists = intervalUtils.setAsyncExcludingInterval( + this._cleanupAsync.bind(this), + this._cleanupJobInterval, + (err: Error) => { + this.unsubscribe(); + callback(err); + }, + ); + } + /** + * Ends an orderStateWatcher subscription. + */ + public unsubscribe(): void { + if (_.isUndefined(this._callbackIfExists) || _.isUndefined(this._cleanupJobIntervalIdIfExists)) { + throw new Error(ZeroExError.SubscriptionNotFound); + } + this._balanceAndProxyAllowanceLazyStore.deleteAll(); + this._orderFilledCancelledLazyStore.deleteAll(); + delete this._callbackIfExists; + this._eventWatcher.unsubscribe(); + this._expirationWatcher.unsubscribe(); + intervalUtils.clearAsyncExcludingInterval(this._cleanupJobIntervalIdIfExists); + } + private async _cleanupAsync(): Promise<void> { + for (const orderHash of _.keys(this._orderByOrderHash)) { + this._cleanupOrderRelatedState(orderHash); + await this._emitRevalidateOrdersAsync([orderHash]); + } + } + private _cleanupOrderRelatedState(orderHash: string): void { + const signedOrder = this._orderByOrderHash[orderHash]; - this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(orderHash); - this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(orderHash); + this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(orderHash); + this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(orderHash); - this._balanceAndProxyAllowanceLazyStore.deleteBalance(signedOrder.makerTokenAddress, signedOrder.maker); - this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(signedOrder.makerTokenAddress, signedOrder.maker); - this._balanceAndProxyAllowanceLazyStore.deleteBalance(signedOrder.takerTokenAddress, signedOrder.taker); - this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(signedOrder.takerTokenAddress, signedOrder.taker); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(signedOrder.makerTokenAddress, signedOrder.maker); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(signedOrder.makerTokenAddress, signedOrder.maker); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(signedOrder.takerTokenAddress, signedOrder.taker); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(signedOrder.takerTokenAddress, signedOrder.taker); - const zrxTokenAddress = this._getZRXTokenAddress(); - if (!signedOrder.makerFee.isZero()) { - this._balanceAndProxyAllowanceLazyStore.deleteBalance(zrxTokenAddress, signedOrder.maker); - this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(zrxTokenAddress, signedOrder.maker); - } - if (!signedOrder.takerFee.isZero()) { - this._balanceAndProxyAllowanceLazyStore.deleteBalance(zrxTokenAddress, signedOrder.taker); - this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(zrxTokenAddress, signedOrder.taker); - } - } - private _onOrderExpired(orderHash: string): void { - const orderState: OrderState = { - isValid: false, - orderHash, - error: ExchangeContractErrs.OrderFillExpired, - }; - if (!_.isUndefined(this._orderByOrderHash[orderHash])) { - this.removeOrder(orderHash); - if (!_.isUndefined(this._callbackIfExists)) { - this._callbackIfExists(null, orderState); - } - } - } - private async _onEventWatcherCallbackAsync(err: Error | null, logIfExists?: LogEvent): Promise<void> { - if (!_.isNull(err)) { - if (!_.isUndefined(this._callbackIfExists)) { - this._callbackIfExists(err); - this.unsubscribe(); - } - return; - } - const log = logIfExists as LogEvent; // At this moment we are sure that no error occured and log is defined. - const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log); - const isLogDecoded = !_.isUndefined((maybeDecodedLog as LogWithDecodedArgs<any>).event); - if (!isLogDecoded) { - return; // noop - } - const decodedLog = maybeDecodedLog as LogWithDecodedArgs<ContractEventArgs>; - let makerToken: string; - let makerAddress: string; - switch (decodedLog.event) { - case TokenEvents.Approval: { - // Invalidate cache - const args = decodedLog.args as ApprovalContractEventArgs; - this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(decodedLog.address, args._owner); - // Revalidate orders - makerToken = decodedLog.address; - makerAddress = args._owner; - if ( - !_.isUndefined(this._dependentOrderHashes[makerAddress]) && - !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken]) - ) { - const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]); - await this._emitRevalidateOrdersAsync(orderHashes); - } - break; - } - case TokenEvents.Transfer: { - // Invalidate cache - const args = decodedLog.args as TransferContractEventArgs; - this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._from); - this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._to); - // Revalidate orders - makerToken = decodedLog.address; - makerAddress = args._from; - if ( - !_.isUndefined(this._dependentOrderHashes[makerAddress]) && - !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken]) - ) { - const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]); - await this._emitRevalidateOrdersAsync(orderHashes); - } - break; - } - case EtherTokenEvents.Deposit: { - // Invalidate cache - const args = decodedLog.args as DepositContractEventArgs; - this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._owner); - // Revalidate orders - makerToken = decodedLog.address; - makerAddress = args._owner; - if ( - !_.isUndefined(this._dependentOrderHashes[makerAddress]) && - !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken]) - ) { - const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]); - await this._emitRevalidateOrdersAsync(orderHashes); - } - break; - } - case EtherTokenEvents.Withdrawal: { - // Invalidate cache - const args = decodedLog.args as WithdrawalContractEventArgs; - this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._owner); - // Revalidate orders - makerToken = decodedLog.address; - makerAddress = args._owner; - if ( - !_.isUndefined(this._dependentOrderHashes[makerAddress]) && - !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken]) - ) { - const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]); - await this._emitRevalidateOrdersAsync(orderHashes); - } - break; - } - case ExchangeEvents.LogFill: { - // Invalidate cache - const args = decodedLog.args as LogFillContractEventArgs; - this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(args.orderHash); - // Revalidate orders - const orderHash = args.orderHash; - const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); - if (isOrderWatched) { - await this._emitRevalidateOrdersAsync([orderHash]); - } - break; - } - case ExchangeEvents.LogCancel: { - // Invalidate cache - const args = decodedLog.args as LogCancelContractEventArgs; - this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(args.orderHash); - // Revalidate orders - const orderHash = args.orderHash; - const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); - if (isOrderWatched) { - await this._emitRevalidateOrdersAsync([orderHash]); - } - break; - } - case ExchangeEvents.LogError: - return; // noop + const zrxTokenAddress = this._getZRXTokenAddress(); + if (!signedOrder.makerFee.isZero()) { + this._balanceAndProxyAllowanceLazyStore.deleteBalance(zrxTokenAddress, signedOrder.maker); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(zrxTokenAddress, signedOrder.maker); + } + if (!signedOrder.takerFee.isZero()) { + this._balanceAndProxyAllowanceLazyStore.deleteBalance(zrxTokenAddress, signedOrder.taker); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(zrxTokenAddress, signedOrder.taker); + } + } + private _onOrderExpired(orderHash: string): void { + const orderState: OrderState = { + isValid: false, + orderHash, + error: ExchangeContractErrs.OrderFillExpired, + }; + if (!_.isUndefined(this._orderByOrderHash[orderHash])) { + this.removeOrder(orderHash); + if (!_.isUndefined(this._callbackIfExists)) { + this._callbackIfExists(null, orderState); + } + } + } + private async _onEventWatcherCallbackAsync(err: Error | null, logIfExists?: LogEvent): Promise<void> { + if (!_.isNull(err)) { + if (!_.isUndefined(this._callbackIfExists)) { + this._callbackIfExists(err); + this.unsubscribe(); + } + return; + } + const log = logIfExists as LogEvent; // At this moment we are sure that no error occured and log is defined. + const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log); + const isLogDecoded = !_.isUndefined((maybeDecodedLog as LogWithDecodedArgs<any>).event); + if (!isLogDecoded) { + return; // noop + } + const decodedLog = maybeDecodedLog as LogWithDecodedArgs<ContractEventArgs>; + let makerToken: string; + let makerAddress: string; + switch (decodedLog.event) { + case TokenEvents.Approval: { + // Invalidate cache + const args = decodedLog.args as ApprovalContractEventArgs; + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(decodedLog.address, args._owner); + // Revalidate orders + makerToken = decodedLog.address; + makerAddress = args._owner; + if ( + !_.isUndefined(this._dependentOrderHashes[makerAddress]) && + !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken]) + ) { + const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]); + await this._emitRevalidateOrdersAsync(orderHashes); + } + break; + } + case TokenEvents.Transfer: { + // Invalidate cache + const args = decodedLog.args as TransferContractEventArgs; + this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._from); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._to); + // Revalidate orders + makerToken = decodedLog.address; + makerAddress = args._from; + if ( + !_.isUndefined(this._dependentOrderHashes[makerAddress]) && + !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken]) + ) { + const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]); + await this._emitRevalidateOrdersAsync(orderHashes); + } + break; + } + case EtherTokenEvents.Deposit: { + // Invalidate cache + const args = decodedLog.args as DepositContractEventArgs; + this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._owner); + // Revalidate orders + makerToken = decodedLog.address; + makerAddress = args._owner; + if ( + !_.isUndefined(this._dependentOrderHashes[makerAddress]) && + !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken]) + ) { + const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]); + await this._emitRevalidateOrdersAsync(orderHashes); + } + break; + } + case EtherTokenEvents.Withdrawal: { + // Invalidate cache + const args = decodedLog.args as WithdrawalContractEventArgs; + this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._owner); + // Revalidate orders + makerToken = decodedLog.address; + makerAddress = args._owner; + if ( + !_.isUndefined(this._dependentOrderHashes[makerAddress]) && + !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken]) + ) { + const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]); + await this._emitRevalidateOrdersAsync(orderHashes); + } + break; + } + case ExchangeEvents.LogFill: { + // Invalidate cache + const args = decodedLog.args as LogFillContractEventArgs; + this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(args.orderHash); + // Revalidate orders + const orderHash = args.orderHash; + const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); + if (isOrderWatched) { + await this._emitRevalidateOrdersAsync([orderHash]); + } + break; + } + case ExchangeEvents.LogCancel: { + // Invalidate cache + const args = decodedLog.args as LogCancelContractEventArgs; + this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(args.orderHash); + // Revalidate orders + const orderHash = args.orderHash; + const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); + if (isOrderWatched) { + await this._emitRevalidateOrdersAsync([orderHash]); + } + break; + } + case ExchangeEvents.LogError: + return; // noop - default: - throw utils.spawnSwitchErr('decodedLog.event', decodedLog.event); - } - } - private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise<void> { - for (const orderHash of orderHashes) { - const signedOrder = this._orderByOrderHash[orderHash]; - // 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._callbackIfExists)) { - break; // Unsubscribe was called - } - if (_.isEqual(orderState, this._orderStateByOrderHashCache[orderHash])) { - // Actual order state didn't change - continue; - } else { - this._orderStateByOrderHashCache[orderHash] = orderState; - } - this._callbackIfExists(null, orderState); - } - } - private _addToDependentOrderHashes(signedOrder: SignedOrder, orderHash: string): void { - if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker])) { - this._dependentOrderHashes[signedOrder.maker] = {}; - } - if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress])) { - this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress] = new Set(); - } - this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress].add(orderHash); - const zrxTokenAddress = this._getZRXTokenAddress(); - 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, 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]; - } - } - private _getZRXTokenAddress(): string { - const exchange = (this._orderFilledCancelledLazyStore as any)._exchange as ExchangeWrapper; - const zrxTokenAddress = exchange.getZRXTokenAddress(); - return zrxTokenAddress; - } + default: + throw utils.spawnSwitchErr('decodedLog.event', decodedLog.event); + } + } + private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise<void> { + for (const orderHash of orderHashes) { + const signedOrder = this._orderByOrderHash[orderHash]; + // 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._callbackIfExists)) { + break; // Unsubscribe was called + } + if (_.isEqual(orderState, this._orderStateByOrderHashCache[orderHash])) { + // Actual order state didn't change + continue; + } else { + this._orderStateByOrderHashCache[orderHash] = orderState; + } + this._callbackIfExists(null, orderState); + } + } + private _addToDependentOrderHashes(signedOrder: SignedOrder, orderHash: string): void { + if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker])) { + this._dependentOrderHashes[signedOrder.maker] = {}; + } + if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress])) { + this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress] = new Set(); + } + this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress].add(orderHash); + const zrxTokenAddress = this._getZRXTokenAddress(); + 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, 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]; + } + } + private _getZRXTokenAddress(): string { + const exchange = (this._orderFilledCancelledLazyStore as any)._exchange as ExchangeWrapper; + const zrxTokenAddress = exchange.getZRXTokenAddress(); + return zrxTokenAddress; + } } diff --git a/packages/0x.js/src/order_watcher/remaining_fillable_calculator.ts b/packages/0x.js/src/order_watcher/remaining_fillable_calculator.ts index 20b09d606..e010092af 100644 --- a/packages/0x.js/src/order_watcher/remaining_fillable_calculator.ts +++ b/packages/0x.js/src/order_watcher/remaining_fillable_calculator.ts @@ -3,94 +3,94 @@ import { BigNumber } from '@0xproject/utils'; import { SignedOrder } from '../types'; export class RemainingFillableCalculator { - private _signedOrder: SignedOrder; - private _isMakerTokenZRX: boolean; - // Transferrable Amount is the minimum of Approval and Balance - private _transferrableMakerTokenAmount: BigNumber; - private _transferrableMakerFeeTokenAmount: BigNumber; - private _remainingMakerTokenAmount: BigNumber; - private _remainingMakerFeeAmount: BigNumber; - constructor( - signedOrder: SignedOrder, - isMakerTokenZRX: boolean, - transferrableMakerTokenAmount: BigNumber, - transferrableMakerFeeTokenAmount: BigNumber, - remainingMakerTokenAmount: BigNumber, - ) { - this._signedOrder = signedOrder; - this._isMakerTokenZRX = isMakerTokenZRX; - this._transferrableMakerTokenAmount = transferrableMakerTokenAmount; - this._transferrableMakerFeeTokenAmount = transferrableMakerFeeTokenAmount; - this._remainingMakerTokenAmount = remainingMakerTokenAmount; - this._remainingMakerFeeAmount = remainingMakerTokenAmount - .times(signedOrder.makerFee) - .dividedToIntegerBy(signedOrder.makerTokenAmount); - } - public computeRemainingMakerFillable(): BigNumber { - if (this._hasSufficientFundsForFeeAndTransferAmount()) { - return this._remainingMakerTokenAmount; - } - if (this._signedOrder.makerFee.isZero()) { - return BigNumber.min(this._remainingMakerTokenAmount, this._transferrableMakerTokenAmount); - } - return this._calculatePartiallyFillableMakerTokenAmount(); - } - public computeRemainingTakerFillable(): BigNumber { - return this.computeRemainingMakerFillable() - .times(this._signedOrder.takerTokenAmount) - .dividedToIntegerBy(this._signedOrder.makerTokenAmount); - } - private _hasSufficientFundsForFeeAndTransferAmount(): boolean { - if (this._isMakerTokenZRX) { - const totalZRXTransferAmountRequired = this._remainingMakerTokenAmount.plus(this._remainingMakerFeeAmount); - const hasSufficientFunds = this._transferrableMakerTokenAmount.greaterThanOrEqualTo( - totalZRXTransferAmountRequired, - ); - return hasSufficientFunds; - } else { - const hasSufficientFundsForTransferAmount = this._transferrableMakerTokenAmount.greaterThanOrEqualTo( - this._remainingMakerTokenAmount, - ); - const hasSufficientFundsForFeeAmount = this._transferrableMakerFeeTokenAmount.greaterThanOrEqualTo( - this._remainingMakerFeeAmount, - ); - const hasSufficientFunds = hasSufficientFundsForTransferAmount && hasSufficientFundsForFeeAmount; - return hasSufficientFunds; - } - } - private _calculatePartiallyFillableMakerTokenAmount(): BigNumber { - // Given an order for 200 wei for 2 ZRXwei fee, find 100 wei for 1 ZRXwei. Order ratio is then 100:1 - const orderToFeeRatio = this._signedOrder.makerTokenAmount.dividedBy(this._signedOrder.makerFee); - // The number of times the maker can fill the order, if each fill only required the transfer of a single - // baseUnit of fee tokens. - // Given 2 ZRXwei, the maximum amount of times Maker can fill this order, in terms of fees, is 2 - const fillableTimesInFeeTokenBaseUnits = BigNumber.min( - this._transferrableMakerFeeTokenAmount, - this._remainingMakerFeeAmount, - ); - // The number of times the Maker can fill the order, given the Maker Token Balance - // Assuming a balance of 150 wei, and an orderToFeeRatio of 100:1, maker can fill this order 1 time. - let fillableTimesInMakerTokenUnits = this._transferrableMakerTokenAmount.dividedBy(orderToFeeRatio); - if (this._isMakerTokenZRX) { - // If ZRX is the maker token, the Fee and the Maker amount need to be removed from the same pool; - // 200 ZRXwei for 2ZRXwei fee can only be filled once (need 202 ZRXwei) - const totalZRXTokenPooled = this._transferrableMakerTokenAmount; - // The purchasing power here is less as the tokens are taken from the same Pool - // For every one number of fills, we have to take an extra ZRX out of the pool - fillableTimesInMakerTokenUnits = totalZRXTokenPooled.dividedBy(orderToFeeRatio.plus(new BigNumber(1))); - } - // When Ratio is not fully divisible there can be remainders which cannot be represented, so they are floored. - // This can result in a RoundingError being thrown by the Exchange Contract. - const partiallyFillableMakerTokenAmount = fillableTimesInMakerTokenUnits - .times(this._signedOrder.makerTokenAmount) - .dividedToIntegerBy(this._signedOrder.makerFee); - const partiallyFillableFeeTokenAmount = fillableTimesInFeeTokenBaseUnits - .times(this._signedOrder.makerTokenAmount) - .dividedToIntegerBy(this._signedOrder.makerFee); - const partiallyFillableAmount = BigNumber.min( - partiallyFillableMakerTokenAmount, - partiallyFillableFeeTokenAmount, - ); - return partiallyFillableAmount; - } + private _signedOrder: SignedOrder; + private _isMakerTokenZRX: boolean; + // Transferrable Amount is the minimum of Approval and Balance + private _transferrableMakerTokenAmount: BigNumber; + private _transferrableMakerFeeTokenAmount: BigNumber; + private _remainingMakerTokenAmount: BigNumber; + private _remainingMakerFeeAmount: BigNumber; + constructor( + signedOrder: SignedOrder, + isMakerTokenZRX: boolean, + transferrableMakerTokenAmount: BigNumber, + transferrableMakerFeeTokenAmount: BigNumber, + remainingMakerTokenAmount: BigNumber, + ) { + this._signedOrder = signedOrder; + this._isMakerTokenZRX = isMakerTokenZRX; + this._transferrableMakerTokenAmount = transferrableMakerTokenAmount; + this._transferrableMakerFeeTokenAmount = transferrableMakerFeeTokenAmount; + this._remainingMakerTokenAmount = remainingMakerTokenAmount; + this._remainingMakerFeeAmount = remainingMakerTokenAmount + .times(signedOrder.makerFee) + .dividedToIntegerBy(signedOrder.makerTokenAmount); + } + public computeRemainingMakerFillable(): BigNumber { + if (this._hasSufficientFundsForFeeAndTransferAmount()) { + return this._remainingMakerTokenAmount; + } + if (this._signedOrder.makerFee.isZero()) { + return BigNumber.min(this._remainingMakerTokenAmount, this._transferrableMakerTokenAmount); + } + return this._calculatePartiallyFillableMakerTokenAmount(); + } + public computeRemainingTakerFillable(): BigNumber { + return this.computeRemainingMakerFillable() + .times(this._signedOrder.takerTokenAmount) + .dividedToIntegerBy(this._signedOrder.makerTokenAmount); + } + private _hasSufficientFundsForFeeAndTransferAmount(): boolean { + if (this._isMakerTokenZRX) { + const totalZRXTransferAmountRequired = this._remainingMakerTokenAmount.plus(this._remainingMakerFeeAmount); + const hasSufficientFunds = this._transferrableMakerTokenAmount.greaterThanOrEqualTo( + totalZRXTransferAmountRequired, + ); + return hasSufficientFunds; + } else { + const hasSufficientFundsForTransferAmount = this._transferrableMakerTokenAmount.greaterThanOrEqualTo( + this._remainingMakerTokenAmount, + ); + const hasSufficientFundsForFeeAmount = this._transferrableMakerFeeTokenAmount.greaterThanOrEqualTo( + this._remainingMakerFeeAmount, + ); + const hasSufficientFunds = hasSufficientFundsForTransferAmount && hasSufficientFundsForFeeAmount; + return hasSufficientFunds; + } + } + private _calculatePartiallyFillableMakerTokenAmount(): BigNumber { + // Given an order for 200 wei for 2 ZRXwei fee, find 100 wei for 1 ZRXwei. Order ratio is then 100:1 + const orderToFeeRatio = this._signedOrder.makerTokenAmount.dividedBy(this._signedOrder.makerFee); + // The number of times the maker can fill the order, if each fill only required the transfer of a single + // baseUnit of fee tokens. + // Given 2 ZRXwei, the maximum amount of times Maker can fill this order, in terms of fees, is 2 + const fillableTimesInFeeTokenBaseUnits = BigNumber.min( + this._transferrableMakerFeeTokenAmount, + this._remainingMakerFeeAmount, + ); + // The number of times the Maker can fill the order, given the Maker Token Balance + // Assuming a balance of 150 wei, and an orderToFeeRatio of 100:1, maker can fill this order 1 time. + let fillableTimesInMakerTokenUnits = this._transferrableMakerTokenAmount.dividedBy(orderToFeeRatio); + if (this._isMakerTokenZRX) { + // If ZRX is the maker token, the Fee and the Maker amount need to be removed from the same pool; + // 200 ZRXwei for 2ZRXwei fee can only be filled once (need 202 ZRXwei) + const totalZRXTokenPooled = this._transferrableMakerTokenAmount; + // The purchasing power here is less as the tokens are taken from the same Pool + // For every one number of fills, we have to take an extra ZRX out of the pool + fillableTimesInMakerTokenUnits = totalZRXTokenPooled.dividedBy(orderToFeeRatio.plus(new BigNumber(1))); + } + // When Ratio is not fully divisible there can be remainders which cannot be represented, so they are floored. + // This can result in a RoundingError being thrown by the Exchange Contract. + const partiallyFillableMakerTokenAmount = fillableTimesInMakerTokenUnits + .times(this._signedOrder.makerTokenAmount) + .dividedToIntegerBy(this._signedOrder.makerFee); + const partiallyFillableFeeTokenAmount = fillableTimesInFeeTokenBaseUnits + .times(this._signedOrder.makerTokenAmount) + .dividedToIntegerBy(this._signedOrder.makerFee); + const partiallyFillableAmount = BigNumber.min( + partiallyFillableMakerTokenAmount, + partiallyFillableFeeTokenAmount, + ); + return partiallyFillableAmount; + } } |