From f53472e7170798f56ea4837c310cfd4188326af8 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 8e1cfd502..e615a8a3a 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 161f62dc27b95f214bfaf8dd38fa40e7a4910584 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 e615a8a3a..0511a448a 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 02d470892f68abb5f92a9b79b4a581979288f2f3 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 1bb9c912cd3f33bf677a5e64904faba4014d7b87 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 eac467fe9a7409ddbce49a81fa18b5fb6b7e0f67 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 e4d8b1c4d2a3498d5bc0ce84006d15d6b80a9587 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 68a8556cd20e45731343053ece9fb92944799a4c 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 f21f42f11ed11ef7b4e8ec7de1281150bf8b695c 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 26394813f4824590bac00da05ab1026aa360c77f 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 7bf6e6188aad6f5ae3de4fe04074abed33c73c53 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 02b33f988f2ffa3e02fcb1c88987498cf35c42cc 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 a6c110f55827f06c6942806a1eec58dfa23f5385 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 fd2c5d46ad7f052dfa6c04d14451f80e1efce943 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 6ca6290f6a7f000dbabdbc0d32a6bbabfe277336 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 1c90c3af4211b56994a11dd1f583513c961e0f6d 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 2a25ece3636015e8429fe4556b62fc84545dc7c7 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 ed7917f9dfc83ea4dc150fbfc14145f038c9642d 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 456f7e73048bd9176b2126f856fb9b5415c4089b 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 6bfcd253f8e9689ce787899a42f80914b067a4f1 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 a896904ae7c453f51b1f46de2be3a28416db72d1 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 c57894633f8bf856c9cda13b54ff6812d524b61b 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 d39852c0cf8112015235e938df16a481e84b9ab0 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 ee3115550e26b276c35b33fed085f46a08ca05d5 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 641dff8991e05f25a6b46bbde3b121711ed2cab5 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 5a6ed252c400b0442045354b35a06162f3d1b89e 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 6007609f7132d5f919c8e9de04ae6c652ce38980 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 c96c681758a9bb62b4444ce21747c3781e9dc742 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 a8585df81bed4a1d4da8b0bb1f50e7f6e39c73ad 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 453f3405a7069bce2cefabdc26c5b1d5ca834981 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 04e01997906b0859dc920c8acb1b92993028016a 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 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 From 709fa06af6efc762ccd7a4219b7b52076190831e Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Nov 2017 14:23:53 -0500 Subject: Pass orderHash instead of an order to removeOrder and adjust the tests --- src/mempool/order_state_watcher.ts | 9 ++++++--- test/order_state_watcher_test.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/mempool/order_state_watcher.ts b/src/mempool/order_state_watcher.ts index dc24d5b4a..528b8ceff 100644 --- a/src/mempool/order_state_watcher.ts +++ b/src/mempool/order_state_watcher.ts @@ -56,9 +56,12 @@ export class OrderStateWatcher { this._orders[orderHash] = signedOrder; this.addToDependentOrderHashes(signedOrder, orderHash); } - public removeOrder(signedOrder: SignedOrder): void { - assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); - const orderHash = ZeroEx.getOrderHashHex(signedOrder); + public removeOrder(orderHash: string): void { + assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); + const signedOrder = this._orders[orderHash]; + if (_.isUndefined(signedOrder)) { + return; + } 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 diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index f1b027c40..f5fc5d4ea 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -57,7 +57,7 @@ describe('OrderStateWatcher', () => { afterEach(async () => { zeroEx.orderStateWatcher.unsubscribe(); const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.removeOrder(signedOrder); + zeroEx.orderStateWatcher.removeOrder(orderHash); }); it('should emit orderStateInvalid when maker allowance set to 0 for watched order', (done: DoneCallback) => { (async () => { -- cgit v1.2.3 From 41a0ce146da576b9bba180e5d51f2121032671fa Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Nov 2017 14:54:55 -0500 Subject: Add tests for order removals --- test/order_state_watcher_test.ts | 201 ++++++++++++++++++++++----------------- 1 file changed, 116 insertions(+), 85 deletions(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index f5fc5d4ea..966eec2f7 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -54,108 +54,139 @@ describe('OrderStateWatcher', () => { [makerToken, takerToken] = tokenUtils.getNonProtocolTokens(); web3Wrapper = (zeroEx as any)._web3Wrapper; }); - afterEach(async () => { - zeroEx.orderStateWatcher.unsubscribe(); - const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.removeOrder(orderHash); - }); - it('should emit orderStateInvalid when maker allowance set to 0 for watched order', (done: DoneCallback) => { - (async () => { + describe('#removeOrder', async () => { + it('should successfully remove existing order', 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 () => { + expect((zeroEx.orderStateWatcher as any)._orders).to.include({ + [orderHash]: signedOrder, + }); + let dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes; + expect(dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress]).to.have.keys(orderHash); + zeroEx.orderStateWatcher.removeOrder(orderHash); + expect((zeroEx.orderStateWatcher as any)._orders).to.not.include({ + [orderHash]: signedOrder, + }); + dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes; + expect(dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress]).to.not.have.keys(orderHash); + }); + it('should no-op when removing a non-existing order', async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( makerToken.address, takerToken.address, maker, taker, fillableAmount, ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); + const nonExistentOrderHash = `0x${orderHash.substr(2).split('').reverse().join('')}`; 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); + zeroEx.orderStateWatcher.removeOrder(nonExistentOrderHash); + }); }); - it('should emit orderStateInvalid when watched order fully filled', (done: DoneCallback) => { - (async () => { - signedOrder = await fillScenarios.createFillableSignedOrderAsync( - makerToken.address, takerToken.address, maker, taker, fillableAmount, - ); + describe('tests with cleanup', async () => { + afterEach(async () => { + zeroEx.orderStateWatcher.unsubscribe(); 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) { + zeroEx.orderStateWatcher.removeOrder(orderHash); + }); + 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); + }; + 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 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, - ); + 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 makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); - const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker); + 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 fillAmountInBaseUnits = new BigNumber(2); - const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.addOrder(signedOrder); + const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); + const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker); - 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); + 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); + }); }); }); -- cgit v1.2.3 From 9ff42053c3f145ab6d5486d62325ed222363a8c5 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 15:02:28 -0500 Subject: Add numConfirmations arg so that caller can decide on numConfirmations at which they want to watch orders --- src/mempool/event_watcher.ts | 24 ++++++++++++++++-------- src/mempool/order_state_watcher.ts | 3 ++- src/web3_wrapper.ts | 4 ++++ test/order_state_watcher_test.ts | 9 +++++---- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/mempool/event_watcher.ts b/src/mempool/event_watcher.ts index cb8921cfd..3f40606e7 100644 --- a/src/mempool/event_watcher.ts +++ b/src/mempool/event_watcher.ts @@ -19,10 +19,10 @@ export class EventWatcher { DEFAULT_MEMPOOL_POLLING_INTERVAL : pollingIntervalMs; } - public subscribe(callback: MempoolEventCallback): void { + public subscribe(callback: MempoolEventCallback, numConfirmations: number): void { this._callbackAsync = callback; this._intervalId = intervalUtils.setAsyncExcludingInterval( - this._pollForMempoolEventsAsync.bind(this), this._pollingIntervalMs, + this._pollForMempoolEventsAsync.bind(this, numConfirmations), this._pollingIntervalMs, ); } public unsubscribe(): void { @@ -30,8 +30,8 @@ export class EventWatcher { this._lastMempoolEvents = []; intervalUtils.clearAsyncExcludingInterval(this._intervalId); } - private async _pollForMempoolEventsAsync(): Promise { - const pendingEvents = await this._getMempoolEventsAsync(); + private async _pollForMempoolEventsAsync(numConfirmations: number): Promise { + const pendingEvents = await this._getMempoolEventsAsync(numConfirmations); 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, @@ -46,11 +46,19 @@ export class EventWatcher { await this._emitDifferencesAsync(newEvents, isRemoved); this._lastMempoolEvents = pendingEvents; } - private async _getMempoolEventsAsync(): Promise { - // TODO: Allow users to listen to any number of confirmations deep, not just mempool + private async _getMempoolEventsAsync(numConfirmations: number): Promise { + let fromBlock: BlockParamLiteral|number; + let toBlock: BlockParamLiteral|number; + if (numConfirmations === 0) { + fromBlock = BlockParamLiteral.Pending; + toBlock = BlockParamLiteral.Pending; + } else { + toBlock = await this._web3Wrapper.getBlockNumberAsync(); + fromBlock = toBlock - numConfirmations; + } const mempoolFilter = { - fromBlock: BlockParamLiteral.Pending, - toBlock: BlockParamLiteral.Pending, + fromBlock, + toBlock, }; const pendingEvents = await this._web3Wrapper.getLogsAsync(mempoolFilter); return pendingEvents; diff --git a/src/mempool/order_state_watcher.ts b/src/mempool/order_state_watcher.ts index dc24d5b4a..05d77d15f 100644 --- a/src/mempool/order_state_watcher.ts +++ b/src/mempool/order_state_watcher.ts @@ -64,9 +64,10 @@ export class OrderStateWatcher { // We currently do not remove the maker/makerToken keys from the mapping when all orderHashes removed } public subscribe(callback: OnOrderStateChangeCallback): void { + public subscribe(callback: OnOrderStateChangeCallback, numConfirmations: number): void { assert.isFunction('callback', callback); this._callbackAsync = callback; - this._eventWatcher.subscribe(this._onMempoolEventCallbackAsync.bind(this)); + this._eventWatcher.subscribe(this._onMempoolEventCallbackAsync.bind(this), numConfirmations); } public unsubscribe(): void { delete this._callbackAsync; diff --git a/src/web3_wrapper.ts b/src/web3_wrapper.ts index 01d572654..d03c102b2 100644 --- a/src/web3_wrapper.ts +++ b/src/web3_wrapper.ts @@ -100,6 +100,10 @@ export class Web3Wrapper { const signData = await promisify(this.web3.eth.sign)(address, message); return signData; } + public async getBlockNumberAsync(): Promise { + const blockNumber = await promisify(this.web3.eth.getBlockNumber)(); + return blockNumber; + } public async getBlockAsync(blockParam: string|Web3.BlockParam): Promise { const block = await promisify(this.web3.eth.getBlock)(blockParam); return block; diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index f1b027c40..9d0e1a625 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -41,6 +41,7 @@ describe('OrderStateWatcher', () => { let web3Wrapper: Web3Wrapper; let signedOrder: SignedOrder; const fillableAmount = new BigNumber(5); + const numConfirmations = 0; before(async () => { web3 = web3Factory.create(); zeroEx = new ZeroEx(web3.currentProvider); @@ -73,7 +74,7 @@ describe('OrderStateWatcher', () => { expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerAllowance); done(); }; - zeroEx.orderStateWatcher.subscribe(callback); + zeroEx.orderStateWatcher.subscribe(callback, numConfirmations); await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0)); })().catch(done); }); @@ -91,7 +92,7 @@ describe('OrderStateWatcher', () => { expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerBalance); done(); }; - zeroEx.orderStateWatcher.subscribe(callback); + zeroEx.orderStateWatcher.subscribe(callback, numConfirmations); const anyRecipient = taker; const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); await zeroEx.token.transferAsync(makerToken.address, maker, anyRecipient, makerBalance); @@ -116,7 +117,7 @@ describe('OrderStateWatcher', () => { done(); } }; - zeroEx.orderStateWatcher.subscribe(callback); + zeroEx.orderStateWatcher.subscribe(callback, numConfirmations); const shouldThrowOnInsufficientBalanceOrAllowance = true; await zeroEx.exchange.fillOrderAsync( @@ -150,7 +151,7 @@ describe('OrderStateWatcher', () => { done(); } }; - zeroEx.orderStateWatcher.subscribe(callback); + zeroEx.orderStateWatcher.subscribe(callback, numConfirmations); const shouldThrowOnInsufficientBalanceOrAllowance = true; await zeroEx.exchange.fillOrderAsync( signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker, -- cgit v1.2.3 From 545cc0b026dbff64fadc7ba9e9e077e3a8371079 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 15:02:44 -0500 Subject: Add comments to public methods --- src/mempool/order_state_watcher.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/mempool/order_state_watcher.ts b/src/mempool/order_state_watcher.ts index 05d77d15f..db44344a7 100644 --- a/src/mempool/order_state_watcher.ts +++ b/src/mempool/order_state_watcher.ts @@ -50,12 +50,20 @@ export class OrderStateWatcher { this._abiDecoder = abiDecoder; this._orderStateUtils = orderStateUtils; } + /** + * Add an order to the orderStateWatcher + * @param signedOrder The order you wish to start watching. + */ public addOrder(signedOrder: SignedOrder): void { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); const orderHash = ZeroEx.getOrderHashHex(signedOrder); this._orders[orderHash] = signedOrder; this.addToDependentOrderHashes(signedOrder, orderHash); } + /** + * Removes an order from the orderStateWatcher + * @param signedOrder The order you wish to stop watching. + */ public removeOrder(signedOrder: SignedOrder): void { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); const orderHash = ZeroEx.getOrderHashHex(signedOrder); @@ -63,12 +71,24 @@ export class OrderStateWatcher { 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 { + /** + * Starts an orderStateWatcher subscription. The callback will be notified every time a watched order's + * backing blockchain state has changed. This is a call-to-action for the caller to re-validate the order + * @param callback Receives the orderHash of the order that should be re-validated, together. + * with all the order-relevant blockchain state needed to re-validate the order + * @param numConfirmations Number of confirmed blocks deeps you want to run the orderWatcher from. Passing + * is 0 will watch the backing node's mempool, 3 will emit events when blockchain + * state relevant to a watched order changed 3 blocks ago. + */ public subscribe(callback: OnOrderStateChangeCallback, numConfirmations: number): void { assert.isFunction('callback', callback); this._callbackAsync = callback; this._eventWatcher.subscribe(this._onMempoolEventCallbackAsync.bind(this), numConfirmations); } + /** + * Ends an orderStateWatcher subscription. + * @param signedOrder The order you wish to stop watching. + */ public unsubscribe(): void { delete this._callbackAsync; this._eventWatcher.unsubscribe(); -- cgit v1.2.3 From 31f6934787f5f6d06abba4f488ce49f6d0cdbbee Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Nov 2017 15:18:08 -0500 Subject: Add a test that a second subscription fails --- test/order_state_watcher_test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 5569d2354..913ccf602 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -84,6 +84,20 @@ describe('OrderStateWatcher', () => { zeroEx.orderStateWatcher.removeOrder(nonExistentOrderHash); }); }); + describe('#subscribe', async () => { + afterEach(async () => { + zeroEx.orderStateWatcher.unsubscribe(); + }); + it('should fail when trying to subscribe twice', (done: DoneCallback) => { + zeroEx.orderStateWatcher.subscribe(_.noop); + try { + zeroEx.orderStateWatcher.subscribe(_.noop); + done(new Error('Expected the second subscription to fail')); + } catch (err) { + done(); + } + }); + }); describe('tests with cleanup', async () => { afterEach(async () => { zeroEx.orderStateWatcher.unsubscribe(); -- cgit v1.2.3 From 7a231b31660de45824e7e6265039fd64fc961f13 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Nov 2017 15:30:41 -0500 Subject: Removed unused order adding in tests --- test/order_state_watcher_test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 913ccf602..0a0e3e996 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -80,7 +80,6 @@ describe('OrderStateWatcher', () => { ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); const nonExistentOrderHash = `0x${orderHash.substr(2).split('').reverse().join('')}`; - zeroEx.orderStateWatcher.addOrder(signedOrder); zeroEx.orderStateWatcher.removeOrder(nonExistentOrderHash); }); }); -- cgit v1.2.3 From 3a96fec03b5b258a8a2ec5f1c2771cf2a9742e8b Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Nov 2017 15:38:23 -0500 Subject: Pass numConfirmations --- test/order_state_watcher_test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 0a0e3e996..938e1be4c 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -88,9 +88,9 @@ describe('OrderStateWatcher', () => { zeroEx.orderStateWatcher.unsubscribe(); }); it('should fail when trying to subscribe twice', (done: DoneCallback) => { - zeroEx.orderStateWatcher.subscribe(_.noop); + zeroEx.orderStateWatcher.subscribe(_.noop, numConfirmations); try { - zeroEx.orderStateWatcher.subscribe(_.noop); + zeroEx.orderStateWatcher.subscribe(_.noop, numConfirmations); done(new Error('Expected the second subscription to fail')); } catch (err) { done(); -- cgit v1.2.3 From c9e0b298781ca1522f69cae66ac966a0e4800469 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Nov 2017 15:44:03 -0500 Subject: Add SubscriptionAlreadyPresent error --- src/mempool/order_state_watcher.ts | 4 ++++ src/types.ts | 1 + test/event_watcher_test.ts | 5 +++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/mempool/order_state_watcher.ts b/src/mempool/order_state_watcher.ts index 63e812054..d0bf5d89c 100644 --- a/src/mempool/order_state_watcher.ts +++ b/src/mempool/order_state_watcher.ts @@ -17,6 +17,7 @@ import { OnOrderStateChangeCallback, ExchangeEvents, TokenEvents, + ZeroExError, } from '../types'; import {Web3Wrapper} from '../web3_wrapper'; @@ -85,6 +86,9 @@ export class OrderStateWatcher { */ public subscribe(callback: OnOrderStateChangeCallback, numConfirmations: number): void { assert.isFunction('callback', callback); + if (!_.isUndefined(this._callbackAsync)) { + throw new Error(ZeroExError.SubscriptionAlreadyPresent); + } this._callbackAsync = callback; this._eventWatcher.subscribe(this._onMempoolEventCallbackAsync.bind(this), numConfirmations); } diff --git a/src/types.ts b/src/types.ts index 969f2e96d..246fd8641 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,6 +16,7 @@ export enum ZeroExError { OutOfGas = 'OUT_OF_GAS', NoNetworkId = 'NO_NETWORK_ID', SubscriptionNotFound = 'SUBSCRIPTION_NOT_FOUND', + SubscriptionAlreadyPresent = 'SUBSCRIPTION_ALREADY_PRESENT', } export enum InternalZeroExError { diff --git a/test/event_watcher_test.ts b/test/event_watcher_test.ts index 208871ea8..a246805a0 100644 --- a/test/event_watcher_test.ts +++ b/test/event_watcher_test.ts @@ -23,6 +23,7 @@ describe('EventWatcher', () => { let stubs: Sinon.SinonStub[] = []; let eventWatcher: EventWatcher; let web3Wrapper: Web3Wrapper; + const numConfirmations = 0; const logA = { address: '0x71d271f8b14adef568f8f28f1587ce7271ac4ca5', blockHash: null, @@ -87,7 +88,7 @@ describe('EventWatcher', () => { done(); } }; - eventWatcher.subscribe(callback); + eventWatcher.subscribe(callback, numConfirmations); }); it('correctly computes the difference and emits only changes', (done: DoneCallback) => { const initialLogs: Web3.LogEntry[] = [logA, logB]; @@ -121,6 +122,6 @@ describe('EventWatcher', () => { done(); } }; - eventWatcher.subscribe(callback); + eventWatcher.subscribe(callback, numConfirmations); }); }); -- cgit v1.2.3 From ce11a38d700a89ae5cda5810dff34a0efd2068ba Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 16:23:39 -0500 Subject: Improve comment --- src/0x.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/0x.ts b/src/0x.ts index f1b271810..313f723d7 100644 --- a/src/0x.ts +++ b/src/0x.ts @@ -68,7 +68,8 @@ export class ZeroEx { */ public proxy: TokenTransferProxyWrapper; /** - * An instance of the OrderStateWatcher class containing methods for watching the order state changes. + * An instance of the OrderStateWatcher class containing methods for watching a set of orders for relevant + * blockchain state changes. */ public orderStateWatcher: OrderStateWatcher; private _web3Wrapper: Web3Wrapper; -- cgit v1.2.3 From ecc54b07c70c9f191a0eb0dece8137f088250a41 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 16:30:14 -0500 Subject: Use _.get for optional configs --- src/0x.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/0x.ts b/src/0x.ts index 313f723d7..51744d2d6 100644 --- a/src/0x.ts +++ b/src/0x.ts @@ -1,5 +1,6 @@ import * as _ from 'lodash'; import BigNumber from 'bignumber.js'; +import * as Web3 from 'web3'; import {SchemaValidator, schemas} from '0x-json-schemas'; import {bigNumberConfigs} from './bignumber_config'; import * as ethUtil from 'ethereumjs-util'; @@ -187,7 +188,7 @@ export class ZeroEx { const artifactJSONs = _.values(artifacts); const abiArrays = _.map(artifactJSONs, artifact => artifact.abi); this._abiDecoder = new AbiDecoder(abiArrays); - const gasPrice = _.isUndefined(config) ? undefined : config.gasPrice; + const gasPrice: number|undefined = _.get(config, 'gasPrice'); const defaults = { gasPrice, }; @@ -197,7 +198,7 @@ export class ZeroEx { this._abiDecoder, this._getTokenTransferProxyAddressAsync.bind(this), ); - const exchageContractAddressIfExists = _.isUndefined(config) ? undefined : config.exchangeContractAddress; + const exchageContractAddressIfExists: string|undefined = _.get(config, 'exchangeContractAddress'); this.exchange = new ExchangeWrapper( this._web3Wrapper, this._abiDecoder, @@ -208,13 +209,11 @@ export class ZeroEx { this._web3Wrapper, this._getTokenTransferProxyAddressAsync.bind(this), ); - const tokenRegistryContractAddressIfExists = _.isUndefined(config) ? - undefined : - config.tokenRegistryContractAddress; - 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 tokenRegistryContractAddressIfExists = _.get(config, 'tokenRegistryContractAddress'); + this.tokenRegistry = new TokenRegistryWrapper(this._web3Wrapper, 'tokenRegistryContractAddressIfExists'); + const etherTokenContractAddressIfExists = _.get(config, 'etherTokenContractAddress'); + this.etherToken = new EtherTokenWrapper(this._web3Wrapper, this.token, 'etherTokenContractAddressIfExists'); + const mempoolPollingIntervalMs: number|undefined = _.get(config, 'mempoolPollingIntervalMs'); const orderStateUtils = new OrderStateUtils(this.token, this.exchange); this.orderStateWatcher = new OrderStateWatcher( this._web3Wrapper, this._abiDecoder, orderStateUtils, mempoolPollingIntervalMs, -- cgit v1.2.3 From 6aa91d89e0fd02601f7e4df86e8499edc00316ce Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Nov 2017 16:30:32 -0500 Subject: Remove redundant assertions --- 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 | 1 + 4 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/contract_wrappers/ether_token_wrapper.ts b/src/contract_wrappers/ether_token_wrapper.ts index 6db07228e..3cd2f0224 100644 --- a/src/contract_wrappers/ether_token_wrapper.ts +++ b/src/contract_wrappers/ether_token_wrapper.ts @@ -29,7 +29,6 @@ export class EtherTokenWrapper extends ContractWrapper { * @return Transaction hash. */ public async depositAsync(amountInWei: BigNumber, depositor: string): Promise { - assert.isBigNumber('amountInWei', amountInWei); assert.isValidBaseUnitAmount('amountInWei', amountInWei); await assert.isSenderAddressAsync('depositor', depositor, this._web3Wrapper); @@ -51,7 +50,6 @@ export class EtherTokenWrapper extends ContractWrapper { * @return Transaction hash. */ public async withdrawAsync(amountInWei: BigNumber, withdrawer: string): Promise { - assert.isBigNumber('amountInWei', amountInWei); assert.isValidBaseUnitAmount('amountInWei', amountInWei); await assert.isSenderAddressAsync('withdrawer', withdrawer, this._web3Wrapper); diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index ddb1d6bd4..f68e80b5d 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -166,7 +166,6 @@ export class ExchangeWrapper extends ContractWrapper { takerAddress: string, 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,7 +239,6 @@ export class ExchangeWrapper extends ContractWrapper { const exchangeContractAddresses = _.map(signedOrders, signedOrder => signedOrder.exchangeContractAddress); assert.hasAtMostOneUniqueValue(exchangeContractAddresses, ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress); - assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); @@ -410,7 +408,6 @@ export class ExchangeWrapper extends ContractWrapper { takerAddress: string, orderTransactionOpts?: OrderTransactionOpts): Promise { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); - assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); @@ -546,7 +543,6 @@ export class ExchangeWrapper extends ContractWrapper { cancelTakerTokenAmount: BigNumber, 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); @@ -742,7 +738,6 @@ export class ExchangeWrapper extends ContractWrapper { fillTakerTokenAmount: BigNumber, 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(); @@ -759,7 +754,6 @@ export class ExchangeWrapper extends ContractWrapper { public async validateCancelOrderThrowIfInvalidAsync( 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); @@ -778,7 +772,6 @@ export class ExchangeWrapper extends ContractWrapper { fillTakerTokenAmount: BigNumber, 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(); @@ -798,11 +791,8 @@ export class ExchangeWrapper extends ContractWrapper { public async isRoundingErrorAsync(fillTakerTokenAmount: BigNumber, 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( diff --git a/src/contract_wrappers/token_wrapper.ts b/src/contract_wrappers/token_wrapper.ts index c479aa30f..dd87ebc9f 100644 --- a/src/contract_wrappers/token_wrapper.ts +++ b/src/contract_wrappers/token_wrapper.ts @@ -72,7 +72,6 @@ export class TokenWrapper extends ContractWrapper { await assert.isSenderAddressAsync('ownerAddress', ownerAddress, this._web3Wrapper); assert.isETHAddressHex('spenderAddress', spenderAddress); assert.isETHAddressHex('tokenAddress', tokenAddress); - assert.isBigNumber('amountInBaseUnits', amountInBaseUnits); assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); const tokenContract = await this._getTokenContractAsync(tokenAddress); @@ -153,7 +152,6 @@ export class TokenWrapper extends ContractWrapper { amountInBaseUnits: BigNumber): Promise { assert.isETHAddressHex('ownerAddress', ownerAddress); assert.isETHAddressHex('tokenAddress', tokenAddress); - assert.isBigNumber('amountInBaseUnits', amountInBaseUnits); assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); const proxyAddress = await this._getTokenTransferProxyAddressAsync(); @@ -189,7 +187,6 @@ export class TokenWrapper extends ContractWrapper { assert.isETHAddressHex('tokenAddress', tokenAddress); 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); @@ -224,7 +221,6 @@ export class TokenWrapper extends ContractWrapper { assert.isETHAddressHex('fromAddress', fromAddress); 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 48aed6ad3..667b41b7b 100644 --- a/src/utils/assert.ts +++ b/src/utils/assert.ts @@ -12,6 +12,7 @@ export const assert = { this.assert(isBigNumber, this.typeAssertionMessage(variableName, 'BigNumber', value)); }, isValidBaseUnitAmount(variableName: string, value: BigNumber) { + assert.isBigNumber(variableName, value); const hasDecimals = value.decimalPlaces() !== 0; this.assert( !hasDecimals, `${variableName} should be in baseUnits (no decimals), found value: ${value.toNumber()}`, -- cgit v1.2.3 From d98d8859248b3ba926f1f5a7360468b3c24e730d Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Nov 2017 16:37:00 -0500 Subject: Fix namings --- src/mempool/event_watcher.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mempool/event_watcher.ts b/src/mempool/event_watcher.ts index 3f40606e7..398b74ddc 100644 --- a/src/mempool/event_watcher.ts +++ b/src/mempool/event_watcher.ts @@ -31,7 +31,7 @@ export class EventWatcher { intervalUtils.clearAsyncExcludingInterval(this._intervalId); } private async _pollForMempoolEventsAsync(numConfirmations: number): Promise { - const pendingEvents = await this._getMempoolEventsAsync(numConfirmations); + const pendingEvents = await this._getEventsAsync(numConfirmations); 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, @@ -46,7 +46,7 @@ export class EventWatcher { await this._emitDifferencesAsync(newEvents, isRemoved); this._lastMempoolEvents = pendingEvents; } - private async _getMempoolEventsAsync(numConfirmations: number): Promise { + private async _getEventsAsync(numConfirmations: number): Promise { let fromBlock: BlockParamLiteral|number; let toBlock: BlockParamLiteral|number; if (numConfirmations === 0) { @@ -56,12 +56,12 @@ export class EventWatcher { toBlock = await this._web3Wrapper.getBlockNumberAsync(); fromBlock = toBlock - numConfirmations; } - const mempoolFilter = { + const eventFilter = { fromBlock, toBlock, }; - const pendingEvents = await this._web3Wrapper.getLogsAsync(mempoolFilter); - return pendingEvents; + const events = await this._web3Wrapper.getLogsAsync(eventFilter); + return events; } // TODO: Let's emit out own LogEntry type that has property isRemoved rather then removed private async _emitDifferencesAsync(logs: Web3.LogEntry[], isRemoved: boolean): Promise { -- cgit v1.2.3 From 441c1f9ab77bbbaef7186bf52964cbfdf7c12208 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 16:41:57 -0500 Subject: rename folder to order_watcher --- src/0x.ts | 2 +- src/mempool/event_watcher.ts | 78 -------------- src/mempool/order_state_watcher.ts | 175 ------------------------------- src/order_watcher/event_watcher.ts | 78 ++++++++++++++ src/order_watcher/order_state_watcher.ts | 175 +++++++++++++++++++++++++++++++ 5 files changed, 254 insertions(+), 254 deletions(-) delete mode 100644 src/mempool/event_watcher.ts delete mode 100644 src/mempool/order_state_watcher.ts create mode 100644 src/order_watcher/event_watcher.ts create mode 100644 src/order_watcher/order_state_watcher.ts diff --git a/src/0x.ts b/src/0x.ts index 51744d2d6..75a154930 100644 --- a/src/0x.ts +++ b/src/0x.ts @@ -17,7 +17,7 @@ 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 {OrderStateWatcher} from './order_watcher/order_state_watcher'; import {OrderStateUtils} from './utils/order_state_utils'; import { ECSignature, diff --git a/src/mempool/event_watcher.ts b/src/mempool/event_watcher.ts deleted file mode 100644 index 3f40606e7..000000000 --- a/src/mempool/event_watcher.ts +++ /dev/null @@ -1,78 +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 EventWatcher { - private _web3Wrapper: Web3Wrapper; - private _pollingIntervalMs: number; - private _intervalId: NodeJS.Timer; - private _lastMempoolEvents: Web3.LogEntry[] = []; - private _callbackAsync?: MempoolEventCallback; - constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) { - this._web3Wrapper = web3Wrapper; - this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? - DEFAULT_MEMPOOL_POLLING_INTERVAL : - pollingIntervalMs; - } - public subscribe(callback: MempoolEventCallback, numConfirmations: number): void { - this._callbackAsync = callback; - this._intervalId = intervalUtils.setAsyncExcludingInterval( - this._pollForMempoolEventsAsync.bind(this, numConfirmations), this._pollingIntervalMs, - ); - } - public unsubscribe(): void { - delete this._callbackAsync; - this._lastMempoolEvents = []; - intervalUtils.clearAsyncExcludingInterval(this._intervalId); - } - private async _pollForMempoolEventsAsync(numConfirmations: number): Promise { - const pendingEvents = await this._getMempoolEventsAsync(numConfirmations); - 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; - await this._emitDifferencesAsync(removedEvents, isRemoved); - isRemoved = false; - await this._emitDifferencesAsync(newEvents, isRemoved); - this._lastMempoolEvents = pendingEvents; - } - private async _getMempoolEventsAsync(numConfirmations: number): Promise { - let fromBlock: BlockParamLiteral|number; - let toBlock: BlockParamLiteral|number; - if (numConfirmations === 0) { - fromBlock = BlockParamLiteral.Pending; - toBlock = BlockParamLiteral.Pending; - } else { - toBlock = await this._web3Wrapper.getBlockNumberAsync(); - fromBlock = toBlock - numConfirmations; - } - const mempoolFilter = { - fromBlock, - toBlock, - }; - 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 = { - removed: isRemoved, - ...log, - }; - if (!_.isUndefined(this._callbackAsync)) { - await this._callbackAsync(logEvent); - } - } - } -} diff --git a/src/mempool/order_state_watcher.ts b/src/mempool/order_state_watcher.ts deleted file mode 100644 index d0bf5d89c..000000000 --- a/src/mempool/order_state_watcher.ts +++ /dev/null @@ -1,175 +0,0 @@ -import * as _ from 'lodash'; -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'; -import { - LogEvent, - OrderState, - SignedOrder, - Web3Provider, - BlockParamLiteral, - LogWithDecodedArgs, - OnOrderStateChangeCallback, - ExchangeEvents, - TokenEvents, - ZeroExError, -} from '../types'; -import {Web3Wrapper} from '../web3_wrapper'; - -interface DependentOrderHashes { - [makerAddress: string]: { - [makerToken: string]: Set, - }; -} - -interface OrderByOrderHash { - [orderHash: string]: SignedOrder; -} - -export class OrderStateWatcher { - private _orders: OrderByOrderHash; - private _dependentOrderHashes: DependentOrderHashes; - private _web3Wrapper: Web3Wrapper; - private _callbackAsync?: OnOrderStateChangeCallback; - private _eventWatcher: EventWatcher; - private _abiDecoder: AbiDecoder; - private _orderStateUtils: OrderStateUtils; - constructor( - web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, orderStateUtils: OrderStateUtils, - mempoolPollingIntervalMs?: number) { - this._web3Wrapper = web3Wrapper; - this._orders = {}; - this._dependentOrderHashes = {}; - this._eventWatcher = new EventWatcher( - this._web3Wrapper, mempoolPollingIntervalMs, - ); - this._abiDecoder = abiDecoder; - this._orderStateUtils = orderStateUtils; - } - /** - * Add an order to the orderStateWatcher - * @param signedOrder The order you wish to start watching. - */ - public addOrder(signedOrder: SignedOrder): void { - assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); - const orderHash = ZeroEx.getOrderHashHex(signedOrder); - this._orders[orderHash] = signedOrder; - this.addToDependentOrderHashes(signedOrder, orderHash); - } - /** - * Removes an order from the orderStateWatcher - * @param orderHash The orderHash of the order you wish to stop watching. - */ - public removeOrder(orderHash: string): void { - assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); - const signedOrder = this._orders[orderHash]; - if (_.isUndefined(signedOrder)) { - return; - } - 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 - } - /** - * Starts an orderStateWatcher subscription. The callback will be notified every time a watched order's - * backing blockchain state has changed. This is a call-to-action for the caller to re-validate the order - * @param callback Receives the orderHash of the order that should be re-validated, together. - * with all the order-relevant blockchain state needed to re-validate the order - * @param numConfirmations Number of confirmed blocks deeps you want to run the orderWatcher from. Passing - * is 0 will watch the backing node's mempool, 3 will emit events when blockchain - * state relevant to a watched order changed 3 blocks ago. - */ - public subscribe(callback: OnOrderStateChangeCallback, numConfirmations: number): void { - assert.isFunction('callback', callback); - if (!_.isUndefined(this._callbackAsync)) { - throw new Error(ZeroExError.SubscriptionAlreadyPresent); - } - this._callbackAsync = callback; - this._eventWatcher.subscribe(this._onMempoolEventCallbackAsync.bind(this), numConfirmations); - } - /** - * Ends an orderStateWatcher subscription. - * @param signedOrder The order you wish to stop watching. - */ - public unsubscribe(): void { - delete this._callbackAsync; - this._eventWatcher.unsubscribe(); - } - private async _onMempoolEventCallbackAsync(log: LogEvent): Promise { - const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log); - 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 _emitRevalidateOrdersAsync(orderHashes: string[]): Promise { - // TODO: Make defaultBlock a passed in option - const methodOpts = { - defaultBlock: BlockParamLiteral.Pending, - }; - - for (const orderHash of orderHashes) { - const signedOrder = this._orders[orderHash] as SignedOrder; - const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder, methodOpts); - if (!_.isUndefined(this._callbackAsync)) { - await this._callbackAsync(orderState); - } else { - break; // Unsubscribe was called - } - } - } - 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/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts new file mode 100644 index 000000000..205885f96 --- /dev/null +++ b/src/order_watcher/event_watcher.ts @@ -0,0 +1,78 @@ +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_EVENT_POLLING_INTERVAL = 200; + +export class EventWatcher { + private _web3Wrapper: Web3Wrapper; + private _pollingIntervalMs: number; + private _intervalId: NodeJS.Timer; + private _lastEvents: Web3.LogEntry[] = []; + private _callbackAsync?: MempoolEventCallback; + constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) { + this._web3Wrapper = web3Wrapper; + this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? + DEFAULT_EVENT_POLLING_INTERVAL : + pollingIntervalMs; + } + public subscribe(callback: MempoolEventCallback, numConfirmations: number): void { + this._callbackAsync = callback; + this._intervalId = intervalUtils.setAsyncExcludingInterval( + this._pollForMempoolEventsAsync.bind(this, numConfirmations), this._pollingIntervalMs, + ); + } + public unsubscribe(): void { + delete this._callbackAsync; + this._lastEvents = []; + intervalUtils.clearAsyncExcludingInterval(this._intervalId); + } + private async _pollForMempoolEventsAsync(numConfirmations: number): Promise { + const pendingEvents = await this._getMempoolEventsAsync(numConfirmations); + 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._lastEvents, pendingEvents, JSON.stringify); + const newEvents = _.differenceBy(pendingEvents, this._lastEvents, JSON.stringify); + let isRemoved = true; + await this._emitDifferencesAsync(removedEvents, isRemoved); + isRemoved = false; + await this._emitDifferencesAsync(newEvents, isRemoved); + this._lastEvents = pendingEvents; + } + private async _getMempoolEventsAsync(numConfirmations: number): Promise { + let fromBlock: BlockParamLiteral|number; + let toBlock: BlockParamLiteral|number; + if (numConfirmations === 0) { + fromBlock = BlockParamLiteral.Pending; + toBlock = BlockParamLiteral.Pending; + } else { + toBlock = await this._web3Wrapper.getBlockNumberAsync(); + fromBlock = toBlock - numConfirmations; + } + const mempoolFilter = { + fromBlock, + toBlock, + }; + 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 = { + removed: isRemoved, + ...log, + }; + if (!_.isUndefined(this._callbackAsync)) { + await this._callbackAsync(logEvent); + } + } + } +} diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts new file mode 100644 index 000000000..8710c5a84 --- /dev/null +++ b/src/order_watcher/order_state_watcher.ts @@ -0,0 +1,175 @@ +import * as _ from 'lodash'; +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'; +import { + LogEvent, + OrderState, + SignedOrder, + Web3Provider, + BlockParamLiteral, + LogWithDecodedArgs, + OnOrderStateChangeCallback, + ExchangeEvents, + TokenEvents, + ZeroExError, +} from '../types'; +import {Web3Wrapper} from '../web3_wrapper'; + +interface DependentOrderHashes { + [makerAddress: string]: { + [makerToken: string]: Set, + }; +} + +interface OrderByOrderHash { + [orderHash: string]: SignedOrder; +} + +export class OrderStateWatcher { + private _orders: OrderByOrderHash; + private _dependentOrderHashes: DependentOrderHashes; + private _web3Wrapper: Web3Wrapper; + private _callbackAsync?: OnOrderStateChangeCallback; + private _eventWatcher: EventWatcher; + private _abiDecoder: AbiDecoder; + private _orderStateUtils: OrderStateUtils; + constructor( + web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, orderStateUtils: OrderStateUtils, + eventPollingIntervalMs?: number) { + this._web3Wrapper = web3Wrapper; + this._orders = {}; + this._dependentOrderHashes = {}; + this._eventWatcher = new EventWatcher( + this._web3Wrapper, eventPollingIntervalMs, + ); + this._abiDecoder = abiDecoder; + this._orderStateUtils = orderStateUtils; + } + /** + * Add an order to the orderStateWatcher + * @param signedOrder The order you wish to start watching. + */ + public addOrder(signedOrder: SignedOrder): void { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + this._orders[orderHash] = signedOrder; + this.addToDependentOrderHashes(signedOrder, orderHash); + } + /** + * Removes an order from the orderStateWatcher + * @param orderHash The orderHash of the order you wish to stop watching. + */ + public removeOrder(orderHash: string): void { + assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); + const signedOrder = this._orders[orderHash]; + if (_.isUndefined(signedOrder)) { + return; + } + 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 + } + /** + * Starts an orderStateWatcher subscription. The callback will be notified every time a watched order's + * backing blockchain state has changed. This is a call-to-action for the caller to re-validate the order + * @param callback Receives the orderHash of the order that should be re-validated, together. + * with all the order-relevant blockchain state needed to re-validate the order + * @param numConfirmations Number of confirmed blocks deeps you want to run the orderWatcher from. Passing + * is 0 will watch the backing node's mempool, 3 will emit events when blockchain + * state relevant to a watched order changed 3 blocks ago. + */ + public subscribe(callback: OnOrderStateChangeCallback, numConfirmations: number): void { + assert.isFunction('callback', callback); + if (!_.isUndefined(this._callbackAsync)) { + throw new Error(ZeroExError.SubscriptionAlreadyPresent); + } + this._callbackAsync = callback; + this._eventWatcher.subscribe(this._onMempoolEventCallbackAsync.bind(this), numConfirmations); + } + /** + * Ends an orderStateWatcher subscription. + * @param signedOrder The order you wish to stop watching. + */ + public unsubscribe(): void { + delete this._callbackAsync; + this._eventWatcher.unsubscribe(); + } + private async _onMempoolEventCallbackAsync(log: LogEvent): Promise { + const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log); + 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 _emitRevalidateOrdersAsync(orderHashes: string[]): Promise { + // TODO: Make defaultBlock a passed in option + const methodOpts = { + defaultBlock: BlockParamLiteral.Pending, + }; + + for (const orderHash of orderHashes) { + const signedOrder = this._orders[orderHash] as SignedOrder; + const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder, methodOpts); + if (!_.isUndefined(this._callbackAsync)) { + await this._callbackAsync(orderState); + } else { + break; // Unsubscribe was called + } + } + } + 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); + } +} -- cgit v1.2.3 From 6f5a55b5fe4c6f0f56303b6ac5dcbd99129ee7bd Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 16:43:19 -0500 Subject: Rename MempoolEventCallback to EventWatcherCallback --- src/index.ts | 2 +- src/order_watcher/event_watcher.ts | 6 +++--- src/order_watcher/order_state_watcher.ts | 4 ++-- src/types.ts | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index ffd59fe37..7963ec43e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,7 @@ export { FilterObject, LogEvent, DecodedLogEvent, - MempoolEventCallback, + EventWatcherCallback, OnOrderStateChangeCallback, OrderStateValid, OrderStateInvalid, diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index 7d3719282..9ace32b29 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -1,7 +1,7 @@ import * as Web3 from 'web3'; import * as _ from 'lodash'; import {Web3Wrapper} from '../web3_wrapper'; -import {BlockParamLiteral, EventCallback, MempoolEventCallback} from '../types'; +import {BlockParamLiteral, EventCallback, EventWatcherCallback} from '../types'; import {AbiDecoder} from '../utils/abi_decoder'; import {intervalUtils} from '../utils/interval_utils'; @@ -12,14 +12,14 @@ export class EventWatcher { private _pollingIntervalMs: number; private _intervalId: NodeJS.Timer; private _lastEvents: Web3.LogEntry[] = []; - private _callbackAsync?: MempoolEventCallback; + private _callbackAsync?: EventWatcherCallback; constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) { this._web3Wrapper = web3Wrapper; this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? DEFAULT_EVENT_POLLING_INTERVAL : pollingIntervalMs; } - public subscribe(callback: MempoolEventCallback, numConfirmations: number): void { + public subscribe(callback: EventWatcherCallback, numConfirmations: number): void { this._callbackAsync = callback; this._intervalId = intervalUtils.setAsyncExcludingInterval( this._pollForMempoolEventsAsync.bind(this, numConfirmations), this._pollingIntervalMs, diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 8710c5a84..e31fc962d 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -90,7 +90,7 @@ export class OrderStateWatcher { throw new Error(ZeroExError.SubscriptionAlreadyPresent); } this._callbackAsync = callback; - this._eventWatcher.subscribe(this._onMempoolEventCallbackAsync.bind(this), numConfirmations); + this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this), numConfirmations); } /** * Ends an orderStateWatcher subscription. @@ -100,7 +100,7 @@ export class OrderStateWatcher { delete this._callbackAsync; this._eventWatcher.unsubscribe(); } - private async _onMempoolEventCallbackAsync(log: LogEvent): Promise { + private async _onEventWatcherCallbackAsync(log: LogEvent): Promise { const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log); const isDecodedLog = !_.isUndefined((maybeDecodedLog as LogWithDecodedArgs).event); if (!isDecodedLog) { diff --git a/src/types.ts b/src/types.ts index 246fd8641..4a0a74826 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,9 +45,9 @@ export type EventCallbackAsync = (log: DecodedLogEvent) => P 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 type EventWatcherCallbackSync = (log: LogEvent) => void; +export type EventWatcherCallbackAsync = (log: LogEvent) => Promise; +export type EventWatcherCallback = EventWatcherCallbackSync|EventWatcherCallbackAsync; export interface ExchangeContract extends Web3.ContractInstance { isValidSignature: { -- cgit v1.2.3 From 1c6e6842c66a61795794556cb08ed778494358f5 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Nov 2017 16:48:36 -0500 Subject: Revert "Use _.get for optional configs" This reverts commit ecc54b07c70c9f191a0eb0dece8137f088250a41. --- src/0x.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/0x.ts b/src/0x.ts index 75a154930..7c381a498 100644 --- a/src/0x.ts +++ b/src/0x.ts @@ -1,6 +1,5 @@ import * as _ from 'lodash'; import BigNumber from 'bignumber.js'; -import * as Web3 from 'web3'; import {SchemaValidator, schemas} from '0x-json-schemas'; import {bigNumberConfigs} from './bignumber_config'; import * as ethUtil from 'ethereumjs-util'; @@ -188,7 +187,7 @@ export class ZeroEx { const artifactJSONs = _.values(artifacts); const abiArrays = _.map(artifactJSONs, artifact => artifact.abi); this._abiDecoder = new AbiDecoder(abiArrays); - const gasPrice: number|undefined = _.get(config, 'gasPrice'); + const gasPrice = _.isUndefined(config) ? undefined : config.gasPrice; const defaults = { gasPrice, }; @@ -198,7 +197,7 @@ export class ZeroEx { this._abiDecoder, this._getTokenTransferProxyAddressAsync.bind(this), ); - const exchageContractAddressIfExists: string|undefined = _.get(config, 'exchangeContractAddress'); + const exchageContractAddressIfExists = _.isUndefined(config) ? undefined : config.exchangeContractAddress; this.exchange = new ExchangeWrapper( this._web3Wrapper, this._abiDecoder, @@ -209,11 +208,13 @@ export class ZeroEx { this._web3Wrapper, this._getTokenTransferProxyAddressAsync.bind(this), ); - const tokenRegistryContractAddressIfExists = _.get(config, 'tokenRegistryContractAddress'); - this.tokenRegistry = new TokenRegistryWrapper(this._web3Wrapper, 'tokenRegistryContractAddressIfExists'); - const etherTokenContractAddressIfExists = _.get(config, 'etherTokenContractAddress'); - this.etherToken = new EtherTokenWrapper(this._web3Wrapper, this.token, 'etherTokenContractAddressIfExists'); - const mempoolPollingIntervalMs: number|undefined = _.get(config, 'mempoolPollingIntervalMs'); + const tokenRegistryContractAddressIfExists = _.isUndefined(config) ? + undefined : + config.tokenRegistryContractAddress; + 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, -- cgit v1.2.3 From 126a165f558326625d892c4a379c0ebd66088c9a Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 16:59:41 -0500 Subject: Add nested config for orderWatcher --- src/0x.ts | 5 +++-- src/order_watcher/order_state_watcher.ts | 4 +++- src/schemas/zero_ex_config_schema.ts | 11 ++++++++--- src/types.ts | 11 +++++++++-- test/event_watcher_test.ts | 6 +++--- test/order_state_watcher_test.ts | 2 +- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/0x.ts b/src/0x.ts index 75a154930..dfe64f2df 100644 --- a/src/0x.ts +++ b/src/0x.ts @@ -26,6 +26,7 @@ import { SignedOrder, Web3Provider, ZeroExConfig, + OrderStateWatcherConfig, TransactionReceiptWithDecodedLogs, } from './types'; import {zeroExConfigSchema} from './schemas/zero_ex_config_schema'; @@ -213,10 +214,10 @@ export class ZeroEx { this.tokenRegistry = new TokenRegistryWrapper(this._web3Wrapper, 'tokenRegistryContractAddressIfExists'); const etherTokenContractAddressIfExists = _.get(config, 'etherTokenContractAddress'); this.etherToken = new EtherTokenWrapper(this._web3Wrapper, this.token, 'etherTokenContractAddressIfExists'); - const mempoolPollingIntervalMs: number|undefined = _.get(config, 'mempoolPollingIntervalMs'); + const orderWatcherConfig: OrderStateWatcherConfig|undefined = _.get(config, 'orderWatcherConfig'); const orderStateUtils = new OrderStateUtils(this.token, this.exchange); this.orderStateWatcher = new OrderStateWatcher( - this._web3Wrapper, this._abiDecoder, orderStateUtils, mempoolPollingIntervalMs, + this._web3Wrapper, this._abiDecoder, orderStateUtils, orderWatcherConfig, ); } /** diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index e31fc962d..14b5b6cf9 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -15,6 +15,7 @@ import { BlockParamLiteral, LogWithDecodedArgs, OnOrderStateChangeCallback, + OrderStateWatcherConfig, ExchangeEvents, TokenEvents, ZeroExError, @@ -41,10 +42,11 @@ export class OrderStateWatcher { private _orderStateUtils: OrderStateUtils; constructor( web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, orderStateUtils: OrderStateUtils, - eventPollingIntervalMs?: number) { + config?: OrderStateWatcherConfig) { this._web3Wrapper = web3Wrapper; this._orders = {}; this._dependentOrderHashes = {}; + const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.pollingIntervalMs; this._eventWatcher = new EventWatcher( this._web3Wrapper, eventPollingIntervalMs, ); diff --git a/src/schemas/zero_ex_config_schema.ts b/src/schemas/zero_ex_config_schema.ts index 5be651a9a..5a2afeaa2 100644 --- a/src/schemas/zero_ex_config_schema.ts +++ b/src/schemas/zero_ex_config_schema.ts @@ -5,9 +5,14 @@ export const zeroExConfigSchema = { exchangeContractAddress: {$ref: '/Address'}, tokenRegistryContractAddress: {$ref: '/Address'}, etherTokenContractAddress: {$ref: '/Address'}, - mempoolPollingIntervalMs: { - type: 'number', - min: 0, + orderWatcherConfig: { + type: 'object', + properties: { + pollingIntervalMs: { + type: 'number', + minimum: 0, + }, + }, }, }, type: 'object', diff --git a/src/types.ts b/src/types.ts index 4a0a74826..704c0b866 100644 --- a/src/types.ts +++ b/src/types.ts @@ -395,19 +395,26 @@ export interface JSONRPCPayload { method: string; } +/* + * pollingIntervalMs: How often to check for new mempool events + */ +export interface OrderStateWatcherConfig { + pollingIntervalMs?: number; +} + /* * gasPrice: Gas price to use with every transaction * 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 + * orderWatcherConfig: All the configs related to the orderWatcher */ export interface ZeroExConfig { gasPrice?: BigNumber; // Gas price to use with every transaction exchangeContractAddress?: string; tokenRegistryContractAddress?: string; etherTokenContractAddress?: string; - mempoolPollingIntervalMs?: number; + orderWatcherConfig?: OrderStateWatcherConfig; } export type TransactionReceipt = Web3.TransactionReceipt; diff --git a/test/event_watcher_test.ts b/test/event_watcher_test.ts index a246805a0..8f3898287 100644 --- a/test/event_watcher_test.ts +++ b/test/event_watcher_test.ts @@ -7,7 +7,7 @@ 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 {EventWatcher} from '../src/order_watcher/event_watcher'; import { ZeroEx, LogEvent, @@ -56,9 +56,9 @@ describe('EventWatcher', () => { }; before(async () => { web3 = web3Factory.create(); - const mempoolPollingIntervalMs = 10; + const pollingIntervalMs = 10; web3Wrapper = new Web3Wrapper(web3.currentProvider); - eventWatcher = new EventWatcher(web3Wrapper, mempoolPollingIntervalMs); + eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalMs); }); afterEach(() => { // clean up any stubs after the test has completed diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 938e1be4c..6060d64c5 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -6,7 +6,7 @@ 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 { OrderStateWatcher } from '../src/order_watcher/order_state_watcher'; import { Token, ZeroEx, -- cgit v1.2.3 From 0c8886ad0c0e1cffa7da0495a648028d1fe54476 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 17:03:54 -0500 Subject: Fix comment --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 704c0b866..79def4560 100644 --- a/src/types.ts +++ b/src/types.ts @@ -396,7 +396,7 @@ export interface JSONRPCPayload { } /* - * pollingIntervalMs: How often to check for new mempool events + * pollingIntervalMs: How often to poll the Ethereum node for new events */ export interface OrderStateWatcherConfig { pollingIntervalMs?: number; -- cgit v1.2.3 From cd3c7f1b97471ea8f64a9696d5c4134fff34eb58 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Nov 2017 17:14:01 -0500 Subject: Revert test amount changes --- 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 23020c47a..2f6f126c1 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('1000000000000000000000000000'); + const expectedBalance = new BigNumber('100000000000000000000000000'); 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('1000000000000000000000000000'); + const expectedBalance = new BigNumber('100000000000000000000000000'); return expect(balance).to.be.bignumber.equal(expectedBalance); }); }); -- cgit v1.2.3 From 62861d1e1315a14e955b30a45db05e7907b0d22d Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 17:18:03 -0500 Subject: Move isValidSignature implementation into signatureUtils --- src/0x.ts | 15 ++------------- src/utils/signature_utils.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/0x.ts b/src/0x.ts index cb0090469..59688e948 100644 --- a/src/0x.ts +++ b/src/0x.ts @@ -88,19 +88,8 @@ export class ZeroEx { assert.doesConformToSchema('signature', signature, schemas.ecSignatureSchema); assert.isETHAddressHex('signerAddress', signerAddress); - const dataBuff = ethUtil.toBuffer(data); - const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff); - try { - const pubKey = ethUtil.ecrecover( - msgHashBuff, - signature.v, - ethUtil.toBuffer(signature.r), - ethUtil.toBuffer(signature.s)); - const retrievedAddress = ethUtil.bufferToHex(ethUtil.pubToAddress(pubKey)); - return retrievedAddress === signerAddress; - } catch (err) { - return false; - } + const isValidSignature = signatureUtils.isValidSignature(data, signature, signerAddress); + return isValidSignature; } /** * Generates a pseudo-random 256-bit salt. diff --git a/src/utils/signature_utils.ts b/src/utils/signature_utils.ts index b312b5554..d066f8bf0 100644 --- a/src/utils/signature_utils.ts +++ b/src/utils/signature_utils.ts @@ -2,6 +2,21 @@ import * as ethUtil from 'ethereumjs-util'; import {ECSignature} from '../types'; export const signatureUtils = { + isValidSignature(data: string, signature: ECSignature, signerAddress: string): boolean { + const dataBuff = ethUtil.toBuffer(data); + const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff); + try { + const pubKey = ethUtil.ecrecover( + msgHashBuff, + signature.v, + ethUtil.toBuffer(signature.r), + ethUtil.toBuffer(signature.s)); + const retrievedAddress = ethUtil.bufferToHex(ethUtil.pubToAddress(pubKey)); + return retrievedAddress === signerAddress; + } catch (err) { + return false; + } + }, parseSignatureHexAsVRS(signatureHex: string): ECSignature { const signatureBuffer = ethUtil.toBuffer(signatureHex); let v = signatureBuffer[0]; -- cgit v1.2.3 From 02cc3f911624d31d581d4637d17ad2fa6fd95bc6 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 17:18:30 -0500 Subject: Create assert.isValidSignature method and use it in `addOrder` --- src/order_watcher/order_state_watcher.ts | 1 + src/utils/assert.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 14b5b6cf9..6d56293ef 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -60,6 +60,7 @@ export class OrderStateWatcher { public addOrder(signedOrder: SignedOrder): void { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); const orderHash = ZeroEx.getOrderHashHex(signedOrder); + assert.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker); this._orders[orderHash] = signedOrder; this.addToDependentOrderHashes(signedOrder, orderHash); } diff --git a/src/utils/assert.ts b/src/utils/assert.ts index 667b41b7b..e5c9439f3 100644 --- a/src/utils/assert.ts +++ b/src/utils/assert.ts @@ -1,8 +1,10 @@ import * as _ from 'lodash'; -import BigNumber from 'bignumber.js'; import * as Web3 from 'web3'; -import {Web3Wrapper} from '../web3_wrapper'; +import BigNumber from 'bignumber.js'; import {SchemaValidator, Schema} from '0x-json-schemas'; +import {Web3Wrapper} from '../web3_wrapper'; +import {signatureUtils} from '../utils/signature_utils'; +import {ECSignature} from '../types'; const HEX_REGEX = /^0x[0-9A-F]*$/i; @@ -18,6 +20,10 @@ export const assert = { !hasDecimals, `${variableName} should be in baseUnits (no decimals), found value: ${value.toNumber()}`, ); }, + isValidSignature(orderHash: string, ecSignature: ECSignature, signerAddress: string) { + const isValidSignature = signatureUtils.isValidSignature(orderHash, ecSignature, signerAddress); + this.assert(isValidSignature, `Expected order with hash '${orderHash}' to have a valid signature`); + }, isUndefined(value: any, variableName?: string): void { this.assert(_.isUndefined(value), this.typeAssertionMessage(variableName, 'undefined', value)); }, -- cgit v1.2.3 From 50d3a1482567d0e4f572877b50547ae4fbad9942 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 17:39:03 -0500 Subject: Remove finished TODOs --- src/order_watcher/event_watcher.ts | 1 - src/order_watcher/order_state_watcher.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index 9ace32b29..471dcd21a 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -63,7 +63,6 @@ export class EventWatcher { const events = await this._web3Wrapper.getLogsAsync(eventFilter); return events; } - // 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/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 6d56293ef..9f0773ffb 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -151,7 +151,6 @@ export class OrderStateWatcher { } } private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise { - // TODO: Make defaultBlock a passed in option const methodOpts = { defaultBlock: BlockParamLiteral.Pending, }; -- cgit v1.2.3 From c60d7e2db857331f66140c9d9a49f4161a8bf8a4 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Nov 2017 17:44:36 -0500 Subject: Fix getting events from non-mempool --- src/order_watcher/event_watcher.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index 471dcd21a..f1a2b5729 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -53,8 +53,9 @@ export class EventWatcher { fromBlock = BlockParamLiteral.Pending; toBlock = BlockParamLiteral.Pending; } else { - toBlock = await this._web3Wrapper.getBlockNumberAsync(); - fromBlock = toBlock - numConfirmations; + const currentBlock = await this._web3Wrapper.getBlockNumberAsync(); + toBlock = currentBlock - numConfirmations; + fromBlock = currentBlock - numConfirmations; } const eventFilter = { fromBlock, -- cgit v1.2.3 From 90348e08c14d12e107af5ce103781399ade7fb63 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 17:45:30 -0500 Subject: use explicit import --- src/order_watcher/order_state_watcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 9f0773ffb..88f6aae69 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -1,6 +1,6 @@ import * as _ from 'lodash'; import {schemas} from '0x-json-schemas'; -import {ZeroEx} from '../'; +import {ZeroEx} from '../0x'; import {EventWatcher} from './event_watcher'; import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; -- cgit v1.2.3 From b0491b0ee28e6d9fd7e3a5647284c99bd79d4930 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 17:48:05 -0500 Subject: Rename _callbackAsync to _callbackIfExistsAsync for clarity --- src/order_watcher/event_watcher.ts | 10 +++++----- src/order_watcher/order_state_watcher.ts | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index f1a2b5729..ce471b58d 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -12,7 +12,7 @@ export class EventWatcher { private _pollingIntervalMs: number; private _intervalId: NodeJS.Timer; private _lastEvents: Web3.LogEntry[] = []; - private _callbackAsync?: EventWatcherCallback; + private _callbackIfExistsAsync?: EventWatcherCallback; constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) { this._web3Wrapper = web3Wrapper; this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? @@ -20,13 +20,13 @@ export class EventWatcher { pollingIntervalMs; } public subscribe(callback: EventWatcherCallback, numConfirmations: number): void { - this._callbackAsync = callback; + this._callbackIfExistsAsync = callback; this._intervalId = intervalUtils.setAsyncExcludingInterval( this._pollForMempoolEventsAsync.bind(this, numConfirmations), this._pollingIntervalMs, ); } public unsubscribe(): void { - delete this._callbackAsync; + delete this._callbackIfExistsAsync; this._lastEvents = []; intervalUtils.clearAsyncExcludingInterval(this._intervalId); } @@ -70,8 +70,8 @@ export class EventWatcher { removed: isRemoved, ...log, }; - if (!_.isUndefined(this._callbackAsync)) { - await this._callbackAsync(logEvent); + if (!_.isUndefined(this._callbackIfExistsAsync)) { + await this._callbackIfExistsAsync(logEvent); } } } diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 88f6aae69..1c5898c28 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -36,7 +36,7 @@ export class OrderStateWatcher { private _orders: OrderByOrderHash; private _dependentOrderHashes: DependentOrderHashes; private _web3Wrapper: Web3Wrapper; - private _callbackAsync?: OnOrderStateChangeCallback; + private _callbackIfExistsAsync?: OnOrderStateChangeCallback; private _eventWatcher: EventWatcher; private _abiDecoder: AbiDecoder; private _orderStateUtils: OrderStateUtils; @@ -89,10 +89,10 @@ export class OrderStateWatcher { */ public subscribe(callback: OnOrderStateChangeCallback, numConfirmations: number): void { assert.isFunction('callback', callback); - if (!_.isUndefined(this._callbackAsync)) { + if (!_.isUndefined(this._callbackIfExistsAsync)) { throw new Error(ZeroExError.SubscriptionAlreadyPresent); } - this._callbackAsync = callback; + this._callbackIfExistsAsync = callback; this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this), numConfirmations); } /** @@ -100,7 +100,7 @@ export class OrderStateWatcher { * @param signedOrder The order you wish to stop watching. */ public unsubscribe(): void { - delete this._callbackAsync; + delete this._callbackIfExistsAsync; this._eventWatcher.unsubscribe(); } private async _onEventWatcherCallbackAsync(log: LogEvent): Promise { @@ -158,8 +158,8 @@ export class OrderStateWatcher { for (const orderHash of orderHashes) { const signedOrder = this._orders[orderHash] as SignedOrder; const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder, methodOpts); - if (!_.isUndefined(this._callbackAsync)) { - await this._callbackAsync(orderState); + if (!_.isUndefined(this._callbackIfExistsAsync)) { + await this._callbackIfExistsAsync(orderState); } else { break; // Unsubscribe was called } -- cgit v1.2.3 From 7f606e1e6493b36ec4fbfd738a0e98ce89afef2d Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 17:50:48 -0500 Subject: Closing paren on same level as open --- src/order_watcher/order_state_watcher.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 1c5898c28..8eb936077 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -42,7 +42,8 @@ export class OrderStateWatcher { private _orderStateUtils: OrderStateUtils; constructor( web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, orderStateUtils: OrderStateUtils, - config?: OrderStateWatcherConfig) { + config?: OrderStateWatcherConfig, + ) { this._web3Wrapper = web3Wrapper; this._orders = {}; this._dependentOrderHashes = {}; -- cgit v1.2.3 From 322e054f1ab3f1325c203f61f1edb2f56867fc6d Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 17:57:41 -0500 Subject: comment improvements --- src/order_watcher/order_state_watcher.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 8eb936077..f6ce4edb7 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -73,15 +73,15 @@ export class OrderStateWatcher { assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); const signedOrder = this._orders[orderHash]; if (_.isUndefined(signedOrder)) { - return; + return; // noop } 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 } /** - * Starts an orderStateWatcher subscription. The callback will be notified every time a watched order's - * backing blockchain state has changed. This is a call-to-action for the caller to re-validate the order + * Starts an orderStateWatcher subscription. The callback will be called every time a watched order's + * backing blockchain state has changed. This is a call-to-action for the caller to re-validate the order. * @param callback Receives the orderHash of the order that should be re-validated, together. * with all the order-relevant blockchain state needed to re-validate the order * @param numConfirmations Number of confirmed blocks deeps you want to run the orderWatcher from. Passing -- cgit v1.2.3 From 595dc6de0360a8d21db78aba81d9cd44bffc6a1c Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 18:04:24 -0500 Subject: Fix comments --- src/order_watcher/order_state_watcher.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index f6ce4edb7..000e92773 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -82,8 +82,8 @@ export class OrderStateWatcher { /** * Starts an orderStateWatcher subscription. The callback will be called every time a watched order's * backing blockchain state has changed. This is a call-to-action for the caller to re-validate the order. - * @param callback Receives the orderHash of the order that should be re-validated, together. - * with all the order-relevant blockchain state needed to re-validate the order + * @param callback Receives the orderHash of the order that should be re-validated, together + * with all the order-relevant blockchain state needed to re-validate the order. * @param numConfirmations Number of confirmed blocks deeps you want to run the orderWatcher from. Passing * is 0 will watch the backing node's mempool, 3 will emit events when blockchain * state relevant to a watched order changed 3 blocks ago. @@ -98,7 +98,6 @@ export class OrderStateWatcher { } /** * Ends an orderStateWatcher subscription. - * @param signedOrder The order you wish to stop watching. */ public unsubscribe(): void { delete this._callbackIfExistsAsync; -- cgit v1.2.3 From d98435b4dc76d524c9f2d8258b14004fe06bc579 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Nov 2017 18:28:33 -0500 Subject: Add order state watcher tests for LogCancel --- test/order_state_watcher_test.ts | 68 ++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 6060d64c5..949974e6b 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -21,6 +21,7 @@ import { import { TokenUtils } from './utils/token_utils'; import { FillScenarios } from './utils/fill_scenarios'; import { DoneCallback } from '../src/types'; +import {reportCallbackErrors} from './utils/report_callback_errors'; chaiSetup.configure(); const expect = chai.expect; @@ -110,13 +111,13 @@ describe('OrderStateWatcher', () => { ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); zeroEx.orderStateWatcher.addOrder(signedOrder); - const callback = (orderState: OrderState) => { + const callback = reportCallbackErrors(done)((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, numConfirmations); await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0)); })().catch(done); @@ -128,13 +129,13 @@ describe('OrderStateWatcher', () => { ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); zeroEx.orderStateWatcher.addOrder(signedOrder); - const callback = (orderState: OrderState) => { + const callback = reportCallbackErrors(done)((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, numConfirmations); const anyRecipient = taker; const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); @@ -150,7 +151,7 @@ describe('OrderStateWatcher', () => { zeroEx.orderStateWatcher.addOrder(signedOrder); let eventCount = 0; - const callback = (orderState: OrderState) => { + const callback = reportCallbackErrors(done)((orderState: OrderState) => { eventCount++; expect(orderState.isValid).to.be.false(); const invalidOrderState = orderState as OrderStateInvalid; @@ -159,7 +160,7 @@ describe('OrderStateWatcher', () => { if (eventCount === 2) { done(); } - }; + }); zeroEx.orderStateWatcher.subscribe(callback, numConfirmations); const shouldThrowOnInsufficientBalanceOrAllowance = true; @@ -182,7 +183,7 @@ describe('OrderStateWatcher', () => { zeroEx.orderStateWatcher.addOrder(signedOrder); let eventCount = 0; - const callback = (orderState: OrderState) => { + const callback = reportCallbackErrors(done)((orderState: OrderState) => { eventCount++; expect(orderState.isValid).to.be.true(); const validOrderState = orderState as OrderStateValid; @@ -193,7 +194,7 @@ describe('OrderStateWatcher', () => { if (eventCount === 2) { done(); } - }; + }); zeroEx.orderStateWatcher.subscribe(callback, numConfirmations); const shouldThrowOnInsufficientBalanceOrAllowance = true; await zeroEx.exchange.fillOrderAsync( @@ -201,10 +202,51 @@ describe('OrderStateWatcher', () => { ); })().catch(done); }); + it('should emit orderStateValid when watched order cancelled', (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 = reportCallbackErrors(done)((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, numConfirmations); + + const shouldThrowOnInsufficientBalanceOrAllowance = true; + await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount); + })().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 cancelAmountInBaseUnits = new BigNumber(2); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + zeroEx.orderStateWatcher.addOrder(signedOrder); + + const callback = reportCallbackErrors(done)((orderState: OrderState) => { + expect(orderState.isValid).to.be.true(); + const validOrderState = orderState as OrderStateValid; + expect(validOrderState.orderHash).to.be.equal(orderHash); + const orderRelevantState = validOrderState.orderRelevantState; + expect(orderRelevantState.canceledTakerTokenAmount).to.be.bignumber.equal(cancelAmountInBaseUnits); + done(); + }); + zeroEx.orderStateWatcher.subscribe(callback, numConfirmations); + await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmountInBaseUnits); + })().catch(done); + }); }); }); - -/* - * - it should emit orderState when watched order partially filled - * - it should emit orderState when watched order is cancelled - */ -- cgit v1.2.3 From c0db88168b250ef4db960e9eddced8f5a10ee63f Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 18:29:13 -0500 Subject: Fix bug where we hard-coded using pendingBlock for fetching the orderState. Moved numConfirmations to become a global orderStateWatcher config --- src/order_watcher/event_watcher.ts | 22 ++++++++++++---------- src/order_watcher/order_state_watcher.ts | 17 +++++++++++++---- src/schemas/zero_ex_config_schema.ts | 4 ++++ src/types.ts | 1 + test/order_state_watcher_test.ts | 12 ++++++------ 5 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index ce471b58d..f71b14afb 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -13,16 +13,18 @@ export class EventWatcher { private _intervalId: NodeJS.Timer; private _lastEvents: Web3.LogEntry[] = []; private _callbackIfExistsAsync?: EventWatcherCallback; - constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) { + private _numConfirmations: number; + constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number, numConfirmations: number) { this._web3Wrapper = web3Wrapper; + this._numConfirmations = numConfirmations; this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? DEFAULT_EVENT_POLLING_INTERVAL : pollingIntervalMs; } - public subscribe(callback: EventWatcherCallback, numConfirmations: number): void { + public subscribe(callback: EventWatcherCallback): void { this._callbackIfExistsAsync = callback; this._intervalId = intervalUtils.setAsyncExcludingInterval( - this._pollForMempoolEventsAsync.bind(this, numConfirmations), this._pollingIntervalMs, + this._pollForMempoolEventsAsync.bind(this), this._pollingIntervalMs, ); } public unsubscribe(): void { @@ -30,8 +32,8 @@ export class EventWatcher { this._lastEvents = []; intervalUtils.clearAsyncExcludingInterval(this._intervalId); } - private async _pollForMempoolEventsAsync(numConfirmations: number): Promise { - const pendingEvents = await this._getEventsAsync(numConfirmations); + private async _pollForMempoolEventsAsync(): Promise { + const pendingEvents = await this._getEventsAsync(); 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, @@ -46,16 +48,16 @@ export class EventWatcher { await this._emitDifferencesAsync(newEvents, isRemoved); this._lastEvents = pendingEvents; } - private async _getEventsAsync(numConfirmations: number): Promise { + private async _getEventsAsync(): Promise { let fromBlock: BlockParamLiteral|number; let toBlock: BlockParamLiteral|number; - if (numConfirmations === 0) { + if (this._numConfirmations === 0) { fromBlock = BlockParamLiteral.Pending; - toBlock = BlockParamLiteral.Pending; + toBlock = fromBlock; } else { const currentBlock = await this._web3Wrapper.getBlockNumberAsync(); - toBlock = currentBlock - numConfirmations; - fromBlock = currentBlock - numConfirmations; + toBlock = currentBlock - this._numConfirmations; + fromBlock = toBlock; } const eventFilter = { fromBlock, diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 000e92773..fa91d1f44 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -22,6 +22,8 @@ import { } from '../types'; import {Web3Wrapper} from '../web3_wrapper'; +const DEFAULT_NUM_CONFIRMATIONS = 0; + interface DependentOrderHashes { [makerAddress: string]: { [makerToken: string]: Set, @@ -40,6 +42,7 @@ export class OrderStateWatcher { private _eventWatcher: EventWatcher; private _abiDecoder: AbiDecoder; private _orderStateUtils: OrderStateUtils; + private _numConfirmations: number; constructor( web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, orderStateUtils: OrderStateUtils, config?: OrderStateWatcherConfig, @@ -48,8 +51,11 @@ export class OrderStateWatcher { this._orders = {}; this._dependentOrderHashes = {}; const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.pollingIntervalMs; + this._numConfirmations = _.isUndefined(config) ? + DEFAULT_NUM_CONFIRMATIONS + : config.numConfirmations; this._eventWatcher = new EventWatcher( - this._web3Wrapper, eventPollingIntervalMs, + this._web3Wrapper, eventPollingIntervalMs, this._numConfirmations, ); this._abiDecoder = abiDecoder; this._orderStateUtils = orderStateUtils; @@ -88,13 +94,13 @@ export class OrderStateWatcher { * is 0 will watch the backing node's mempool, 3 will emit events when blockchain * state relevant to a watched order changed 3 blocks ago. */ - public subscribe(callback: OnOrderStateChangeCallback, numConfirmations: number): void { + public subscribe(callback: OnOrderStateChangeCallback): void { assert.isFunction('callback', callback); if (!_.isUndefined(this._callbackIfExistsAsync)) { throw new Error(ZeroExError.SubscriptionAlreadyPresent); } this._callbackIfExistsAsync = callback; - this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this), numConfirmations); + this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this)); } /** * Ends an orderStateWatcher subscription. @@ -151,8 +157,11 @@ export class OrderStateWatcher { } } private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise { + const defaultBlock = this._numConfirmations === 0 ? + BlockParamLiteral.Pending : + this._numConfirmations; const methodOpts = { - defaultBlock: BlockParamLiteral.Pending, + defaultBlock, }; for (const orderHash of orderHashes) { diff --git a/src/schemas/zero_ex_config_schema.ts b/src/schemas/zero_ex_config_schema.ts index 5a2afeaa2..6d4b3ed27 100644 --- a/src/schemas/zero_ex_config_schema.ts +++ b/src/schemas/zero_ex_config_schema.ts @@ -12,6 +12,10 @@ export const zeroExConfigSchema = { type: 'number', minimum: 0, }, + numConfirmations: { + type: 'number', + minimum: 0, + }, }, }, }, diff --git a/src/types.ts b/src/types.ts index 79def4560..352818bc2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -400,6 +400,7 @@ export interface JSONRPCPayload { */ export interface OrderStateWatcherConfig { pollingIntervalMs?: number; + numConfirmations: number; } /* diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 6060d64c5..8eb19dcef 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -88,9 +88,9 @@ describe('OrderStateWatcher', () => { zeroEx.orderStateWatcher.unsubscribe(); }); it('should fail when trying to subscribe twice', (done: DoneCallback) => { - zeroEx.orderStateWatcher.subscribe(_.noop, numConfirmations); + zeroEx.orderStateWatcher.subscribe(_.noop); try { - zeroEx.orderStateWatcher.subscribe(_.noop, numConfirmations); + zeroEx.orderStateWatcher.subscribe(_.noop); done(new Error('Expected the second subscription to fail')); } catch (err) { done(); @@ -117,7 +117,7 @@ describe('OrderStateWatcher', () => { expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerAllowance); done(); }; - zeroEx.orderStateWatcher.subscribe(callback, numConfirmations); + zeroEx.orderStateWatcher.subscribe(callback); await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0)); })().catch(done); }); @@ -135,7 +135,7 @@ describe('OrderStateWatcher', () => { expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerBalance); done(); }; - zeroEx.orderStateWatcher.subscribe(callback, numConfirmations); + 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); @@ -160,7 +160,7 @@ describe('OrderStateWatcher', () => { done(); } }; - zeroEx.orderStateWatcher.subscribe(callback, numConfirmations); + zeroEx.orderStateWatcher.subscribe(callback); const shouldThrowOnInsufficientBalanceOrAllowance = true; await zeroEx.exchange.fillOrderAsync( @@ -194,7 +194,7 @@ describe('OrderStateWatcher', () => { done(); } }; - zeroEx.orderStateWatcher.subscribe(callback, numConfirmations); + zeroEx.orderStateWatcher.subscribe(callback); const shouldThrowOnInsufficientBalanceOrAllowance = true; await zeroEx.exchange.fillOrderAsync( signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker, -- cgit v1.2.3 From 46e2da24a472560a8ff0fc9b068ec017dd5173dd Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 9 Nov 2017 22:12:29 -0500 Subject: Add forgotten file --- test/utils/report_callback_errors.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 test/utils/report_callback_errors.ts diff --git a/test/utils/report_callback_errors.ts b/test/utils/report_callback_errors.ts new file mode 100644 index 000000000..d471b2af2 --- /dev/null +++ b/test/utils/report_callback_errors.ts @@ -0,0 +1,14 @@ +import { DoneCallback } from '../../src/types'; + +export const reportCallbackErrors = (done: DoneCallback) => { + return (f: (...args: any[]) => void) => { + const wrapped = (...args: any[]) => { + try { + f(...args); + } catch (err) { + done(err); + } + }; + return wrapped; + }; +}; -- cgit v1.2.3 From 27519e1dfa882d6b8b8a3febf3bea88be845e48f Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 22:57:38 -0500 Subject: rename intervalId to intervalIdIfExists --- src/order_watcher/event_watcher.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index f71b14afb..4cb741617 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -10,7 +10,7 @@ const DEFAULT_EVENT_POLLING_INTERVAL = 200; export class EventWatcher { private _web3Wrapper: Web3Wrapper; private _pollingIntervalMs: number; - private _intervalId: NodeJS.Timer; + private _intervalIdIfExists?: NodeJS.Timer; private _lastEvents: Web3.LogEntry[] = []; private _callbackIfExistsAsync?: EventWatcherCallback; private _numConfirmations: number; @@ -23,14 +23,16 @@ export class EventWatcher { } public subscribe(callback: EventWatcherCallback): void { this._callbackIfExistsAsync = callback; - this._intervalId = intervalUtils.setAsyncExcludingInterval( + this._intervalIdIfExists = intervalUtils.setAsyncExcludingInterval( this._pollForMempoolEventsAsync.bind(this), this._pollingIntervalMs, ); } public unsubscribe(): void { delete this._callbackIfExistsAsync; this._lastEvents = []; - intervalUtils.clearAsyncExcludingInterval(this._intervalId); + if (!_.isUndefined(this._intervalIdIfExists)) { + intervalUtils.clearAsyncExcludingInterval(this._intervalIdIfExists); + } } private async _pollForMempoolEventsAsync(): Promise { const pendingEvents = await this._getEventsAsync(); -- cgit v1.2.3 From 1b145617482486cac3e9d0f9f32b18f6dd58a626 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 22:58:06 -0500 Subject: remove no longer needed arg --- test/order_state_watcher_test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 4701f39d4..72b481ad3 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -42,7 +42,6 @@ describe('OrderStateWatcher', () => { let web3Wrapper: Web3Wrapper; let signedOrder: SignedOrder; const fillableAmount = new BigNumber(5); - const numConfirmations = 0; before(async () => { web3 = web3Factory.create(); zeroEx = new ZeroEx(web3.currentProvider); @@ -217,7 +216,7 @@ describe('OrderStateWatcher', () => { expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderRemainingFillAmountZero); done(); }); - zeroEx.orderStateWatcher.subscribe(callback, numConfirmations); + zeroEx.orderStateWatcher.subscribe(callback); const shouldThrowOnInsufficientBalanceOrAllowance = true; await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount); @@ -244,7 +243,7 @@ describe('OrderStateWatcher', () => { expect(orderRelevantState.canceledTakerTokenAmount).to.be.bignumber.equal(cancelAmountInBaseUnits); done(); }); - zeroEx.orderStateWatcher.subscribe(callback, numConfirmations); + zeroEx.orderStateWatcher.subscribe(callback); await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmountInBaseUnits); })().catch(done); }); -- cgit v1.2.3 From 960c83315b60398df7ca7ac0a3f4ae5dde7771f1 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 23:05:46 -0500 Subject: Add assertion --- src/order_watcher/event_watcher.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index 4cb741617..786470f1d 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -4,6 +4,7 @@ import {Web3Wrapper} from '../web3_wrapper'; import {BlockParamLiteral, EventCallback, EventWatcherCallback} from '../types'; import {AbiDecoder} from '../utils/abi_decoder'; import {intervalUtils} from '../utils/interval_utils'; +import {assert} from '../utils/assert'; const DEFAULT_EVENT_POLLING_INTERVAL = 200; @@ -22,6 +23,7 @@ export class EventWatcher { pollingIntervalMs; } public subscribe(callback: EventWatcherCallback): void { + assert.isFunction('callback', callback); this._callbackIfExistsAsync = callback; this._intervalIdIfExists = intervalUtils.setAsyncExcludingInterval( this._pollForMempoolEventsAsync.bind(this), this._pollingIntervalMs, -- cgit v1.2.3 From 0205f9ede337e5574245b45e1ee940b8ace89456 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 23:16:26 -0500 Subject: Simplify to/from block code --- src/order_watcher/event_watcher.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index 786470f1d..2a1b6dacf 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -53,19 +53,16 @@ export class EventWatcher { this._lastEvents = pendingEvents; } private async _getEventsAsync(): Promise { - let fromBlock: BlockParamLiteral|number; - let toBlock: BlockParamLiteral|number; + let latestBlock: BlockParamLiteral|number; if (this._numConfirmations === 0) { - fromBlock = BlockParamLiteral.Pending; - toBlock = fromBlock; + latestBlock = BlockParamLiteral.Pending; } else { const currentBlock = await this._web3Wrapper.getBlockNumberAsync(); - toBlock = currentBlock - this._numConfirmations; - fromBlock = toBlock; + latestBlock = currentBlock - this._numConfirmations; } const eventFilter = { - fromBlock, - toBlock, + fromBlock: latestBlock, + toBlock: latestBlock, }; const events = await this._web3Wrapper.getLogsAsync(eventFilter); return events; -- cgit v1.2.3 From 0cd5bf796766f653999591607a2ee87706f80383 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 23:24:46 -0500 Subject: Make sure to set the defaultBlock to the blockNumber rather then the number of confirmations --- src/order_watcher/order_state_watcher.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index fa91d1f44..303ec8bd3 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -1,5 +1,6 @@ import * as _ from 'lodash'; import {schemas} from '0x-json-schemas'; +import * as ethUtil from 'ethereumjs-util'; import {ZeroEx} from '../0x'; import {EventWatcher} from './event_watcher'; import {assert} from '../utils/assert'; @@ -115,6 +116,9 @@ export class OrderStateWatcher { if (!isDecodedLog) { return; // noop } + const blockNumberBuff = ethUtil.toBuffer(maybeDecodedLog.blockNumber); + const blockNumber = ethUtil.bufferToInt(blockNumberBuff); + const decodedLog = maybeDecodedLog as LogWithDecodedArgs; let makerToken: string; let makerAddress: string; @@ -126,7 +130,7 @@ export class OrderStateWatcher { orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]); if (!_.isUndefined(orderHashesSet)) { const orderHashes = Array.from(orderHashesSet); - await this._emitRevalidateOrdersAsync(orderHashes); + await this._emitRevalidateOrdersAsync(orderHashes, blockNumber); } break; @@ -136,7 +140,7 @@ export class OrderStateWatcher { orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]); if (!_.isUndefined(orderHashesSet)) { const orderHashes = Array.from(orderHashesSet); - await this._emitRevalidateOrdersAsync(orderHashes); + await this._emitRevalidateOrdersAsync(orderHashes, blockNumber); } break; @@ -145,7 +149,7 @@ export class OrderStateWatcher { const orderHash = decodedLog.args.orderHash; const isOrderWatched = !_.isUndefined(this._orders[orderHash]); if (isOrderWatched) { - await this._emitRevalidateOrdersAsync([orderHash]); + await this._emitRevalidateOrdersAsync([orderHash], blockNumber); } break; @@ -156,10 +160,10 @@ export class OrderStateWatcher { throw utils.spawnSwitchErr('decodedLog.event', decodedLog.event); } } - private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise { + private async _emitRevalidateOrdersAsync(orderHashes: string[], blockNumber: number): Promise { const defaultBlock = this._numConfirmations === 0 ? BlockParamLiteral.Pending : - this._numConfirmations; + blockNumber; const methodOpts = { defaultBlock, }; -- cgit v1.2.3 From a1bc18e5cf4e7c26bfce7feca0abe4e17c14db83 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 23:32:04 -0500 Subject: Improve comment --- src/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 352818bc2..13867dac8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -396,7 +396,8 @@ export interface JSONRPCPayload { } /* - * pollingIntervalMs: How often to poll the Ethereum node for new events + * pollingIntervalMs: How often to poll the Ethereum node for new events. + * numConfirmations: How many confirmed blocks deep you wish to listen for events at. */ export interface OrderStateWatcherConfig { pollingIntervalMs?: number; -- cgit v1.2.3 From 47f9e171fcc1e3e24c38b1bb381b8303dc8341d4 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 23:32:22 -0500 Subject: Move numConfirmations to constructor call --- test/event_watcher_test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/event_watcher_test.ts b/test/event_watcher_test.ts index 8f3898287..36153c207 100644 --- a/test/event_watcher_test.ts +++ b/test/event_watcher_test.ts @@ -58,7 +58,7 @@ describe('EventWatcher', () => { web3 = web3Factory.create(); const pollingIntervalMs = 10; web3Wrapper = new Web3Wrapper(web3.currentProvider); - eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalMs); + eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalMs, numConfirmations); }); afterEach(() => { // clean up any stubs after the test has completed @@ -88,7 +88,7 @@ describe('EventWatcher', () => { done(); } }; - eventWatcher.subscribe(callback, numConfirmations); + eventWatcher.subscribe(callback); }); it('correctly computes the difference and emits only changes', (done: DoneCallback) => { const initialLogs: Web3.LogEntry[] = [logA, logB]; @@ -122,6 +122,6 @@ describe('EventWatcher', () => { done(); } }; - eventWatcher.subscribe(callback, numConfirmations); + eventWatcher.subscribe(callback); }); }); -- cgit v1.2.3 From d5746652a265d7764bf43d1510b47d6d6437923a Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 9 Nov 2017 23:37:01 -0500 Subject: Fix test description --- test/order_state_watcher_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 72b481ad3..4a72b250f 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -222,7 +222,7 @@ describe('OrderStateWatcher', () => { await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount); })().catch(done); }); - it('should emit orderStateValid when watched order partially filled', (done: DoneCallback) => { + it('should emit orderStateValid when watched order partially cancelled', (done: DoneCallback) => { (async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( makerToken.address, takerToken.address, maker, taker, fillableAmount, -- cgit v1.2.3 From 9b9ab983d618fd5857955a2d816e85060e80e82d Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Fri, 10 Nov 2017 09:04:25 -0500 Subject: Fix test description --- test/order_state_watcher_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 4a72b250f..10ed6bd77 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -201,7 +201,7 @@ describe('OrderStateWatcher', () => { ); })().catch(done); }); - it('should emit orderStateValid when watched order cancelled', (done: DoneCallback) => { + it('should emit orderStateInvalid when watched order cancelled', (done: DoneCallback) => { (async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( makerToken.address, takerToken.address, maker, taker, fillableAmount, -- cgit v1.2.3 From d90756e8ef2634737d41633f5427446b47223e98 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Fri, 10 Nov 2017 10:07:29 -0500 Subject: Test that the orderStateWatcher doesn't emit an event when an irrelevant blockchain event is received. --- test/order_state_watcher_test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 10ed6bd77..1231d7e16 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -121,6 +121,28 @@ describe('OrderStateWatcher', () => { await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0)); })().catch(done); }); + it('should not emit an orderState event when irrelevant Transfer event received', (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 = reportCallbackErrors(done)((orderState: OrderState) => { + throw new Error('OrderState callback fired for irrelevant order'); + }); + zeroEx.orderStateWatcher.subscribe(callback); + const notTheMaker = userAddresses[0]; + const anyRecipient = taker; + const transferAmount = new BigNumber(2); + const notTheMakerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, notTheMaker); + await zeroEx.token.transferAsync(makerToken.address, notTheMaker, anyRecipient, transferAmount); + const timeoutInMs = 150; + setTimeout(() => { + done(); + }, timeoutInMs); + })().catch(done); + }); it('should emit orderStateInvalid when maker moves balance backing watched order', (done: DoneCallback) => { (async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( -- cgit v1.2.3 From 2bf65fda1f6b3018edbb4158574465326d4918be Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Fri, 10 Nov 2017 16:34:21 -0500 Subject: Add tests for the numConfirmations config to ensure that the events are being emitted for the confirmation depth specified --- test/order_state_watcher_test.ts | 70 ++++++++++++++++++++++++++++++++++++-- test/utils/blockchain_lifecycle.ts | 3 ++ test/utils/rpc.ts | 7 ++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 1231d7e16..269956400 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -12,6 +12,7 @@ import { ZeroEx, LogEvent, DecodedLogEvent, + ZeroExConfig, OrderState, SignedOrder, OrderStateValid, @@ -21,10 +22,14 @@ import { import { TokenUtils } from './utils/token_utils'; import { FillScenarios } from './utils/fill_scenarios'; import { DoneCallback } from '../src/types'; +import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; import {reportCallbackErrors} from './utils/report_callback_errors'; +const TIMEOUT_MS = 150; + chaiSetup.configure(); const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(); describe('OrderStateWatcher', () => { let web3: Web3; @@ -137,10 +142,9 @@ describe('OrderStateWatcher', () => { const transferAmount = new BigNumber(2); const notTheMakerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, notTheMaker); await zeroEx.token.transferAsync(makerToken.address, notTheMaker, anyRecipient, transferAmount); - const timeoutInMs = 150; setTimeout(() => { done(); - }, timeoutInMs); + }, TIMEOUT_MS); })().catch(done); }); it('should emit orderStateInvalid when maker moves balance backing watched order', (done: DoneCallback) => { @@ -269,5 +273,67 @@ describe('OrderStateWatcher', () => { await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmountInBaseUnits); })().catch(done); }); + describe('check numConfirmations behavior', () => { + before(() => { + const configs: ZeroExConfig = { + orderWatcherConfig: { + numConfirmations: 1, + }, + }; + zeroEx = new ZeroEx(web3.currentProvider, configs); + }); + it('should emit orderState when watching at 1 confirmation deep and event is one block deep', + (done: DoneCallback) => { + (async () => { + fillScenarios = new FillScenarios( + zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress, + ); + + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerToken.address, takerToken.address, maker, taker, fillableAmount, + ); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + zeroEx.orderStateWatcher.addOrder(signedOrder); + const callback = reportCallbackErrors(done)((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); + blockchainLifecycle.mineABlock(); + })().catch(done); + }); + it('shouldn\'t emit orderState when watching at 1 confirmation deep and event is in mempool', + (done: DoneCallback) => { + (async () => { + fillScenarios = new FillScenarios( + zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress, + ); + + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerToken.address, takerToken.address, maker, taker, fillableAmount, + ); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + zeroEx.orderStateWatcher.addOrder(signedOrder); + const callback = reportCallbackErrors(done)((orderState: OrderState) => { + throw new Error('OrderState callback fired when it shouldn\'t have'); + }); + 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); + setTimeout(() => { + done(); + }, TIMEOUT_MS); + })().catch(done); + }); + }); }); }); diff --git a/test/utils/blockchain_lifecycle.ts b/test/utils/blockchain_lifecycle.ts index 9fdf0e856..9a44ccd6f 100644 --- a/test/utils/blockchain_lifecycle.ts +++ b/test/utils/blockchain_lifecycle.ts @@ -20,4 +20,7 @@ export class BlockchainLifecycle { throw new Error(`Snapshot with id #${snapshotId} failed to revert`); } } + public async mineABlock(): Promise { + await this.rpc.mineBlockAsync(); + } } diff --git a/test/utils/rpc.ts b/test/utils/rpc.ts index f28a85340..1fc9f2428 100644 --- a/test/utils/rpc.ts +++ b/test/utils/rpc.ts @@ -26,6 +26,13 @@ export class RPC { const didRevert = await this.sendAsync(payload); return didRevert; } + public async mineBlockAsync(): Promise { + const method = 'evm_mine'; + const params: any[] = []; + const payload = this.toPayload(method, params); + const didRevert = await this.sendAsync(payload); + return didRevert; + } private toPayload(method: string, params: any[] = []): string { const payload = JSON.stringify({ id: this.id, -- cgit v1.2.3 From 697926641ff2996e561811f077f0f51afb543484 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Fri, 10 Nov 2017 17:47:30 -0500 Subject: Rename method since it's not more then just mempool --- src/order_watcher/event_watcher.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index 2a1b6dacf..0174288cc 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -26,7 +26,7 @@ export class EventWatcher { assert.isFunction('callback', callback); this._callbackIfExistsAsync = callback; this._intervalIdIfExists = intervalUtils.setAsyncExcludingInterval( - this._pollForMempoolEventsAsync.bind(this), this._pollingIntervalMs, + this._pollForBlockchainEventsAsync.bind(this), this._pollingIntervalMs, ); } public unsubscribe(): void { @@ -36,7 +36,7 @@ export class EventWatcher { intervalUtils.clearAsyncExcludingInterval(this._intervalIdIfExists); } } - private async _pollForMempoolEventsAsync(): Promise { + private async _pollForBlockchainEventsAsync(): Promise { const pendingEvents = await this._getEventsAsync(); if (pendingEvents.length === 0) { // HACK: Sometimes when node rebuilds the pending block we get back the empty result. -- cgit v1.2.3 From 0d957ea71d9323445add682d7ee98d9b3cb1a973 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Fri, 10 Nov 2017 17:47:41 -0500 Subject: Add comment above the eventWatcher class --- src/order_watcher/event_watcher.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index 0174288cc..304b3e994 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -8,6 +8,10 @@ import {assert} from '../utils/assert'; const DEFAULT_EVENT_POLLING_INTERVAL = 200; +/* + * The EventWatcher watches for blockchain events at the specified block confirmation + * depth. + */ export class EventWatcher { private _web3Wrapper: Web3Wrapper; private _pollingIntervalMs: number; -- cgit v1.2.3 From ca9c1bca4a694026981a804c58138f08c8b23321 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Fri, 10 Nov 2017 18:06:57 -0500 Subject: Fix alignment --- src/order_watcher/event_watcher.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index 304b3e994..286872a6f 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -23,8 +23,8 @@ export class EventWatcher { this._web3Wrapper = web3Wrapper; this._numConfirmations = numConfirmations; this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? - DEFAULT_EVENT_POLLING_INTERVAL : - pollingIntervalMs; + DEFAULT_EVENT_POLLING_INTERVAL : + pollingIntervalMs; } public subscribe(callback: EventWatcherCallback): void { assert.isFunction('callback', callback); -- cgit v1.2.3 From 6012926e8278bed55b173a8e8380231c75a1a48c Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Fri, 10 Nov 2017 18:07:11 -0500 Subject: Throw if trying to subscribe multiple times --- src/order_watcher/event_watcher.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index 286872a6f..c11b78f2e 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -1,7 +1,12 @@ import * as Web3 from 'web3'; import * as _ from 'lodash'; import {Web3Wrapper} from '../web3_wrapper'; -import {BlockParamLiteral, EventCallback, EventWatcherCallback} from '../types'; +import { + BlockParamLiteral, + EventCallback, + EventWatcherCallback, + ZeroExError, +} from '../types'; import {AbiDecoder} from '../utils/abi_decoder'; import {intervalUtils} from '../utils/interval_utils'; import {assert} from '../utils/assert'; @@ -28,6 +33,9 @@ export class EventWatcher { } public subscribe(callback: EventWatcherCallback): void { assert.isFunction('callback', callback); + if (!_.isUndefined(this._callbackIfExistsAsync)) { + throw new Error(ZeroExError.SubscriptionAlreadyPresent); + } this._callbackIfExistsAsync = callback; this._intervalIdIfExists = intervalUtils.setAsyncExcludingInterval( this._pollForBlockchainEventsAsync.bind(this), this._pollingIntervalMs, -- cgit v1.2.3 From 0ec51b124bb6030cd59a57ab53444af452e0e674 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 08:57:49 -0500 Subject: Feather the callback down to _emitDifferencesAsync and don't store it as a class instance. This will make supporting multiple subscriptions easier later on and reduces the amount of unsubscription cleanup --- src/order_watcher/event_watcher.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index c11b78f2e..f86d1f59f 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -22,7 +22,6 @@ export class EventWatcher { private _pollingIntervalMs: number; private _intervalIdIfExists?: NodeJS.Timer; private _lastEvents: Web3.LogEntry[] = []; - private _callbackIfExistsAsync?: EventWatcherCallback; private _numConfirmations: number; constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number, numConfirmations: number) { this._web3Wrapper = web3Wrapper; @@ -33,22 +32,21 @@ export class EventWatcher { } public subscribe(callback: EventWatcherCallback): void { assert.isFunction('callback', callback); - if (!_.isUndefined(this._callbackIfExistsAsync)) { + if (!_.isUndefined(this._intervalIdIfExists)) { throw new Error(ZeroExError.SubscriptionAlreadyPresent); } - this._callbackIfExistsAsync = callback; this._intervalIdIfExists = intervalUtils.setAsyncExcludingInterval( - this._pollForBlockchainEventsAsync.bind(this), this._pollingIntervalMs, + this._pollForBlockchainEventsAsync.bind(this, callback), this._pollingIntervalMs, ); } public unsubscribe(): void { - delete this._callbackIfExistsAsync; this._lastEvents = []; if (!_.isUndefined(this._intervalIdIfExists)) { intervalUtils.clearAsyncExcludingInterval(this._intervalIdIfExists); + delete this._intervalIdIfExists; } } - private async _pollForBlockchainEventsAsync(): Promise { + private async _pollForBlockchainEventsAsync(callback: EventWatcherCallback): Promise { const pendingEvents = await this._getEventsAsync(); if (pendingEvents.length === 0) { // HACK: Sometimes when node rebuilds the pending block we get back the empty result. @@ -59,9 +57,9 @@ export class EventWatcher { const removedEvents = _.differenceBy(this._lastEvents, pendingEvents, JSON.stringify); const newEvents = _.differenceBy(pendingEvents, this._lastEvents, JSON.stringify); let isRemoved = true; - await this._emitDifferencesAsync(removedEvents, isRemoved); + await this._emitDifferencesAsync(removedEvents, isRemoved, callback); isRemoved = false; - await this._emitDifferencesAsync(newEvents, isRemoved); + await this._emitDifferencesAsync(newEvents, isRemoved, callback); this._lastEvents = pendingEvents; } private async _getEventsAsync(): Promise { @@ -79,14 +77,16 @@ export class EventWatcher { const events = await this._web3Wrapper.getLogsAsync(eventFilter); return events; } - private async _emitDifferencesAsync(logs: Web3.LogEntry[], isRemoved: boolean): Promise { + private async _emitDifferencesAsync( + logs: Web3.LogEntry[], isRemoved: boolean, callback: EventWatcherCallback, + ): Promise { for (const log of logs) { const logEvent = { removed: isRemoved, ...log, }; - if (!_.isUndefined(this._callbackIfExistsAsync)) { - await this._callbackIfExistsAsync(logEvent); + if (!_.isUndefined(this._intervalIdIfExists)) { + await callback(logEvent); } } } -- cgit v1.2.3 From 041d00301c050a85ac7b41c7f367b319f09ed698 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 08:58:01 -0500 Subject: Fix type declaration in test --- test/event_watcher_test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/event_watcher_test.ts b/test/event_watcher_test.ts index 36153c207..98dab93b5 100644 --- a/test/event_watcher_test.ts +++ b/test/event_watcher_test.ts @@ -24,7 +24,7 @@ describe('EventWatcher', () => { let eventWatcher: EventWatcher; let web3Wrapper: Web3Wrapper; const numConfirmations = 0; - const logA = { + const logA: Web3.LogEntry = { address: '0x71d271f8b14adef568f8f28f1587ce7271ac4ca5', blockHash: null, blockNumber: null, @@ -32,9 +32,9 @@ describe('EventWatcher', () => { logIndex: null, topics: [], transactionHash: '0x004881d38cd4a8f72f1a0d68c8b9b8124504706041ff37019c1d1ed6bfda8e17', - transactionIndex: null, + transactionIndex: 0, }; - const logB = { + const logB: Web3.LogEntry = { address: '0x8d12a197cb00d4747a1fe03395095ce2a5cc6819', blockHash: null, blockNumber: null, @@ -42,9 +42,9 @@ describe('EventWatcher', () => { logIndex: null, topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ], transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25', - transactionIndex: null, + transactionIndex: 0, }; - const logC = { + const logC: Web3.LogEntry = { address: '0x1d271f8b174adef58f1587ce68f8f27271ac4ca5', blockHash: null, blockNumber: null, @@ -52,7 +52,7 @@ describe('EventWatcher', () => { logIndex: null, topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ], transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25', - transactionIndex: null, + transactionIndex: 0, }; before(async () => { web3 = web3Factory.create(); -- cgit v1.2.3 From 4e708c81ca3309e1ac9ac195e3169c09d5358abb Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 09:19:51 -0500 Subject: Fix expected balance --- 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 d4f763aa68b62b33e82af438e11b07d0e741973e Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 09:45:50 -0500 Subject: Add comment above orderStateWatcher class --- src/order_watcher/order_state_watcher.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 303ec8bd3..f1b757abf 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -35,6 +35,12 @@ interface OrderByOrderHash { [orderHash: string]: SignedOrder; } +/** + * This class includes all the functionality related to watching a set of orders + * for potential changes in order validity/fillability. The orderWatcher notifies + * the subscriber of these changes so that a final decison can be made on whether + * the order should be deemed invalid. + */ export class OrderStateWatcher { private _orders: OrderByOrderHash; private _dependentOrderHashes: DependentOrderHashes; -- cgit v1.2.3 From 0db0694aad5396aad3bc295120d45da731c53911 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 09:47:32 -0500 Subject: rename _orders to _orderByOrderhash for clarity --- src/order_watcher/order_state_watcher.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index f1b757abf..dd5e3f73c 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -42,7 +42,7 @@ interface OrderByOrderHash { * the order should be deemed invalid. */ export class OrderStateWatcher { - private _orders: OrderByOrderHash; + private _orderByOrderHash: OrderByOrderHash; private _dependentOrderHashes: DependentOrderHashes; private _web3Wrapper: Web3Wrapper; private _callbackIfExistsAsync?: OnOrderStateChangeCallback; @@ -55,7 +55,7 @@ export class OrderStateWatcher { config?: OrderStateWatcherConfig, ) { this._web3Wrapper = web3Wrapper; - this._orders = {}; + this._orderByOrderHash = {}; this._dependentOrderHashes = {}; const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.pollingIntervalMs; this._numConfirmations = _.isUndefined(config) ? @@ -75,7 +75,7 @@ export class OrderStateWatcher { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); const orderHash = ZeroEx.getOrderHashHex(signedOrder); assert.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker); - this._orders[orderHash] = signedOrder; + this._orderByOrderHash[orderHash] = signedOrder; this.addToDependentOrderHashes(signedOrder, orderHash); } /** @@ -84,11 +84,11 @@ export class OrderStateWatcher { */ public removeOrder(orderHash: string): void { assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); - const signedOrder = this._orders[orderHash]; + const signedOrder = this._orderByOrderHash[orderHash]; if (_.isUndefined(signedOrder)) { return; // noop } - delete this._orders[orderHash]; + delete this._orderByOrderHash[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 } @@ -153,7 +153,7 @@ export class OrderStateWatcher { case ExchangeEvents.LogFill: case ExchangeEvents.LogCancel: const orderHash = decodedLog.args.orderHash; - const isOrderWatched = !_.isUndefined(this._orders[orderHash]); + const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); if (isOrderWatched) { await this._emitRevalidateOrdersAsync([orderHash], blockNumber); } @@ -175,7 +175,7 @@ export class OrderStateWatcher { }; for (const orderHash of orderHashes) { - const signedOrder = this._orders[orderHash] as SignedOrder; + const signedOrder = this._orderByOrderHash[orderHash] as SignedOrder; const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder, methodOpts); if (!_.isUndefined(this._callbackIfExistsAsync)) { await this._callbackIfExistsAsync(orderState); -- cgit v1.2.3 From 12023073f43ad431a9acb5f28bb0a9abea4ab089 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 09:54:27 -0500 Subject: Use enum instead of boolean to avoid potential bugs from isRemoved incorrectly being set to true --- src/order_watcher/event_watcher.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index f86d1f59f..c9e72281c 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -10,9 +10,15 @@ import { import {AbiDecoder} from '../utils/abi_decoder'; import {intervalUtils} from '../utils/interval_utils'; import {assert} from '../utils/assert'; +import {utils} from '../utils/utils'; const DEFAULT_EVENT_POLLING_INTERVAL = 200; +enum LogEventState { + Removed, + Added, +} + /* * The EventWatcher watches for blockchain events at the specified block confirmation * depth. @@ -56,10 +62,8 @@ export class EventWatcher { } const removedEvents = _.differenceBy(this._lastEvents, pendingEvents, JSON.stringify); const newEvents = _.differenceBy(pendingEvents, this._lastEvents, JSON.stringify); - let isRemoved = true; - await this._emitDifferencesAsync(removedEvents, isRemoved, callback); - isRemoved = false; - await this._emitDifferencesAsync(newEvents, isRemoved, callback); + await this._emitDifferencesAsync(removedEvents, LogEventState.Removed, callback); + await this._emitDifferencesAsync(newEvents, LogEventState.Added, callback); this._lastEvents = pendingEvents; } private async _getEventsAsync(): Promise { @@ -78,11 +82,11 @@ export class EventWatcher { return events; } private async _emitDifferencesAsync( - logs: Web3.LogEntry[], isRemoved: boolean, callback: EventWatcherCallback, + logs: Web3.LogEntry[], logEventState: LogEventState, callback: EventWatcherCallback, ): Promise { for (const log of logs) { const logEvent = { - removed: isRemoved, + removed: logEventState === LogEventState.Removed, ...log, }; if (!_.isUndefined(this._intervalIdIfExists)) { -- cgit v1.2.3 From 037e992de457fc9f5012576719470d6b4f2b118d Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 09:57:45 -0500 Subject: establish convention of initializing empty instances in instance declaration --- src/order_watcher/order_state_watcher.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index dd5e3f73c..f7e3a38a0 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -42,8 +42,8 @@ interface OrderByOrderHash { * the order should be deemed invalid. */ export class OrderStateWatcher { - private _orderByOrderHash: OrderByOrderHash; - private _dependentOrderHashes: DependentOrderHashes; + private _orderByOrderHash: OrderByOrderHash = {}; + private _dependentOrderHashes: DependentOrderHashes = {}; private _web3Wrapper: Web3Wrapper; private _callbackIfExistsAsync?: OnOrderStateChangeCallback; private _eventWatcher: EventWatcher; @@ -55,8 +55,6 @@ export class OrderStateWatcher { config?: OrderStateWatcherConfig, ) { this._web3Wrapper = web3Wrapper; - this._orderByOrderHash = {}; - this._dependentOrderHashes = {}; const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.pollingIntervalMs; this._numConfirmations = _.isUndefined(config) ? DEFAULT_NUM_CONFIRMATIONS -- cgit v1.2.3 From 0fe5c5dac33ec11bc3517f16f1fd613d720c8b8d Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 10:13:42 -0500 Subject: Remove keys from dependentOrderHashes if empty --- src/order_watcher/order_state_watcher.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index f7e3a38a0..f06e32a32 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -87,8 +87,7 @@ export class OrderStateWatcher { return; // noop } delete this._orderByOrderHash[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 + this.removeFromDependentOrderHashes(signedOrder.maker, signedOrder.makerTokenAddress, orderHash); } /** * Starts an orderStateWatcher subscription. The callback will be called every time a watched order's @@ -191,4 +190,13 @@ export class OrderStateWatcher { } this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress].add(orderHash); } + private removeFromDependentOrderHashes(makerAddress: string, makerTokenAddress: string, orderHash: string) { + this._dependentOrderHashes[makerAddress][makerTokenAddress].delete(orderHash); + if (this._dependentOrderHashes[makerAddress][makerTokenAddress].size === 0) { + delete this._dependentOrderHashes[makerAddress][makerTokenAddress]; + } + if (_.isEmpty(this._dependentOrderHashes[makerAddress])) { + delete this._dependentOrderHashes[makerAddress]; + } + } } -- cgit v1.2.3 From 252fdd03d718173a6c1ffa75c3b18d6f1a7d051c Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 10:21:40 -0500 Subject: Fix comment --- src/order_watcher/order_state_watcher.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index f06e32a32..b92aa8cab 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -94,9 +94,6 @@ export class OrderStateWatcher { * backing blockchain state has changed. This is a call-to-action for the caller to re-validate the order. * @param callback Receives the orderHash of the order that should be re-validated, together * with all the order-relevant blockchain state needed to re-validate the order. - * @param numConfirmations Number of confirmed blocks deeps you want to run the orderWatcher from. Passing - * is 0 will watch the backing node's mempool, 3 will emit events when blockchain - * state relevant to a watched order changed 3 blocks ago. */ public subscribe(callback: OnOrderStateChangeCallback): void { assert.isFunction('callback', callback); -- cgit v1.2.3 From d61f34ec125884ff1c17f41dbc8c844b866390f5 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 10:21:59 -0500 Subject: Declare OnOrderStateChangeCallback as either sync or async --- src/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types.ts b/src/types.ts index 13867dac8..411ff3bfd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -508,6 +508,6 @@ export interface OrderStateInvalid { export type OrderState = OrderStateValid|OrderStateInvalid; -export type OnOrderStateChangeCallback = ( - orderState: OrderState, -) => void; +export type OnOrderStateChangeCallbackSync = (orderState: OrderState) => void; +export type OnOrderStateChangeCallbackAsync = (orderState: OrderState) => Promise; +export type OnOrderStateChangeCallback = OnOrderStateChangeCallbackAsync|OnOrderStateChangeCallbackSync; -- cgit v1.2.3 From 62ac8e1952f907085427e9c00a7e922d62c0ec23 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 10:22:09 -0500 Subject: Fix missing renames --- test/order_state_watcher_test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 10ed6bd77..9ed68ca10 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -62,13 +62,13 @@ describe('OrderStateWatcher', () => { ); const orderHash = ZeroEx.getOrderHashHex(signedOrder); zeroEx.orderStateWatcher.addOrder(signedOrder); - expect((zeroEx.orderStateWatcher as any)._orders).to.include({ + expect((zeroEx.orderStateWatcher as any)._orderByOrderHash).to.include({ [orderHash]: signedOrder, }); let dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes; expect(dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress]).to.have.keys(orderHash); zeroEx.orderStateWatcher.removeOrder(orderHash); - expect((zeroEx.orderStateWatcher as any)._orders).to.not.include({ + expect((zeroEx.orderStateWatcher as any)._orderByOrderHash).to.not.include({ [orderHash]: signedOrder, }); dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes; -- cgit v1.2.3 From e5d04f4467d38353c13efd58ffb6125e27262c0e Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 10:50:08 -0500 Subject: Fix test given that we now do delete the keys in dependentOrderHashes --- test/order_state_watcher_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 9ed68ca10..3421353e3 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -72,7 +72,7 @@ describe('OrderStateWatcher', () => { [orderHash]: signedOrder, }); dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes; - expect(dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress]).to.not.have.keys(orderHash); + expect(dependentOrderHashes[signedOrder.maker]).to.be.undefined(); }); it('should no-op when removing a non-existing order', async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( -- cgit v1.2.3 From a8b6bbd6bc5a81828ca4bbc5db6452ecefd1a898 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 10:57:27 -0500 Subject: Improve comment --- src/order_watcher/order_state_watcher.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index b92aa8cab..69efada2a 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -66,7 +66,8 @@ export class OrderStateWatcher { this._orderStateUtils = orderStateUtils; } /** - * Add an order to the orderStateWatcher + * Add an order to the orderStateWatcher. Before the order is added, it's + * signature is verified. * @param signedOrder The order you wish to start watching. */ public addOrder(signedOrder: SignedOrder): void { -- cgit v1.2.3 From 72fcf7b2ab46bb25a72ef508592d11aa99e4d7c0 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 10:57:39 -0500 Subject: rename isDecodedLog to isLogDecoded --- src/order_watcher/order_state_watcher.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 69efada2a..dfa830402 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -113,8 +113,8 @@ export class OrderStateWatcher { } private async _onEventWatcherCallbackAsync(log: LogEvent): Promise { const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log); - const isDecodedLog = !_.isUndefined((maybeDecodedLog as LogWithDecodedArgs).event); - if (!isDecodedLog) { + const isLogDecoded = !_.isUndefined((maybeDecodedLog as LogWithDecodedArgs).event); + if (!isLogDecoded) { return; // noop } const blockNumberBuff = ethUtil.toBuffer(maybeDecodedLog.blockNumber); -- cgit v1.2.3 From b66600338e0057baa6964328bdf337a905aa5ff9 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sat, 11 Nov 2017 11:05:52 -0500 Subject: Add comment --- src/order_watcher/order_state_watcher.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index dfa830402..4866f8409 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -117,6 +117,8 @@ export class OrderStateWatcher { if (!isLogDecoded) { return; // noop } + // Unfortunately blockNumber is returned as a hex-encoded string, so we + // convert it to a number here. const blockNumberBuff = ethUtil.toBuffer(maybeDecodedLog.blockNumber); const blockNumber = ethUtil.bufferToInt(blockNumberBuff); -- cgit v1.2.3 From abb23631df4fd775d2f9c96332e16c0732b2955e Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sun, 12 Nov 2017 10:39:09 -0500 Subject: Update testRPC snapshot used by CircleCi --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 5a8d340f0..3dc00bd03 100644 --- a/circle.yml +++ b/circle.yml @@ -2,7 +2,7 @@ machine: node: version: 6.5.0 environment: - CONTRACTS_COMMIT_HASH: '35053f9' + CONTRACTS_COMMIT_HASH: '78fe8dd' PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" dependencies: -- cgit v1.2.3 From 3e2a614eb9a4f1219e2f11ea29c8a7cd59dd74dd Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Sun, 12 Nov 2017 11:28:01 -0500 Subject: Calculate the remaining order amount in maker units --- src/types.ts | 1 + src/utils/order_state_utils.ts | 16 ++++++++++++++++ test/order_state_watcher_test.ts | 2 ++ 3 files changed, 19 insertions(+) diff --git a/src/types.ts b/src/types.ts index 160b71fda..a366fc31e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -493,6 +493,7 @@ export interface OrderRelevantState { makerFeeProxyAllowance: BigNumber; filledTakerTokenAmount: BigNumber; canceledTakerTokenAmount: BigNumber; + remainingFillableMakerTokenAmount: BigNumber; } export interface OrderStateValid { diff --git a/src/utils/order_state_utils.ts b/src/utils/order_state_utils.ts index 2a5becf9a..ff5c1880d 100644 --- a/src/utils/order_state_utils.ts +++ b/src/utils/order_state_utils.ts @@ -60,6 +60,21 @@ export class OrderStateUtils { ); const filledTakerTokenAmount = await this.exchangeWrapper.getFilledTakerAmountAsync(orderHash, methodOpts); const canceledTakerTokenAmount = await this.exchangeWrapper.getCanceledTakerAmountAsync(orderHash, methodOpts); + const unavailableTakerTokenAmount = + await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash, methodOpts); + const totalMakerTokenAmount = signedOrder.makerTokenAmount; + const totalTakerTokenAmount = signedOrder.takerTokenAmount; + const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount); + // 200 in order, 100 unavailable = 100 remaning, 0.5 remaning proportion + const remainingTakerProportion = remainingTakerTokenAmount.dividedBy(totalTakerTokenAmount); + const remainingMakerTokenAmount = remainingTakerProportion.times(totalMakerTokenAmount); + // min allowance, balance in account of maker + const fillableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]); + // min ^, remaining order maker token amount + const remainingFillableMakerTokenAmount = BigNumber.min(fillableMakerTokenAmount, remainingMakerTokenAmount); + // TODO + // edge case when maker token is ZRX + // rounding issues, check if its fillabae const orderRelevantState = { makerBalance, makerProxyAllowance, @@ -67,6 +82,7 @@ export class OrderStateUtils { makerFeeProxyAllowance, filledTakerTokenAmount, canceledTakerTokenAmount, + remainingFillableMakerTokenAmount, }; return orderRelevantState; } diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 3421353e3..039bcef18 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -189,6 +189,8 @@ describe('OrderStateWatcher', () => { expect(validOrderState.orderHash).to.be.equal(orderHash); const orderRelevantState = validOrderState.orderRelevantState; const remainingMakerBalance = makerBalance.sub(fillAmountInBaseUnits); + const remainingFillable = fillableAmount.minus(fillAmountInBaseUnits); + expect(remainingFillable).to.be.bignumber.equal(fillableAmount.minus(fillAmountInBaseUnits)); expect(orderRelevantState.makerBalance).to.be.bignumber.equal(remainingMakerBalance); if (eventCount === 2) { done(); -- cgit v1.2.3 From d4cab6e62f1d96923c99cf58bcfd3b2392b35a30 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sun, 12 Nov 2017 16:57:13 -0500 Subject: Update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 029144b5a..00164bb74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +v0.23.0 - _November 12, 2017_ +------------------------ + * Fixed unhandled promise rejection error in subscribe methods (#209) + * Subscribe callbacks now receive an error object as their first argument + v0.22.6 - _November 10, 2017_ ------------------------ * Add a timeout parameter to transaction awaiting (#206) -- cgit v1.2.3 From 1392a855bb17981f7680548a23062842fb6dc4e0 Mon Sep 17 00:00:00 2001 From: Brandon Millman Date: Sun, 12 Nov 2017 17:01:58 -0500 Subject: 0.23.0 --- 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 6b4a97d87..8e2823103 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "0x.js", - "version": "0.22.6", + "version": "0.23.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f8aed6436..1f4c2de5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "0x.js", - "version": "0.22.6", + "version": "0.23.0", "description": "A javascript library for interacting with the 0x protocol", "keywords": [ "0x.js", -- cgit v1.2.3 From fdb3fa6801e07972f53a05c312c2ee933809f823 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Sun, 12 Nov 2017 17:24:31 -0500 Subject: Added specs for allowance and balance changes --- src/utils/order_state_utils.ts | 2 - test/order_state_watcher_test.ts | 79 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/utils/order_state_utils.ts b/src/utils/order_state_utils.ts index ff5c1880d..fde7a7e02 100644 --- a/src/utils/order_state_utils.ts +++ b/src/utils/order_state_utils.ts @@ -72,9 +72,7 @@ export class OrderStateUtils { const fillableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]); // min ^, remaining order maker token amount const remainingFillableMakerTokenAmount = BigNumber.min(fillableMakerTokenAmount, remainingMakerTokenAmount); - // TODO // edge case when maker token is ZRX - // rounding issues, check if its fillabae const orderRelevantState = { makerBalance, makerProxyAllowance, diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 039bcef18..43340aadf 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -203,6 +203,85 @@ describe('OrderStateWatcher', () => { ); })().catch(done); }); + describe('remainingFillableMakerTokenAmount', () => { + it.only('should calculate correct reamining fillable', (done: DoneCallback) => { + (async () => { + const takerFillableAmount = new BigNumber(10); + const makerFillableAmount = new BigNumber(20); + signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync( + makerToken.address, takerToken.address, maker, taker, makerFillableAmount, takerFillableAmount); + 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 = reportCallbackErrors(done)((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; + expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal( + new BigNumber(16)); + if (eventCount === 2) { + done(); + } + }); + zeroEx.orderStateWatcher.subscribe(callback); + const shouldThrowOnInsufficientBalanceOrAllowance = true; + await zeroEx.exchange.fillOrderAsync( + signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker, + ); + })().catch(done); + }); + it.only('should emit approved amount when approved amount is lower', (done: DoneCallback) => { + (async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerToken.address, takerToken.address, maker, taker, fillableAmount, + ); + + const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); + + const changedMakerApprovalAmount = new BigNumber(3); + zeroEx.orderStateWatcher.addOrder(signedOrder); + + const callback = reportCallbackErrors(done)((orderState: OrderState) => { + const validOrderState = orderState as OrderStateValid; + const orderRelevantState = validOrderState.orderRelevantState; + expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal( + changedMakerApprovalAmount); + done(); + }); + zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, changedMakerApprovalAmount); + })().catch(done); + }); + it.only('should emit balance amount when balance amount is lower', (done: DoneCallback) => { + (async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerToken.address, takerToken.address, maker, taker, fillableAmount, + ); + + const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); + + const transferAmount = new BigNumber(1); + zeroEx.orderStateWatcher.addOrder(signedOrder); + + const callback = reportCallbackErrors(done)((orderState: OrderState) => { + const validOrderState = orderState as OrderStateValid; + const orderRelevantState = validOrderState.orderRelevantState; + + expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal( + transferAmount); + done(); + }); + zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.token.transferAsync(makerToken.address, maker, ZeroEx.NULL_ADDRESS, + makerBalance.minus(transferAmount)); + })().catch(done); + }); + }); it('should emit orderStateInvalid when watched order cancelled', (done: DoneCallback) => { (async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( -- cgit v1.2.3 From e06539e76d91898af544f321c70aa0aa1d9388f9 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Sun, 12 Nov 2017 17:25:42 -0500 Subject: remove only --- test/order_state_watcher_test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 43340aadf..307da4780 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -204,7 +204,7 @@ describe('OrderStateWatcher', () => { })().catch(done); }); describe('remainingFillableMakerTokenAmount', () => { - it.only('should calculate correct reamining fillable', (done: DoneCallback) => { + it('should calculate correct reamining fillable', (done: DoneCallback) => { (async () => { const takerFillableAmount = new BigNumber(10); const makerFillableAmount = new BigNumber(20); @@ -235,7 +235,7 @@ describe('OrderStateWatcher', () => { ); })().catch(done); }); - it.only('should emit approved amount when approved amount is lower', (done: DoneCallback) => { + it('should emit approved amount when approved amount is lower', (done: DoneCallback) => { (async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( makerToken.address, takerToken.address, maker, taker, fillableAmount, @@ -257,7 +257,7 @@ describe('OrderStateWatcher', () => { await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, changedMakerApprovalAmount); })().catch(done); }); - it.only('should emit balance amount when balance amount is lower', (done: DoneCallback) => { + it('should emit balance amount when balance amount is lower', (done: DoneCallback) => { (async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( makerToken.address, takerToken.address, maker, taker, fillableAmount, -- cgit v1.2.3 From 1b3f84c9ad4d13f49041ed7f56a2a126f9c40f38 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Sun, 12 Nov 2017 17:28:34 -0500 Subject: text description update --- test/order_state_watcher_test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 307da4780..07d18ae61 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -235,7 +235,7 @@ describe('OrderStateWatcher', () => { ); })().catch(done); }); - it('should emit approved amount when approved amount is lower', (done: DoneCallback) => { + it('should equal approved amount when approved amount is lowest', (done: DoneCallback) => { (async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( makerToken.address, takerToken.address, maker, taker, fillableAmount, @@ -257,7 +257,7 @@ describe('OrderStateWatcher', () => { await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, changedMakerApprovalAmount); })().catch(done); }); - it('should emit balance amount when balance amount is lower', (done: DoneCallback) => { + it('should equal balance amount when balance amount is lowest', (done: DoneCallback) => { (async () => { signedOrder = await fillScenarios.createFillableSignedOrderAsync( makerToken.address, takerToken.address, maker, taker, fillableAmount, -- cgit v1.2.3 From 5e77e8809abd96136fb11ebdc84a2aefa32d4cea Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Sun, 12 Nov 2017 17:30:57 -0500 Subject: Update comment --- src/utils/order_state_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/order_state_utils.ts b/src/utils/order_state_utils.ts index fde7a7e02..4f57de9fe 100644 --- a/src/utils/order_state_utils.ts +++ b/src/utils/order_state_utils.ts @@ -65,7 +65,7 @@ export class OrderStateUtils { const totalMakerTokenAmount = signedOrder.makerTokenAmount; const totalTakerTokenAmount = signedOrder.takerTokenAmount; const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount); - // 200 in order, 100 unavailable = 100 remaning, 0.5 remaning proportion + // 200 in order, 100 unavailable = 100 remaning, 0.5 remaining in taker proportion const remainingTakerProportion = remainingTakerTokenAmount.dividedBy(totalTakerTokenAmount); const remainingMakerTokenAmount = remainingTakerProportion.times(totalMakerTokenAmount); // min allowance, balance in account of maker -- cgit v1.2.3 From 12298ea392525ed1de87115003e7d89267489c88 Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Sun, 12 Nov 2017 18:11:30 -0500 Subject: Don't return anything --- test/utils/rpc.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/utils/rpc.ts b/test/utils/rpc.ts index 1fc9f2428..299e72e79 100644 --- a/test/utils/rpc.ts +++ b/test/utils/rpc.ts @@ -30,8 +30,7 @@ export class RPC { const method = 'evm_mine'; const params: any[] = []; const payload = this.toPayload(method, params); - const didRevert = await this.sendAsync(payload); - return didRevert; + await this.sendAsync(payload); } private toPayload(method: string, params: any[] = []): string { const payload = JSON.stringify({ -- cgit v1.2.3 From 42e3ab91a794f61d65008d969b3d48080b5035d7 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Sun, 12 Nov 2017 19:17:27 -0500 Subject: Perform the division after multiplication to reduce compounding the rounding errors --- src/utils/order_state_utils.ts | 6 +++--- test/order_state_watcher_test.ts | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/utils/order_state_utils.ts b/src/utils/order_state_utils.ts index 4f57de9fe..7c10a5480 100644 --- a/src/utils/order_state_utils.ts +++ b/src/utils/order_state_utils.ts @@ -66,13 +66,13 @@ export class OrderStateUtils { const totalTakerTokenAmount = signedOrder.takerTokenAmount; const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount); // 200 in order, 100 unavailable = 100 remaning, 0.5 remaining in taker proportion - const remainingTakerProportion = remainingTakerTokenAmount.dividedBy(totalTakerTokenAmount); - const remainingMakerTokenAmount = remainingTakerProportion.times(totalMakerTokenAmount); + const remainingMakerTokenAmount = remainingTakerTokenAmount.times(totalMakerTokenAmount) + .dividedToIntegerBy(totalTakerTokenAmount); // min allowance, balance in account of maker const fillableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]); // min ^, remaining order maker token amount const remainingFillableMakerTokenAmount = BigNumber.min(fillableMakerTokenAmount, remainingMakerTokenAmount); - // edge case when maker token is ZRX + // TODO: Handle edge case where maker token is ZRX with fee const orderRelevantState = { makerBalance, makerProxyAllowance, diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 07d18ae61..924e54772 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -190,7 +190,8 @@ describe('OrderStateWatcher', () => { const orderRelevantState = validOrderState.orderRelevantState; const remainingMakerBalance = makerBalance.sub(fillAmountInBaseUnits); const remainingFillable = fillableAmount.minus(fillAmountInBaseUnits); - expect(remainingFillable).to.be.bignumber.equal(fillableAmount.minus(fillAmountInBaseUnits)); + expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal( + remainingFillable); expect(orderRelevantState.makerBalance).to.be.bignumber.equal(remainingMakerBalance); if (eventCount === 2) { done(); @@ -204,7 +205,7 @@ describe('OrderStateWatcher', () => { })().catch(done); }); describe('remainingFillableMakerTokenAmount', () => { - it('should calculate correct reamining fillable', (done: DoneCallback) => { + it('should calculate correct remaining fillable', (done: DoneCallback) => { (async () => { const takerFillableAmount = new BigNumber(10); const makerFillableAmount = new BigNumber(20); @@ -265,20 +266,20 @@ describe('OrderStateWatcher', () => { const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); - const transferAmount = new BigNumber(1); + const remainingAmount = new BigNumber(1); + const transferAmount = makerBalance.sub(remainingAmount); zeroEx.orderStateWatcher.addOrder(signedOrder); const callback = reportCallbackErrors(done)((orderState: OrderState) => { const validOrderState = orderState as OrderStateValid; const orderRelevantState = validOrderState.orderRelevantState; - expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal( - transferAmount); + remainingAmount); done(); }); zeroEx.orderStateWatcher.subscribe(callback); - await zeroEx.token.transferAsync(makerToken.address, maker, ZeroEx.NULL_ADDRESS, - makerBalance.minus(transferAmount)); + await zeroEx.token.transferAsync( + makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferAmount); })().catch(done); }); }); -- cgit v1.2.3 From 32246fd26bc71b09818491d513a380a8bd85fdec Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Sun, 12 Nov 2017 19:37:03 -0500 Subject: remove comments --- src/utils/order_state_utils.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/utils/order_state_utils.ts b/src/utils/order_state_utils.ts index 7c10a5480..36a4b68d6 100644 --- a/src/utils/order_state_utils.ts +++ b/src/utils/order_state_utils.ts @@ -65,12 +65,9 @@ export class OrderStateUtils { const totalMakerTokenAmount = signedOrder.makerTokenAmount; const totalTakerTokenAmount = signedOrder.takerTokenAmount; const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount); - // 200 in order, 100 unavailable = 100 remaning, 0.5 remaining in taker proportion const remainingMakerTokenAmount = remainingTakerTokenAmount.times(totalMakerTokenAmount) .dividedToIntegerBy(totalTakerTokenAmount); - // min allowance, balance in account of maker const fillableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]); - // min ^, remaining order maker token amount const remainingFillableMakerTokenAmount = BigNumber.min(fillableMakerTokenAmount, remainingMakerTokenAmount); // TODO: Handle edge case where maker token is ZRX with fee const orderRelevantState = { -- cgit v1.2.3 From ddbcf5f4706a9d75cd5fd4af508f3c07a78f2886 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 11:11:32 -0500 Subject: Refactor out BalanceAndProxyAllowanceLazyStore --- src/stores/balance_proxy_allowance_lazy_store.ts | 54 +++++++++++++++++++++++ src/utils/exchange_transfer_simulator.ts | 55 +----------------------- 2 files changed, 56 insertions(+), 53 deletions(-) create mode 100644 src/stores/balance_proxy_allowance_lazy_store.ts diff --git a/src/stores/balance_proxy_allowance_lazy_store.ts b/src/stores/balance_proxy_allowance_lazy_store.ts new file mode 100644 index 000000000..e8178b9a9 --- /dev/null +++ b/src/stores/balance_proxy_allowance_lazy_store.ts @@ -0,0 +1,54 @@ +import * as _ from 'lodash'; +import {BigNumber} from 'bignumber.js'; +import {TokenWrapper} from '../contract_wrappers/token_wrapper'; + +/** + * Copy on read store for balances/proxyAllowances of tokens/accounts + */ +export class BalanceAndProxyAllowanceLazyStore { + protected token: TokenWrapper; + private balance: { + [tokenAddress: string]: { + [userAddress: string]: BigNumber, + }, + }; + private proxyAllowance: { + [tokenAddress: string]: { + [userAddress: string]: BigNumber, + }, + }; + constructor(token: TokenWrapper) { + this.token = token; + this.balance = {}; + this.proxyAllowance = {}; + } + protected async getBalanceAsync(tokenAddress: string, userAddress: string): Promise { + if (_.isUndefined(this.balance[tokenAddress]) || _.isUndefined(this.balance[tokenAddress][userAddress])) { + const balance = await this.token.getBalanceAsync(tokenAddress, userAddress); + this.setBalance(tokenAddress, userAddress, balance); + } + const cachedBalance = this.balance[tokenAddress][userAddress]; + return cachedBalance; + } + protected setBalance(tokenAddress: string, userAddress: string, balance: BigNumber): void { + if (_.isUndefined(this.balance[tokenAddress])) { + this.balance[tokenAddress] = {}; + } + this.balance[tokenAddress][userAddress] = balance; + } + protected async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise { + if (_.isUndefined(this.proxyAllowance[tokenAddress]) || + _.isUndefined(this.proxyAllowance[tokenAddress][userAddress])) { + const proxyAllowance = await this.token.getProxyAllowanceAsync(tokenAddress, userAddress); + this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance); + } + const cachedProxyAllowance = this.proxyAllowance[tokenAddress][userAddress]; + return cachedProxyAllowance; + } + protected setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber): void { + if (_.isUndefined(this.proxyAllowance[tokenAddress])) { + this.proxyAllowance[tokenAddress] = {}; + } + this.proxyAllowance[tokenAddress][userAddress] = proxyAllowance; + } +} diff --git a/src/utils/exchange_transfer_simulator.ts b/src/utils/exchange_transfer_simulator.ts index 89b23c8ab..6812759fc 100644 --- a/src/utils/exchange_transfer_simulator.ts +++ b/src/utils/exchange_transfer_simulator.ts @@ -1,7 +1,7 @@ import * as _ from 'lodash'; import BigNumber from 'bignumber.js'; import {ExchangeContractErrs, TradeSide, TransferType} from '../types'; -import {TokenWrapper} from '../contract_wrappers/token_wrapper'; +import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store'; enum FailureReason { Balance = 'balance', @@ -31,57 +31,6 @@ const ERR_MSG_MAPPING = { }, }; -/** - * Copy on read store for balances/proxyAllowances of tokens/accounts touched in trades - */ -export class BalanceAndProxyAllowanceLazyStore { - protected _token: TokenWrapper; - private _balance: { - [tokenAddress: string]: { - [userAddress: string]: BigNumber, - }, - }; - private _proxyAllowance: { - [tokenAddress: string]: { - [userAddress: string]: BigNumber, - }, - }; - constructor(token: TokenWrapper) { - this._token = token; - this._balance = {}; - this._proxyAllowance = {}; - } - protected async getBalanceAsync(tokenAddress: string, userAddress: string): Promise { - if (_.isUndefined(this._balance[tokenAddress]) || _.isUndefined(this._balance[tokenAddress][userAddress])) { - const balance = await this._token.getBalanceAsync(tokenAddress, userAddress); - this.setBalance(tokenAddress, userAddress, balance); - } - const cachedBalance = this._balance[tokenAddress][userAddress]; - return cachedBalance; - } - protected setBalance(tokenAddress: string, userAddress: string, balance: BigNumber): void { - if (_.isUndefined(this._balance[tokenAddress])) { - this._balance[tokenAddress] = {}; - } - this._balance[tokenAddress][userAddress] = balance; - } - protected async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise { - if (_.isUndefined(this._proxyAllowance[tokenAddress]) || - _.isUndefined(this._proxyAllowance[tokenAddress][userAddress])) { - const proxyAllowance = await this._token.getProxyAllowanceAsync(tokenAddress, userAddress); - this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance); - } - const cachedProxyAllowance = this._proxyAllowance[tokenAddress][userAddress]; - return cachedProxyAllowance; - } - protected setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber): void { - if (_.isUndefined(this._proxyAllowance[tokenAddress])) { - this._proxyAllowance[tokenAddress] = {}; - } - this._proxyAllowance[tokenAddress][userAddress] = proxyAllowance; - } -} - export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore { /** * Simulates transferFrom call performed by a proxy @@ -110,7 +59,7 @@ export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore private async decreaseProxyAllowanceAsync(tokenAddress: string, userAddress: string, amountInBaseUnits: BigNumber): Promise { const proxyAllowance = await this.getProxyAllowanceAsync(tokenAddress, userAddress); - if (!proxyAllowance.eq(this._token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) { + if (!proxyAllowance.eq(this.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) { this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits)); } } -- cgit v1.2.3 From 742660591f66b1bccd194803a1bf1e43a60a3b31 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 11:15:08 -0500 Subject: Make a store an instance variable of exchange transfer simulator and stop inheriting it --- src/stores/balance_proxy_allowance_lazy_store.ts | 10 ++++----- src/utils/exchange_transfer_simulator.ts | 27 +++++++++++++++--------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/stores/balance_proxy_allowance_lazy_store.ts b/src/stores/balance_proxy_allowance_lazy_store.ts index e8178b9a9..7f392cb82 100644 --- a/src/stores/balance_proxy_allowance_lazy_store.ts +++ b/src/stores/balance_proxy_allowance_lazy_store.ts @@ -6,7 +6,7 @@ import {TokenWrapper} from '../contract_wrappers/token_wrapper'; * Copy on read store for balances/proxyAllowances of tokens/accounts */ export class BalanceAndProxyAllowanceLazyStore { - protected token: TokenWrapper; + private token: TokenWrapper; private balance: { [tokenAddress: string]: { [userAddress: string]: BigNumber, @@ -22,7 +22,7 @@ export class BalanceAndProxyAllowanceLazyStore { this.balance = {}; this.proxyAllowance = {}; } - protected async getBalanceAsync(tokenAddress: string, userAddress: string): Promise { + public async getBalanceAsync(tokenAddress: string, userAddress: string): Promise { if (_.isUndefined(this.balance[tokenAddress]) || _.isUndefined(this.balance[tokenAddress][userAddress])) { const balance = await this.token.getBalanceAsync(tokenAddress, userAddress); this.setBalance(tokenAddress, userAddress, balance); @@ -30,13 +30,13 @@ export class BalanceAndProxyAllowanceLazyStore { const cachedBalance = this.balance[tokenAddress][userAddress]; return cachedBalance; } - protected setBalance(tokenAddress: string, userAddress: string, balance: BigNumber): void { + public setBalance(tokenAddress: string, userAddress: string, balance: BigNumber): void { if (_.isUndefined(this.balance[tokenAddress])) { this.balance[tokenAddress] = {}; } this.balance[tokenAddress][userAddress] = balance; } - protected async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise { + public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise { if (_.isUndefined(this.proxyAllowance[tokenAddress]) || _.isUndefined(this.proxyAllowance[tokenAddress][userAddress])) { const proxyAllowance = await this.token.getProxyAllowanceAsync(tokenAddress, userAddress); @@ -45,7 +45,7 @@ export class BalanceAndProxyAllowanceLazyStore { const cachedProxyAllowance = this.proxyAllowance[tokenAddress][userAddress]; return cachedProxyAllowance; } - protected setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber): void { + public setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber): void { if (_.isUndefined(this.proxyAllowance[tokenAddress])) { this.proxyAllowance[tokenAddress] = {}; } diff --git a/src/utils/exchange_transfer_simulator.ts b/src/utils/exchange_transfer_simulator.ts index 6812759fc..ecdec0be0 100644 --- a/src/utils/exchange_transfer_simulator.ts +++ b/src/utils/exchange_transfer_simulator.ts @@ -1,6 +1,7 @@ import * as _ from 'lodash'; import BigNumber from 'bignumber.js'; import {ExchangeContractErrs, TradeSide, TransferType} from '../types'; +import {TokenWrapper} from '../contract_wrappers/token_wrapper'; import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store'; enum FailureReason { @@ -31,7 +32,13 @@ const ERR_MSG_MAPPING = { }, }; -export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore { +export class ExchangeTransferSimulator { + private store: BalanceAndProxyAllowanceLazyStore; + private UNLIMITED_ALLOWANCE_IN_BASE_UNITS: BigNumber; + constructor(token: TokenWrapper) { + this.store = new BalanceAndProxyAllowanceLazyStore(token); + this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS = token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; + } /** * Simulates transferFrom call performed by a proxy * @param tokenAddress Address of the token to be transferred @@ -44,8 +51,8 @@ export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore public async transferFromAsync(tokenAddress: string, from: string, to: string, amountInBaseUnits: BigNumber, tradeSide: TradeSide, transferType: TransferType): Promise { - const balance = await this.getBalanceAsync(tokenAddress, from); - const proxyAllowance = await this.getProxyAllowanceAsync(tokenAddress, from); + const balance = await this.store.getBalanceAsync(tokenAddress, from); + const proxyAllowance = await this.store.getProxyAllowanceAsync(tokenAddress, from); if (proxyAllowance.lessThan(amountInBaseUnits)) { this.throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType); } @@ -58,20 +65,20 @@ export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore } private async decreaseProxyAllowanceAsync(tokenAddress: string, userAddress: string, amountInBaseUnits: BigNumber): Promise { - const proxyAllowance = await this.getProxyAllowanceAsync(tokenAddress, userAddress); - if (!proxyAllowance.eq(this.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) { - this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits)); + const proxyAllowance = await this.store.getProxyAllowanceAsync(tokenAddress, userAddress); + if (!proxyAllowance.eq(this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) { + this.store.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits)); } } private async increaseBalanceAsync(tokenAddress: string, userAddress: string, amountInBaseUnits: BigNumber): Promise { - const balance = await this.getBalanceAsync(tokenAddress, userAddress); - this.setBalance(tokenAddress, userAddress, balance.plus(amountInBaseUnits)); + const balance = await this.store.getBalanceAsync(tokenAddress, userAddress); + this.store.setBalance(tokenAddress, userAddress, balance.plus(amountInBaseUnits)); } private async decreaseBalanceAsync(tokenAddress: string, userAddress: string, amountInBaseUnits: BigNumber): Promise { - const balance = await this.getBalanceAsync(tokenAddress, userAddress); - this.setBalance(tokenAddress, userAddress, balance.minus(amountInBaseUnits)); + const balance = await this.store.getBalanceAsync(tokenAddress, userAddress); + this.store.setBalance(tokenAddress, userAddress, balance.minus(amountInBaseUnits)); } private throwValidationError(failureReason: FailureReason, tradeSide: TradeSide, transferType: TransferType): Promise { -- cgit v1.2.3 From f163e6d8cc53dbbfd65168c977aa53c5ab9b3c08 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 11:22:05 -0500 Subject: Fix tests --- test/exchange_transfer_simulator_test.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/test/exchange_transfer_simulator_test.ts b/test/exchange_transfer_simulator_test.ts index 3373ebf03..99cb7fb4f 100644 --- a/test/exchange_transfer_simulator_test.ts +++ b/test/exchange_transfer_simulator_test.ts @@ -59,11 +59,10 @@ describe('ExchangeTransferSimulator', () => { await exchangeTransferSimulator.transferFromAsync( exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Taker, TransferType.Trade, ); - const senderBalance = await (exchangeTransferSimulator as any).getBalanceAsync(exampleTokenAddress, sender); - const recipientBalance = await (exchangeTransferSimulator as any).getBalanceAsync( - exampleTokenAddress, recipient); - const senderProxyAllowance = await (exchangeTransferSimulator as any).getProxyAllowanceAsync( - exampleTokenAddress, sender); + const store = (exchangeTransferSimulator as any).store; + const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender); + const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient); + const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender); expect(senderBalance).to.be.bignumber.equal(0); expect(recipientBalance).to.be.bignumber.equal(transferAmount); expect(senderProxyAllowance).to.be.bignumber.equal(0); @@ -76,11 +75,10 @@ describe('ExchangeTransferSimulator', () => { await exchangeTransferSimulator.transferFromAsync( exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Taker, TransferType.Trade, ); - const senderBalance = await (exchangeTransferSimulator as any).getBalanceAsync(exampleTokenAddress, sender); - const recipientBalance = await (exchangeTransferSimulator as any).getBalanceAsync( - exampleTokenAddress, recipient); - const senderProxyAllowance = await (exchangeTransferSimulator as any).getProxyAllowanceAsync( - exampleTokenAddress, sender); + const store = (exchangeTransferSimulator as any).store; + const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender); + const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient); + const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender); expect(senderBalance).to.be.bignumber.equal(0); expect(recipientBalance).to.be.bignumber.equal(transferAmount); expect(senderProxyAllowance).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS); -- cgit v1.2.3 From 6edae865168dc86a5a3b39558158d22a4ff3bd99 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 12:12:30 -0500 Subject: Make store configurable by blockParam --- src/stores/balance_proxy_allowance_lazy_store.ts | 15 ++++++++++++--- src/utils/exchange_transfer_simulator.ts | 4 ++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/stores/balance_proxy_allowance_lazy_store.ts b/src/stores/balance_proxy_allowance_lazy_store.ts index 7f392cb82..2580fd449 100644 --- a/src/stores/balance_proxy_allowance_lazy_store.ts +++ b/src/stores/balance_proxy_allowance_lazy_store.ts @@ -1,4 +1,5 @@ import * as _ from 'lodash'; +import * as Web3 from 'web3'; import {BigNumber} from 'bignumber.js'; import {TokenWrapper} from '../contract_wrappers/token_wrapper'; @@ -7,6 +8,7 @@ import {TokenWrapper} from '../contract_wrappers/token_wrapper'; */ export class BalanceAndProxyAllowanceLazyStore { private token: TokenWrapper; + private defaultBlock: Web3.BlockParam; private balance: { [tokenAddress: string]: { [userAddress: string]: BigNumber, @@ -17,14 +19,18 @@ export class BalanceAndProxyAllowanceLazyStore { [userAddress: string]: BigNumber, }, }; - constructor(token: TokenWrapper) { + constructor(token: TokenWrapper, defaultBlock: Web3.BlockParam) { this.token = token; + this.defaultBlock = defaultBlock; this.balance = {}; this.proxyAllowance = {}; } public async getBalanceAsync(tokenAddress: string, userAddress: string): Promise { if (_.isUndefined(this.balance[tokenAddress]) || _.isUndefined(this.balance[tokenAddress][userAddress])) { - const balance = await this.token.getBalanceAsync(tokenAddress, userAddress); + const methodOpts = { + defaultBlock: this.defaultBlock, + }; + const balance = await this.token.getBalanceAsync(tokenAddress, userAddress, methodOpts); this.setBalance(tokenAddress, userAddress, balance); } const cachedBalance = this.balance[tokenAddress][userAddress]; @@ -39,7 +45,10 @@ export class BalanceAndProxyAllowanceLazyStore { public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise { if (_.isUndefined(this.proxyAllowance[tokenAddress]) || _.isUndefined(this.proxyAllowance[tokenAddress][userAddress])) { - const proxyAllowance = await this.token.getProxyAllowanceAsync(tokenAddress, userAddress); + const methodOpts = { + defaultBlock: this.defaultBlock, + }; + const proxyAllowance = await this.token.getProxyAllowanceAsync(tokenAddress, userAddress, methodOpts); this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance); } const cachedProxyAllowance = this.proxyAllowance[tokenAddress][userAddress]; diff --git a/src/utils/exchange_transfer_simulator.ts b/src/utils/exchange_transfer_simulator.ts index ecdec0be0..56bd48f17 100644 --- a/src/utils/exchange_transfer_simulator.ts +++ b/src/utils/exchange_transfer_simulator.ts @@ -1,6 +1,6 @@ import * as _ from 'lodash'; import BigNumber from 'bignumber.js'; -import {ExchangeContractErrs, TradeSide, TransferType} from '../types'; +import {ExchangeContractErrs, TradeSide, TransferType, BlockParamLiteral} from '../types'; import {TokenWrapper} from '../contract_wrappers/token_wrapper'; import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store'; @@ -36,7 +36,7 @@ export class ExchangeTransferSimulator { private store: BalanceAndProxyAllowanceLazyStore; private UNLIMITED_ALLOWANCE_IN_BASE_UNITS: BigNumber; constructor(token: TokenWrapper) { - this.store = new BalanceAndProxyAllowanceLazyStore(token); + this.store = new BalanceAndProxyAllowanceLazyStore(token, BlockParamLiteral.Latest); this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS = token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; } /** -- cgit v1.2.3 From dcda8fe538a6db82bed4f036ef63bad5882e681a Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 12:12:47 -0500 Subject: Add store for order filled/cancelled state --- src/stores/order_filled_cancelled_lazy_store.ts | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/stores/order_filled_cancelled_lazy_store.ts diff --git a/src/stores/order_filled_cancelled_lazy_store.ts b/src/stores/order_filled_cancelled_lazy_store.ts new file mode 100644 index 000000000..beb8fe6d7 --- /dev/null +++ b/src/stores/order_filled_cancelled_lazy_store.ts @@ -0,0 +1,52 @@ +import * as _ from 'lodash'; +import * as Web3 from 'web3'; +import {BigNumber} from 'bignumber.js'; +import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper'; + +/** + * Copy on read store for filled/cancelled taker amounts + */ +export class OrderFilledCancelledLazyStore { + private exchange: ExchangeWrapper; + private defaultBlock: Web3.BlockParam; + private filledTakerAmount: { + [orderHash: string]: BigNumber, + }; + private cancelledTakerAmount: { + [orderHash: string]: BigNumber, + }; + constructor(exchange: ExchangeWrapper, defaultBlock: Web3.BlockParam) { + this.exchange = exchange; + this.defaultBlock = defaultBlock; + this.filledTakerAmount = {}; + this.cancelledTakerAmount = {}; + } + public async getFilledTakerAmountAsync(orderHash: string): Promise { + if (_.isUndefined(this.filledTakerAmount[orderHash])) { + const methodOpts = { + defaultBlock: this.defaultBlock, + }; + const filledTakerAmount = await this.exchange.getFilledTakerAmountAsync(orderHash, methodOpts); + this.setFilledTakerAmount(orderHash, filledTakerAmount); + } + const cachedFilled = this.filledTakerAmount[orderHash]; + return cachedFilled; + } + public setFilledTakerAmount(orderHash: string, filledTakerAmount: BigNumber): void { + this.filledTakerAmount[orderHash] = filledTakerAmount; + } + public async getCancelledTakerAmountAsync(orderHash: string): Promise { + if (_.isUndefined(this.cancelledTakerAmount[orderHash])) { + const methodOpts = { + defaultBlock: this.defaultBlock, + }; + const cancelledTakerAmount = await this.exchange.getCanceledTakerAmountAsync(orderHash, methodOpts); + this.setCancelledTakerAmount(orderHash, cancelledTakerAmount); + } + const cachedCancelled = this.cancelledTakerAmount[orderHash]; + return cachedCancelled; + } + public setCancelledTakerAmount(orderHash: string, cancelledTakerAmount: BigNumber): void { + this.cancelledTakerAmount[orderHash] = cancelledTakerAmount; + } +} -- cgit v1.2.3 From 75b390cf93e0903b1859f02a3f3508bc16a4565a Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 12:22:19 -0500 Subject: Add functions to clear stores cache --- src/stores/balance_proxy_allowance_lazy_store.ts | 10 ++++++++++ src/stores/order_filled_cancelled_lazy_store.ts | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/src/stores/balance_proxy_allowance_lazy_store.ts b/src/stores/balance_proxy_allowance_lazy_store.ts index 2580fd449..92a6aaa51 100644 --- a/src/stores/balance_proxy_allowance_lazy_store.ts +++ b/src/stores/balance_proxy_allowance_lazy_store.ts @@ -42,6 +42,11 @@ export class BalanceAndProxyAllowanceLazyStore { } this.balance[tokenAddress][userAddress] = balance; } + public deleteBalance(tokenAddress: string, userAddress: string): void { + if (!_.isUndefined(this.balance[tokenAddress])) { + delete this.balance[tokenAddress][userAddress]; + } + } public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise { if (_.isUndefined(this.proxyAllowance[tokenAddress]) || _.isUndefined(this.proxyAllowance[tokenAddress][userAddress])) { @@ -60,4 +65,9 @@ export class BalanceAndProxyAllowanceLazyStore { } this.proxyAllowance[tokenAddress][userAddress] = proxyAllowance; } + public deleteProxyAllowance(tokenAddress: string, userAddress: string): void { + if (!_.isUndefined(this.proxyAllowance[tokenAddress])) { + delete this.proxyAllowance[tokenAddress][userAddress]; + } + } } diff --git a/src/stores/order_filled_cancelled_lazy_store.ts b/src/stores/order_filled_cancelled_lazy_store.ts index beb8fe6d7..cb4c786f3 100644 --- a/src/stores/order_filled_cancelled_lazy_store.ts +++ b/src/stores/order_filled_cancelled_lazy_store.ts @@ -35,6 +35,9 @@ export class OrderFilledCancelledLazyStore { public setFilledTakerAmount(orderHash: string, filledTakerAmount: BigNumber): void { this.filledTakerAmount[orderHash] = filledTakerAmount; } + public deleteFilledTakerAmount(orderHash: string): void { + delete this.filledTakerAmount[orderHash]; + } public async getCancelledTakerAmountAsync(orderHash: string): Promise { if (_.isUndefined(this.cancelledTakerAmount[orderHash])) { const methodOpts = { @@ -49,4 +52,7 @@ export class OrderFilledCancelledLazyStore { public setCancelledTakerAmount(orderHash: string, cancelledTakerAmount: BigNumber): void { this.cancelledTakerAmount[orderHash] = cancelledTakerAmount; } + public deleteCancelledTakerAmount(orderHash: string): void { + delete this.cancelledTakerAmount[orderHash]; + } } -- cgit v1.2.3 From 4921f61e76d2e6f1b2c8718766a7e93e11ad4671 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 15:03:04 -0500 Subject: Add LatestBlockNumberNotSet internal error --- src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types.ts b/src/types.ts index a366fc31e..8aaa666c3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,6 +23,7 @@ export enum ZeroExError { export enum InternalZeroExError { NoAbiDecoder = 'NO_ABI_DECODER', ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY', + LatestBlockNumberNotSet = 'LATEST_BLOCK_NUMBER_NOT_SET', } /** -- cgit v1.2.3 From 70436fa535f51eca5c1b5951e1218e72f9a767e0 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 15:03:53 -0500 Subject: Make stores accept numConfirmations and blockStore instead of defaultBlock --- src/stores/balance_proxy_allowance_lazy_store.ts | 15 ++++++++++----- src/stores/order_filled_cancelled_lazy_store.ts | 15 ++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/stores/balance_proxy_allowance_lazy_store.ts b/src/stores/balance_proxy_allowance_lazy_store.ts index 92a6aaa51..a2b8fcf67 100644 --- a/src/stores/balance_proxy_allowance_lazy_store.ts +++ b/src/stores/balance_proxy_allowance_lazy_store.ts @@ -2,13 +2,15 @@ import * as _ from 'lodash'; import * as Web3 from 'web3'; import {BigNumber} from 'bignumber.js'; import {TokenWrapper} from '../contract_wrappers/token_wrapper'; +import {BlockStore} from './block_store'; /** * Copy on read store for balances/proxyAllowances of tokens/accounts */ export class BalanceAndProxyAllowanceLazyStore { private token: TokenWrapper; - private defaultBlock: Web3.BlockParam; + private numConfirmations: number; + private blockStore: BlockStore; private balance: { [tokenAddress: string]: { [userAddress: string]: BigNumber, @@ -19,16 +21,18 @@ export class BalanceAndProxyAllowanceLazyStore { [userAddress: string]: BigNumber, }, }; - constructor(token: TokenWrapper, defaultBlock: Web3.BlockParam) { + constructor(token: TokenWrapper, blockStore: BlockStore, numConfirmations: number) { this.token = token; - this.defaultBlock = defaultBlock; + this.numConfirmations = numConfirmations; + this.blockStore = blockStore; this.balance = {}; this.proxyAllowance = {}; } public async getBalanceAsync(tokenAddress: string, userAddress: string): Promise { if (_.isUndefined(this.balance[tokenAddress]) || _.isUndefined(this.balance[tokenAddress][userAddress])) { + const defaultBlock = this.blockStore.getBlockNumberWithNConfirmations(this.numConfirmations); const methodOpts = { - defaultBlock: this.defaultBlock, + defaultBlock, }; const balance = await this.token.getBalanceAsync(tokenAddress, userAddress, methodOpts); this.setBalance(tokenAddress, userAddress, balance); @@ -50,8 +54,9 @@ export class BalanceAndProxyAllowanceLazyStore { public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise { if (_.isUndefined(this.proxyAllowance[tokenAddress]) || _.isUndefined(this.proxyAllowance[tokenAddress][userAddress])) { + const defaultBlock = this.blockStore.getBlockNumberWithNConfirmations(this.numConfirmations); const methodOpts = { - defaultBlock: this.defaultBlock, + defaultBlock, }; const proxyAllowance = await this.token.getProxyAllowanceAsync(tokenAddress, userAddress, methodOpts); this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance); diff --git a/src/stores/order_filled_cancelled_lazy_store.ts b/src/stores/order_filled_cancelled_lazy_store.ts index cb4c786f3..978de0b18 100644 --- a/src/stores/order_filled_cancelled_lazy_store.ts +++ b/src/stores/order_filled_cancelled_lazy_store.ts @@ -2,29 +2,33 @@ import * as _ from 'lodash'; import * as Web3 from 'web3'; import {BigNumber} from 'bignumber.js'; import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper'; +import {BlockStore} from './block_store'; /** * Copy on read store for filled/cancelled taker amounts */ export class OrderFilledCancelledLazyStore { private exchange: ExchangeWrapper; - private defaultBlock: Web3.BlockParam; + private numConfirmations: number; + private blockStore: BlockStore; private filledTakerAmount: { [orderHash: string]: BigNumber, }; private cancelledTakerAmount: { [orderHash: string]: BigNumber, }; - constructor(exchange: ExchangeWrapper, defaultBlock: Web3.BlockParam) { + constructor(exchange: ExchangeWrapper, blockStore: BlockStore, numConfirmations: number) { this.exchange = exchange; - this.defaultBlock = defaultBlock; + this.numConfirmations = numConfirmations; + this.blockStore = blockStore; this.filledTakerAmount = {}; this.cancelledTakerAmount = {}; } public async getFilledTakerAmountAsync(orderHash: string): Promise { if (_.isUndefined(this.filledTakerAmount[orderHash])) { + const defaultBlock = this.blockStore.getBlockNumberWithNConfirmations(this.numConfirmations); const methodOpts = { - defaultBlock: this.defaultBlock, + defaultBlock, }; const filledTakerAmount = await this.exchange.getFilledTakerAmountAsync(orderHash, methodOpts); this.setFilledTakerAmount(orderHash, filledTakerAmount); @@ -40,8 +44,9 @@ export class OrderFilledCancelledLazyStore { } public async getCancelledTakerAmountAsync(orderHash: string): Promise { if (_.isUndefined(this.cancelledTakerAmount[orderHash])) { + const defaultBlock = this.blockStore.getBlockNumberWithNConfirmations(this.numConfirmations); const methodOpts = { - defaultBlock: this.defaultBlock, + defaultBlock, }; const cancelledTakerAmount = await this.exchange.getCanceledTakerAmountAsync(orderHash, methodOpts); this.setCancelledTakerAmount(orderHash, cancelledTakerAmount); -- cgit v1.2.3 From 473ce8b61750103843173518bac599186405f7b3 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 15:04:14 -0500 Subject: Add initial incomplete BlockStore implementation --- src/stores/block_store.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/stores/block_store.ts diff --git a/src/stores/block_store.ts b/src/stores/block_store.ts new file mode 100644 index 000000000..d1b4d3c54 --- /dev/null +++ b/src/stores/block_store.ts @@ -0,0 +1,38 @@ +import * as _ from 'lodash'; +import * as Web3 from 'web3'; +import {BigNumber} from 'bignumber.js'; +import {BlockParamLiteral, InternalZeroExError} from '../types'; +import {Web3Wrapper} from '../web3_wrapper'; + +/** + * Store for a current latest block number + */ +export class BlockStore { + private web3Wrapper?: Web3Wrapper; + private latestBlockNumber?: number; + constructor(web3Wrapper?: Web3Wrapper) { + this.web3Wrapper = web3Wrapper; + // TODO start a subscription + } + public getBlockNumberWithNConfirmations(numConfirmations: number): Web3.BlockParam { + let blockNumber; + if (numConfirmations === 0) { + blockNumber = BlockParamLiteral.Pending; + } else if (numConfirmations === 1) { + // HACK: We special-case `numConfirmations === 1` so that we can use this block store without actually + // setting `latestBlockNumber` when block number is latest (in order validation) whhich allows us to omit + // an async call in a constructor of `ExchangeTransferSimulator` + blockNumber = BlockParamLiteral.Latest; + } else { + if (_.isUndefined(this.latestBlockNumber)) { + throw new Error(InternalZeroExError.LatestBlockNumberNotSet); + } + // Latest block already has 1 confirmation + blockNumber = this.latestBlockNumber - numConfirmations + 1; + } + return blockNumber; + } + public setLatestBlock(latestBlockNumber: number): void { + this.latestBlockNumber = latestBlockNumber; + } +} -- cgit v1.2.3 From ffcc4877638262b29c8a88535e9535da07428396 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 15:05:24 -0500 Subject: Create fake blockStore for exchange transfer simulator --- src/utils/exchange_transfer_simulator.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/exchange_transfer_simulator.ts b/src/utils/exchange_transfer_simulator.ts index 56bd48f17..cd46397ed 100644 --- a/src/utils/exchange_transfer_simulator.ts +++ b/src/utils/exchange_transfer_simulator.ts @@ -3,6 +3,7 @@ import BigNumber from 'bignumber.js'; import {ExchangeContractErrs, TradeSide, TransferType, BlockParamLiteral} from '../types'; import {TokenWrapper} from '../contract_wrappers/token_wrapper'; import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store'; +import {BlockStore} from '../stores/block_store'; enum FailureReason { Balance = 'balance', @@ -36,7 +37,9 @@ export class ExchangeTransferSimulator { private store: BalanceAndProxyAllowanceLazyStore; private UNLIMITED_ALLOWANCE_IN_BASE_UNITS: BigNumber; constructor(token: TokenWrapper) { - this.store = new BalanceAndProxyAllowanceLazyStore(token, BlockParamLiteral.Latest); + const blockStore = new BlockStore(); + const latestBlockConfirmationNumber = 1; + this.store = new BalanceAndProxyAllowanceLazyStore(token, blockStore, latestBlockConfirmationNumber); this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS = token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; } /** -- cgit v1.2.3 From e72ba39c41a86cd17a3115426ad579efbfe11ddc Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 15:07:05 -0500 Subject: Make orderStateUtils operate on stores --- src/order_watcher/order_state_watcher.ts | 9 +----- src/utils/order_state_utils.ts | 53 +++++++++++++++++++------------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 4866f8409..ec7e69b72 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -164,16 +164,9 @@ export class OrderStateWatcher { } } private async _emitRevalidateOrdersAsync(orderHashes: string[], blockNumber: number): Promise { - const defaultBlock = this._numConfirmations === 0 ? - BlockParamLiteral.Pending : - blockNumber; - const methodOpts = { - defaultBlock, - }; - for (const orderHash of orderHashes) { const signedOrder = this._orderByOrderHash[orderHash] as SignedOrder; - const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder, methodOpts); + const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder); if (!_.isUndefined(this._callbackIfExistsAsync)) { await this._callbackIfExistsAsync(orderState); } else { diff --git a/src/utils/order_state_utils.ts b/src/utils/order_state_utils.ts index 36a4b68d6..f82601cae 100644 --- a/src/utils/order_state_utils.ts +++ b/src/utils/order_state_utils.ts @@ -1,4 +1,5 @@ import * as _ from 'lodash'; +import * as Web3 from 'web3'; import BigNumber from 'bignumber.js'; import { ExchangeContractErrs, @@ -14,16 +15,19 @@ import {TokenWrapper} from '../contract_wrappers/token_wrapper'; import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper'; import {utils} from '../utils/utils'; import {constants} from '../utils/constants'; +import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store'; +import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store'; export class OrderStateUtils { - private tokenWrapper: TokenWrapper; - private exchangeWrapper: ExchangeWrapper; - constructor(tokenWrapper: TokenWrapper, exchangeWrapper: ExchangeWrapper) { - this.tokenWrapper = tokenWrapper; - this.exchangeWrapper = exchangeWrapper; + private balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; + private orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; + constructor(balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore, + orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore) { + this.balanceAndProxyAllowanceLazyStore = balanceAndProxyAllowanceLazyStore; + this.orderFilledCancelledLazyStore = orderFilledCancelledLazyStore; } - public async getOrderStateAsync(signedOrder: SignedOrder, methodOpts?: MethodOpts): Promise { - const orderRelevantState = await this.getOrderRelevantStateAsync(signedOrder, methodOpts); + public async getOrderStateAsync(signedOrder: SignedOrder): Promise { + const orderRelevantState = await this.getOrderRelevantStateAsync(signedOrder); const orderHash = ZeroEx.getOrderHashHex(signedOrder); try { this.validateIfOrderIsValid(signedOrder, orderRelevantState); @@ -42,26 +46,31 @@ export class OrderStateUtils { return orderState; } } - public async getOrderRelevantStateAsync( - signedOrder: SignedOrder, methodOpts?: MethodOpts): Promise { - const zrxTokenAddress = await this.exchangeWrapper.getZRXTokenAddressAsync(); + public async getOrderRelevantStateAsync(signedOrder: SignedOrder): Promise { + // HACK: We access the private property here but otherwise the interface will be less nice. + // If we pass it from the instantiator - there is no opportunity to get it there + // because JS doesn't support async constructors. + // Moreover - it's cached under the hood so it's equivalent to an async constructor. + const exchange = (this.orderFilledCancelledLazyStore as any).exchange as ExchangeWrapper; + const zrxTokenAddress = await exchange.getZRXTokenAddressAsync(); const orderHash = ZeroEx.getOrderHashHex(signedOrder); - const makerBalance = await this.tokenWrapper.getBalanceAsync( - signedOrder.makerTokenAddress, signedOrder.maker, methodOpts, + const makerBalance = await this.balanceAndProxyAllowanceLazyStore.getBalanceAsync( + signedOrder.makerTokenAddress, signedOrder.maker, ); - const makerProxyAllowance = await this.tokenWrapper.getProxyAllowanceAsync( - signedOrder.makerTokenAddress, signedOrder.maker, methodOpts, + const makerProxyAllowance = await this.balanceAndProxyAllowanceLazyStore.getProxyAllowanceAsync( + signedOrder.makerTokenAddress, signedOrder.maker, ); - const makerFeeBalance = await this.tokenWrapper.getBalanceAsync( - zrxTokenAddress, signedOrder.maker, methodOpts, + const makerFeeBalance = await this.balanceAndProxyAllowanceLazyStore.getBalanceAsync( + zrxTokenAddress, signedOrder.maker, ); - const makerFeeProxyAllowance = await this.tokenWrapper.getProxyAllowanceAsync( - zrxTokenAddress, signedOrder.maker, methodOpts, + const makerFeeProxyAllowance = await this.balanceAndProxyAllowanceLazyStore.getProxyAllowanceAsync( + zrxTokenAddress, signedOrder.maker, ); - const filledTakerTokenAmount = await this.exchangeWrapper.getFilledTakerAmountAsync(orderHash, methodOpts); - const canceledTakerTokenAmount = await this.exchangeWrapper.getCanceledTakerAmountAsync(orderHash, methodOpts); - const unavailableTakerTokenAmount = - await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash, methodOpts); + const filledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getFilledTakerAmountAsync(orderHash); + const canceledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getCancelledTakerAmountAsync( + orderHash, + ); + const unavailableTakerTokenAmount = await exchange.getUnavailableTakerAmountAsync(orderHash); const totalMakerTokenAmount = signedOrder.makerTokenAmount; const totalTakerTokenAmount = signedOrder.takerTokenAmount; const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount); -- cgit v1.2.3 From 9d3fe1258a53d9e2169b3d733f64d3931fefcce0 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 15:09:22 -0500 Subject: Create stores in orderStateWatcher --- src/0x.ts | 3 +-- src/order_watcher/order_state_watcher.ts | 38 +++++++++++++++++++------------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/0x.ts b/src/0x.ts index a1841eaa8..fe765bbbe 100644 --- a/src/0x.ts +++ b/src/0x.ts @@ -205,9 +205,8 @@ export class ZeroEx { const etherTokenContractAddressIfExists = _.isUndefined(config) ? undefined : config.etherTokenContractAddress; this.etherToken = new EtherTokenWrapper(this._web3Wrapper, this.token, etherTokenContractAddressIfExists); const orderWatcherConfig = _.isUndefined(config) ? undefined : config.orderWatcherConfig; - const orderStateUtils = new OrderStateUtils(this.token, this.exchange); this.orderStateWatcher = new OrderStateWatcher( - this._web3Wrapper, this._abiDecoder, orderStateUtils, orderWatcherConfig, + this._web3Wrapper, this._abiDecoder, this.token, this.exchange, orderWatcherConfig, ); } /** diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index ec7e69b72..8e0b46eb0 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -1,6 +1,5 @@ import * as _ from 'lodash'; import {schemas} from '0x-json-schemas'; -import * as ethUtil from 'ethereumjs-util'; import {ZeroEx} from '../0x'; import {EventWatcher} from './event_watcher'; import {assert} from '../utils/assert'; @@ -22,6 +21,11 @@ import { ZeroExError, } from '../types'; import {Web3Wrapper} from '../web3_wrapper'; +import {BlockStore} from '../stores/block_store'; +import {TokenWrapper} from '../contract_wrappers/token_wrapper'; +import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper'; +import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store'; +import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store'; const DEFAULT_NUM_CONFIRMATIONS = 0; @@ -44,26 +48,35 @@ interface OrderByOrderHash { export class OrderStateWatcher { private _orderByOrderHash: OrderByOrderHash = {}; private _dependentOrderHashes: DependentOrderHashes = {}; - private _web3Wrapper: Web3Wrapper; private _callbackIfExistsAsync?: OnOrderStateChangeCallback; private _eventWatcher: EventWatcher; private _abiDecoder: AbiDecoder; private _orderStateUtils: OrderStateUtils; private _numConfirmations: number; + private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; + private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; constructor( - web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, orderStateUtils: OrderStateUtils, + web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, token: TokenWrapper, exchange: ExchangeWrapper, config?: OrderStateWatcherConfig, ) { - this._web3Wrapper = web3Wrapper; const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.pollingIntervalMs; this._numConfirmations = _.isUndefined(config) ? DEFAULT_NUM_CONFIRMATIONS : config.numConfirmations; this._eventWatcher = new EventWatcher( - this._web3Wrapper, eventPollingIntervalMs, this._numConfirmations, + web3Wrapper, eventPollingIntervalMs, this._numConfirmations, ); this._abiDecoder = abiDecoder; - this._orderStateUtils = orderStateUtils; + const blockStore = new BlockStore(web3Wrapper); + this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( + token, blockStore, this._numConfirmations, + ); + this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore( + exchange, blockStore, this._numConfirmations, + ); + this._orderStateUtils = new OrderStateUtils( + this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore, + ); } /** * Add an order to the orderStateWatcher. Before the order is added, it's @@ -117,11 +130,6 @@ export class OrderStateWatcher { if (!isLogDecoded) { return; // noop } - // Unfortunately blockNumber is returned as a hex-encoded string, so we - // convert it to a number here. - const blockNumberBuff = ethUtil.toBuffer(maybeDecodedLog.blockNumber); - const blockNumber = ethUtil.bufferToInt(blockNumberBuff); - const decodedLog = maybeDecodedLog as LogWithDecodedArgs; let makerToken: string; let makerAddress: string; @@ -133,7 +141,7 @@ export class OrderStateWatcher { orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]); if (!_.isUndefined(orderHashesSet)) { const orderHashes = Array.from(orderHashesSet); - await this._emitRevalidateOrdersAsync(orderHashes, blockNumber); + await this._emitRevalidateOrdersAsync(orderHashes); } break; @@ -143,7 +151,7 @@ export class OrderStateWatcher { orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]); if (!_.isUndefined(orderHashesSet)) { const orderHashes = Array.from(orderHashesSet); - await this._emitRevalidateOrdersAsync(orderHashes, blockNumber); + await this._emitRevalidateOrdersAsync(orderHashes); } break; @@ -152,7 +160,7 @@ export class OrderStateWatcher { const orderHash = decodedLog.args.orderHash; const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); if (isOrderWatched) { - await this._emitRevalidateOrdersAsync([orderHash], blockNumber); + await this._emitRevalidateOrdersAsync([orderHash]); } break; @@ -163,7 +171,7 @@ export class OrderStateWatcher { throw utils.spawnSwitchErr('decodedLog.event', decodedLog.event); } } - private async _emitRevalidateOrdersAsync(orderHashes: string[], blockNumber: number): Promise { + private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise { for (const orderHash of orderHashes) { const signedOrder = this._orderByOrderHash[orderHash] as SignedOrder; const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder); -- cgit v1.2.3 From 44c15fc1ef955a4e188708590eb45eebfea60f12 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 16:31:35 -0500 Subject: Add more errors --- src/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types.ts b/src/types.ts index 8aaa666c3..10faa5cf3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,12 +18,14 @@ export enum ZeroExError { SubscriptionNotFound = 'SUBSCRIPTION_NOT_FOUND', SubscriptionAlreadyPresent = 'SUBSCRIPTION_ALREADY_PRESENT', TransactionMiningTimeout = 'TRANSACTION_MINING_TIMEOUT', + FailedToFetchLatestBlock = 'FAILED_TO_FETCH_LATEST_BLOCK', } export enum InternalZeroExError { NoAbiDecoder = 'NO_ABI_DECODER', ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY', LatestBlockNumberNotSet = 'LATEST_BLOCK_NUMBER_NOT_SET', + Web3WrapperRequiredToStartBlockStore = 'WEB3_WRAPPER_REQUIRED_TO_START_BLOCK_STORE', } /** -- cgit v1.2.3 From 61e7b735dcae37195e4b306cab3e4c50cd9c3ba5 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 16:31:48 -0500 Subject: Adjust tests to new interface --- test/order_state_watcher_test.ts | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 41f938584..880ef0fc3 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -15,6 +15,7 @@ import { ZeroExConfig, OrderState, SignedOrder, + ZeroExError, OrderStateValid, OrderStateInvalid, ExchangeContractErrs, @@ -92,14 +93,10 @@ describe('OrderStateWatcher', () => { afterEach(async () => { zeroEx.orderStateWatcher.unsubscribe(); }); - it('should fail when trying to subscribe twice', (done: DoneCallback) => { - zeroEx.orderStateWatcher.subscribe(_.noop); - try { - zeroEx.orderStateWatcher.subscribe(_.noop); - done(new Error('Expected the second subscription to fail')); - } catch (err) { - done(); - } + it('should fail when trying to subscribe twice', async () => { + await zeroEx.orderStateWatcher.subscribeAsync(_.noop); + return expect(zeroEx.orderStateWatcher.subscribeAsync(_.noop)) + .to.be.rejectedWith(ZeroExError.SubscriptionAlreadyPresent); }); }); describe('tests with cleanup', async () => { @@ -122,7 +119,7 @@ describe('OrderStateWatcher', () => { expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerAllowance); done(); }); - zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.orderStateWatcher.subscribeAsync(callback); await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0)); })().catch(done); }); @@ -161,7 +158,7 @@ describe('OrderStateWatcher', () => { expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerBalance); done(); }); - zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.orderStateWatcher.subscribeAsync(callback); const anyRecipient = taker; const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); await zeroEx.token.transferAsync(makerToken.address, maker, anyRecipient, makerBalance); @@ -186,7 +183,7 @@ describe('OrderStateWatcher', () => { done(); } }); - zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.orderStateWatcher.subscribeAsync(callback); const shouldThrowOnInsufficientBalanceOrAllowance = true; await zeroEx.exchange.fillOrderAsync( @@ -223,7 +220,7 @@ describe('OrderStateWatcher', () => { done(); } }); - zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.orderStateWatcher.subscribeAsync(callback); const shouldThrowOnInsufficientBalanceOrAllowance = true; await zeroEx.exchange.fillOrderAsync( signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker, @@ -324,7 +321,7 @@ describe('OrderStateWatcher', () => { expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderRemainingFillAmountZero); done(); }); - zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.orderStateWatcher.subscribeAsync(callback); const shouldThrowOnInsufficientBalanceOrAllowance = true; await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount); @@ -351,7 +348,7 @@ describe('OrderStateWatcher', () => { expect(orderRelevantState.canceledTakerTokenAmount).to.be.bignumber.equal(cancelAmountInBaseUnits); done(); }); - zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.orderStateWatcher.subscribeAsync(callback); await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmountInBaseUnits); })().catch(done); }); -- cgit v1.2.3 From 6bcd9adb9e41ccc45ef5e117df243526bbd281ec Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 16:32:23 -0500 Subject: Make subscribe function async and make blockStore operational --- src/order_watcher/order_state_watcher.ts | 51 +++++++++++++++++++++----------- src/stores/block_store.ts | 29 +++++++++++++++--- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 8e0b46eb0..affb350a3 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -50,11 +50,15 @@ export class OrderStateWatcher { private _dependentOrderHashes: DependentOrderHashes = {}; private _callbackIfExistsAsync?: OnOrderStateChangeCallback; private _eventWatcher: EventWatcher; + private _web3Wrapper: Web3Wrapper; + private _token: TokenWrapper; + private _exchange: ExchangeWrapper; private _abiDecoder: AbiDecoder; - private _orderStateUtils: OrderStateUtils; + private _orderStateUtils?: OrderStateUtils; + private _blockStore?: BlockStore; private _numConfirmations: number; - private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; - private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; + private _orderFilledCancelledLazyStore?: OrderFilledCancelledLazyStore; + private _balanceAndProxyAllowanceLazyStore?: BalanceAndProxyAllowanceLazyStore; constructor( web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, token: TokenWrapper, exchange: ExchangeWrapper, config?: OrderStateWatcherConfig, @@ -67,16 +71,8 @@ export class OrderStateWatcher { web3Wrapper, eventPollingIntervalMs, this._numConfirmations, ); this._abiDecoder = abiDecoder; - const blockStore = new BlockStore(web3Wrapper); - this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( - token, blockStore, this._numConfirmations, - ); - this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore( - exchange, blockStore, this._numConfirmations, - ); - this._orderStateUtils = new OrderStateUtils( - this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore, - ); + this._token = token; + this._exchange = exchange; } /** * Add an order to the orderStateWatcher. Before the order is added, it's @@ -109,11 +105,22 @@ export class OrderStateWatcher { * @param callback Receives the orderHash of the order that should be re-validated, together * with all the order-relevant blockchain state needed to re-validate the order. */ - public subscribe(callback: OnOrderStateChangeCallback): void { + public async subscribeAsync(callback: OnOrderStateChangeCallback): Promise { assert.isFunction('callback', callback); if (!_.isUndefined(this._callbackIfExistsAsync)) { throw new Error(ZeroExError.SubscriptionAlreadyPresent); } + this._blockStore = new BlockStore(this._web3Wrapper); + await this._blockStore.startAsync(); + this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( + this._token, this._blockStore, this._numConfirmations, + ); + this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore( + this._exchange, this._blockStore, this._numConfirmations, + ); + this._orderStateUtils = new OrderStateUtils( + this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore, + ); this._callbackIfExistsAsync = callback; this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this)); } @@ -121,7 +128,15 @@ export class OrderStateWatcher { * Ends an orderStateWatcher subscription. */ public unsubscribe(): void { + if (_.isUndefined(this._blockStore)) { + throw new Error(ZeroExError.SubscriptionNotFound); + } + this._blockStore.stop(); + delete this._blockStore; delete this._callbackIfExistsAsync; + delete this._balanceAndProxyAllowanceLazyStore; + delete this._orderFilledCancelledLazyStore; + delete this._orderStateUtils; this._eventWatcher.unsubscribe(); } private async _onEventWatcherCallbackAsync(log: LogEvent): Promise { @@ -174,12 +189,14 @@ export class OrderStateWatcher { private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise { for (const orderHash of orderHashes) { const signedOrder = this._orderByOrderHash[orderHash] as SignedOrder; + if (_.isUndefined(this._orderStateUtils)) { + break; // Unsubscribe was called + } const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder); - if (!_.isUndefined(this._callbackIfExistsAsync)) { - await this._callbackIfExistsAsync(orderState); - } else { + if (_.isUndefined(this._callbackIfExistsAsync)) { break; // Unsubscribe was called } + await this._callbackIfExistsAsync(orderState); } } private addToDependentOrderHashes(signedOrder: SignedOrder, orderHash: string) { diff --git a/src/stores/block_store.ts b/src/stores/block_store.ts index d1b4d3c54..70798a999 100644 --- a/src/stores/block_store.ts +++ b/src/stores/block_store.ts @@ -1,8 +1,11 @@ import * as _ from 'lodash'; import * as Web3 from 'web3'; import {BigNumber} from 'bignumber.js'; -import {BlockParamLiteral, InternalZeroExError} from '../types'; +import {BlockParamLiteral, InternalZeroExError, ZeroExError} from '../types'; import {Web3Wrapper} from '../web3_wrapper'; +import {intervalUtils} from '../utils/interval_utils'; + +const POLLING_INTERVAL_MS = 500; /** * Store for a current latest block number @@ -10,9 +13,9 @@ import {Web3Wrapper} from '../web3_wrapper'; export class BlockStore { private web3Wrapper?: Web3Wrapper; private latestBlockNumber?: number; + private intervalId?: NodeJS.Timer; constructor(web3Wrapper?: Web3Wrapper) { this.web3Wrapper = web3Wrapper; - // TODO start a subscription } public getBlockNumberWithNConfirmations(numConfirmations: number): Web3.BlockParam { let blockNumber; @@ -32,7 +35,25 @@ export class BlockStore { } return blockNumber; } - public setLatestBlock(latestBlockNumber: number): void { - this.latestBlockNumber = latestBlockNumber; + public async startAsync(): Promise { + await this.updateLatestBlockAsync(); + this.intervalId = intervalUtils.setAsyncExcludingInterval( + this.updateLatestBlockAsync.bind(this), POLLING_INTERVAL_MS, + ); + } + public stop(): void { + if (!_.isUndefined(this.intervalId)) { + intervalUtils.clearAsyncExcludingInterval(this.intervalId); + } + } + private async updateLatestBlockAsync(): Promise { + if (_.isUndefined(this.web3Wrapper)) { + throw new Error(InternalZeroExError.Web3WrapperRequiredToStartBlockStore); + } + const block = await this.web3Wrapper.getBlockAsync(BlockParamLiteral.Latest); + if (_.isNull(block.number)) { + throw new Error(ZeroExError.FailedToFetchLatestBlock); + } + this.latestBlockNumber = block.number; } } -- cgit v1.2.3 From 81ce4a02290cc38e6dc30873c18b0f9c8c9c4db3 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 17:30:16 -0500 Subject: Add more configs for order watcher --- src/types.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/types.ts b/src/types.ts index 10faa5cf3..555fef855 100644 --- a/src/types.ts +++ b/src/types.ts @@ -400,12 +400,14 @@ export interface JSONRPCPayload { } /* - * pollingIntervalMs: How often to poll the Ethereum node for new events. + * eventPollingIntervalMs: How often to poll the Ethereum node for new events + * blockPollingIntervalMs: How often to poll the Ethereum node for new blocks * numConfirmations: How many confirmed blocks deep you wish to listen for events at. */ export interface OrderStateWatcherConfig { - pollingIntervalMs?: number; - numConfirmations: number; + eventPollingIntervalMs?: number; + blockPollingIntervalMs?: number; + numConfirmations?: number; } /* -- cgit v1.2.3 From 009f81fe4f8dd8ed6b671d309553a6d04edd90ca Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 10 Nov 2017 17:30:55 -0500 Subject: Clear store cache on events --- src/order_watcher/order_state_watcher.ts | 101 ++++++++++++++++++++++--------- src/stores/block_store.ts | 8 ++- 2 files changed, 76 insertions(+), 33 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index affb350a3..c4914fc64 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -14,8 +14,13 @@ import { Web3Provider, BlockParamLiteral, LogWithDecodedArgs, + ContractEventArgs, OnOrderStateChangeCallback, OrderStateWatcherConfig, + ApprovalContractEventArgs, + TransferContractEventArgs, + LogFillContractEventArgs, + LogCancelContractEventArgs, ExchangeEvents, TokenEvents, ZeroExError, @@ -54,25 +59,46 @@ export class OrderStateWatcher { private _token: TokenWrapper; private _exchange: ExchangeWrapper; private _abiDecoder: AbiDecoder; - private _orderStateUtils?: OrderStateUtils; - private _blockStore?: BlockStore; + private _orderStateUtils: OrderStateUtils; + private _blockStore: BlockStore; private _numConfirmations: number; - private _orderFilledCancelledLazyStore?: OrderFilledCancelledLazyStore; - private _balanceAndProxyAllowanceLazyStore?: BalanceAndProxyAllowanceLazyStore; + private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; + private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; constructor( web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, token: TokenWrapper, exchange: ExchangeWrapper, config?: OrderStateWatcherConfig, ) { +<<<<<<< HEAD const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.pollingIntervalMs; this._numConfirmations = _.isUndefined(config) ? DEFAULT_NUM_CONFIRMATIONS : config.numConfirmations; - this._eventWatcher = new EventWatcher( - web3Wrapper, eventPollingIntervalMs, this._numConfirmations, - ); +======= + this._orders = {}; this._abiDecoder = abiDecoder; this._token = token; this._exchange = exchange; + this._web3Wrapper = web3Wrapper; + this._dependentOrderHashes = {}; + const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; + const blockPollingIntervalMs = _.isUndefined(config) ? undefined : config.blockPollingIntervalMs; + this._numConfirmations = (_.isUndefined(config) || _.isUndefined(config.numConfirmations)) ? + DEFAULT_NUM_CONFIRMATIONS : + config.numConfirmations; +>>>>>>> Clear store cache on events + this._eventWatcher = new EventWatcher( + web3Wrapper, eventPollingIntervalMs, this._numConfirmations, + ); + this._blockStore = new BlockStore(this._web3Wrapper, blockPollingIntervalMs); + this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( + this._token, this._blockStore, this._numConfirmations, + ); + this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore( + this._exchange, this._blockStore, this._numConfirmations, + ); + this._orderStateUtils = new OrderStateUtils( + this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore, + ); } /** * Add an order to the orderStateWatcher. Before the order is added, it's @@ -110,17 +136,7 @@ export class OrderStateWatcher { if (!_.isUndefined(this._callbackIfExistsAsync)) { throw new Error(ZeroExError.SubscriptionAlreadyPresent); } - this._blockStore = new BlockStore(this._web3Wrapper); await this._blockStore.startAsync(); - this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( - this._token, this._blockStore, this._numConfirmations, - ); - this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore( - this._exchange, this._blockStore, this._numConfirmations, - ); - this._orderStateUtils = new OrderStateUtils( - this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore, - ); this._callbackIfExistsAsync = callback; this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this)); } @@ -128,15 +144,11 @@ export class OrderStateWatcher { * Ends an orderStateWatcher subscription. */ public unsubscribe(): void { - if (_.isUndefined(this._blockStore)) { + if (_.isUndefined(this._callbackIfExistsAsync)) { throw new Error(ZeroExError.SubscriptionNotFound); } this._blockStore.stop(); - delete this._blockStore; delete this._callbackIfExistsAsync; - delete this._balanceAndProxyAllowanceLazyStore; - delete this._orderFilledCancelledLazyStore; - delete this._orderStateUtils; this._eventWatcher.unsubscribe(); } private async _onEventWatcherCallbackAsync(log: LogEvent): Promise { @@ -145,40 +157,69 @@ export class OrderStateWatcher { if (!isLogDecoded) { return; // noop } - const decodedLog = maybeDecodedLog as LogWithDecodedArgs; + const decodedLog = maybeDecodedLog as LogWithDecodedArgs; let makerToken: string; let makerAddress: string; let orderHashesSet: Set; switch (decodedLog.event) { case TokenEvents.Approval: + { + // Invalidate cache + const args = decodedLog.args as ApprovalContractEventArgs; + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(decodedLog.address, args._owner); makerToken = decodedLog.address; - makerAddress = decodedLog.args._owner; + makerAddress = args._owner; orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]); if (!_.isUndefined(orderHashesSet)) { const orderHashes = Array.from(orderHashesSet); await this._emitRevalidateOrdersAsync(orderHashes); } break; - + } case TokenEvents.Transfer: + { + // Invalidate cache + const args = decodedLog.args as TransferContractEventArgs; + this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._from); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._to); + // Revalidate orders makerToken = decodedLog.address; - makerAddress = decodedLog.args._from; + makerAddress = args._from; orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]); if (!_.isUndefined(orderHashesSet)) { const orderHashes = Array.from(orderHashesSet); await this._emitRevalidateOrdersAsync(orderHashes); } break; - + } case ExchangeEvents.LogFill: + { + // Invalidate cache + const args = decodedLog.args as LogFillContractEventArgs; + this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(args.orderHash); + this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(args.orderHash); + // Revalidate orders + const orderHash = args.orderHash; + const isOrderWatched = !_.isUndefined(this._orders[orderHash]); + if (isOrderWatched) { + await this._emitRevalidateOrdersAsync([orderHash]); + } + break; + } case ExchangeEvents.LogCancel: - const orderHash = decodedLog.args.orderHash; - const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); + { + // Invalidate cache + const args = decodedLog.args as LogCancelContractEventArgs; + this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(args.orderHash); + this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(args.orderHash); + // Revalidate orders + const orderHash = args.orderHash; + const isOrderWatched = !_.isUndefined(this._orders[orderHash]); if (isOrderWatched) { await this._emitRevalidateOrdersAsync([orderHash]); } break; - + } case ExchangeEvents.LogError: return; // noop diff --git a/src/stores/block_store.ts b/src/stores/block_store.ts index 70798a999..d1f6c329a 100644 --- a/src/stores/block_store.ts +++ b/src/stores/block_store.ts @@ -5,7 +5,7 @@ import {BlockParamLiteral, InternalZeroExError, ZeroExError} from '../types'; import {Web3Wrapper} from '../web3_wrapper'; import {intervalUtils} from '../utils/interval_utils'; -const POLLING_INTERVAL_MS = 500; +const DEFAULT_BLOCK_POLLING_INTERVAL_MS = 500; /** * Store for a current latest block number @@ -14,8 +14,10 @@ export class BlockStore { private web3Wrapper?: Web3Wrapper; private latestBlockNumber?: number; private intervalId?: NodeJS.Timer; - constructor(web3Wrapper?: Web3Wrapper) { + private blockPollingIntervalMs: number; + constructor(web3Wrapper?: Web3Wrapper, blockPollingIntervalMs?: number) { this.web3Wrapper = web3Wrapper; + this.blockPollingIntervalMs = blockPollingIntervalMs || DEFAULT_BLOCK_POLLING_INTERVAL_MS; } public getBlockNumberWithNConfirmations(numConfirmations: number): Web3.BlockParam { let blockNumber; @@ -38,7 +40,7 @@ export class BlockStore { public async startAsync(): Promise { await this.updateLatestBlockAsync(); this.intervalId = intervalUtils.setAsyncExcludingInterval( - this.updateLatestBlockAsync.bind(this), POLLING_INTERVAL_MS, + this.updateLatestBlockAsync.bind(this), this.blockPollingIntervalMs, ); } public stop(): void { -- cgit v1.2.3 From 53c918cc785ee3769103acc3e15aa9275c9383d9 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 12:09:14 -0500 Subject: Clear cache on unsubscribe --- src/order_watcher/order_state_watcher.ts | 2 ++ src/stores/balance_proxy_allowance_lazy_store.ts | 4 ++++ src/stores/order_filled_cancelled_lazy_store.ts | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index c4914fc64..a19d6be9c 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -148,6 +148,8 @@ export class OrderStateWatcher { throw new Error(ZeroExError.SubscriptionNotFound); } this._blockStore.stop(); + this._balanceAndProxyAllowanceLazyStore.deleteAll(); + this._orderFilledCancelledLazyStore.deleteAll(); delete this._callbackIfExistsAsync; this._eventWatcher.unsubscribe(); } diff --git a/src/stores/balance_proxy_allowance_lazy_store.ts b/src/stores/balance_proxy_allowance_lazy_store.ts index a2b8fcf67..ac21c0818 100644 --- a/src/stores/balance_proxy_allowance_lazy_store.ts +++ b/src/stores/balance_proxy_allowance_lazy_store.ts @@ -75,4 +75,8 @@ export class BalanceAndProxyAllowanceLazyStore { delete this.proxyAllowance[tokenAddress][userAddress]; } } + public deleteAll(): void { + this.balance = {}; + this.proxyAllowance = {}; + } } diff --git a/src/stores/order_filled_cancelled_lazy_store.ts b/src/stores/order_filled_cancelled_lazy_store.ts index 978de0b18..5b0dab35c 100644 --- a/src/stores/order_filled_cancelled_lazy_store.ts +++ b/src/stores/order_filled_cancelled_lazy_store.ts @@ -60,4 +60,8 @@ export class OrderFilledCancelledLazyStore { public deleteCancelledTakerAmount(orderHash: string): void { delete this.cancelledTakerAmount[orderHash]; } + public deleteAll(): void { + this.filledTakerAmount = {}; + this.cancelledTakerAmount = {}; + } } -- cgit v1.2.3 From bcad937003fa925878d34e5fa83e5a89369d0957 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 12:26:57 -0500 Subject: Fix last merge conflicts --- src/order_watcher/order_state_watcher.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index a19d6be9c..59de1cc2f 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -68,13 +68,7 @@ export class OrderStateWatcher { web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, token: TokenWrapper, exchange: ExchangeWrapper, config?: OrderStateWatcherConfig, ) { -<<<<<<< HEAD - const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.pollingIntervalMs; - this._numConfirmations = _.isUndefined(config) ? - DEFAULT_NUM_CONFIRMATIONS - : config.numConfirmations; -======= - this._orders = {}; + this._orderByOrderHash = {}; this._abiDecoder = abiDecoder; this._token = token; this._exchange = exchange; @@ -82,10 +76,6 @@ export class OrderStateWatcher { this._dependentOrderHashes = {}; const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; const blockPollingIntervalMs = _.isUndefined(config) ? undefined : config.blockPollingIntervalMs; - this._numConfirmations = (_.isUndefined(config) || _.isUndefined(config.numConfirmations)) ? - DEFAULT_NUM_CONFIRMATIONS : - config.numConfirmations; ->>>>>>> Clear store cache on events this._eventWatcher = new EventWatcher( web3Wrapper, eventPollingIntervalMs, this._numConfirmations, ); @@ -202,7 +192,7 @@ export class OrderStateWatcher { this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(args.orderHash); // Revalidate orders const orderHash = args.orderHash; - const isOrderWatched = !_.isUndefined(this._orders[orderHash]); + const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); if (isOrderWatched) { await this._emitRevalidateOrdersAsync([orderHash]); } @@ -216,7 +206,7 @@ export class OrderStateWatcher { this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(args.orderHash); // Revalidate orders const orderHash = args.orderHash; - const isOrderWatched = !_.isUndefined(this._orders[orderHash]); + const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); if (isOrderWatched) { await this._emitRevalidateOrdersAsync([orderHash]); } -- cgit v1.2.3 From f5608d2c94bcee05a76ef102f235f5e860820567 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 12:53:03 -0500 Subject: Pass blockStore to eventWatcher --- src/order_watcher/event_watcher.ts | 18 ++++++++---------- src/order_watcher/order_state_watcher.ts | 13 ++++++++----- test/event_watcher_test.ts | 7 ++++++- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index c9e72281c..5303bb651 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -11,6 +11,7 @@ import {AbiDecoder} from '../utils/abi_decoder'; import {intervalUtils} from '../utils/interval_utils'; import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; +import {BlockStore} from '../stores/block_store'; const DEFAULT_EVENT_POLLING_INTERVAL = 200; @@ -29,8 +30,11 @@ export class EventWatcher { private _intervalIdIfExists?: NodeJS.Timer; private _lastEvents: Web3.LogEntry[] = []; private _numConfirmations: number; - constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number, numConfirmations: number) { + private _blockStore: BlockStore; + constructor(web3Wrapper: Web3Wrapper, blockStore: BlockStore, pollingIntervalMs: undefined|number, + numConfirmations: number) { this._web3Wrapper = web3Wrapper; + this._blockStore = blockStore; this._numConfirmations = numConfirmations; this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? DEFAULT_EVENT_POLLING_INTERVAL : @@ -67,16 +71,10 @@ export class EventWatcher { this._lastEvents = pendingEvents; } private async _getEventsAsync(): Promise { - let latestBlock: BlockParamLiteral|number; - if (this._numConfirmations === 0) { - latestBlock = BlockParamLiteral.Pending; - } else { - const currentBlock = await this._web3Wrapper.getBlockNumberAsync(); - latestBlock = currentBlock - this._numConfirmations; - } + const blockNumber = this._blockStore.getBlockNumberWithNConfirmations(this._numConfirmations); const eventFilter = { - fromBlock: latestBlock, - toBlock: latestBlock, + fromBlock: blockNumber, + toBlock: blockNumber, }; const events = await this._web3Wrapper.getLogsAsync(eventFilter); return events; diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 59de1cc2f..685d67455 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -61,7 +61,6 @@ export class OrderStateWatcher { private _abiDecoder: AbiDecoder; private _orderStateUtils: OrderStateUtils; private _blockStore: BlockStore; - private _numConfirmations: number; private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; constructor( @@ -76,15 +75,18 @@ export class OrderStateWatcher { this._dependentOrderHashes = {}; const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; const blockPollingIntervalMs = _.isUndefined(config) ? undefined : config.blockPollingIntervalMs; + const numConfirmations = (_.isUndefined(config) || _.isUndefined(config.numConfirmations)) ? + DEFAULT_NUM_CONFIRMATIONS : + config.numConfirmations; + this._blockStore = new BlockStore(this._web3Wrapper, blockPollingIntervalMs); this._eventWatcher = new EventWatcher( - web3Wrapper, eventPollingIntervalMs, this._numConfirmations, + web3Wrapper, this._blockStore, eventPollingIntervalMs, numConfirmations, ); - this._blockStore = new BlockStore(this._web3Wrapper, blockPollingIntervalMs); this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( - this._token, this._blockStore, this._numConfirmations, + this._token, this._blockStore, numConfirmations, ); this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore( - this._exchange, this._blockStore, this._numConfirmations, + this._exchange, this._blockStore, numConfirmations, ); this._orderStateUtils = new OrderStateUtils( this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore, @@ -159,6 +161,7 @@ export class OrderStateWatcher { // Invalidate cache const args = decodedLog.args as ApprovalContractEventArgs; this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(decodedLog.address, args._owner); + // Revalidate orders makerToken = decodedLog.address; makerAddress = args._owner; orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]); diff --git a/test/event_watcher_test.ts b/test/event_watcher_test.ts index 98dab93b5..0a1e7eb63 100644 --- a/test/event_watcher_test.ts +++ b/test/event_watcher_test.ts @@ -8,6 +8,7 @@ import {chaiSetup} from './utils/chai_setup'; import {web3Factory} from './utils/web3_factory'; import {Web3Wrapper} from '../src/web3_wrapper'; import {EventWatcher} from '../src/order_watcher/event_watcher'; +import {BlockStore} from '../src/stores/block_store'; import { ZeroEx, LogEvent, @@ -23,6 +24,7 @@ describe('EventWatcher', () => { let stubs: Sinon.SinonStub[] = []; let eventWatcher: EventWatcher; let web3Wrapper: Web3Wrapper; + let blockStore: BlockStore; const numConfirmations = 0; const logA: Web3.LogEntry = { address: '0x71d271f8b14adef568f8f28f1587ce7271ac4ca5', @@ -58,12 +60,15 @@ describe('EventWatcher', () => { web3 = web3Factory.create(); const pollingIntervalMs = 10; web3Wrapper = new Web3Wrapper(web3.currentProvider); - eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalMs, numConfirmations); + blockStore = new BlockStore(); + await blockStore.startAsync(); + eventWatcher = new EventWatcher(web3Wrapper, blockStore, pollingIntervalMs, numConfirmations); }); afterEach(() => { // clean up any stubs after the test has completed _.each(stubs, s => s.restore()); stubs = []; + blockStore.stop(); eventWatcher.unsubscribe(); }); it('correctly emits initial log events', (done: DoneCallback) => { -- cgit v1.2.3 From d4dc428124a210cc6b8e1c50527a08e902bfadd0 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 12:55:51 -0500 Subject: Remove tautology check --- src/order_watcher/order_state_watcher.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 685d67455..e6250cef5 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -225,9 +225,6 @@ export class OrderStateWatcher { private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise { for (const orderHash of orderHashes) { const signedOrder = this._orderByOrderHash[orderHash] as SignedOrder; - if (_.isUndefined(this._orderStateUtils)) { - break; // Unsubscribe was called - } const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder); if (_.isUndefined(this._callbackIfExistsAsync)) { break; // Unsubscribe was called -- cgit v1.2.3 From a9ae555b88cc36ff2cbd92fdd37a339702860c01 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 13:06:25 -0500 Subject: Store number of confirmations in a blockStore --- src/order_watcher/event_watcher.ts | 7 ++----- src/order_watcher/order_state_watcher.ts | 8 ++++---- src/stores/balance_proxy_allowance_lazy_store.ts | 8 +++----- src/stores/block_store.ts | 15 ++++++++++----- src/stores/order_filled_cancelled_lazy_store.ts | 8 +++----- src/utils/exchange_transfer_simulator.ts | 4 ++-- test/event_watcher_test.ts | 6 ++---- 7 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index 5303bb651..7e27d5e79 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -29,13 +29,10 @@ export class EventWatcher { private _pollingIntervalMs: number; private _intervalIdIfExists?: NodeJS.Timer; private _lastEvents: Web3.LogEntry[] = []; - private _numConfirmations: number; private _blockStore: BlockStore; - constructor(web3Wrapper: Web3Wrapper, blockStore: BlockStore, pollingIntervalMs: undefined|number, - numConfirmations: number) { + constructor(web3Wrapper: Web3Wrapper, blockStore: BlockStore, pollingIntervalMs: undefined|number) { this._web3Wrapper = web3Wrapper; this._blockStore = blockStore; - this._numConfirmations = numConfirmations; this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? DEFAULT_EVENT_POLLING_INTERVAL : pollingIntervalMs; @@ -71,7 +68,7 @@ export class EventWatcher { this._lastEvents = pendingEvents; } private async _getEventsAsync(): Promise { - const blockNumber = this._blockStore.getBlockNumberWithNConfirmations(this._numConfirmations); + const blockNumber = this._blockStore.getBlockNumber(); const eventFilter = { fromBlock: blockNumber, toBlock: blockNumber, diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index e6250cef5..0f5b5c30c 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -78,15 +78,15 @@ export class OrderStateWatcher { const numConfirmations = (_.isUndefined(config) || _.isUndefined(config.numConfirmations)) ? DEFAULT_NUM_CONFIRMATIONS : config.numConfirmations; - this._blockStore = new BlockStore(this._web3Wrapper, blockPollingIntervalMs); + this._blockStore = new BlockStore(numConfirmations, this._web3Wrapper, blockPollingIntervalMs); this._eventWatcher = new EventWatcher( - web3Wrapper, this._blockStore, eventPollingIntervalMs, numConfirmations, + web3Wrapper, this._blockStore, eventPollingIntervalMs, ); this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( - this._token, this._blockStore, numConfirmations, + this._token, this._blockStore, ); this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore( - this._exchange, this._blockStore, numConfirmations, + this._exchange, this._blockStore, ); this._orderStateUtils = new OrderStateUtils( this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore, diff --git a/src/stores/balance_proxy_allowance_lazy_store.ts b/src/stores/balance_proxy_allowance_lazy_store.ts index ac21c0818..88152e145 100644 --- a/src/stores/balance_proxy_allowance_lazy_store.ts +++ b/src/stores/balance_proxy_allowance_lazy_store.ts @@ -9,7 +9,6 @@ import {BlockStore} from './block_store'; */ export class BalanceAndProxyAllowanceLazyStore { private token: TokenWrapper; - private numConfirmations: number; private blockStore: BlockStore; private balance: { [tokenAddress: string]: { @@ -21,16 +20,15 @@ export class BalanceAndProxyAllowanceLazyStore { [userAddress: string]: BigNumber, }, }; - constructor(token: TokenWrapper, blockStore: BlockStore, numConfirmations: number) { + constructor(token: TokenWrapper, blockStore: BlockStore) { this.token = token; - this.numConfirmations = numConfirmations; this.blockStore = blockStore; this.balance = {}; this.proxyAllowance = {}; } public async getBalanceAsync(tokenAddress: string, userAddress: string): Promise { if (_.isUndefined(this.balance[tokenAddress]) || _.isUndefined(this.balance[tokenAddress][userAddress])) { - const defaultBlock = this.blockStore.getBlockNumberWithNConfirmations(this.numConfirmations); + const defaultBlock = this.blockStore.getBlockNumber(); const methodOpts = { defaultBlock, }; @@ -54,7 +52,7 @@ export class BalanceAndProxyAllowanceLazyStore { public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise { if (_.isUndefined(this.proxyAllowance[tokenAddress]) || _.isUndefined(this.proxyAllowance[tokenAddress][userAddress])) { - const defaultBlock = this.blockStore.getBlockNumberWithNConfirmations(this.numConfirmations); + const defaultBlock = this.blockStore.getBlockNumber(); const methodOpts = { defaultBlock, }; diff --git a/src/stores/block_store.ts b/src/stores/block_store.ts index d1f6c329a..ae9c232bb 100644 --- a/src/stores/block_store.ts +++ b/src/stores/block_store.ts @@ -15,15 +15,17 @@ export class BlockStore { private latestBlockNumber?: number; private intervalId?: NodeJS.Timer; private blockPollingIntervalMs: number; - constructor(web3Wrapper?: Web3Wrapper, blockPollingIntervalMs?: number) { + private numConfirmations: number; + constructor(numConfirmations: number, web3Wrapper?: Web3Wrapper, blockPollingIntervalMs?: number) { + this.numConfirmations = numConfirmations; this.web3Wrapper = web3Wrapper; this.blockPollingIntervalMs = blockPollingIntervalMs || DEFAULT_BLOCK_POLLING_INTERVAL_MS; } - public getBlockNumberWithNConfirmations(numConfirmations: number): Web3.BlockParam { + public getBlockNumber(): Web3.BlockParam { let blockNumber; - if (numConfirmations === 0) { + if (this.numConfirmations === 0) { blockNumber = BlockParamLiteral.Pending; - } else if (numConfirmations === 1) { + } else if (this.numConfirmations === 1) { // HACK: We special-case `numConfirmations === 1` so that we can use this block store without actually // setting `latestBlockNumber` when block number is latest (in order validation) whhich allows us to omit // an async call in a constructor of `ExchangeTransferSimulator` @@ -33,11 +35,14 @@ export class BlockStore { throw new Error(InternalZeroExError.LatestBlockNumberNotSet); } // Latest block already has 1 confirmation - blockNumber = this.latestBlockNumber - numConfirmations + 1; + blockNumber = this.latestBlockNumber - this.numConfirmations + 1; } return blockNumber; } public async startAsync(): Promise { + if (this.numConfirmations === 0 || this.numConfirmations === 1) { + return; // no-op + } await this.updateLatestBlockAsync(); this.intervalId = intervalUtils.setAsyncExcludingInterval( this.updateLatestBlockAsync.bind(this), this.blockPollingIntervalMs, diff --git a/src/stores/order_filled_cancelled_lazy_store.ts b/src/stores/order_filled_cancelled_lazy_store.ts index 5b0dab35c..104a8240e 100644 --- a/src/stores/order_filled_cancelled_lazy_store.ts +++ b/src/stores/order_filled_cancelled_lazy_store.ts @@ -9,7 +9,6 @@ import {BlockStore} from './block_store'; */ export class OrderFilledCancelledLazyStore { private exchange: ExchangeWrapper; - private numConfirmations: number; private blockStore: BlockStore; private filledTakerAmount: { [orderHash: string]: BigNumber, @@ -17,16 +16,15 @@ export class OrderFilledCancelledLazyStore { private cancelledTakerAmount: { [orderHash: string]: BigNumber, }; - constructor(exchange: ExchangeWrapper, blockStore: BlockStore, numConfirmations: number) { + constructor(exchange: ExchangeWrapper, blockStore: BlockStore) { this.exchange = exchange; - this.numConfirmations = numConfirmations; this.blockStore = blockStore; this.filledTakerAmount = {}; this.cancelledTakerAmount = {}; } public async getFilledTakerAmountAsync(orderHash: string): Promise { if (_.isUndefined(this.filledTakerAmount[orderHash])) { - const defaultBlock = this.blockStore.getBlockNumberWithNConfirmations(this.numConfirmations); + const defaultBlock = this.blockStore.getBlockNumber(); const methodOpts = { defaultBlock, }; @@ -44,7 +42,7 @@ export class OrderFilledCancelledLazyStore { } public async getCancelledTakerAmountAsync(orderHash: string): Promise { if (_.isUndefined(this.cancelledTakerAmount[orderHash])) { - const defaultBlock = this.blockStore.getBlockNumberWithNConfirmations(this.numConfirmations); + const defaultBlock = this.blockStore.getBlockNumber(); const methodOpts = { defaultBlock, }; diff --git a/src/utils/exchange_transfer_simulator.ts b/src/utils/exchange_transfer_simulator.ts index cd46397ed..475dcf953 100644 --- a/src/utils/exchange_transfer_simulator.ts +++ b/src/utils/exchange_transfer_simulator.ts @@ -37,9 +37,9 @@ export class ExchangeTransferSimulator { private store: BalanceAndProxyAllowanceLazyStore; private UNLIMITED_ALLOWANCE_IN_BASE_UNITS: BigNumber; constructor(token: TokenWrapper) { - const blockStore = new BlockStore(); const latestBlockConfirmationNumber = 1; - this.store = new BalanceAndProxyAllowanceLazyStore(token, blockStore, latestBlockConfirmationNumber); + const blockStore = new BlockStore(latestBlockConfirmationNumber); + this.store = new BalanceAndProxyAllowanceLazyStore(token, blockStore); this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS = token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; } /** diff --git a/test/event_watcher_test.ts b/test/event_watcher_test.ts index 0a1e7eb63..2673f978b 100644 --- a/test/event_watcher_test.ts +++ b/test/event_watcher_test.ts @@ -60,15 +60,13 @@ describe('EventWatcher', () => { web3 = web3Factory.create(); const pollingIntervalMs = 10; web3Wrapper = new Web3Wrapper(web3.currentProvider); - blockStore = new BlockStore(); - await blockStore.startAsync(); - eventWatcher = new EventWatcher(web3Wrapper, blockStore, pollingIntervalMs, numConfirmations); + blockStore = new BlockStore(numConfirmations); + eventWatcher = new EventWatcher(web3Wrapper, blockStore, pollingIntervalMs); }); afterEach(() => { // clean up any stubs after the test has completed _.each(stubs, s => s.restore()); stubs = []; - blockStore.stop(); eventWatcher.unsubscribe(); }); it('correctly emits initial log events', (done: DoneCallback) => { -- cgit v1.2.3 From 22cd6989a0217f2a49e59ce64bcc69b2c238aba4 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 15:00:48 -0500 Subject: Add a comment --- src/order_watcher/order_state_watcher.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 0f5b5c30c..ab80f231f 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -225,6 +225,8 @@ export class OrderStateWatcher { private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise { for (const orderHash of orderHashes) { const signedOrder = this._orderByOrderHash[orderHash] as SignedOrder; + // Most of those calls will never reach the network because the data is fetched from stores + // and only updated when cache is invalidated const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder); if (_.isUndefined(this._callbackIfExistsAsync)) { break; // Unsubscribe was called -- cgit v1.2.3 From 84c965d459b948eb03cb8a70a66663bd7b35f463 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 18:10:47 -0500 Subject: Remove blockStore and default to numConfirmations === 0 --- src/order_watcher/event_watcher.ts | 10 ++-- src/order_watcher/order_state_watcher.ts | 20 ++----- src/stores/balance_proxy_allowance_lazy_store.ts | 12 ++--- src/stores/block_store.ts | 66 ------------------------ src/stores/order_filled_cancelled_lazy_store.ts | 12 ++--- src/types.ts | 2 - src/utils/exchange_transfer_simulator.ts | 5 +- test/event_watcher_test.ts | 5 +- test/order_state_watcher_test.ts | 16 +++--- 9 files changed, 25 insertions(+), 123 deletions(-) delete mode 100644 src/stores/block_store.ts diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts index 7e27d5e79..81529a98c 100644 --- a/src/order_watcher/event_watcher.ts +++ b/src/order_watcher/event_watcher.ts @@ -11,7 +11,6 @@ import {AbiDecoder} from '../utils/abi_decoder'; import {intervalUtils} from '../utils/interval_utils'; import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; -import {BlockStore} from '../stores/block_store'; const DEFAULT_EVENT_POLLING_INTERVAL = 200; @@ -29,10 +28,8 @@ export class EventWatcher { private _pollingIntervalMs: number; private _intervalIdIfExists?: NodeJS.Timer; private _lastEvents: Web3.LogEntry[] = []; - private _blockStore: BlockStore; - constructor(web3Wrapper: Web3Wrapper, blockStore: BlockStore, pollingIntervalMs: undefined|number) { + constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) { this._web3Wrapper = web3Wrapper; - this._blockStore = blockStore; this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ? DEFAULT_EVENT_POLLING_INTERVAL : pollingIntervalMs; @@ -68,10 +65,9 @@ export class EventWatcher { this._lastEvents = pendingEvents; } private async _getEventsAsync(): Promise { - const blockNumber = this._blockStore.getBlockNumber(); const eventFilter = { - fromBlock: blockNumber, - toBlock: blockNumber, + fromBlock: BlockParamLiteral.Pending, + toBlock: BlockParamLiteral.Pending, }; const events = await this._web3Wrapper.getLogsAsync(eventFilter); return events; diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index ab80f231f..c40131bcb 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -26,7 +26,6 @@ import { ZeroExError, } from '../types'; import {Web3Wrapper} from '../web3_wrapper'; -import {BlockStore} from '../stores/block_store'; import {TokenWrapper} from '../contract_wrappers/token_wrapper'; import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper'; import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store'; @@ -60,7 +59,6 @@ export class OrderStateWatcher { private _exchange: ExchangeWrapper; private _abiDecoder: AbiDecoder; private _orderStateUtils: OrderStateUtils; - private _blockStore: BlockStore; private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; constructor( @@ -75,19 +73,11 @@ export class OrderStateWatcher { this._dependentOrderHashes = {}; const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; const blockPollingIntervalMs = _.isUndefined(config) ? undefined : config.blockPollingIntervalMs; - const numConfirmations = (_.isUndefined(config) || _.isUndefined(config.numConfirmations)) ? - DEFAULT_NUM_CONFIRMATIONS : - config.numConfirmations; - this._blockStore = new BlockStore(numConfirmations, this._web3Wrapper, blockPollingIntervalMs); - this._eventWatcher = new EventWatcher( - web3Wrapper, this._blockStore, eventPollingIntervalMs, - ); + this._eventWatcher = new EventWatcher(web3Wrapper, eventPollingIntervalMs); this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( - this._token, this._blockStore, - ); - this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore( - this._exchange, this._blockStore, + this._token, ); + this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore(this._exchange); this._orderStateUtils = new OrderStateUtils( this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore, ); @@ -123,12 +113,11 @@ export class OrderStateWatcher { * @param callback Receives the orderHash of the order that should be re-validated, together * with all the order-relevant blockchain state needed to re-validate the order. */ - public async subscribeAsync(callback: OnOrderStateChangeCallback): Promise { + public subscribe(callback: OnOrderStateChangeCallback): void { assert.isFunction('callback', callback); if (!_.isUndefined(this._callbackIfExistsAsync)) { throw new Error(ZeroExError.SubscriptionAlreadyPresent); } - await this._blockStore.startAsync(); this._callbackIfExistsAsync = callback; this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this)); } @@ -139,7 +128,6 @@ export class OrderStateWatcher { if (_.isUndefined(this._callbackIfExistsAsync)) { throw new Error(ZeroExError.SubscriptionNotFound); } - this._blockStore.stop(); this._balanceAndProxyAllowanceLazyStore.deleteAll(); this._orderFilledCancelledLazyStore.deleteAll(); delete this._callbackIfExistsAsync; diff --git a/src/stores/balance_proxy_allowance_lazy_store.ts b/src/stores/balance_proxy_allowance_lazy_store.ts index 88152e145..5c54fbb3b 100644 --- a/src/stores/balance_proxy_allowance_lazy_store.ts +++ b/src/stores/balance_proxy_allowance_lazy_store.ts @@ -2,14 +2,13 @@ import * as _ from 'lodash'; import * as Web3 from 'web3'; import {BigNumber} from 'bignumber.js'; import {TokenWrapper} from '../contract_wrappers/token_wrapper'; -import {BlockStore} from './block_store'; +import {BlockParamLiteral} from '../types'; /** * Copy on read store for balances/proxyAllowances of tokens/accounts */ export class BalanceAndProxyAllowanceLazyStore { private token: TokenWrapper; - private blockStore: BlockStore; private balance: { [tokenAddress: string]: { [userAddress: string]: BigNumber, @@ -20,17 +19,15 @@ export class BalanceAndProxyAllowanceLazyStore { [userAddress: string]: BigNumber, }, }; - constructor(token: TokenWrapper, blockStore: BlockStore) { + constructor(token: TokenWrapper) { this.token = token; - this.blockStore = blockStore; this.balance = {}; this.proxyAllowance = {}; } public async getBalanceAsync(tokenAddress: string, userAddress: string): Promise { if (_.isUndefined(this.balance[tokenAddress]) || _.isUndefined(this.balance[tokenAddress][userAddress])) { - const defaultBlock = this.blockStore.getBlockNumber(); const methodOpts = { - defaultBlock, + defaultBlock: BlockParamLiteral.Pending, }; const balance = await this.token.getBalanceAsync(tokenAddress, userAddress, methodOpts); this.setBalance(tokenAddress, userAddress, balance); @@ -52,9 +49,8 @@ export class BalanceAndProxyAllowanceLazyStore { public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise { if (_.isUndefined(this.proxyAllowance[tokenAddress]) || _.isUndefined(this.proxyAllowance[tokenAddress][userAddress])) { - const defaultBlock = this.blockStore.getBlockNumber(); const methodOpts = { - defaultBlock, + defaultBlock: BlockParamLiteral.Pending, }; const proxyAllowance = await this.token.getProxyAllowanceAsync(tokenAddress, userAddress, methodOpts); this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance); diff --git a/src/stores/block_store.ts b/src/stores/block_store.ts deleted file mode 100644 index ae9c232bb..000000000 --- a/src/stores/block_store.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as _ from 'lodash'; -import * as Web3 from 'web3'; -import {BigNumber} from 'bignumber.js'; -import {BlockParamLiteral, InternalZeroExError, ZeroExError} from '../types'; -import {Web3Wrapper} from '../web3_wrapper'; -import {intervalUtils} from '../utils/interval_utils'; - -const DEFAULT_BLOCK_POLLING_INTERVAL_MS = 500; - -/** - * Store for a current latest block number - */ -export class BlockStore { - private web3Wrapper?: Web3Wrapper; - private latestBlockNumber?: number; - private intervalId?: NodeJS.Timer; - private blockPollingIntervalMs: number; - private numConfirmations: number; - constructor(numConfirmations: number, web3Wrapper?: Web3Wrapper, blockPollingIntervalMs?: number) { - this.numConfirmations = numConfirmations; - this.web3Wrapper = web3Wrapper; - this.blockPollingIntervalMs = blockPollingIntervalMs || DEFAULT_BLOCK_POLLING_INTERVAL_MS; - } - public getBlockNumber(): Web3.BlockParam { - let blockNumber; - if (this.numConfirmations === 0) { - blockNumber = BlockParamLiteral.Pending; - } else if (this.numConfirmations === 1) { - // HACK: We special-case `numConfirmations === 1` so that we can use this block store without actually - // setting `latestBlockNumber` when block number is latest (in order validation) whhich allows us to omit - // an async call in a constructor of `ExchangeTransferSimulator` - blockNumber = BlockParamLiteral.Latest; - } else { - if (_.isUndefined(this.latestBlockNumber)) { - throw new Error(InternalZeroExError.LatestBlockNumberNotSet); - } - // Latest block already has 1 confirmation - blockNumber = this.latestBlockNumber - this.numConfirmations + 1; - } - return blockNumber; - } - public async startAsync(): Promise { - if (this.numConfirmations === 0 || this.numConfirmations === 1) { - return; // no-op - } - await this.updateLatestBlockAsync(); - this.intervalId = intervalUtils.setAsyncExcludingInterval( - this.updateLatestBlockAsync.bind(this), this.blockPollingIntervalMs, - ); - } - public stop(): void { - if (!_.isUndefined(this.intervalId)) { - intervalUtils.clearAsyncExcludingInterval(this.intervalId); - } - } - private async updateLatestBlockAsync(): Promise { - if (_.isUndefined(this.web3Wrapper)) { - throw new Error(InternalZeroExError.Web3WrapperRequiredToStartBlockStore); - } - const block = await this.web3Wrapper.getBlockAsync(BlockParamLiteral.Latest); - if (_.isNull(block.number)) { - throw new Error(ZeroExError.FailedToFetchLatestBlock); - } - this.latestBlockNumber = block.number; - } -} diff --git a/src/stores/order_filled_cancelled_lazy_store.ts b/src/stores/order_filled_cancelled_lazy_store.ts index 104a8240e..39adff4d1 100644 --- a/src/stores/order_filled_cancelled_lazy_store.ts +++ b/src/stores/order_filled_cancelled_lazy_store.ts @@ -2,31 +2,28 @@ import * as _ from 'lodash'; import * as Web3 from 'web3'; import {BigNumber} from 'bignumber.js'; import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper'; -import {BlockStore} from './block_store'; +import {BlockParamLiteral} from '../types'; /** * Copy on read store for filled/cancelled taker amounts */ export class OrderFilledCancelledLazyStore { private exchange: ExchangeWrapper; - private blockStore: BlockStore; private filledTakerAmount: { [orderHash: string]: BigNumber, }; private cancelledTakerAmount: { [orderHash: string]: BigNumber, }; - constructor(exchange: ExchangeWrapper, blockStore: BlockStore) { + constructor(exchange: ExchangeWrapper) { this.exchange = exchange; - this.blockStore = blockStore; this.filledTakerAmount = {}; this.cancelledTakerAmount = {}; } public async getFilledTakerAmountAsync(orderHash: string): Promise { if (_.isUndefined(this.filledTakerAmount[orderHash])) { - const defaultBlock = this.blockStore.getBlockNumber(); const methodOpts = { - defaultBlock, + defaultBlock: BlockParamLiteral.Pending, }; const filledTakerAmount = await this.exchange.getFilledTakerAmountAsync(orderHash, methodOpts); this.setFilledTakerAmount(orderHash, filledTakerAmount); @@ -42,9 +39,8 @@ export class OrderFilledCancelledLazyStore { } public async getCancelledTakerAmountAsync(orderHash: string): Promise { if (_.isUndefined(this.cancelledTakerAmount[orderHash])) { - const defaultBlock = this.blockStore.getBlockNumber(); const methodOpts = { - defaultBlock, + defaultBlock: BlockParamLiteral.Pending, }; const cancelledTakerAmount = await this.exchange.getCanceledTakerAmountAsync(orderHash, methodOpts); this.setCancelledTakerAmount(orderHash, cancelledTakerAmount); diff --git a/src/types.ts b/src/types.ts index 555fef855..b34dbfb4e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -402,12 +402,10 @@ export interface JSONRPCPayload { /* * eventPollingIntervalMs: How often to poll the Ethereum node for new events * blockPollingIntervalMs: How often to poll the Ethereum node for new blocks - * numConfirmations: How many confirmed blocks deep you wish to listen for events at. */ export interface OrderStateWatcherConfig { eventPollingIntervalMs?: number; blockPollingIntervalMs?: number; - numConfirmations?: number; } /* diff --git a/src/utils/exchange_transfer_simulator.ts b/src/utils/exchange_transfer_simulator.ts index 475dcf953..308ef06db 100644 --- a/src/utils/exchange_transfer_simulator.ts +++ b/src/utils/exchange_transfer_simulator.ts @@ -3,7 +3,6 @@ import BigNumber from 'bignumber.js'; import {ExchangeContractErrs, TradeSide, TransferType, BlockParamLiteral} from '../types'; import {TokenWrapper} from '../contract_wrappers/token_wrapper'; import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store'; -import {BlockStore} from '../stores/block_store'; enum FailureReason { Balance = 'balance', @@ -37,9 +36,7 @@ export class ExchangeTransferSimulator { private store: BalanceAndProxyAllowanceLazyStore; private UNLIMITED_ALLOWANCE_IN_BASE_UNITS: BigNumber; constructor(token: TokenWrapper) { - const latestBlockConfirmationNumber = 1; - const blockStore = new BlockStore(latestBlockConfirmationNumber); - this.store = new BalanceAndProxyAllowanceLazyStore(token, blockStore); + this.store = new BalanceAndProxyAllowanceLazyStore(token); this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS = token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; } /** diff --git a/test/event_watcher_test.ts b/test/event_watcher_test.ts index 2673f978b..b4164fe63 100644 --- a/test/event_watcher_test.ts +++ b/test/event_watcher_test.ts @@ -8,7 +8,6 @@ import {chaiSetup} from './utils/chai_setup'; import {web3Factory} from './utils/web3_factory'; import {Web3Wrapper} from '../src/web3_wrapper'; import {EventWatcher} from '../src/order_watcher/event_watcher'; -import {BlockStore} from '../src/stores/block_store'; import { ZeroEx, LogEvent, @@ -24,7 +23,6 @@ describe('EventWatcher', () => { let stubs: Sinon.SinonStub[] = []; let eventWatcher: EventWatcher; let web3Wrapper: Web3Wrapper; - let blockStore: BlockStore; const numConfirmations = 0; const logA: Web3.LogEntry = { address: '0x71d271f8b14adef568f8f28f1587ce7271ac4ca5', @@ -60,8 +58,7 @@ describe('EventWatcher', () => { web3 = web3Factory.create(); const pollingIntervalMs = 10; web3Wrapper = new Web3Wrapper(web3.currentProvider); - blockStore = new BlockStore(numConfirmations); - eventWatcher = new EventWatcher(web3Wrapper, blockStore, pollingIntervalMs); + eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalMs); }); afterEach(() => { // clean up any stubs after the test has completed diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 880ef0fc3..76ecd5dd4 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -94,8 +94,8 @@ describe('OrderStateWatcher', () => { zeroEx.orderStateWatcher.unsubscribe(); }); it('should fail when trying to subscribe twice', async () => { - await zeroEx.orderStateWatcher.subscribeAsync(_.noop); - return expect(zeroEx.orderStateWatcher.subscribeAsync(_.noop)) + await zeroEx.orderStateWatcher.subscribe(_.noop); + return expect(zeroEx.orderStateWatcher.subscribe(_.noop)) .to.be.rejectedWith(ZeroExError.SubscriptionAlreadyPresent); }); }); @@ -119,7 +119,7 @@ describe('OrderStateWatcher', () => { expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerAllowance); done(); }); - await zeroEx.orderStateWatcher.subscribeAsync(callback); + await zeroEx.orderStateWatcher.subscribe(callback); await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0)); })().catch(done); }); @@ -158,7 +158,7 @@ describe('OrderStateWatcher', () => { expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerBalance); done(); }); - await zeroEx.orderStateWatcher.subscribeAsync(callback); + 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); @@ -183,7 +183,7 @@ describe('OrderStateWatcher', () => { done(); } }); - await zeroEx.orderStateWatcher.subscribeAsync(callback); + zeroEx.orderStateWatcher.subscribe(callback); const shouldThrowOnInsufficientBalanceOrAllowance = true; await zeroEx.exchange.fillOrderAsync( @@ -220,7 +220,7 @@ describe('OrderStateWatcher', () => { done(); } }); - await zeroEx.orderStateWatcher.subscribeAsync(callback); + zeroEx.orderStateWatcher.subscribe(callback); const shouldThrowOnInsufficientBalanceOrAllowance = true; await zeroEx.exchange.fillOrderAsync( signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker, @@ -321,7 +321,7 @@ describe('OrderStateWatcher', () => { expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderRemainingFillAmountZero); done(); }); - await zeroEx.orderStateWatcher.subscribeAsync(callback); + zeroEx.orderStateWatcher.subscribe(callback); const shouldThrowOnInsufficientBalanceOrAllowance = true; await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount); @@ -348,7 +348,7 @@ describe('OrderStateWatcher', () => { expect(orderRelevantState.canceledTakerTokenAmount).to.be.bignumber.equal(cancelAmountInBaseUnits); done(); }); - await zeroEx.orderStateWatcher.subscribeAsync(callback); + zeroEx.orderStateWatcher.subscribe(callback); await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmountInBaseUnits); })().catch(done); }); -- cgit v1.2.3 From d52825a5b18b0ac7591ca2f32a5062e949e4a65a Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 18:52:22 -0500 Subject: Fix tests --- test/order_state_watcher_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 76ecd5dd4..df007588e 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -96,7 +96,7 @@ describe('OrderStateWatcher', () => { it('should fail when trying to subscribe twice', async () => { await zeroEx.orderStateWatcher.subscribe(_.noop); return expect(zeroEx.orderStateWatcher.subscribe(_.noop)) - .to.be.rejectedWith(ZeroExError.SubscriptionAlreadyPresent); + .to.be.eventually.rejectedWith(ZeroExError.SubscriptionAlreadyPresent); }); }); describe('tests with cleanup', async () => { -- cgit v1.2.3 From 3204c077d1f2095ceda3f2cd91735e3b7db66df2 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 18:53:21 -0500 Subject: Remove redundant instance variables --- src/order_watcher/order_state_watcher.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index c40131bcb..3e8fd7e3a 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -55,8 +55,6 @@ export class OrderStateWatcher { private _callbackIfExistsAsync?: OnOrderStateChangeCallback; private _eventWatcher: EventWatcher; private _web3Wrapper: Web3Wrapper; - private _token: TokenWrapper; - private _exchange: ExchangeWrapper; private _abiDecoder: AbiDecoder; private _orderStateUtils: OrderStateUtils; private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; @@ -67,17 +65,13 @@ export class OrderStateWatcher { ) { this._orderByOrderHash = {}; this._abiDecoder = abiDecoder; - this._token = token; - this._exchange = exchange; this._web3Wrapper = web3Wrapper; this._dependentOrderHashes = {}; const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; const blockPollingIntervalMs = _.isUndefined(config) ? undefined : config.blockPollingIntervalMs; this._eventWatcher = new EventWatcher(web3Wrapper, eventPollingIntervalMs); - this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( - this._token, - ); - this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore(this._exchange); + this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore(token); + this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore(exchange); this._orderStateUtils = new OrderStateUtils( this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore, ); -- cgit v1.2.3 From a5876978838baa9a66c2dd9714dd2810839aa55b Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 18:54:12 -0500 Subject: Remove duplicate operations --- src/order_watcher/order_state_watcher.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 3e8fd7e3a..3c09a2743 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -174,7 +174,6 @@ export class OrderStateWatcher { // Invalidate cache const args = decodedLog.args as LogFillContractEventArgs; this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(args.orderHash); - this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(args.orderHash); // Revalidate orders const orderHash = args.orderHash; const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); @@ -188,7 +187,6 @@ export class OrderStateWatcher { // Invalidate cache const args = decodedLog.args as LogCancelContractEventArgs; this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(args.orderHash); - this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(args.orderHash); // Revalidate orders const orderHash = args.orderHash; const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); -- cgit v1.2.3 From fdb82d5dd4ea14e35ee03cf9abca6105fa1e0595 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 18:54:31 -0500 Subject: Fix a typo --- src/order_watcher/order_state_watcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index 3c09a2743..b69540716 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -205,7 +205,7 @@ export class OrderStateWatcher { private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise { for (const orderHash of orderHashes) { const signedOrder = this._orderByOrderHash[orderHash] as SignedOrder; - // Most of those calls will never reach the network because the data is fetched from stores + // Most of these calls will never reach the network because the data is fetched from stores // and only updated when cache is invalidated const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder); if (_.isUndefined(this._callbackIfExistsAsync)) { -- cgit v1.2.3 From 7b50a6490db820b4e7f7b972ae0dcb602e5118b6 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 18:55:42 -0500 Subject: Don't store empty objects --- src/stores/balance_proxy_allowance_lazy_store.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/stores/balance_proxy_allowance_lazy_store.ts b/src/stores/balance_proxy_allowance_lazy_store.ts index 5c54fbb3b..c83e61606 100644 --- a/src/stores/balance_proxy_allowance_lazy_store.ts +++ b/src/stores/balance_proxy_allowance_lazy_store.ts @@ -44,6 +44,9 @@ export class BalanceAndProxyAllowanceLazyStore { public deleteBalance(tokenAddress: string, userAddress: string): void { if (!_.isUndefined(this.balance[tokenAddress])) { delete this.balance[tokenAddress][userAddress]; + if (_.isEmpty(this.balance[tokenAddress])) { + delete this.balance[tokenAddress]; + } } } public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise { @@ -67,6 +70,9 @@ export class BalanceAndProxyAllowanceLazyStore { public deleteProxyAllowance(tokenAddress: string, userAddress: string): void { if (!_.isUndefined(this.proxyAllowance[tokenAddress])) { delete this.proxyAllowance[tokenAddress][userAddress]; + if (_.isEmpty(this.proxyAllowance[tokenAddress])) { + delete this.proxyAllowance[tokenAddress]; + } } } public deleteAll(): void { -- cgit v1.2.3 From 610298a25d5a5d1bb7a6baa407f169a44e7695bf Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 18:56:09 -0500 Subject: Remove redundant spaces --- src/stores/order_filled_cancelled_lazy_store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/order_filled_cancelled_lazy_store.ts b/src/stores/order_filled_cancelled_lazy_store.ts index 39adff4d1..9d74da096 100644 --- a/src/stores/order_filled_cancelled_lazy_store.ts +++ b/src/stores/order_filled_cancelled_lazy_store.ts @@ -5,7 +5,7 @@ import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper'; import {BlockParamLiteral} from '../types'; /** - * Copy on read store for filled/cancelled taker amounts + * Copy on read store for filled/cancelled taker amounts */ export class OrderFilledCancelledLazyStore { private exchange: ExchangeWrapper; -- cgit v1.2.3 From d73fb5a23c0e23eaec90aee0b563c06fa8f380d6 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 19:24:58 -0500 Subject: Fix tests --- test/order_state_watcher_test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index df007588e..aa387f495 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -94,9 +94,9 @@ describe('OrderStateWatcher', () => { zeroEx.orderStateWatcher.unsubscribe(); }); it('should fail when trying to subscribe twice', async () => { - await zeroEx.orderStateWatcher.subscribe(_.noop); - return expect(zeroEx.orderStateWatcher.subscribe(_.noop)) - .to.be.eventually.rejectedWith(ZeroExError.SubscriptionAlreadyPresent); + zeroEx.orderStateWatcher.subscribe(_.noop); + expect(() => zeroEx.orderStateWatcher.subscribe(_.noop)) + .to.throw(); }); }); describe('tests with cleanup', async () => { @@ -119,7 +119,7 @@ describe('OrderStateWatcher', () => { expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerAllowance); done(); }); - await zeroEx.orderStateWatcher.subscribe(callback); + zeroEx.orderStateWatcher.subscribe(callback); await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0)); })().catch(done); }); -- cgit v1.2.3 From 7ea0b138bc7a61037636eb5b4d2ad9b995a27f79 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 20:02:54 -0500 Subject: Remove unused code --- package.json | 2 +- src/order_watcher/order_state_watcher.ts | 3 --- src/types.ts | 5 ----- test/order_state_watcher_test.ts | 2 +- yarn.lock | 4 ++++ 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index e7e21bdce..9faaeb8c1 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "chai-as-promised": "^7.1.0", "chai-as-promised-typescript-typings": "0.0.3", "chai-bignumber": "^2.0.1", - "chai-typescript-typings": "^0.0.0", + "chai-typescript-typings": "^0.0.1", "copyfiles": "^1.2.0", "coveralls": "^3.0.0", "dirty-chai": "^2.0.1", diff --git a/src/order_watcher/order_state_watcher.ts b/src/order_watcher/order_state_watcher.ts index b69540716..139f13fdf 100644 --- a/src/order_watcher/order_state_watcher.ts +++ b/src/order_watcher/order_state_watcher.ts @@ -63,12 +63,9 @@ export class OrderStateWatcher { web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, token: TokenWrapper, exchange: ExchangeWrapper, config?: OrderStateWatcherConfig, ) { - this._orderByOrderHash = {}; this._abiDecoder = abiDecoder; this._web3Wrapper = web3Wrapper; - this._dependentOrderHashes = {}; const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; - const blockPollingIntervalMs = _.isUndefined(config) ? undefined : config.blockPollingIntervalMs; this._eventWatcher = new EventWatcher(web3Wrapper, eventPollingIntervalMs); this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore(token); this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore(exchange); diff --git a/src/types.ts b/src/types.ts index b34dbfb4e..fcfbdc92b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,14 +18,11 @@ export enum ZeroExError { SubscriptionNotFound = 'SUBSCRIPTION_NOT_FOUND', SubscriptionAlreadyPresent = 'SUBSCRIPTION_ALREADY_PRESENT', TransactionMiningTimeout = 'TRANSACTION_MINING_TIMEOUT', - FailedToFetchLatestBlock = 'FAILED_TO_FETCH_LATEST_BLOCK', } export enum InternalZeroExError { NoAbiDecoder = 'NO_ABI_DECODER', ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY', - LatestBlockNumberNotSet = 'LATEST_BLOCK_NUMBER_NOT_SET', - Web3WrapperRequiredToStartBlockStore = 'WEB3_WRAPPER_REQUIRED_TO_START_BLOCK_STORE', } /** @@ -401,11 +398,9 @@ export interface JSONRPCPayload { /* * eventPollingIntervalMs: How often to poll the Ethereum node for new events - * blockPollingIntervalMs: How often to poll the Ethereum node for new blocks */ export interface OrderStateWatcherConfig { eventPollingIntervalMs?: number; - blockPollingIntervalMs?: number; } /* diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index aa387f495..2b1aa68b7 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -96,7 +96,7 @@ describe('OrderStateWatcher', () => { it('should fail when trying to subscribe twice', async () => { zeroEx.orderStateWatcher.subscribe(_.noop); expect(() => zeroEx.orderStateWatcher.subscribe(_.noop)) - .to.throw(); + .to.throw(ZeroExError.SubscriptionAlreadyPresent); }); }); describe('tests with cleanup', async () => { diff --git a/yarn.lock b/yarn.lock index 55f3f7d1b..864b87712 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1041,6 +1041,10 @@ chai-typescript-typings@^0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/chai-typescript-typings/-/chai-typescript-typings-0.0.0.tgz#52e076d72cf29129c94ab1dba6e33ce3828a0724" +chai-typescript-typings@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/chai-typescript-typings/-/chai-typescript-typings-0.0.1.tgz#433dee303b0b2978ad0dd03129df0a5afb791274" + chai@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/chai/-/chai-4.0.2.tgz#2f7327c4de6f385dd7787999e2ab02697a32b83b" -- cgit v1.2.3 From e512e38efbbec030add83ce2bc50f0d17862bdd6 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Sun, 12 Nov 2017 20:21:24 -0500 Subject: Remove old tests --- test/order_state_watcher_test.ts | 62 ---------------------------------------- 1 file changed, 62 deletions(-) diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts index 2b1aa68b7..c8a4a8064 100644 --- a/test/order_state_watcher_test.ts +++ b/test/order_state_watcher_test.ts @@ -352,67 +352,5 @@ describe('OrderStateWatcher', () => { await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmountInBaseUnits); })().catch(done); }); - describe('check numConfirmations behavior', () => { - before(() => { - const configs: ZeroExConfig = { - orderWatcherConfig: { - numConfirmations: 1, - }, - }; - zeroEx = new ZeroEx(web3.currentProvider, configs); - }); - it('should emit orderState when watching at 1 confirmation deep and event is one block deep', - (done: DoneCallback) => { - (async () => { - fillScenarios = new FillScenarios( - zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress, - ); - - signedOrder = await fillScenarios.createFillableSignedOrderAsync( - makerToken.address, takerToken.address, maker, taker, fillableAmount, - ); - const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.addOrder(signedOrder); - const callback = reportCallbackErrors(done)((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); - blockchainLifecycle.mineABlock(); - })().catch(done); - }); - it('shouldn\'t emit orderState when watching at 1 confirmation deep and event is in mempool', - (done: DoneCallback) => { - (async () => { - fillScenarios = new FillScenarios( - zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress, - ); - - signedOrder = await fillScenarios.createFillableSignedOrderAsync( - makerToken.address, takerToken.address, maker, taker, fillableAmount, - ); - const orderHash = ZeroEx.getOrderHashHex(signedOrder); - zeroEx.orderStateWatcher.addOrder(signedOrder); - const callback = reportCallbackErrors(done)((orderState: OrderState) => { - throw new Error('OrderState callback fired when it shouldn\'t have'); - }); - 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); - setTimeout(() => { - done(); - }, TIMEOUT_MS); - })().catch(done); - }); - }); }); }); -- cgit v1.2.3