aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/mempool/order_state_watcher.ts89
-rw-r--r--test/order_watcher_test.ts107
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<string>,
+ };
+}
+
+interface OrderByOrderHash {
+ [orderHash: string]: SignedOrder;
+}
+
export class OrderStateWatcher {
- private _orders = new Map<string, SignedOrder>();
+ 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<void> {
const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log);
- if (!_.isUndefined((maybeDecodedLog as LogWithDecodedArgs<any>).event)) {
- await this._revalidateOrdersAsync();
+ const isDecodedLog = !_.isUndefined((maybeDecodedLog as LogWithDecodedArgs<any>).event);
+ if (!isDecodedLog) {
+ return; // noop
+ }
+ const decodedLog = maybeDecodedLog as LogWithDecodedArgs<any>;
+ let makerToken: string;
+ let makerAddress: string;
+ let orderHashesSet: Set<string>;
+ 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<void> {
+ private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise<void> {
+ // 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);
});
});