Look for relevant events in the decodedLogs and emit orderState events for orders impacted by the blockchain state changes
This commit is contained in:
committed by
Leonid Logvinov
parent
6f00c422c7
commit
e952c98ca8
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user