Look for relevant events in the decodedLogs and emit orderState events for orders impacted by the blockchain state changes

This commit is contained in:
Fabio Berger
2017-11-08 19:01:57 -05:00
committed by Leonid Logvinov
parent 6f00c422c7
commit e952c98ca8
2 changed files with 144 additions and 52 deletions

View File

@@ -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);
}
}

View File

@@ -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);
});
});