From e952c98ca835627063cb675931d8de11aee84e78 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Wed, 8 Nov 2017 19:01:57 -0500 Subject: Look for relevant events in the decodedLogs and emit orderState events for orders impacted by the blockchain state changes --- src/mempool/order_state_watcher.ts | 89 +++++++++++++++++++++++++++--- test/order_watcher_test.ts | 107 ++++++++++++++++++++++--------------- 2 files changed, 144 insertions(+), 52 deletions(-) diff --git a/src/mempool/order_state_watcher.ts b/src/mempool/order_state_watcher.ts index 3da48005d..436f86554 100644 --- a/src/mempool/order_state_watcher.ts +++ b/src/mempool/order_state_watcher.ts @@ -3,6 +3,7 @@ import {schemas} from '0x-json-schemas'; import {ZeroEx} from '../'; import {EventWatcher} from './event_watcher'; import {assert} from '../utils/assert'; +import {utils} from '../utils/utils'; import {artifacts} from '../artifacts'; import {AbiDecoder} from '../utils/abi_decoder'; import {OrderStateUtils} from '../utils/order_state_utils'; @@ -14,11 +15,24 @@ import { BlockParamLiteral, LogWithDecodedArgs, OnOrderStateChangeCallback, + ExchangeEvents, + TokenEvents, } from '../types'; import {Web3Wrapper} from '../web3_wrapper'; +interface DependentOrderHashes { + [makerAddress: string]: { + [makerToken: string]: Set, + }; +} + +interface OrderByOrderHash { + [orderHash: string]: SignedOrder; +} + export class OrderStateWatcher { - private _orders = new Map(); + private _orders: OrderByOrderHash; + private _dependentOrderHashes: DependentOrderHashes; private _web3Wrapper: Web3Wrapper; private _callbackAsync?: OnOrderStateChangeCallback; private _eventWatcher: EventWatcher; @@ -28,6 +42,8 @@ export class OrderStateWatcher { web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, orderStateUtils: OrderStateUtils, mempoolPollingIntervalMs?: number) { this._web3Wrapper = web3Wrapper; + this._orders = {}; + this._dependentOrderHashes = {}; this._eventWatcher = new EventWatcher( this._web3Wrapper, mempoolPollingIntervalMs, ); @@ -37,12 +53,18 @@ export class OrderStateWatcher { public addOrder(signedOrder: SignedOrder): void { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); const orderHash = ZeroEx.getOrderHashHex(signedOrder); - this._orders.set(orderHash, signedOrder); + this._orders[orderHash] = signedOrder; + this.addToDependentOrderHashes(signedOrder, orderHash); } public removeOrder(signedOrder: SignedOrder): void { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress])) { + return; // noop if user tries to remove order that wasn't added + } const orderHash = ZeroEx.getOrderHashHex(signedOrder); - this._orders.delete(orderHash); + 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 } public subscribe(callback: OnOrderStateChangeCallback): void { assert.isFunction('callback', callback); @@ -55,17 +77,59 @@ export class OrderStateWatcher { } private async _onMempoolEventCallbackAsync(log: LogEvent): Promise { const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log); - if (!_.isUndefined((maybeDecodedLog as LogWithDecodedArgs).event)) { - await this._revalidateOrdersAsync(); + const isDecodedLog = !_.isUndefined((maybeDecodedLog as LogWithDecodedArgs).event); + if (!isDecodedLog) { + return; // noop + } + const decodedLog = maybeDecodedLog as LogWithDecodedArgs; + let makerToken: string; + let makerAddress: string; + let orderHashesSet: Set; + switch (decodedLog.event) { + case TokenEvents.Approval: + makerToken = decodedLog.address; + makerAddress = decodedLog.args._owner; + orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]); + if (!_.isUndefined(orderHashesSet)) { + const orderHashes = Array.from(orderHashesSet); + await this._emitRevalidateOrdersAsync(orderHashes); + } + break; + + case TokenEvents.Transfer: + makerToken = decodedLog.address; + makerAddress = decodedLog.args._from; + orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]); + if (!_.isUndefined(orderHashesSet)) { + const orderHashes = Array.from(orderHashesSet); + await this._emitRevalidateOrdersAsync(orderHashes); + } + break; + + case ExchangeEvents.LogFill: + case ExchangeEvents.LogCancel: + const orderHash = decodedLog.args.orderHash; + const isOrderWatched = !_.isUndefined(this._orders[orderHash]); + if (isOrderWatched) { + await this._emitRevalidateOrdersAsync([orderHash]); + } + break; + + case ExchangeEvents.LogError: + return; // noop + + default: + throw utils.spawnSwitchErr('decodedLog.event', decodedLog.event); } } - private async _revalidateOrdersAsync(): Promise { + private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise { + // TODO: Make defaultBlock a passed in option const methodOpts = { defaultBlock: BlockParamLiteral.Pending, }; - const orderHashes = Array.from(this._orders.keys()); + for (const orderHash of orderHashes) { - const signedOrder = this._orders.get(orderHash) as SignedOrder; + const signedOrder = this._orders[orderHash] as SignedOrder; const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder, methodOpts); if (!_.isUndefined(this._callbackAsync)) { await this._callbackAsync(orderState); @@ -74,4 +138,13 @@ export class OrderStateWatcher { } } } + private addToDependentOrderHashes(signedOrder: SignedOrder, orderHash: string) { + 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); + } } diff --git a/test/order_watcher_test.ts b/test/order_watcher_test.ts index 3ce60d863..11138567c 100644 --- a/test/order_watcher_test.ts +++ b/test/order_watcher_test.ts @@ -1,31 +1,32 @@ 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 {Web3Wrapper} from '../src/web3_wrapper'; -import {OrderStateWatcher} from '../src/mempool/order_state_watcher'; +import { chaiSetup } from './utils/chai_setup'; +import { web3Factory } from './utils/web3_factory'; +import { Web3Wrapper } from '../src/web3_wrapper'; +import { OrderStateWatcher } from '../src/mempool/order_state_watcher'; import { Token, ZeroEx, LogEvent, DecodedLogEvent, OrderState, + SignedOrder, OrderStateValid, + OrderStateInvalid, + ExchangeContractErrs, } from '../src'; -import {TokenUtils} from './utils/token_utils'; -import {FillScenarios} from './utils/fill_scenarios'; -import {DoneCallback} from '../src/types'; +import { TokenUtils } from './utils/token_utils'; +import { FillScenarios } from './utils/fill_scenarios'; +import { DoneCallback } from '../src/types'; chaiSetup.configure(); const expect = chai.expect; -describe('EventWatcher', () => { +describe.only('EventWatcher', () => { let web3: Web3; - let stubs: Sinon.SinonStub[] = []; let zeroEx: ZeroEx; let tokens: Token[]; let tokenUtils: TokenUtils; @@ -38,22 +39,8 @@ describe('EventWatcher', () => { let maker: string; let taker: string; let web3Wrapper: Web3Wrapper; + let signedOrder: SignedOrder; const fillableAmount = new BigNumber(5); - const fakeLog = { - address: '0xcdb594a32b1cc3479d8746279712c39d18a07fc0', - blockHash: '0x2d5cec6e3239d40993b74008f684af82b69f238697832e4c4d58e0ba5a2fa99e', - blockNumber: '0x34', - data: '0x0000000000000000000000000000000000000000000000000000000000000028', - logIndex: '0x00', - topics: [ - '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', - '0x0000000000000000000000006ecbe1db9ef729cbe972c83fb886247691fb6beb', - '0x000000000000000000000000871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c', - ], - transactionHash: '0xa550fbe937985c383ed7ed077cf6011960a3c2d38ea39dea209426546f0e95cb', - transactionIndex: '0x00', - type: 'mined', - }; before(async () => { web3 = web3Factory.create(); zeroEx = new ZeroEx(web3.currentProvider); @@ -67,36 +54,68 @@ describe('EventWatcher', () => { [makerToken, takerToken] = tokenUtils.getNonProtocolTokens(); web3Wrapper = (zeroEx as any)._web3Wrapper; }); - beforeEach(() => { - const getLogsStub = Sinon.stub(web3Wrapper, 'getLogsAsync'); - getLogsStub.onCall(0).returns([fakeLog]); - }); - afterEach(() => { - // clean up any stubs after the test has completed - _.each(stubs, s => s.restore()); - stubs = []; + afterEach(async () => { zeroEx.orderStateWatcher.unsubscribe(); + zeroEx.orderStateWatcher.removeOrder(signedOrder); }); - it('should receive OrderState when order state is changed', (done: DoneCallback) => { + it('should emit orderStateInvalid when maker allowance set to 0 for watched order', (done: DoneCallback) => { (async () => { - const signedOrder = await fillScenarios.createFillableSignedOrderAsync( + signedOrder = await fillScenarios.createFillableSignedOrderAsync( makerToken.address, takerToken.address, maker, taker, fillableAmount, ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); zeroEx.orderStateWatcher.addOrder(signedOrder); const callback = (orderState: OrderState) => { - expect(orderState.isValid).to.be.true(); - expect(orderState.orderHash).to.be.equal(orderHash); - const orderRelevantState = (orderState as OrderStateValid).orderRelevantState; - expect(orderRelevantState.makerBalance).to.be.bignumber.equal(fillableAmount); - expect(orderRelevantState.makerProxyAllowance).to.be.bignumber.equal(fillableAmount); - expect(orderRelevantState.makerFeeBalance).to.be.bignumber.equal(0); - expect(orderRelevantState.makerFeeProxyAllowance).to.be.bignumber.equal(0); - expect(orderRelevantState.filledTakerTokenAmount).to.be.bignumber.equal(0); - expect(orderRelevantState.canceledTakerTokenAmount).to.be.bignumber.equal(0); + expect(orderState.isValid).to.be.false(); + const invalidOrderState = orderState as OrderStateInvalid; + expect(invalidOrderState.orderHash).to.be.equal(orderHash); + expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerAllowance); done(); }; zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0)); + })().catch(done); + }); + it('should emit orderStateInvalid when maker moves balance backing watched order', (done: DoneCallback) => { + (async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerToken.address, takerToken.address, maker, taker, fillableAmount, + ); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + zeroEx.orderStateWatcher.addOrder(signedOrder); + const callback = (orderState: OrderState) => { + expect(orderState.isValid).to.be.false(); + const invalidOrderState = orderState as OrderStateInvalid; + expect(invalidOrderState.orderHash).to.be.equal(orderHash); + expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerBalance); + done(); + }; + zeroEx.orderStateWatcher.subscribe(callback); + const anyRecipient = taker; + const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); + await zeroEx.token.transferAsync(makerToken.address, maker, anyRecipient, makerBalance); + })().catch(done); + }); + it('should emit orderStateInvalid when watched order fully filled', (done: DoneCallback) => { + (async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerToken.address, takerToken.address, maker, taker, fillableAmount, + ); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + zeroEx.orderStateWatcher.addOrder(signedOrder); + const callback = (orderState: OrderState) => { + expect(orderState.isValid).to.be.false(); + const invalidOrderState = orderState as OrderStateInvalid; + expect(invalidOrderState.orderHash).to.be.equal(orderHash); + expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderRemainingFillAmountZero); + done(); + }; + zeroEx.orderStateWatcher.subscribe(callback); + + const shouldThrowOnInsufficientBalanceOrAllowance = true; + await zeroEx.exchange.fillOrderAsync( + signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, taker, + ); })().catch(done); }); }); -- cgit v1.2.3