From 92df3d953f3aabbee22635c11f0e8ac794d09b19 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Tue, 7 Nov 2017 18:02:40 -0500 Subject: Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ede566299..156fb1222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +v0.22.5 - _November 7, 2017_ +------------------------ + * Re-publish v0.22.4 to fix publishing issue + v0.22.4 - _October 25, 2017_ ------------------------ * Upgraded bignumber.js to a new version that ships with native typings -- cgit v1.2.3 From a7bedad9f020cf0bbd91d3823a14a0711ea78e0b Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Tue, 7 Nov 2017 18:02:57 -0500 Subject: 0.22.5 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd0e32e66..c3c4962e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "0x.js", - "version": "0.22.4", + "version": "0.22.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8e1cfd502..aa22ba657 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "0x.js", - "version": "0.22.4", + "version": "0.22.5", "description": "A javascript library for interacting with the 0x protocol", "keywords": [ "0x.js", -- cgit v1.2.3 From cb3cae0f30404abffc238e47e17e0c842f720c97 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 26 Oct 2017 12:50:02 +0300 Subject: Add initial mempool watching implememtation --- package.json | 4 +-- src/0x.ts | 10 +++++++ src/index.ts | 2 ++ src/mempool/mempool_watcher.ts | 64 ++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 24 ++++++++-------- test/exchange_wrapper_test.ts | 24 +++++++++------- test/token_wrapper_test.ts | 11 ++++---- yarn.lock | 8 +++--- 8 files changed, 115 insertions(+), 32 deletions(-) create mode 100644 src/mempool/mempool_watcher.ts diff --git a/package.json b/package.json index aa22ba657..dcba9f452 100644 --- a/package.json +++ b/package.json @@ -78,14 +78,14 @@ "sinon": "^4.0.0", "source-map-support": "^0.5.0", "truffle-hdwallet-provider": "^0.0.3", - "tslint": "^5.3.2", + "tslint": "~5.5.0", "tslint-config-0xproject": "^0.0.2", "typedoc": "~0.8.0", "types-bn": "^0.0.1", "types-ethereumjs-util": "0xProject/types-ethereumjs-util", "typescript": "^2.4.1", "web3-provider-engine": "^13.0.1", - "web3-typescript-typings": "^0.6.2", + "web3-typescript-typings": "^0.7.0", "webpack": "^3.1.0" }, "dependencies": { diff --git a/src/0x.ts b/src/0x.ts index bc753434c..10db7e158 100644 --- a/src/0x.ts +++ b/src/0x.ts @@ -11,6 +11,7 @@ import {assert} from './utils/assert'; import {AbiDecoder} from './utils/abi_decoder'; import {intervalUtils} from './utils/interval_utils'; import {artifacts} from './artifacts'; +import {MempoolWatcher} from './mempool/mempool_watcher'; import {ExchangeWrapper} from './contract_wrappers/exchange_wrapper'; import {TokenRegistryWrapper} from './contract_wrappers/token_registry_wrapper'; import {EtherTokenWrapper} from './contract_wrappers/ether_token_wrapper'; @@ -65,6 +66,10 @@ export class ZeroEx { * tokenTransferProxy smart contract. */ public proxy: TokenTransferProxyWrapper; + /** + * An instance of the MempoolWatcher class containing methods for watching pending events. + */ + public mempool: MempoolWatcher; private _web3Wrapper: Web3Wrapper; private _abiDecoder: AbiDecoder; /** @@ -191,6 +196,11 @@ export class ZeroEx { gasPrice, }; this._web3Wrapper = new Web3Wrapper(provider, defaults); + const mempoolPollingIntervalMs = _.isUndefined(config) ? undefined : config.mempoolPollingIntervalMs; + this.mempool = new MempoolWatcher( + this._web3Wrapper, + mempoolPollingIntervalMs, + ); this.token = new TokenWrapper( this._web3Wrapper, this._abiDecoder, diff --git a/src/index.ts b/src/index.ts index 249c20519..7a9b8aa63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,4 +35,6 @@ export { OrderTransactionOpts, FilterObject, LogEvent, + DecodedLogEvent, + MempoolEventCallback, } from './types'; diff --git a/src/mempool/mempool_watcher.ts b/src/mempool/mempool_watcher.ts new file mode 100644 index 000000000..be598c28f --- /dev/null +++ b/src/mempool/mempool_watcher.ts @@ -0,0 +1,64 @@ +import * as Web3 from 'web3'; +import * as _ from 'lodash'; +import {Web3Wrapper} from '../web3_wrapper'; +import {BlockParamLiteral, EventCallback, MempoolEventCallback} from '../types'; +import {AbiDecoder} from '../utils/abi_decoder'; +import {intervalUtils} from '../utils/interval_utils'; + +const DEFAULT_MEMPOOL_POLLING_INTERVAL = 200; + +export class MempoolWatcher { + private _web3Wrapper: Web3Wrapper; + private _pollingIntervalMs: number; + private _intervalId: NodeJS.Timer; + private _lastMempoolEvents: Web3.LogEntry[] = []; + private _callback?: MempoolEventCallback; + constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) { + this._web3Wrapper = web3Wrapper; + this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? + DEFAULT_MEMPOOL_POLLING_INTERVAL : + pollingIntervalMs; + } + public subscribe(callback: MempoolEventCallback): void { + this._callback = callback; + this._intervalId = intervalUtils.setAsyncExcludingInterval( + this._pollForMempoolEventsAsync.bind(this), this._pollingIntervalMs); + } + public unsubscribe(): void { + delete this._callback; + intervalUtils.clearAsyncExcludingInterval(this._intervalId); + } + private async _pollForMempoolEventsAsync(): Promise { + const pendingEvents = await this._getMempoolEventsAsync(); + 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._lastMempoolEvents, pendingEvents, _.isEqual); + const newEvents = _.differenceBy(pendingEvents, this._lastMempoolEvents, _.isEqual); + let isRemoved = true; + this._emitDifferences(removedEvents, isRemoved); + isRemoved = false; + this._emitDifferences(newEvents, isRemoved); + this._lastMempoolEvents = pendingEvents; + } + private async _getMempoolEventsAsync(): Promise { + const mempoolFilter = { + fromBlock: BlockParamLiteral.Pending, + toBlock: BlockParamLiteral.Pending, + }; + const pendingEvents = await this._web3Wrapper.getLogsAsync(mempoolFilter); + return pendingEvents; + } + private _emitDifferences(logs: Web3.LogEntry[], isRemoved: boolean): void { + _.forEach(logs, log => { + const logWithDecodedArgsEvent = { + removed: isRemoved, + ...log, + }; + (this._callback as MempoolEventCallback)(logWithDecodedArgsEvent); + }); + } +} diff --git a/src/types.ts b/src/types.ts index 9ac726ef8..1b32ccdf9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,12 +37,17 @@ export type OrderAddresses = [string, string, string, string, string]; export type OrderValues = [BigNumber, BigNumber, BigNumber, BigNumber, BigNumber, BigNumber]; -export interface LogEvent extends LogWithDecodedArgs { - removed: boolean; -} -export type EventCallbackAsync = (log: LogEvent) => Promise; -export type EventCallbackSync = (log: LogEvent) => void; +export type LogEvent = Web3.LogEntryEvent; +export type DecodedLogEvent = Web3.DecodedLogEntryEvent; + +export type EventCallbackAsync = (log: DecodedLogEvent) => Promise; +export type EventCallbackSync = (log: DecodedLogEvent) => void; export type EventCallback = EventCallbackSync|EventCallbackAsync; + +export type MempoolEventCallbackSync = (log: LogEvent) => void; +export type MempoolEventCallbackAsync = (log: LogEvent) => Promise; +export type MempoolEventCallback = MempoolEventCallbackSync|MempoolEventCallbackAsync; + export interface ExchangeContract extends Web3.ContractInstance { isValidSignature: { callAsync: (signerAddressHex: string, dataHex: string, v: number, r: string, s: string, @@ -394,12 +399,14 @@ export interface JSONRPCPayload { * exchangeContractAddress: The address of an exchange contract to use * tokenRegistryContractAddress: The address of a token registry contract to use * etherTokenContractAddress: The address of an ether token contract to use + * mempoolPollingIntervalMs: How often to check for new mempool events */ export interface ZeroExConfig { gasPrice?: BigNumber; // Gas price to use with every transaction exchangeContractAddress?: string; tokenRegistryContractAddress?: string; etherTokenContractAddress?: string; + mempoolPollingIntervalMs?: number; } export type TransactionReceipt = Web3.TransactionReceipt; @@ -415,12 +422,7 @@ export interface DecodedLogArgs { [argName: string]: ContractEventArg; } -export interface DecodedArgs { - args: ArgsType; - event: string; -} - -export interface LogWithDecodedArgs extends Web3.LogEntry, DecodedArgs {} +export interface LogWithDecodedArgs extends Web3.DecodedLogEntry {} export interface TransactionReceiptWithDecodedLogs extends Web3.TransactionReceipt { logs: Array|Web3.LogEntry>; diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index 7c76499d5..15d1cb3e4 100644 --- a/test/exchange_wrapper_test.ts +++ b/test/exchange_wrapper_test.ts @@ -18,6 +18,7 @@ import { LogFillContractEventArgs, LogCancelContractEventArgs, LogEvent, + DecodedLogEvent, } from '../src'; import {DoneCallback, BlockParamLiteral} from '../src/types'; import {FillScenarios} from './utils/fill_scenarios'; @@ -304,11 +305,11 @@ describe('ExchangeWrapper', () => { orderFillBatch = [ { signedOrder, - takerTokenFillAmount: takerTokenFillAmount, + takerTokenFillAmount, }, { signedOrder: anotherSignedOrder, - takerTokenFillAmount: takerTokenFillAmount, + takerTokenFillAmount, }, ]; }); @@ -647,7 +648,7 @@ describe('ExchangeWrapper', () => { // Source: https://github.com/mochajs/mocha/issues/2407 it('Should receive the LogFill event when an order is filled', (done: DoneCallback) => { (async () => { - const callback = (logEvent: LogEvent) => { + const callback = (logEvent: DecodedLogEvent) => { expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill); done(); }; @@ -655,13 +656,14 @@ describe('ExchangeWrapper', () => { ExchangeEvents.LogFill, indexFilterValues, callback, ); await zeroEx.exchange.fillOrderAsync( - signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, ); })().catch(done); }); it('Should receive the LogCancel event when an order is cancelled', (done: DoneCallback) => { (async () => { - const callback = (logEvent: LogEvent) => { + const callback = (logEvent: DecodedLogEvent) => { expect(logEvent.event).to.be.equal(ExchangeEvents.LogCancel); done(); }; @@ -673,7 +675,7 @@ describe('ExchangeWrapper', () => { }); it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => { (async () => { - const callbackNeverToBeCalled = (logEvent: LogEvent) => { + const callbackNeverToBeCalled = (logEvent: DecodedLogEvent) => { done(new Error('Expected this subscription to have been cancelled')); }; await zeroEx.exchange.subscribeAsync( @@ -683,7 +685,7 @@ describe('ExchangeWrapper', () => { const newProvider = web3Factory.getRpcProvider(); await zeroEx.setProviderAsync(newProvider); - const callback = (logEvent: LogEvent) => { + const callback = (logEvent: DecodedLogEvent) => { expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill); done(); }; @@ -691,13 +693,14 @@ describe('ExchangeWrapper', () => { ExchangeEvents.LogFill, indexFilterValues, callback, ); await zeroEx.exchange.fillOrderAsync( - signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, ); })().catch(done); }); it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { (async () => { - const callbackNeverToBeCalled = (logEvent: LogEvent) => { + const callbackNeverToBeCalled = (logEvent: DecodedLogEvent) => { done(new Error('Expected this subscription to have been cancelled')); }; const subscriptionToken = await zeroEx.exchange.subscribeAsync( @@ -705,7 +708,8 @@ describe('ExchangeWrapper', () => { ); zeroEx.exchange.unsubscribe(subscriptionToken); await zeroEx.exchange.fillOrderAsync( - signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, + signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, ); done(); })().catch(done); diff --git a/test/token_wrapper_test.ts b/test/token_wrapper_test.ts index b35fa43f9..2f6f126c1 100644 --- a/test/token_wrapper_test.ts +++ b/test/token_wrapper_test.ts @@ -17,6 +17,7 @@ import { TokenContractEventArgs, LogWithDecodedArgs, LogEvent, + DecodedLogEvent, } from '../src'; import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; import {TokenUtils} from './utils/token_utils'; @@ -358,7 +359,7 @@ describe('TokenWrapper', () => { // Source: https://github.com/mochajs/mocha/issues/2407 it('Should receive the Transfer event when tokens are transfered', (done: DoneCallback) => { (async () => { - const callback = (logEvent: LogEvent) => { + const callback = (logEvent: DecodedLogEvent) => { expect(logEvent).to.not.be.undefined(); const args = logEvent.args; expect(args._from).to.be.equal(coinbase); @@ -373,7 +374,7 @@ describe('TokenWrapper', () => { }); it('Should receive the Approval event when allowance is being set', (done: DoneCallback) => { (async () => { - const callback = (logEvent: LogEvent) => { + const callback = (logEvent: DecodedLogEvent) => { expect(logEvent).to.not.be.undefined(); const args = logEvent.args; expect(args._owner).to.be.equal(coinbase); @@ -388,13 +389,13 @@ describe('TokenWrapper', () => { }); it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => { (async () => { - const callbackNeverToBeCalled = (logEvent: LogEvent) => { + const callbackNeverToBeCalled = (logEvent: DecodedLogEvent) => { done(new Error('Expected this subscription to have been cancelled')); }; zeroEx.token.subscribe( tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackNeverToBeCalled, ); - const callbackToBeCalled = (logEvent: LogEvent) => { + const callbackToBeCalled = (logEvent: DecodedLogEvent) => { done(); }; const newProvider = web3Factory.getRpcProvider(); @@ -407,7 +408,7 @@ describe('TokenWrapper', () => { }); it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { (async () => { - const callbackNeverToBeCalled = (logEvent: LogEvent) => { + const callbackNeverToBeCalled = (logEvent: DecodedLogEvent) => { done(new Error('Expected this subscription to have been cancelled')); }; const subscriptionToken = zeroEx.token.subscribe( diff --git a/yarn.lock b/yarn.lock index eefd9a429..65bfa7476 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4685,7 +4685,7 @@ tslint-react@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/tslint-react/-/tslint-react-3.0.0.tgz#00c48ab7f22e91533b62bdef2c162b49447af00a" -tslint@^5.3.2: +tslint@~5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.5.0.tgz#10e8dab3e3061fa61e9442e8cee3982acf20a6aa" dependencies: @@ -4940,9 +4940,9 @@ web3-provider-engine@^8.4.0: xhr "^2.2.0" xtend "^4.0.1" -web3-typescript-typings@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/web3-typescript-typings/-/web3-typescript-typings-0.6.2.tgz#5dd9bf4dcd1d6dd6897c87d055d1f5cc8f98dfbd" +web3-typescript-typings@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/web3-typescript-typings/-/web3-typescript-typings-0.7.0.tgz#a8adcfaa5f4933eddd53d9e592bace3edfffa050" dependencies: bignumber.js "^4.0.2" -- cgit v1.2.3 From a4e93558aa8f230159f37a8e9726cb001451c996 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 26 Oct 2017 14:02:36 +0300 Subject: Upgrade web3-typescript-typings --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index dcba9f452..4b23bf6e1 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "types-ethereumjs-util": "0xProject/types-ethereumjs-util", "typescript": "^2.4.1", "web3-provider-engine": "^13.0.1", - "web3-typescript-typings": "^0.7.0", + "web3-typescript-typings": "^0.7.1", "webpack": "^3.1.0" }, "dependencies": { diff --git a/yarn.lock b/yarn.lock index 65bfa7476..55f3f7d1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4940,9 +4940,9 @@ web3-provider-engine@^8.4.0: xhr "^2.2.0" xtend "^4.0.1" -web3-typescript-typings@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/web3-typescript-typings/-/web3-typescript-typings-0.7.0.tgz#a8adcfaa5f4933eddd53d9e592bace3edfffa050" +web3-typescript-typings@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/web3-typescript-typings/-/web3-typescript-typings-0.7.1.tgz#4b1145b9fd7e80292c2ab6b75e2359cf95f0efe1" dependencies: bignumber.js "^4.0.2" -- cgit v1.2.3 From f8179bc5a96456c4e5a7979b955caa76e675c4f7 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 26 Oct 2017 14:03:39 +0300 Subject: Compare logs by string representation --- src/mempool/mempool_watcher.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mempool/mempool_watcher.ts b/src/mempool/mempool_watcher.ts index be598c28f..7a485172d 100644 --- a/src/mempool/mempool_watcher.ts +++ b/src/mempool/mempool_watcher.ts @@ -36,8 +36,8 @@ export class MempoolWatcher { // that's why we just ignore those cases. return; } - const removedEvents = _.differenceBy(this._lastMempoolEvents, pendingEvents, _.isEqual); - const newEvents = _.differenceBy(pendingEvents, this._lastMempoolEvents, _.isEqual); + const removedEvents = _.differenceBy(this._lastMempoolEvents, pendingEvents, JSON.stringify); + const newEvents = _.differenceBy(pendingEvents, this._lastMempoolEvents, JSON.stringify); let isRemoved = true; this._emitDifferences(removedEvents, isRemoved); isRemoved = false; -- cgit v1.2.3 From 23d7d7d1400706488d50af2c648cbe9a73530562 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 26 Oct 2017 14:03:54 +0300 Subject: Don't emit new events if already unsubscribed --- src/mempool/mempool_watcher.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mempool/mempool_watcher.ts b/src/mempool/mempool_watcher.ts index 7a485172d..8314afb0c 100644 --- a/src/mempool/mempool_watcher.ts +++ b/src/mempool/mempool_watcher.ts @@ -58,7 +58,9 @@ export class MempoolWatcher { removed: isRemoved, ...log, }; - (this._callback as MempoolEventCallback)(logWithDecodedArgsEvent); + if (!_.isUndefined(this._callback)) { + this._callback(logWithDecodedArgsEvent); + } }); } } -- cgit v1.2.3 From cea2fb0fe6014959def51b20ae574e3b6f547e49 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 26 Oct 2017 14:29:57 +0300 Subject: Add mempool tests --- test/mempool_test.ts | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 test/mempool_test.ts diff --git a/test/mempool_test.ts b/test/mempool_test.ts new file mode 100644 index 000000000..0c8fb921a --- /dev/null +++ b/test/mempool_test.ts @@ -0,0 +1,125 @@ +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 { + ZeroEx, + LogEvent, + DecodedLogEvent, +} from '../src'; +import {DoneCallback} from '../src/types'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('MempoolWatcher', () => { + let web3: Web3; + let zeroEx: ZeroEx; + let stubs: Sinon.SinonStub[] = []; + const logA = { + address: '0x71d271f8b14adef568f8f28f1587ce7271ac4ca5', + blockHash: null, + blockNumber: null, + data: '', + logIndex: null, + topics: [], + transactionHash: '0x004881d38cd4a8f72f1a0d68c8b9b8124504706041ff37019c1d1ed6bfda8e17', + transactionIndex: null, + }; + const logB = { + address: '0x8d12a197cb00d4747a1fe03395095ce2a5cc6819', + blockHash: null, + blockNumber: null, + data: '', + logIndex: null, + topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ], + transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25', + transactionIndex: null, + }; + const logC = { + address: '0x1d271f8b174adef58f1587ce68f8f27271ac4ca5', + blockHash: null, + blockNumber: null, + data: '', + logIndex: null, + topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ], + transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25', + transactionIndex: null, + }; + before(async () => { + web3 = web3Factory.create(); + const config = { + mempoolPollingIntervalMs: 10, + }; + zeroEx = new ZeroEx(web3.currentProvider, config); + }); + afterEach(() => { + // clean up any stubs after the test has completed + _.each(stubs, s => s.restore()); + stubs = []; + zeroEx.mempool.unsubscribe(); + }); + it('correctly emits initial log events', (done: DoneCallback) => { + const logs: Web3.LogEntry[] = [logA, logB]; + const expectedLogEvents = [ + { + removed: false, + ...logA, + }, + { + removed: false, + ...logB, + }, + ]; + const getLogsStub = Sinon.stub((zeroEx.mempool as any)._web3Wrapper, 'getLogsAsync'); + getLogsStub.onCall(0).returns(logs); + stubs.push(getLogsStub); + const callback = (event: LogEvent) => { + const expectedLogEvent = expectedLogEvents.shift(); + expect(event).to.be.deep.equal(expectedLogEvent); + if (_.isEmpty(expectedLogEvents)) { + done(); + } + }; + zeroEx.mempool.subscribe(callback); + }); + it('correctly computes the difference and emits only changes', (done: DoneCallback) => { + const initialLogs: Web3.LogEntry[] = [logA, logB]; + const changedLogs: Web3.LogEntry[] = [logA, logC]; + const expectedLogEvents = [ + { + removed: false, + ...logA, + }, + { + removed: false, + ...logB, + }, + { + removed: true, + ...logB, + }, + { + removed: false, + ...logC, + }, + ]; + const getLogsStub = Sinon.stub((zeroEx.mempool as any)._web3Wrapper, 'getLogsAsync'); + getLogsStub.onCall(0).returns(initialLogs); + getLogsStub.onCall(1).returns(changedLogs); + stubs.push(getLogsStub); + const callback = (event: LogEvent) => { + // console.log(event); + const expectedLogEvent = expectedLogEvents.shift(); + expect(event).to.be.deep.equal(expectedLogEvent); + if (_.isEmpty(expectedLogEvents)) { + done(); + } + }; + zeroEx.mempool.subscribe(callback); + }); +}); -- cgit v1.2.3 From a2ffd7de2ec718b87dcaee950160f000d9e3d28f Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 26 Oct 2017 17:36:42 +0300 Subject: Fix namings --- src/mempool/mempool_watcher.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mempool/mempool_watcher.ts b/src/mempool/mempool_watcher.ts index 8314afb0c..70d263fcb 100644 --- a/src/mempool/mempool_watcher.ts +++ b/src/mempool/mempool_watcher.ts @@ -54,12 +54,12 @@ export class MempoolWatcher { } private _emitDifferences(logs: Web3.LogEntry[], isRemoved: boolean): void { _.forEach(logs, log => { - const logWithDecodedArgsEvent = { + const logEvent = { removed: isRemoved, ...log, }; if (!_.isUndefined(this._callback)) { - this._callback(logWithDecodedArgsEvent); + this._callback(logEvent); } }); } -- cgit v1.2.3 From fd54a6a3ad91a6aaff0e2a81f4fd9856b02ff320 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 10:53:59 +0200 Subject: Rename MempoolWatcher to EventWatcher and remove from public interface --- src/0x.ts | 9 ------ src/mempool/event_watcher.ts | 66 ++++++++++++++++++++++++++++++++++++++++++ src/mempool/mempool_watcher.ts | 66 ------------------------------------------ 3 files changed, 66 insertions(+), 75 deletions(-) create mode 100644 src/mempool/event_watcher.ts delete mode 100644 src/mempool/mempool_watcher.ts diff --git a/src/0x.ts b/src/0x.ts index 10db7e158..4bd37c260 100644 --- a/src/0x.ts +++ b/src/0x.ts @@ -11,7 +11,6 @@ import {assert} from './utils/assert'; import {AbiDecoder} from './utils/abi_decoder'; import {intervalUtils} from './utils/interval_utils'; import {artifacts} from './artifacts'; -import {MempoolWatcher} from './mempool/mempool_watcher'; import {ExchangeWrapper} from './contract_wrappers/exchange_wrapper'; import {TokenRegistryWrapper} from './contract_wrappers/token_registry_wrapper'; import {EtherTokenWrapper} from './contract_wrappers/ether_token_wrapper'; @@ -66,10 +65,6 @@ export class ZeroEx { * tokenTransferProxy smart contract. */ public proxy: TokenTransferProxyWrapper; - /** - * An instance of the MempoolWatcher class containing methods for watching pending events. - */ - public mempool: MempoolWatcher; private _web3Wrapper: Web3Wrapper; private _abiDecoder: AbiDecoder; /** @@ -197,10 +192,6 @@ export class ZeroEx { }; this._web3Wrapper = new Web3Wrapper(provider, defaults); const mempoolPollingIntervalMs = _.isUndefined(config) ? undefined : config.mempoolPollingIntervalMs; - this.mempool = new MempoolWatcher( - this._web3Wrapper, - mempoolPollingIntervalMs, - ); this.token = new TokenWrapper( this._web3Wrapper, this._abiDecoder, diff --git a/src/mempool/event_watcher.ts b/src/mempool/event_watcher.ts new file mode 100644 index 000000000..e28219682 --- /dev/null +++ b/src/mempool/event_watcher.ts @@ -0,0 +1,66 @@ +import * as Web3 from 'web3'; +import * as _ from 'lodash'; +import {Web3Wrapper} from '../web3_wrapper'; +import {BlockParamLiteral, EventCallback, MempoolEventCallback} from '../types'; +import {AbiDecoder} from '../utils/abi_decoder'; +import {intervalUtils} from '../utils/interval_utils'; + +const DEFAULT_MEMPOOL_POLLING_INTERVAL = 200; + +export class EventWatcher { + private _web3Wrapper: Web3Wrapper; + private _pollingIntervalMs: number; + private _intervalId: NodeJS.Timer; + private _lastMempoolEvents: Web3.LogEntry[] = []; + private _callback?: MempoolEventCallback; + constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) { + this._web3Wrapper = web3Wrapper; + this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? + DEFAULT_MEMPOOL_POLLING_INTERVAL : + pollingIntervalMs; + } + public subscribe(callback: MempoolEventCallback): void { + this._callback = callback; + this._intervalId = intervalUtils.setAsyncExcludingInterval( + this._pollForMempoolEventsAsync.bind(this), this._pollingIntervalMs); + } + public unsubscribe(): void { + delete this._callback; + intervalUtils.clearAsyncExcludingInterval(this._intervalId); + } + private async _pollForMempoolEventsAsync(): Promise { + const pendingEvents = await this._getMempoolEventsAsync(); + 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._lastMempoolEvents, pendingEvents, JSON.stringify); + const newEvents = _.differenceBy(pendingEvents, this._lastMempoolEvents, JSON.stringify); + let isRemoved = true; + this._emitDifferences(removedEvents, isRemoved); + isRemoved = false; + this._emitDifferences(newEvents, isRemoved); + this._lastMempoolEvents = pendingEvents; + } + private async _getMempoolEventsAsync(): Promise { + const mempoolFilter = { + fromBlock: BlockParamLiteral.Pending, + toBlock: BlockParamLiteral.Pending, + }; + const pendingEvents = await this._web3Wrapper.getLogsAsync(mempoolFilter); + return pendingEvents; + } + private _emitDifferences(logs: Web3.LogEntry[], isRemoved: boolean): void { + _.forEach(logs, log => { + const logEvent = { + removed: isRemoved, + ...log, + }; + if (!_.isUndefined(this._callback)) { + this._callback(logEvent); + } + }); + } +} diff --git a/src/mempool/mempool_watcher.ts b/src/mempool/mempool_watcher.ts deleted file mode 100644 index 70d263fcb..000000000 --- a/src/mempool/mempool_watcher.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as Web3 from 'web3'; -import * as _ from 'lodash'; -import {Web3Wrapper} from '../web3_wrapper'; -import {BlockParamLiteral, EventCallback, MempoolEventCallback} from '../types'; -import {AbiDecoder} from '../utils/abi_decoder'; -import {intervalUtils} from '../utils/interval_utils'; - -const DEFAULT_MEMPOOL_POLLING_INTERVAL = 200; - -export class MempoolWatcher { - private _web3Wrapper: Web3Wrapper; - private _pollingIntervalMs: number; - private _intervalId: NodeJS.Timer; - private _lastMempoolEvents: Web3.LogEntry[] = []; - private _callback?: MempoolEventCallback; - constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) { - this._web3Wrapper = web3Wrapper; - this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? - DEFAULT_MEMPOOL_POLLING_INTERVAL : - pollingIntervalMs; - } - public subscribe(callback: MempoolEventCallback): void { - this._callback = callback; - this._intervalId = intervalUtils.setAsyncExcludingInterval( - this._pollForMempoolEventsAsync.bind(this), this._pollingIntervalMs); - } - public unsubscribe(): void { - delete this._callback; - intervalUtils.clearAsyncExcludingInterval(this._intervalId); - } - private async _pollForMempoolEventsAsync(): Promise { - const pendingEvents = await this._getMempoolEventsAsync(); - 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._lastMempoolEvents, pendingEvents, JSON.stringify); - const newEvents = _.differenceBy(pendingEvents, this._lastMempoolEvents, JSON.stringify); - let isRemoved = true; - this._emitDifferences(removedEvents, isRemoved); - isRemoved = false; - this._emitDifferences(newEvents, isRemoved); - this._lastMempoolEvents = pendingEvents; - } - private async _getMempoolEventsAsync(): Promise { - const mempoolFilter = { - fromBlock: BlockParamLiteral.Pending, - toBlock: BlockParamLiteral.Pending, - }; - const pendingEvents = await this._web3Wrapper.getLogsAsync(mempoolFilter); - return pendingEvents; - } - private _emitDifferences(logs: Web3.LogEntry[], isRemoved: boolean): void { - _.forEach(logs, log => { - const logEvent = { - removed: isRemoved, - ...log, - }; - if (!_.isUndefined(this._callback)) { - this._callback(logEvent); - } - }); - } -} -- cgit v1.2.3 From 247eefc33a3c936158c4d71e3cea905635d27a9b Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 10:54:28 +0200 Subject: Add initial interface of an OrderWatcher --- src/mempool/order_watcher.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/mempool/order_watcher.ts diff --git a/src/mempool/order_watcher.ts b/src/mempool/order_watcher.ts new file mode 100644 index 000000000..90c15cd34 --- /dev/null +++ b/src/mempool/order_watcher.ts @@ -0,0 +1,27 @@ +import * as Web3 from 'web3'; +import * as _ from 'lodash'; +import {Web3Provider, SignedOrder} from '../types'; +import {Web3Wrapper} from '../web3_wrapper'; + +export class OrderWatcher { + constructor(provider: Web3Provider) { + if (_.isUndefined((provider as any).sendAsync)) { + // Web3@1.0 provider doesn't support synchronous http requests, + // so it only has an async `send` method, instead of a `send` and `sendAsync` in web3@0.x.x` + // We re-assign the send method so that Web3@1.0 providers work with 0x.js + (provider as any).sendAsync = (provider as any).send; + } + } + public addOrder(signedOrder: SignedOrder): void { + // + } + public removeOrder(signedOrder: SignedOrder): void { + // + } + public subscribe(callback: OnOrderFillabilityStateChangeCallback): void { + // + } + public unsubscribe(): void { + // + } +} -- cgit v1.2.3 From 84b8e77aaae3a2c6848bcd1ffab78a2de81e6138 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 11:34:07 +0200 Subject: Add types for order state watcher --- src/types.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/types.ts b/src/types.ts index 1b32ccdf9..ea83c6885 100644 --- a/src/types.ts +++ b/src/types.ts @@ -474,3 +474,24 @@ export enum TransferType { Trade = 'trade', Fee = 'fee', } + +export interface OrderStateValid { + isValid: true; + orderHash: string; + makerBalance: BigNumber; + makerAllowance: BigNumber; + makerFeeBalance: BigNumber; + makerFeeAllowance: BigNumber; + filledMakerTokenAmount: BigNumber; + cancelledMakerTokenAmount: BigNumber; +} + +export interface OrderStateInvalid { + isValid: false; + orderHash: string; + error: ExchangeContractErrs; +} + +export type OnOrderFillabilityStateChangeCallback = ( + orderState: OrderStateValid|OrderStateInvalid, +) => void; -- cgit v1.2.3 From 3ddb203317065a85531a31daa1ae2d73232ca6df Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 11:34:26 +0200 Subject: Move provider altering logic to Web3Wrapper --- src/0x.ts | 6 ------ src/mempool/order_watcher.ts | 21 +++++++++++---------- src/web3_wrapper.ts | 10 ++++++++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/0x.ts b/src/0x.ts index 4bd37c260..0fb968ef3 100644 --- a/src/0x.ts +++ b/src/0x.ts @@ -177,12 +177,6 @@ export class ZeroEx { if (!_.isUndefined(config)) { assert.doesConformToSchema('config', config, zeroExConfigSchema); } - if (_.isUndefined((provider as any).sendAsync)) { - // Web3@1.0 provider doesn't support synchronous http requests, - // so it only has an async `send` method, instead of a `send` and `sendAsync` in web3@0.x.x` - // We re-assign the send method so that Web3@1.0 providers work with 0x.js - (provider as any).sendAsync = (provider as any).send; - } const artifactJSONs = _.values(artifacts); const abiArrays = _.map(artifactJSONs, artifact => artifact.abi); this._abiDecoder = new AbiDecoder(abiArrays); diff --git a/src/mempool/order_watcher.ts b/src/mempool/order_watcher.ts index 90c15cd34..b2c8598e7 100644 --- a/src/mempool/order_watcher.ts +++ b/src/mempool/order_watcher.ts @@ -1,22 +1,23 @@ -import * as Web3 from 'web3'; import * as _ from 'lodash'; -import {Web3Provider, SignedOrder} from '../types'; +import {ZeroEx} from '../'; +import {assert} from '../utils/assert'; +import {Web3Provider, SignedOrder, OnOrderFillabilityStateChangeCallback} from '../types'; import {Web3Wrapper} from '../web3_wrapper'; export class OrderWatcher { + private _orders = new Map(); + private _web3Wrapper: Web3Wrapper; constructor(provider: Web3Provider) { - if (_.isUndefined((provider as any).sendAsync)) { - // Web3@1.0 provider doesn't support synchronous http requests, - // so it only has an async `send` method, instead of a `send` and `sendAsync` in web3@0.x.x` - // We re-assign the send method so that Web3@1.0 providers work with 0x.js - (provider as any).sendAsync = (provider as any).send; - } + assert.isWeb3Provider('provider', provider); + this._web3Wrapper = new Web3Wrapper(provider); } public addOrder(signedOrder: SignedOrder): void { - // + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + this._orders.set(orderHash, signedOrder); } public removeOrder(signedOrder: SignedOrder): void { - // + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + this._orders.delete(orderHash); } public subscribe(callback: OnOrderFillabilityStateChangeCallback): void { // diff --git a/src/web3_wrapper.ts b/src/web3_wrapper.ts index 3b1e4477b..01d572654 100644 --- a/src/web3_wrapper.ts +++ b/src/web3_wrapper.ts @@ -10,10 +10,16 @@ export class Web3Wrapper { private defaults: Partial; private networkIdIfExists?: number; private jsonRpcRequestId: number; - constructor(provider: Web3.Provider, defaults: Partial) { + constructor(provider: Web3.Provider, defaults?: Partial) { + if (_.isUndefined((provider as any).sendAsync)) { + // Web3@1.0 provider doesn't support synchronous http requests, + // so it only has an async `send` method, instead of a `send` and `sendAsync` in web3@0.x.x` + // We re-assign the send method so that Web3@1.0 providers work with 0x.js + (provider as any).sendAsync = (provider as any).send; + } this.web3 = new Web3(); this.web3.setProvider(provider); - this.defaults = defaults; + this.defaults = defaults || {}; this.jsonRpcRequestId = 0; } public setProvider(provider: Web3.Provider) { -- cgit v1.2.3 From eace1a984091d7528ed2d07073f399db9d4d7286 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 12:04:48 +0200 Subject: Remove mempool event watcher config --- src/0x.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/0x.ts b/src/0x.ts index 0fb968ef3..62d1ff34f 100644 --- a/src/0x.ts +++ b/src/0x.ts @@ -185,7 +185,6 @@ export class ZeroEx { gasPrice, }; this._web3Wrapper = new Web3Wrapper(provider, defaults); - const mempoolPollingIntervalMs = _.isUndefined(config) ? undefined : config.mempoolPollingIntervalMs; this.token = new TokenWrapper( this._web3Wrapper, this._abiDecoder, -- cgit v1.2.3 From 589bd8694f1c57d8d62bcda86af3475aef655bc2 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 12:05:02 +0200 Subject: Clear event cache on unsubscribe --- src/mempool/event_watcher.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mempool/event_watcher.ts b/src/mempool/event_watcher.ts index e28219682..1ad30b790 100644 --- a/src/mempool/event_watcher.ts +++ b/src/mempool/event_watcher.ts @@ -26,6 +26,7 @@ export class EventWatcher { } public unsubscribe(): void { delete this._callback; + this._lastMempoolEvents = []; intervalUtils.clearAsyncExcludingInterval(this._intervalId); } private async _pollForMempoolEventsAsync(): Promise { -- cgit v1.2.3 From e7f60032bc391cf8a3802a6d2141a61aa5204589 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 12:05:31 +0200 Subject: Adjust tests for mempool event watcher --- test/event_watcher_test.ts | 126 +++++++++++++++++++++++++++++++++++++++++++++ test/mempool_test.ts | 125 -------------------------------------------- 2 files changed, 126 insertions(+), 125 deletions(-) create mode 100644 test/event_watcher_test.ts delete mode 100644 test/mempool_test.ts diff --git a/test/event_watcher_test.ts b/test/event_watcher_test.ts new file mode 100644 index 000000000..208871ea8 --- /dev/null +++ b/test/event_watcher_test.ts @@ -0,0 +1,126 @@ +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 {EventWatcher} from '../src/mempool/event_watcher'; +import { + ZeroEx, + LogEvent, + DecodedLogEvent, +} from '../src'; +import {DoneCallback} from '../src/types'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('EventWatcher', () => { + let web3: Web3; + let stubs: Sinon.SinonStub[] = []; + let eventWatcher: EventWatcher; + let web3Wrapper: Web3Wrapper; + const logA = { + address: '0x71d271f8b14adef568f8f28f1587ce7271ac4ca5', + blockHash: null, + blockNumber: null, + data: '', + logIndex: null, + topics: [], + transactionHash: '0x004881d38cd4a8f72f1a0d68c8b9b8124504706041ff37019c1d1ed6bfda8e17', + transactionIndex: null, + }; + const logB = { + address: '0x8d12a197cb00d4747a1fe03395095ce2a5cc6819', + blockHash: null, + blockNumber: null, + data: '', + logIndex: null, + topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ], + transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25', + transactionIndex: null, + }; + const logC = { + address: '0x1d271f8b174adef58f1587ce68f8f27271ac4ca5', + blockHash: null, + blockNumber: null, + data: '', + logIndex: null, + topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ], + transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25', + transactionIndex: null, + }; + before(async () => { + web3 = web3Factory.create(); + const mempoolPollingIntervalMs = 10; + web3Wrapper = new Web3Wrapper(web3.currentProvider); + eventWatcher = new EventWatcher(web3Wrapper, mempoolPollingIntervalMs); + }); + afterEach(() => { + // clean up any stubs after the test has completed + _.each(stubs, s => s.restore()); + stubs = []; + eventWatcher.unsubscribe(); + }); + it('correctly emits initial log events', (done: DoneCallback) => { + const logs: Web3.LogEntry[] = [logA, logB]; + const expectedLogEvents = [ + { + removed: false, + ...logA, + }, + { + removed: false, + ...logB, + }, + ]; + const getLogsStub = Sinon.stub(web3Wrapper, 'getLogsAsync'); + getLogsStub.onCall(0).returns(logs); + stubs.push(getLogsStub); + const callback = (event: LogEvent) => { + const expectedLogEvent = expectedLogEvents.shift(); + expect(event).to.be.deep.equal(expectedLogEvent); + if (_.isEmpty(expectedLogEvents)) { + done(); + } + }; + eventWatcher.subscribe(callback); + }); + it('correctly computes the difference and emits only changes', (done: DoneCallback) => { + const initialLogs: Web3.LogEntry[] = [logA, logB]; + const changedLogs: Web3.LogEntry[] = [logA, logC]; + const expectedLogEvents = [ + { + removed: false, + ...logA, + }, + { + removed: false, + ...logB, + }, + { + removed: true, + ...logB, + }, + { + removed: false, + ...logC, + }, + ]; + const getLogsStub = Sinon.stub(web3Wrapper, 'getLogsAsync'); + getLogsStub.onCall(0).returns(initialLogs); + getLogsStub.onCall(1).returns(changedLogs); + stubs.push(getLogsStub); + const callback = (event: LogEvent) => { + const expectedLogEvent = expectedLogEvents.shift(); + expect(event).to.be.deep.equal(expectedLogEvent); + if (_.isEmpty(expectedLogEvents)) { + done(); + } + }; + eventWatcher.subscribe(callback); + }); +}); diff --git a/test/mempool_test.ts b/test/mempool_test.ts deleted file mode 100644 index 0c8fb921a..000000000 --- a/test/mempool_test.ts +++ /dev/null @@ -1,125 +0,0 @@ -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 { - ZeroEx, - LogEvent, - DecodedLogEvent, -} from '../src'; -import {DoneCallback} from '../src/types'; - -chaiSetup.configure(); -const expect = chai.expect; - -describe('MempoolWatcher', () => { - let web3: Web3; - let zeroEx: ZeroEx; - let stubs: Sinon.SinonStub[] = []; - const logA = { - address: '0x71d271f8b14adef568f8f28f1587ce7271ac4ca5', - blockHash: null, - blockNumber: null, - data: '', - logIndex: null, - topics: [], - transactionHash: '0x004881d38cd4a8f72f1a0d68c8b9b8124504706041ff37019c1d1ed6bfda8e17', - transactionIndex: null, - }; - const logB = { - address: '0x8d12a197cb00d4747a1fe03395095ce2a5cc6819', - blockHash: null, - blockNumber: null, - data: '', - logIndex: null, - topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ], - transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25', - transactionIndex: null, - }; - const logC = { - address: '0x1d271f8b174adef58f1587ce68f8f27271ac4ca5', - blockHash: null, - blockNumber: null, - data: '', - logIndex: null, - topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ], - transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25', - transactionIndex: null, - }; - before(async () => { - web3 = web3Factory.create(); - const config = { - mempoolPollingIntervalMs: 10, - }; - zeroEx = new ZeroEx(web3.currentProvider, config); - }); - afterEach(() => { - // clean up any stubs after the test has completed - _.each(stubs, s => s.restore()); - stubs = []; - zeroEx.mempool.unsubscribe(); - }); - it('correctly emits initial log events', (done: DoneCallback) => { - const logs: Web3.LogEntry[] = [logA, logB]; - const expectedLogEvents = [ - { - removed: false, - ...logA, - }, - { - removed: false, - ...logB, - }, - ]; - const getLogsStub = Sinon.stub((zeroEx.mempool as any)._web3Wrapper, 'getLogsAsync'); - getLogsStub.onCall(0).returns(logs); - stubs.push(getLogsStub); - const callback = (event: LogEvent) => { - const expectedLogEvent = expectedLogEvents.shift(); - expect(event).to.be.deep.equal(expectedLogEvent); - if (_.isEmpty(expectedLogEvents)) { - done(); - } - }; - zeroEx.mempool.subscribe(callback); - }); - it('correctly computes the difference and emits only changes', (done: DoneCallback) => { - const initialLogs: Web3.LogEntry[] = [logA, logB]; - const changedLogs: Web3.LogEntry[] = [logA, logC]; - const expectedLogEvents = [ - { - removed: false, - ...logA, - }, - { - removed: false, - ...logB, - }, - { - removed: true, - ...logB, - }, - { - removed: false, - ...logC, - }, - ]; - const getLogsStub = Sinon.stub((zeroEx.mempool as any)._web3Wrapper, 'getLogsAsync'); - getLogsStub.onCall(0).returns(initialLogs); - getLogsStub.onCall(1).returns(changedLogs); - stubs.push(getLogsStub); - const callback = (event: LogEvent) => { - // console.log(event); - const expectedLogEvent = expectedLogEvents.shift(); - expect(event).to.be.deep.equal(expectedLogEvent); - if (_.isEmpty(expectedLogEvents)) { - done(); - } - }; - zeroEx.mempool.subscribe(callback); - }); -}); -- cgit v1.2.3 From f601a5d35620002db19a7a28b28b8d46a4613cc6 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 12:05:57 +0200 Subject: Move mempoolPollingIntervalMs to OrderWatcherConfig --- src/types.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index ea83c6885..766bf01b3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -399,13 +399,18 @@ export interface JSONRPCPayload { * exchangeContractAddress: The address of an exchange contract to use * tokenRegistryContractAddress: The address of a token registry contract to use * etherTokenContractAddress: The address of an ether token contract to use - * mempoolPollingIntervalMs: How often to check for new mempool events */ export interface ZeroExConfig { gasPrice?: BigNumber; // Gas price to use with every transaction exchangeContractAddress?: string; tokenRegistryContractAddress?: string; etherTokenContractAddress?: string; +} + +/* + * mempoolPollingIntervalMs: How often to check for new mempool events + */ +export interface OrderWatcherConfig { mempoolPollingIntervalMs?: number; } -- cgit v1.2.3 From 6714b8958b4807f86e5f2f46168feb643cae83ff Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 12:07:01 +0200 Subject: Add new public types --- src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.ts b/src/index.ts index 7a9b8aa63..954d9deb8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,4 +37,8 @@ export { LogEvent, DecodedLogEvent, MempoolEventCallback, + OnOrderFillabilityStateChangeCallback, + OrderStateValid, + OrderStateInvalid, + OrderWatcherConfig, } from './types'; -- cgit v1.2.3 From 1980b3fae4ed9c92aa7feb2f1c79ea4b49525341 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 15:01:20 +0200 Subject: Add empty implementation of order state watcher --- src/mempool/order_state_watcher.ts | 70 ++++++++++++++++++++++++++++++ src/mempool/order_watcher.ts | 28 ------------ src/schemas/order_watcher_config_schema.ts | 7 +++ src/types.ts | 2 +- test/order_watcher_test.ts | 42 ++++++++++++++++++ 5 files changed, 120 insertions(+), 29 deletions(-) create mode 100644 src/mempool/order_state_watcher.ts delete mode 100644 src/mempool/order_watcher.ts create mode 100644 src/schemas/order_watcher_config_schema.ts create mode 100644 test/order_watcher_test.ts diff --git a/src/mempool/order_state_watcher.ts b/src/mempool/order_state_watcher.ts new file mode 100644 index 000000000..89f84647d --- /dev/null +++ b/src/mempool/order_state_watcher.ts @@ -0,0 +1,70 @@ +import * as _ from 'lodash'; +import {schemas} from '0x-json-schemas'; +import {ZeroEx} from '../'; +import {EventWatcher} from './event_watcher'; +import {assert} from '../utils/assert'; +import {artifacts} from '../artifacts'; +import {AbiDecoder} from '../utils/abi_decoder'; +import {orderWatcherConfigSchema} from '../schemas/order_watcher_config_schema'; +import { + LogEvent, + SignedOrder, + Web3Provider, + LogWithDecodedArgs, + OrderWatcherConfig, + OnOrderStateChangeCallback, +} from '../types'; +import {Web3Wrapper} from '../web3_wrapper'; + +export class OrderStateWatcher { + private _orders = new Map(); + private _web3Wrapper: Web3Wrapper; + private _config: OrderWatcherConfig; + private _callback?: OnOrderStateChangeCallback; + private _eventWatcher?: EventWatcher; + private _abiDecoder: AbiDecoder; + constructor(provider: Web3Provider, config?: OrderWatcherConfig) { + assert.isWeb3Provider('provider', provider); + if (!_.isUndefined(config)) { + assert.doesConformToSchema('config', config, orderWatcherConfigSchema); + } + this._web3Wrapper = new Web3Wrapper(provider); + this._config = config || {}; + const artifactJSONs = _.values(artifacts); + const abiArrays = _.map(artifactJSONs, artifact => artifact.abi); + this._abiDecoder = new AbiDecoder(abiArrays); + } + public addOrder(signedOrder: SignedOrder): void { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + this._orders.set(orderHash, signedOrder); + } + public removeOrder(signedOrder: SignedOrder): void { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + this._orders.delete(orderHash); + } + public subscribe(callback: OnOrderStateChangeCallback): void { + assert.isFunction('callback', callback); + this._callback = callback; + this._eventWatcher = new EventWatcher( + this._web3Wrapper, this._config.mempoolPollingIntervalMs, + ); + this._eventWatcher.subscribe(this._onMempoolEventCallbackAsync.bind(this)); + } + public unsubscribe(): void { + delete this._callback; + if (!_.isUndefined(this._eventWatcher)) { + this._eventWatcher.unsubscribe(); + } + } + private async _onMempoolEventCallbackAsync(log: LogEvent): Promise { + const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log); + if (!_.isUndefined((maybeDecodedLog as LogWithDecodedArgs).event)) { + await this._revalidateOrdersAsync(); + } + } + private async _revalidateOrdersAsync(): Promise { + _.noop(); + } +} diff --git a/src/mempool/order_watcher.ts b/src/mempool/order_watcher.ts deleted file mode 100644 index b2c8598e7..000000000 --- a/src/mempool/order_watcher.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as _ from 'lodash'; -import {ZeroEx} from '../'; -import {assert} from '../utils/assert'; -import {Web3Provider, SignedOrder, OnOrderFillabilityStateChangeCallback} from '../types'; -import {Web3Wrapper} from '../web3_wrapper'; - -export class OrderWatcher { - private _orders = new Map(); - private _web3Wrapper: Web3Wrapper; - constructor(provider: Web3Provider) { - assert.isWeb3Provider('provider', provider); - this._web3Wrapper = new Web3Wrapper(provider); - } - public addOrder(signedOrder: SignedOrder): void { - const orderHash = ZeroEx.getOrderHashHex(signedOrder); - this._orders.set(orderHash, signedOrder); - } - public removeOrder(signedOrder: SignedOrder): void { - const orderHash = ZeroEx.getOrderHashHex(signedOrder); - this._orders.delete(orderHash); - } - public subscribe(callback: OnOrderFillabilityStateChangeCallback): void { - // - } - public unsubscribe(): void { - // - } -} diff --git a/src/schemas/order_watcher_config_schema.ts b/src/schemas/order_watcher_config_schema.ts new file mode 100644 index 000000000..a88d2ecfd --- /dev/null +++ b/src/schemas/order_watcher_config_schema.ts @@ -0,0 +1,7 @@ +export const orderWatcherConfigSchema = { + id: '/OrderWatcherConfig', + properties: { + mempoolPollingIntervalMs: {$ref: '/Number'}, + }, + type: 'object', +}; diff --git a/src/types.ts b/src/types.ts index 766bf01b3..52b22516b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -497,6 +497,6 @@ export interface OrderStateInvalid { error: ExchangeContractErrs; } -export type OnOrderFillabilityStateChangeCallback = ( +export type OnOrderStateChangeCallback = ( orderState: OrderStateValid|OrderStateInvalid, ) => void; diff --git a/test/order_watcher_test.ts b/test/order_watcher_test.ts new file mode 100644 index 000000000..f273a1d84 --- /dev/null +++ b/test/order_watcher_test.ts @@ -0,0 +1,42 @@ +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 { + ZeroEx, + LogEvent, + DecodedLogEvent, +} from '../src'; +import {DoneCallback} from '../src/types'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('EventWatcher', () => { + let web3: Web3; + let stubs: Sinon.SinonStub[] = []; + let orderStateWatcher: OrderStateWatcher; + before(async () => { + web3 = web3Factory.create(); + const mempoolPollingIntervalMs = 10; + const orderStateWatcherConfig = { + mempoolPollingIntervalMs, + }; + orderStateWatcher = new OrderStateWatcher(web3.currentProvider, orderStateWatcherConfig); + }); + afterEach(() => { + // clean up any stubs after the test has completed + _.each(stubs, s => s.restore()); + stubs = []; + orderStateWatcher.unsubscribe(); + }); + it.only('', (done: DoneCallback) => { + orderStateWatcher.subscribe(console.log); + }).timeout(1000000000000); +}); -- cgit v1.2.3 From ff5d18d327ccde9e47c46dfc5a191778d7d34c83 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 15:08:58 +0200 Subject: Fix config schema --- src/schemas/order_watcher_config_schema.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/schemas/order_watcher_config_schema.ts b/src/schemas/order_watcher_config_schema.ts index a88d2ecfd..9c2dc38a4 100644 --- a/src/schemas/order_watcher_config_schema.ts +++ b/src/schemas/order_watcher_config_schema.ts @@ -1,7 +1,10 @@ export const orderWatcherConfigSchema = { id: '/OrderWatcherConfig', properties: { - mempoolPollingIntervalMs: {$ref: '/Number'}, + mempoolPollingIntervalMs: { + type: 'number', + min: 0, + }, }, type: 'object', }; -- cgit v1.2.3 From 0b84c469d346c85201ff569b683a38b5c972ad73 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 15:19:04 +0200 Subject: Introduce OrderState interface --- src/types.ts | 14 +++++++++----- test/order_watcher_test.ts | 6 +++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/types.ts b/src/types.ts index 52b22516b..89ee4141f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -480,17 +480,21 @@ export enum TransferType { Fee = 'fee', } -export interface OrderStateValid { - isValid: true; - orderHash: string; +export interface OrderState { makerBalance: BigNumber; - makerAllowance: BigNumber; + makerProxyAllowance: BigNumber; makerFeeBalance: BigNumber; - makerFeeAllowance: BigNumber; + makerFeeProxyAllowance: BigNumber; filledMakerTokenAmount: BigNumber; cancelledMakerTokenAmount: BigNumber; } +export interface OrderStateValid { + isValid: true; + orderHash: string; + orderState: OrderState; +} + export interface OrderStateInvalid { isValid: false; orderHash: string; diff --git a/test/order_watcher_test.ts b/test/order_watcher_test.ts index f273a1d84..e62b1aab2 100644 --- a/test/order_watcher_test.ts +++ b/test/order_watcher_test.ts @@ -36,7 +36,7 @@ describe('EventWatcher', () => { stubs = []; orderStateWatcher.unsubscribe(); }); - it.only('', (done: DoneCallback) => { - orderStateWatcher.subscribe(console.log); - }).timeout(1000000000000); + it.skip('TODO', () => { + // TODO + }); }); -- cgit v1.2.3 From 63f16b5f99cd7ca0d71dd822c0e2ecd0eb3f7762 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 15:40:31 +0200 Subject: Change fields in OrderState to represent taker side values --- src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index 89ee4141f..7de875dbc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -485,8 +485,8 @@ export interface OrderState { makerProxyAllowance: BigNumber; makerFeeBalance: BigNumber; makerFeeProxyAllowance: BigNumber; - filledMakerTokenAmount: BigNumber; - cancelledMakerTokenAmount: BigNumber; + filledTakerTokenAmount: BigNumber; + canceledTakerTokenAmount: BigNumber; } export interface OrderStateValid { -- cgit v1.2.3 From bb5474660c5fa90080cc5950a21eb65e1896f9c4 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Mon, 30 Oct 2017 18:38:10 +0200 Subject: Add naive order state watcher implementation Revalidate all orders upon event received and emit order states even if not changed --- src/0x.ts | 11 ++++ src/index.ts | 4 +- src/mempool/event_watcher.ts | 20 +++--- src/mempool/order_state_watcher.ts | 55 +++++++++-------- src/schemas/order_watcher_config_schema.ts | 10 --- src/schemas/zero_ex_config_schema.ts | 4 ++ src/types.ts | 15 ++--- src/utils/order_state_utils.ts | 99 ++++++++++++++++++++++++++++++ test/order_watcher_test.ts | 78 ++++++++++++++++++++--- 9 files changed, 232 insertions(+), 64 deletions(-) delete mode 100644 src/schemas/order_watcher_config_schema.ts create mode 100644 src/utils/order_state_utils.ts diff --git a/src/0x.ts b/src/0x.ts index 62d1ff34f..f1b271810 100644 --- a/src/0x.ts +++ b/src/0x.ts @@ -16,6 +16,8 @@ import {TokenRegistryWrapper} from './contract_wrappers/token_registry_wrapper'; import {EtherTokenWrapper} from './contract_wrappers/ether_token_wrapper'; import {TokenWrapper} from './contract_wrappers/token_wrapper'; import {TokenTransferProxyWrapper} from './contract_wrappers/token_transfer_proxy_wrapper'; +import {OrderStateWatcher} from './mempool/order_state_watcher'; +import {OrderStateUtils} from './utils/order_state_utils'; import { ECSignature, ZeroExError, @@ -65,6 +67,10 @@ export class ZeroEx { * tokenTransferProxy smart contract. */ public proxy: TokenTransferProxyWrapper; + /** + * An instance of the OrderStateWatcher class containing methods for watching the order state changes. + */ + public orderStateWatcher: OrderStateWatcher; private _web3Wrapper: Web3Wrapper; private _abiDecoder: AbiDecoder; /** @@ -207,6 +213,11 @@ export class ZeroEx { this.tokenRegistry = new TokenRegistryWrapper(this._web3Wrapper, tokenRegistryContractAddressIfExists); const etherTokenContractAddressIfExists = _.isUndefined(config) ? undefined : config.etherTokenContractAddress; this.etherToken = new EtherTokenWrapper(this._web3Wrapper, this.token, etherTokenContractAddressIfExists); + const mempoolPollingIntervalMs = _.isUndefined(config) ? undefined : config.mempoolPollingIntervalMs; + const orderStateUtils = new OrderStateUtils(this.token, this.exchange); + this.orderStateWatcher = new OrderStateWatcher( + this._web3Wrapper, this._abiDecoder, orderStateUtils, mempoolPollingIntervalMs, + ); } /** * Sets a new web3 provider for 0x.js. Updating the provider will stop all diff --git a/src/index.ts b/src/index.ts index 954d9deb8..ffd59fe37 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,8 +37,8 @@ export { LogEvent, DecodedLogEvent, MempoolEventCallback, - OnOrderFillabilityStateChangeCallback, + OnOrderStateChangeCallback, OrderStateValid, OrderStateInvalid, - OrderWatcherConfig, + OrderState, } from './types'; diff --git a/src/mempool/event_watcher.ts b/src/mempool/event_watcher.ts index 1ad30b790..27f0c8207 100644 --- a/src/mempool/event_watcher.ts +++ b/src/mempool/event_watcher.ts @@ -12,7 +12,7 @@ export class EventWatcher { private _pollingIntervalMs: number; private _intervalId: NodeJS.Timer; private _lastMempoolEvents: Web3.LogEntry[] = []; - private _callback?: MempoolEventCallback; + private _callbackAsync?: MempoolEventCallback; constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) { this._web3Wrapper = web3Wrapper; this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? @@ -20,12 +20,12 @@ export class EventWatcher { pollingIntervalMs; } public subscribe(callback: MempoolEventCallback): void { - this._callback = callback; + this._callbackAsync = callback; this._intervalId = intervalUtils.setAsyncExcludingInterval( this._pollForMempoolEventsAsync.bind(this), this._pollingIntervalMs); } public unsubscribe(): void { - delete this._callback; + delete this._callbackAsync; this._lastMempoolEvents = []; intervalUtils.clearAsyncExcludingInterval(this._intervalId); } @@ -40,9 +40,9 @@ export class EventWatcher { const removedEvents = _.differenceBy(this._lastMempoolEvents, pendingEvents, JSON.stringify); const newEvents = _.differenceBy(pendingEvents, this._lastMempoolEvents, JSON.stringify); let isRemoved = true; - this._emitDifferences(removedEvents, isRemoved); + await this._emitDifferencesAsync(removedEvents, isRemoved); isRemoved = false; - this._emitDifferences(newEvents, isRemoved); + await this._emitDifferencesAsync(newEvents, isRemoved); this._lastMempoolEvents = pendingEvents; } private async _getMempoolEventsAsync(): Promise { @@ -53,15 +53,15 @@ export class EventWatcher { const pendingEvents = await this._web3Wrapper.getLogsAsync(mempoolFilter); return pendingEvents; } - private _emitDifferences(logs: Web3.LogEntry[], isRemoved: boolean): void { - _.forEach(logs, log => { + private async _emitDifferencesAsync(logs: Web3.LogEntry[], isRemoved: boolean): Promise { + for (const log of logs) { const logEvent = { removed: isRemoved, ...log, }; - if (!_.isUndefined(this._callback)) { - this._callback(logEvent); + if (!_.isUndefined(this._callbackAsync)) { + await this._callbackAsync(logEvent); } - }); + } } } diff --git a/src/mempool/order_state_watcher.ts b/src/mempool/order_state_watcher.ts index 89f84647d..3da48005d 100644 --- a/src/mempool/order_state_watcher.ts +++ b/src/mempool/order_state_watcher.ts @@ -5,13 +5,14 @@ import {EventWatcher} from './event_watcher'; import {assert} from '../utils/assert'; import {artifacts} from '../artifacts'; import {AbiDecoder} from '../utils/abi_decoder'; -import {orderWatcherConfigSchema} from '../schemas/order_watcher_config_schema'; +import {OrderStateUtils} from '../utils/order_state_utils'; import { LogEvent, + OrderState, SignedOrder, Web3Provider, + BlockParamLiteral, LogWithDecodedArgs, - OrderWatcherConfig, OnOrderStateChangeCallback, } from '../types'; import {Web3Wrapper} from '../web3_wrapper'; @@ -19,20 +20,19 @@ import {Web3Wrapper} from '../web3_wrapper'; export class OrderStateWatcher { private _orders = new Map(); private _web3Wrapper: Web3Wrapper; - private _config: OrderWatcherConfig; - private _callback?: OnOrderStateChangeCallback; - private _eventWatcher?: EventWatcher; + private _callbackAsync?: OnOrderStateChangeCallback; + private _eventWatcher: EventWatcher; private _abiDecoder: AbiDecoder; - constructor(provider: Web3Provider, config?: OrderWatcherConfig) { - assert.isWeb3Provider('provider', provider); - if (!_.isUndefined(config)) { - assert.doesConformToSchema('config', config, orderWatcherConfigSchema); - } - this._web3Wrapper = new Web3Wrapper(provider); - this._config = config || {}; - const artifactJSONs = _.values(artifacts); - const abiArrays = _.map(artifactJSONs, artifact => artifact.abi); - this._abiDecoder = new AbiDecoder(abiArrays); + private _orderStateUtils: OrderStateUtils; + constructor( + web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, orderStateUtils: OrderStateUtils, + mempoolPollingIntervalMs?: number) { + this._web3Wrapper = web3Wrapper; + this._eventWatcher = new EventWatcher( + this._web3Wrapper, mempoolPollingIntervalMs, + ); + this._abiDecoder = abiDecoder; + this._orderStateUtils = orderStateUtils; } public addOrder(signedOrder: SignedOrder): void { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); @@ -46,17 +46,12 @@ export class OrderStateWatcher { } public subscribe(callback: OnOrderStateChangeCallback): void { assert.isFunction('callback', callback); - this._callback = callback; - this._eventWatcher = new EventWatcher( - this._web3Wrapper, this._config.mempoolPollingIntervalMs, - ); + this._callbackAsync = callback; this._eventWatcher.subscribe(this._onMempoolEventCallbackAsync.bind(this)); } public unsubscribe(): void { - delete this._callback; - if (!_.isUndefined(this._eventWatcher)) { - this._eventWatcher.unsubscribe(); - } + delete this._callbackAsync; + this._eventWatcher.unsubscribe(); } private async _onMempoolEventCallbackAsync(log: LogEvent): Promise { const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log); @@ -65,6 +60,18 @@ export class OrderStateWatcher { } } private async _revalidateOrdersAsync(): Promise { - _.noop(); + 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 orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder, methodOpts); + if (!_.isUndefined(this._callbackAsync)) { + await this._callbackAsync(orderState); + } else { + break; // Unsubscribe was called + } + } } } diff --git a/src/schemas/order_watcher_config_schema.ts b/src/schemas/order_watcher_config_schema.ts deleted file mode 100644 index 9c2dc38a4..000000000 --- a/src/schemas/order_watcher_config_schema.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const orderWatcherConfigSchema = { - id: '/OrderWatcherConfig', - properties: { - mempoolPollingIntervalMs: { - type: 'number', - min: 0, - }, - }, - type: 'object', -}; diff --git a/src/schemas/zero_ex_config_schema.ts b/src/schemas/zero_ex_config_schema.ts index 179e29c31..5be651a9a 100644 --- a/src/schemas/zero_ex_config_schema.ts +++ b/src/schemas/zero_ex_config_schema.ts @@ -5,6 +5,10 @@ export const zeroExConfigSchema = { exchangeContractAddress: {$ref: '/Address'}, tokenRegistryContractAddress: {$ref: '/Address'}, etherTokenContractAddress: {$ref: '/Address'}, + mempoolPollingIntervalMs: { + type: 'number', + min: 0, + }, }, type: 'object', }; diff --git a/src/types.ts b/src/types.ts index 7de875dbc..969f2e96d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -399,18 +399,13 @@ export interface JSONRPCPayload { * exchangeContractAddress: The address of an exchange contract to use * tokenRegistryContractAddress: The address of a token registry contract to use * etherTokenContractAddress: The address of an ether token contract to use + * mempoolPollingIntervalMs: How often to check for new mempool events */ export interface ZeroExConfig { gasPrice?: BigNumber; // Gas price to use with every transaction exchangeContractAddress?: string; tokenRegistryContractAddress?: string; etherTokenContractAddress?: string; -} - -/* - * mempoolPollingIntervalMs: How often to check for new mempool events - */ -export interface OrderWatcherConfig { mempoolPollingIntervalMs?: number; } @@ -480,7 +475,7 @@ export enum TransferType { Fee = 'fee', } -export interface OrderState { +export interface OrderRelevantState { makerBalance: BigNumber; makerProxyAllowance: BigNumber; makerFeeBalance: BigNumber; @@ -492,7 +487,7 @@ export interface OrderState { export interface OrderStateValid { isValid: true; orderHash: string; - orderState: OrderState; + orderRelevantState: OrderRelevantState; } export interface OrderStateInvalid { @@ -501,6 +496,8 @@ export interface OrderStateInvalid { error: ExchangeContractErrs; } +export type OrderState = OrderStateValid|OrderStateInvalid; + export type OnOrderStateChangeCallback = ( - orderState: OrderStateValid|OrderStateInvalid, + orderState: OrderState, ) => void; diff --git a/src/utils/order_state_utils.ts b/src/utils/order_state_utils.ts new file mode 100644 index 000000000..2a5becf9a --- /dev/null +++ b/src/utils/order_state_utils.ts @@ -0,0 +1,99 @@ +import * as _ from 'lodash'; +import BigNumber from 'bignumber.js'; +import { + ExchangeContractErrs, + SignedOrder, + OrderRelevantState, + MethodOpts, + OrderState, + OrderStateValid, + OrderStateInvalid, +} from '../types'; +import {ZeroEx} from '../0x'; +import {TokenWrapper} from '../contract_wrappers/token_wrapper'; +import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper'; +import {utils} from '../utils/utils'; +import {constants} from '../utils/constants'; + +export class OrderStateUtils { + private tokenWrapper: TokenWrapper; + private exchangeWrapper: ExchangeWrapper; + constructor(tokenWrapper: TokenWrapper, exchangeWrapper: ExchangeWrapper) { + this.tokenWrapper = tokenWrapper; + this.exchangeWrapper = exchangeWrapper; + } + public async getOrderStateAsync(signedOrder: SignedOrder, methodOpts?: MethodOpts): Promise { + const orderRelevantState = await this.getOrderRelevantStateAsync(signedOrder, methodOpts); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + try { + this.validateIfOrderIsValid(signedOrder, orderRelevantState); + const orderState: OrderStateValid = { + isValid: true, + orderHash, + orderRelevantState, + }; + return orderState; + } catch (err) { + const orderState: OrderStateInvalid = { + isValid: false, + orderHash, + error: err.message, + }; + return orderState; + } + } + public async getOrderRelevantStateAsync( + signedOrder: SignedOrder, methodOpts?: MethodOpts): Promise { + const zrxTokenAddress = await this.exchangeWrapper.getZRXTokenAddressAsync(); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + const makerBalance = await this.tokenWrapper.getBalanceAsync( + signedOrder.makerTokenAddress, signedOrder.maker, methodOpts, + ); + const makerProxyAllowance = await this.tokenWrapper.getProxyAllowanceAsync( + signedOrder.makerTokenAddress, signedOrder.maker, methodOpts, + ); + const makerFeeBalance = await this.tokenWrapper.getBalanceAsync( + zrxTokenAddress, signedOrder.maker, methodOpts, + ); + const makerFeeProxyAllowance = await this.tokenWrapper.getProxyAllowanceAsync( + zrxTokenAddress, signedOrder.maker, methodOpts, + ); + const filledTakerTokenAmount = await this.exchangeWrapper.getFilledTakerAmountAsync(orderHash, methodOpts); + const canceledTakerTokenAmount = await this.exchangeWrapper.getCanceledTakerAmountAsync(orderHash, methodOpts); + const orderRelevantState = { + makerBalance, + makerProxyAllowance, + makerFeeBalance, + makerFeeProxyAllowance, + filledTakerTokenAmount, + canceledTakerTokenAmount, + }; + return orderRelevantState; + } + private validateIfOrderIsValid(signedOrder: SignedOrder, orderRelevantState: OrderRelevantState): void { + const unavailableTakerTokenAmount = orderRelevantState.canceledTakerTokenAmount.add( + orderRelevantState.filledTakerTokenAmount, + ); + const availableTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount); + if (availableTakerTokenAmount.eq(0)) { + throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); + } + + if (orderRelevantState.makerBalance.eq(0)) { + throw new Error(ExchangeContractErrs.InsufficientMakerBalance); + } + if (orderRelevantState.makerProxyAllowance.eq(0)) { + throw new Error(ExchangeContractErrs.InsufficientMakerAllowance); + } + if (!signedOrder.makerFee.eq(0)) { + if (orderRelevantState.makerFeeBalance.eq(0)) { + throw new Error(ExchangeContractErrs.InsufficientMakerFeeBalance); + } + if (orderRelevantState.makerFeeProxyAllowance.eq(0)) { + throw new Error(ExchangeContractErrs.InsufficientMakerFeeAllowance); + } + } + // TODO Add linear function solver when maker token is ZRX #badass + // Return the max amount that's fillable + } +} diff --git a/test/order_watcher_test.ts b/test/order_watcher_test.ts index e62b1aab2..3ce60d863 100644 --- a/test/order_watcher_test.ts +++ b/test/order_watcher_test.ts @@ -9,10 +9,15 @@ 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, + OrderStateValid, } from '../src'; +import {TokenUtils} from './utils/token_utils'; +import {FillScenarios} from './utils/fill_scenarios'; import {DoneCallback} from '../src/types'; chaiSetup.configure(); @@ -21,22 +26,77 @@ const expect = chai.expect; describe('EventWatcher', () => { let web3: Web3; let stubs: Sinon.SinonStub[] = []; - let orderStateWatcher: OrderStateWatcher; + let zeroEx: ZeroEx; + let tokens: Token[]; + let tokenUtils: TokenUtils; + let fillScenarios: FillScenarios; + let userAddresses: string[]; + let zrxTokenAddress: string; + let exchangeContractAddress: string; + let makerToken: Token; + let takerToken: Token; + let maker: string; + let taker: string; + let web3Wrapper: Web3Wrapper; + 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(); - const mempoolPollingIntervalMs = 10; - const orderStateWatcherConfig = { - mempoolPollingIntervalMs, - }; - orderStateWatcher = new OrderStateWatcher(web3.currentProvider, orderStateWatcherConfig); + zeroEx = new ZeroEx(web3.currentProvider); + exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync(); + userAddresses = await zeroEx.getAvailableAddressesAsync(); + [, maker, taker] = userAddresses; + tokens = await zeroEx.tokenRegistry.getTokensAsync(); + tokenUtils = new TokenUtils(tokens); + zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address; + fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress); + [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 = []; - orderStateWatcher.unsubscribe(); + zeroEx.orderStateWatcher.unsubscribe(); }); - it.skip('TODO', () => { - // TODO + it('should receive OrderState when order state is changed', (done: DoneCallback) => { + (async () => { + const 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); + done(); + }; + zeroEx.orderStateWatcher.subscribe(callback); + })().catch(done); }); }); -- cgit v1.2.3 From edc0fec8087d322f365a5a4c37c2337c7102e094 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Wed, 8 Nov 2017 18:59:28 -0500 Subject: remove unused type --- src/contract_wrappers/exchange_wrapper.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index b8704e72c..b027d46df 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -13,7 +13,6 @@ import { OrderAddresses, Order, SignedOrder, - ContractEvent, ExchangeEvents, SubscriptionOpts, IndexedFilterValues, -- cgit v1.2.3 From c89eec426152d2a65e1b45ad7d7b1fb10917911c Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Wed, 8 Nov 2017 18:59:40 -0500 Subject: fix styling --- src/mempool/event_watcher.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mempool/event_watcher.ts b/src/mempool/event_watcher.ts index 27f0c8207..ac07badfe 100644 --- a/src/mempool/event_watcher.ts +++ b/src/mempool/event_watcher.ts @@ -22,7 +22,8 @@ export class EventWatcher { public subscribe(callback: MempoolEventCallback): void { this._callbackAsync = callback; this._intervalId = intervalUtils.setAsyncExcludingInterval( - this._pollForMempoolEventsAsync.bind(this), this._pollingIntervalMs); + this._pollForMempoolEventsAsync.bind(this), this._pollingIntervalMs, + ); } public unsubscribe(): void { delete this._callbackAsync; -- cgit v1.2.3 From a10bb4b2fafd7093811277c50fc29456453e7623 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Wed, 8 Nov 2017 18:59:59 -0500 Subject: Add todo comments --- src/mempool/event_watcher.ts | 2 ++ src/utils/abi_decoder.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mempool/event_watcher.ts b/src/mempool/event_watcher.ts index ac07badfe..cb8921cfd 100644 --- a/src/mempool/event_watcher.ts +++ b/src/mempool/event_watcher.ts @@ -47,6 +47,7 @@ export class EventWatcher { this._lastMempoolEvents = pendingEvents; } private async _getMempoolEventsAsync(): Promise { + // TODO: Allow users to listen to any number of confirmations deep, not just mempool const mempoolFilter = { fromBlock: BlockParamLiteral.Pending, toBlock: BlockParamLiteral.Pending, @@ -54,6 +55,7 @@ export class EventWatcher { const pendingEvents = await this._web3Wrapper.getLogsAsync(mempoolFilter); return pendingEvents; } + // TODO: Let's emit out own LogEntry type that has property isRemoved rather then removed private async _emitDifferencesAsync(logs: Web3.LogEntry[], isRemoved: boolean): Promise { for (const log of logs) { const logEvent = { diff --git a/src/utils/abi_decoder.ts b/src/utils/abi_decoder.ts index 247ba0e5b..840ad9be0 100644 --- a/src/utils/abi_decoder.ts +++ b/src/utils/abi_decoder.ts @@ -10,7 +10,7 @@ export class AbiDecoder { constructor(abiArrays: Web3.AbiDefinition[][]) { _.map(abiArrays, this.addABI.bind(this)); } - // This method can only decode logs from the 0x smart contracts + // This method can only decode logs from the 0x & ERC20 smart contracts public tryToDecodeLogOrNoop( log: Web3.LogEntry): LogWithDecodedArgs|RawLog { const methodId = log.topics[0]; -- cgit v1.2.3 From e592cedbb45c381a2e1ea2b486570be4a924a2cc Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Wed, 8 Nov 2017 19:00:14 -0500 Subject: Fix typo --- test/exchange_wrapper_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index 15d1cb3e4..33cc963a0 100644 --- a/test/exchange_wrapper_test.ts +++ b/test/exchange_wrapper_test.ts @@ -71,7 +71,7 @@ describe('ExchangeWrapper', () => { takerTokenAddress = takerToken.address; }); describe('#batchFillOrKillAsync', () => { - it('successfuly batch fillOrKill', async () => { + it('successfully batch fillOrKill', async () => { const fillableAmount = new BigNumber(5); const partialFillTakerAmount = new BigNumber(2); const signedOrder = await fillScenarios.createFillableSignedOrderAsync( -- cgit v1.2.3 From 6f00c422c772c2ccf5f8f049891c12132b36f5f8 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Wed, 8 Nov 2017 19:00:38 -0500 Subject: Remove unused import --- test/exchange_wrapper_test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index 33cc963a0..20b9cf7fc 100644 --- a/test/exchange_wrapper_test.ts +++ b/test/exchange_wrapper_test.ts @@ -11,7 +11,6 @@ import { SignedOrder, SubscriptionOpts, ExchangeEvents, - ContractEvent, ExchangeContractErrs, OrderCancellationRequest, OrderFillRequest, -- cgit v1.2.3 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 From ae74965774e8b0f12165d02135097f25f8fa927b Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 10:09:20 -0500 Subject: Add assert.isValidBaseUnitAmount that checks for decimals in amounts that should be in baseUnits. This can sometimes alert developers whenever they accidentally pass in unitAmounts. --- src/contract_wrappers/ether_token_wrapper.ts | 2 ++ src/contract_wrappers/exchange_wrapper.ts | 10 ++++++++++ src/contract_wrappers/token_wrapper.ts | 4 ++++ src/utils/assert.ts | 6 ++++++ 4 files changed, 22 insertions(+) diff --git a/src/contract_wrappers/ether_token_wrapper.ts b/src/contract_wrappers/ether_token_wrapper.ts index 7c94e314a..6db07228e 100644 --- a/src/contract_wrappers/ether_token_wrapper.ts +++ b/src/contract_wrappers/ether_token_wrapper.ts @@ -30,6 +30,7 @@ export class EtherTokenWrapper extends ContractWrapper { */ public async depositAsync(amountInWei: BigNumber, depositor: string): Promise { assert.isBigNumber('amountInWei', amountInWei); + assert.isValidBaseUnitAmount('amountInWei', amountInWei); await assert.isSenderAddressAsync('depositor', depositor, this._web3Wrapper); const ethBalanceInWei = await this._web3Wrapper.getBalanceInWeiAsync(depositor); @@ -51,6 +52,7 @@ export class EtherTokenWrapper extends ContractWrapper { */ public async withdrawAsync(amountInWei: BigNumber, withdrawer: string): Promise { assert.isBigNumber('amountInWei', amountInWei); + assert.isValidBaseUnitAmount('amountInWei', amountInWei); await assert.isSenderAddressAsync('withdrawer', withdrawer, this._web3Wrapper); const wethContractAddress = await this.getContractAddressAsync(); diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index b027d46df..ddb1d6bd4 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -167,6 +167,7 @@ export class ExchangeWrapper extends ContractWrapper { orderTransactionOpts?: OrderTransactionOpts): Promise { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); @@ -240,6 +241,7 @@ export class ExchangeWrapper extends ContractWrapper { assert.hasAtMostOneUniqueValue(exchangeContractAddresses, ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress); assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); @@ -409,6 +411,7 @@ export class ExchangeWrapper extends ContractWrapper { orderTransactionOpts?: OrderTransactionOpts): Promise { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const exchangeInstance = await this._getExchangeContractAsync(); @@ -544,6 +547,7 @@ export class ExchangeWrapper extends ContractWrapper { orderTransactionOpts?: OrderTransactionOpts): Promise { assert.doesConformToSchema('order', order, schemas.orderSchema); assert.isBigNumber('takerTokenCancelAmount', cancelTakerTokenAmount); + assert.isValidBaseUnitAmount('takerTokenCancelAmount', cancelTakerTokenAmount); await assert.isSenderAddressAsync('order.maker', order.maker, this._web3Wrapper); const exchangeInstance = await this._getExchangeContractAsync(); @@ -739,6 +743,7 @@ export class ExchangeWrapper extends ContractWrapper { takerAddress: string): Promise { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const zrxTokenAddress = await this.getZRXTokenAddressAsync(); const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); @@ -755,6 +760,7 @@ export class ExchangeWrapper extends ContractWrapper { order: Order, cancelTakerTokenAmount: BigNumber): Promise { assert.doesConformToSchema('order', order, schemas.orderSchema); assert.isBigNumber('cancelTakerTokenAmount', cancelTakerTokenAmount); + assert.isValidBaseUnitAmount('cancelTakerTokenAmount', cancelTakerTokenAmount); const orderHash = utils.getOrderHashHex(order); const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash); await this._orderValidationUtils.validateCancelOrderThrowIfInvalidAsync( @@ -773,6 +779,7 @@ export class ExchangeWrapper extends ContractWrapper { takerAddress: string): Promise { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); const zrxTokenAddress = await this.getZRXTokenAddressAsync(); const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); @@ -792,8 +799,11 @@ export class ExchangeWrapper extends ContractWrapper { takerTokenAmount: BigNumber, makerTokenAmount: BigNumber): Promise { assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); assert.isBigNumber('takerTokenAmount', takerTokenAmount); + assert.isValidBaseUnitAmount('takerTokenAmount', takerTokenAmount); assert.isBigNumber('makerTokenAmount', makerTokenAmount); + assert.isValidBaseUnitAmount('makerTokenAmount', makerTokenAmount); const exchangeInstance = await this._getExchangeContractAsync(); const isRoundingError = await exchangeInstance.isRoundingError.callAsync( fillTakerTokenAmount, takerTokenAmount, makerTokenAmount, diff --git a/src/contract_wrappers/token_wrapper.ts b/src/contract_wrappers/token_wrapper.ts index 5d6d61cef..c479aa30f 100644 --- a/src/contract_wrappers/token_wrapper.ts +++ b/src/contract_wrappers/token_wrapper.ts @@ -73,6 +73,7 @@ export class TokenWrapper extends ContractWrapper { assert.isETHAddressHex('spenderAddress', spenderAddress); assert.isETHAddressHex('tokenAddress', tokenAddress); assert.isBigNumber('amountInBaseUnits', amountInBaseUnits); + assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); const tokenContract = await this._getTokenContractAsync(tokenAddress); // Hack: for some reason default estimated gas amount causes `base fee exceeds gas limit` exception @@ -153,6 +154,7 @@ export class TokenWrapper extends ContractWrapper { assert.isETHAddressHex('ownerAddress', ownerAddress); assert.isETHAddressHex('tokenAddress', tokenAddress); assert.isBigNumber('amountInBaseUnits', amountInBaseUnits); + assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); const proxyAddress = await this._getTokenTransferProxyAddressAsync(); const txHash = await this.setAllowanceAsync(tokenAddress, ownerAddress, proxyAddress, amountInBaseUnits); @@ -188,6 +190,7 @@ export class TokenWrapper extends ContractWrapper { await assert.isSenderAddressAsync('fromAddress', fromAddress, this._web3Wrapper); assert.isETHAddressHex('toAddress', toAddress); assert.isBigNumber('amountInBaseUnits', amountInBaseUnits); + assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); const tokenContract = await this._getTokenContractAsync(tokenAddress); @@ -222,6 +225,7 @@ export class TokenWrapper extends ContractWrapper { assert.isETHAddressHex('toAddress', toAddress); await assert.isSenderAddressAsync('senderAddress', senderAddress, this._web3Wrapper); assert.isBigNumber('amountInBaseUnits', amountInBaseUnits); + assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); const tokenContract = await this._getTokenContractAsync(tokenAddress); diff --git a/src/utils/assert.ts b/src/utils/assert.ts index 286105345..48aed6ad3 100644 --- a/src/utils/assert.ts +++ b/src/utils/assert.ts @@ -11,6 +11,12 @@ export const assert = { const isBigNumber = _.isObject(value) && (value as any).isBigNumber; this.assert(isBigNumber, this.typeAssertionMessage(variableName, 'BigNumber', value)); }, + isValidBaseUnitAmount(variableName: string, value: BigNumber) { + const hasDecimals = value.decimalPlaces() !== 0; + this.assert( + !hasDecimals, `${variableName} should be in baseUnits (no decimals), found value: ${value.toNumber()}`, + ); + }, isUndefined(value: any, variableName?: string): void { this.assert(_.isUndefined(value), this.typeAssertionMessage(variableName, 'undefined', value)); }, -- cgit v1.2.3 From c7c81a1f7e11daf0768617501657cb0266c0096a Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 10:21:38 -0500 Subject: Fix tests by making the expected balance be 2^27 not 2^26 --- test/token_wrapper_test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/token_wrapper_test.ts b/test/token_wrapper_test.ts index 2f6f126c1..23020c47a 100644 --- a/test/token_wrapper_test.ts +++ b/test/token_wrapper_test.ts @@ -162,7 +162,7 @@ describe('TokenWrapper', () => { const token = tokens[0]; const ownerAddress = coinbase; const balance = await zeroEx.token.getBalanceAsync(token.address, ownerAddress); - const expectedBalance = new BigNumber('100000000000000000000000000'); + const expectedBalance = new BigNumber('1000000000000000000000000000'); return expect(balance).to.be.bignumber.equal(expectedBalance); }); it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => { @@ -190,7 +190,7 @@ describe('TokenWrapper', () => { const token = tokens[0]; const ownerAddress = coinbase; const balance = await zeroExWithoutAccounts.token.getBalanceAsync(token.address, ownerAddress); - const expectedBalance = new BigNumber('100000000000000000000000000'); + const expectedBalance = new BigNumber('1000000000000000000000000000'); return expect(balance).to.be.bignumber.equal(expectedBalance); }); }); -- cgit v1.2.3 From 4f030ac45c39a30da4f058284b52301d4422a595 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 10:21:54 -0500 Subject: Rename test file and add test for a partial fill --- test/order_state_watcher_test.ts | 165 +++++++++++++++++++++++++++++++++++++++ test/order_watcher_test.ts | 121 ---------------------------- 2 files changed, 165 insertions(+), 121 deletions(-) create mode 100644 test/order_state_watcher_test.ts delete mode 100644 test/order_watcher_test.ts diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts new file mode 100644 index 000000000..f1b027c40 --- /dev/null +++ b/test/order_state_watcher_test.ts @@ -0,0 +1,165 @@ +import 'mocha'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +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 { + 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'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('OrderStateWatcher', () => { + let web3: Web3; + let zeroEx: ZeroEx; + let tokens: Token[]; + let tokenUtils: TokenUtils; + let fillScenarios: FillScenarios; + let userAddresses: string[]; + let zrxTokenAddress: string; + let exchangeContractAddress: string; + let makerToken: Token; + let takerToken: Token; + let maker: string; + let taker: string; + let web3Wrapper: Web3Wrapper; + let signedOrder: SignedOrder; + const fillableAmount = new BigNumber(5); + before(async () => { + web3 = web3Factory.create(); + zeroEx = new ZeroEx(web3.currentProvider); + exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync(); + userAddresses = await zeroEx.getAvailableAddressesAsync(); + [, maker, taker] = userAddresses; + tokens = await zeroEx.tokenRegistry.getTokensAsync(); + tokenUtils = new TokenUtils(tokens); + zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address; + fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress); + [makerToken, takerToken] = tokenUtils.getNonProtocolTokens(); + web3Wrapper = (zeroEx as any)._web3Wrapper; + }); + afterEach(async () => { + zeroEx.orderStateWatcher.unsubscribe(); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + zeroEx.orderStateWatcher.removeOrder(signedOrder); + }); + it('should emit orderStateInvalid when maker allowance set to 0 for 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.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); + + let eventCount = 0; + const callback = (orderState: OrderState) => { + eventCount++; + 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); + if (eventCount === 2) { + done(); + } + }; + zeroEx.orderStateWatcher.subscribe(callback); + + const shouldThrowOnInsufficientBalanceOrAllowance = true; + await zeroEx.exchange.fillOrderAsync( + signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, taker, + ); + })().catch(done); + }); + it('should emit orderStateValid when watched order partially filled', (done: DoneCallback) => { + (async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerToken.address, takerToken.address, maker, taker, fillableAmount, + ); + + const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); + const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker); + + const fillAmountInBaseUnits = new BigNumber(2); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + zeroEx.orderStateWatcher.addOrder(signedOrder); + + let eventCount = 0; + const callback = (orderState: OrderState) => { + eventCount++; + expect(orderState.isValid).to.be.true(); + const validOrderState = orderState as OrderStateValid; + expect(validOrderState.orderHash).to.be.equal(orderHash); + const orderRelevantState = validOrderState.orderRelevantState; + const remainingMakerBalance = makerBalance.sub(fillAmountInBaseUnits); + expect(orderRelevantState.makerBalance).to.be.bignumber.equal(remainingMakerBalance); + if (eventCount === 2) { + done(); + } + }; + zeroEx.orderStateWatcher.subscribe(callback); + const shouldThrowOnInsufficientBalanceOrAllowance = true; + await zeroEx.exchange.fillOrderAsync( + signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker, + ); + })().catch(done); + }); +}); + +/* + * - it should emit orderState when watched order partially filled + * - it should emit orderState when watched order is cancelled + */ diff --git a/test/order_watcher_test.ts b/test/order_watcher_test.ts deleted file mode 100644 index 11138567c..000000000 --- a/test/order_watcher_test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import 'mocha'; -import * as chai from 'chai'; -import * as _ from 'lodash'; -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 { - 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'; - -chaiSetup.configure(); -const expect = chai.expect; - -describe.only('EventWatcher', () => { - let web3: Web3; - let zeroEx: ZeroEx; - let tokens: Token[]; - let tokenUtils: TokenUtils; - let fillScenarios: FillScenarios; - let userAddresses: string[]; - let zrxTokenAddress: string; - let exchangeContractAddress: string; - let makerToken: Token; - let takerToken: Token; - let maker: string; - let taker: string; - let web3Wrapper: Web3Wrapper; - let signedOrder: SignedOrder; - const fillableAmount = new BigNumber(5); - before(async () => { - web3 = web3Factory.create(); - zeroEx = new ZeroEx(web3.currentProvider); - exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync(); - userAddresses = await zeroEx.getAvailableAddressesAsync(); - [, maker, taker] = userAddresses; - tokens = await zeroEx.tokenRegistry.getTokensAsync(); - tokenUtils = new TokenUtils(tokens); - zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address; - fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress); - [makerToken, takerToken] = tokenUtils.getNonProtocolTokens(); - web3Wrapper = (zeroEx as any)._web3Wrapper; - }); - afterEach(async () => { - zeroEx.orderStateWatcher.unsubscribe(); - zeroEx.orderStateWatcher.removeOrder(signedOrder); - }); - it('should emit orderStateInvalid when maker allowance set to 0 for 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.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 From 1351e02065c9b6dca3be4827153fe2a0911fcca4 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 10:22:29 -0500 Subject: Remove check for now, we need a more robust check --- src/mempool/order_state_watcher.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/mempool/order_state_watcher.ts b/src/mempool/order_state_watcher.ts index 436f86554..dc24d5b4a 100644 --- a/src/mempool/order_state_watcher.ts +++ b/src/mempool/order_state_watcher.ts @@ -58,9 +58,6 @@ export class OrderStateWatcher { } 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); delete this._orders[orderHash]; this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress].delete(orderHash); -- cgit v1.2.3