diff options
-rw-r--r-- | package.json | 4 | ||||
-rw-r--r-- | src/0x.ts | 34 | ||||
-rw-r--r-- | src/contract_wrappers/ether_token_wrapper.ts | 4 | ||||
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 21 | ||||
-rw-r--r-- | src/contract_wrappers/token_wrapper.ts | 8 | ||||
-rw-r--r-- | src/index.ts | 6 | ||||
-rw-r--r-- | src/order_watcher/event_watcher.ts | 97 | ||||
-rw-r--r-- | src/order_watcher/order_state_watcher.ts | 202 | ||||
-rw-r--r-- | src/schemas/zero_ex_config_schema.ts | 13 | ||||
-rw-r--r-- | src/types.ts | 62 | ||||
-rw-r--r-- | src/utils/abi_decoder.ts | 2 | ||||
-rw-r--r-- | src/utils/assert.ts | 17 | ||||
-rw-r--r-- | src/utils/order_state_utils.ts | 110 | ||||
-rw-r--r-- | src/utils/signature_utils.ts | 15 | ||||
-rw-r--r-- | src/web3_wrapper.ts | 14 | ||||
-rw-r--r-- | test/event_watcher_test.ts | 127 | ||||
-rw-r--r-- | test/exchange_wrapper_test.ts | 17 | ||||
-rw-r--r-- | test/order_state_watcher_test.ts | 421 | ||||
-rw-r--r-- | test/token_wrapper_test.ts | 11 | ||||
-rw-r--r-- | test/utils/blockchain_lifecycle.ts | 3 | ||||
-rw-r--r-- | test/utils/report_callback_errors.ts | 14 | ||||
-rw-r--r-- | test/utils/rpc.ts | 6 | ||||
-rw-r--r-- | yarn.lock | 8 |
23 files changed, 1146 insertions, 70 deletions
diff --git a/package.json b/package.json index 1f4c2de5f..171cc7706 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.1", "webpack": "^3.1.0" }, "dependencies": { @@ -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 './order_watcher/order_state_watcher'; +import {OrderStateUtils} from './utils/order_state_utils'; import { ECSignature, ZeroExError, @@ -23,6 +25,7 @@ import { SignedOrder, Web3Provider, ZeroExConfig, + OrderStateWatcherConfig, TransactionReceiptWithDecodedLogs, } from './types'; import {zeroExConfigSchema} from './schemas/zero_ex_config_schema'; @@ -65,6 +68,11 @@ export class ZeroEx { * tokenTransferProxy smart contract. */ public proxy: TokenTransferProxyWrapper; + /** + * 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; private _abiDecoder: AbiDecoder; /** @@ -80,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. @@ -177,12 +174,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); @@ -213,6 +204,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 orderWatcherConfig = _.isUndefined(config) ? undefined : config.orderWatcherConfig; + const orderStateUtils = new OrderStateUtils(this.token, this.exchange); + this.orderStateWatcher = new OrderStateWatcher( + this._web3Wrapper, this._abiDecoder, orderStateUtils, orderWatcherConfig, + ); } /** * Sets a new web3 provider for 0x.js. Updating the provider will stop all diff --git a/src/contract_wrappers/ether_token_wrapper.ts b/src/contract_wrappers/ether_token_wrapper.ts index 7c94e314a..3cd2f0224 100644 --- a/src/contract_wrappers/ether_token_wrapper.ts +++ b/src/contract_wrappers/ether_token_wrapper.ts @@ -29,7 +29,7 @@ export class EtherTokenWrapper extends ContractWrapper { * @return Transaction hash. */ public async depositAsync(amountInWei: BigNumber, depositor: string): Promise<string> { - assert.isBigNumber('amountInWei', amountInWei); + assert.isValidBaseUnitAmount('amountInWei', amountInWei); await assert.isSenderAddressAsync('depositor', depositor, this._web3Wrapper); const ethBalanceInWei = await this._web3Wrapper.getBalanceInWeiAsync(depositor); @@ -50,7 +50,7 @@ export class EtherTokenWrapper extends ContractWrapper { * @return Transaction hash. */ public async withdrawAsync(amountInWei: BigNumber, withdrawer: string): Promise<string> { - 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 b608e6931..fe0c5bc00 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, @@ -165,7 +164,7 @@ export class ExchangeWrapper extends ContractWrapper { takerAddress: string, orderTransactionOpts?: OrderTransactionOpts): Promise<string> { 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); @@ -238,7 +237,7 @@ 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); @@ -407,7 +406,7 @@ export class ExchangeWrapper extends ContractWrapper { takerAddress: string, orderTransactionOpts?: OrderTransactionOpts): Promise<string> { 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(); @@ -542,7 +541,7 @@ export class ExchangeWrapper extends ContractWrapper { cancelTakerTokenAmount: BigNumber, orderTransactionOpts?: OrderTransactionOpts): Promise<string> { 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(); @@ -735,7 +734,7 @@ export class ExchangeWrapper extends ContractWrapper { fillTakerTokenAmount: BigNumber, takerAddress: string): Promise<void> { 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); @@ -751,7 +750,7 @@ export class ExchangeWrapper extends ContractWrapper { public async validateCancelOrderThrowIfInvalidAsync( order: Order, cancelTakerTokenAmount: BigNumber): Promise<void> { 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( @@ -769,7 +768,7 @@ export class ExchangeWrapper extends ContractWrapper { fillTakerTokenAmount: BigNumber, takerAddress: string): Promise<void> { 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); @@ -788,9 +787,9 @@ export class ExchangeWrapper extends ContractWrapper { public async isRoundingErrorAsync(fillTakerTokenAmount: BigNumber, takerTokenAmount: BigNumber, makerTokenAmount: BigNumber): Promise<boolean> { - assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount); - assert.isBigNumber('takerTokenAmount', takerTokenAmount); - assert.isBigNumber('makerTokenAmount', makerTokenAmount); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isValidBaseUnitAmount('takerTokenAmount', takerTokenAmount); + 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 081113964..614ac19d4 100644 --- a/src/contract_wrappers/token_wrapper.ts +++ b/src/contract_wrappers/token_wrapper.ts @@ -70,7 +70,7 @@ 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); // Hack: for some reason default estimated gas amount causes `base fee exceeds gas limit` exception @@ -150,7 +150,7 @@ export class TokenWrapper extends ContractWrapper { amountInBaseUnits: BigNumber): Promise<string> { 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); @@ -185,7 +185,7 @@ 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); @@ -219,7 +219,7 @@ 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/index.ts b/src/index.ts index 249c20519..7963ec43e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,4 +35,10 @@ export { OrderTransactionOpts, FilterObject, LogEvent, + DecodedLogEvent, + EventWatcherCallback, + OnOrderStateChangeCallback, + OrderStateValid, + OrderStateInvalid, + OrderState, } from './types'; diff --git a/src/order_watcher/event_watcher.ts b/src/order_watcher/event_watcher.ts new file mode 100644 index 000000000..c9e72281c --- /dev/null +++ b/src/order_watcher/event_watcher.ts @@ -0,0 +1,97 @@ +import * as Web3 from 'web3'; +import * as _ from 'lodash'; +import {Web3Wrapper} from '../web3_wrapper'; +import { + BlockParamLiteral, + EventCallback, + EventWatcherCallback, + ZeroExError, +} from '../types'; +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. + */ +export class EventWatcher { + private _web3Wrapper: Web3Wrapper; + private _pollingIntervalMs: number; + private _intervalIdIfExists?: NodeJS.Timer; + private _lastEvents: Web3.LogEntry[] = []; + 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): void { + assert.isFunction('callback', callback); + if (!_.isUndefined(this._intervalIdIfExists)) { + throw new Error(ZeroExError.SubscriptionAlreadyPresent); + } + this._intervalIdIfExists = intervalUtils.setAsyncExcludingInterval( + this._pollForBlockchainEventsAsync.bind(this, callback), this._pollingIntervalMs, + ); + } + public unsubscribe(): void { + this._lastEvents = []; + if (!_.isUndefined(this._intervalIdIfExists)) { + intervalUtils.clearAsyncExcludingInterval(this._intervalIdIfExists); + delete this._intervalIdIfExists; + } + } + private async _pollForBlockchainEventsAsync(callback: EventWatcherCallback): Promise<void> { + 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, + // 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); + await this._emitDifferencesAsync(removedEvents, LogEventState.Removed, callback); + await this._emitDifferencesAsync(newEvents, LogEventState.Added, callback); + this._lastEvents = pendingEvents; + } + private async _getEventsAsync(): Promise<Web3.LogEntry[]> { + let latestBlock: BlockParamLiteral|number; + if (this._numConfirmations === 0) { + latestBlock = BlockParamLiteral.Pending; + } else { + const currentBlock = await this._web3Wrapper.getBlockNumberAsync(); + latestBlock = currentBlock - this._numConfirmations; + } + const eventFilter = { + fromBlock: latestBlock, + toBlock: latestBlock, + }; + const events = await this._web3Wrapper.getLogsAsync(eventFilter); + return events; + } + private async _emitDifferencesAsync( + logs: Web3.LogEntry[], logEventState: LogEventState, callback: EventWatcherCallback, + ): Promise<void> { + for (const log of logs) { + const logEvent = { + removed: logEventState === LogEventState.Removed, + ...log, + }; + if (!_.isUndefined(this._intervalIdIfExists)) { + await callback(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..4866f8409 --- /dev/null +++ b/src/order_watcher/order_state_watcher.ts @@ -0,0 +1,202 @@ +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'; +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, + OrderStateWatcherConfig, + ExchangeEvents, + TokenEvents, + ZeroExError, +} from '../types'; +import {Web3Wrapper} from '../web3_wrapper'; + +const DEFAULT_NUM_CONFIRMATIONS = 0; + +interface DependentOrderHashes { + [makerAddress: string]: { + [makerToken: string]: Set<string>, + }; +} + +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 _orderByOrderHash: OrderByOrderHash = {}; + private _dependentOrderHashes: DependentOrderHashes = {}; + private _web3Wrapper: Web3Wrapper; + private _callbackIfExistsAsync?: OnOrderStateChangeCallback; + private _eventWatcher: EventWatcher; + private _abiDecoder: AbiDecoder; + private _orderStateUtils: OrderStateUtils; + private _numConfirmations: number; + constructor( + web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, orderStateUtils: OrderStateUtils, + 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, + ); + this._abiDecoder = abiDecoder; + this._orderStateUtils = orderStateUtils; + } + /** + * 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 { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + const orderHash = ZeroEx.getOrderHashHex(signedOrder); + assert.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker); + this._orderByOrderHash[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._orderByOrderHash[orderHash]; + if (_.isUndefined(signedOrder)) { + return; // noop + } + delete this._orderByOrderHash[orderHash]; + this.removeFromDependentOrderHashes(signedOrder.maker, signedOrder.makerTokenAddress, orderHash); + } + /** + * 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. + */ + 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)); + } + /** + * Ends an orderStateWatcher subscription. + */ + public unsubscribe(): void { + delete this._callbackIfExistsAsync; + this._eventWatcher.unsubscribe(); + } + private async _onEventWatcherCallbackAsync(log: LogEvent): Promise<void> { + const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log); + const isLogDecoded = !_.isUndefined((maybeDecodedLog as LogWithDecodedArgs<any>).event); + 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<any>; + let makerToken: string; + let makerAddress: string; + let orderHashesSet: Set<string>; + switch (decodedLog.event) { + case TokenEvents.Approval: + makerToken = decodedLog.address; + makerAddress = decodedLog.args._owner; + orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]); + if (!_.isUndefined(orderHashesSet)) { + const orderHashes = Array.from(orderHashesSet); + await this._emitRevalidateOrdersAsync(orderHashes, blockNumber); + } + 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, blockNumber); + } + break; + + case ExchangeEvents.LogFill: + case ExchangeEvents.LogCancel: + const orderHash = decodedLog.args.orderHash; + const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); + if (isOrderWatched) { + await this._emitRevalidateOrdersAsync([orderHash], blockNumber); + } + break; + + case ExchangeEvents.LogError: + return; // noop + + default: + throw utils.spawnSwitchErr('decodedLog.event', decodedLog.event); + } + } + private async _emitRevalidateOrdersAsync(orderHashes: string[], blockNumber: number): Promise<void> { + 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); + if (!_.isUndefined(this._callbackIfExistsAsync)) { + await this._callbackIfExistsAsync(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); + } + 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]; + } + } +} diff --git a/src/schemas/zero_ex_config_schema.ts b/src/schemas/zero_ex_config_schema.ts index 179e29c31..6d4b3ed27 100644 --- a/src/schemas/zero_ex_config_schema.ts +++ b/src/schemas/zero_ex_config_schema.ts @@ -5,6 +5,19 @@ export const zeroExConfigSchema = { exchangeContractAddress: {$ref: '/Address'}, tokenRegistryContractAddress: {$ref: '/Address'}, etherTokenContractAddress: {$ref: '/Address'}, + orderWatcherConfig: { + type: 'object', + properties: { + pollingIntervalMs: { + type: 'number', + minimum: 0, + }, + numConfirmations: { + type: 'number', + minimum: 0, + }, + }, + }, }, type: 'object', }; diff --git a/src/types.ts b/src/types.ts index af3c36736..09b611019 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', TransactionMiningTimeout = 'TRANSACTION_MINING_TIMEOUT', } @@ -38,12 +39,17 @@ export type OrderAddresses = [string, string, string, string, string]; export type OrderValues = [BigNumber, BigNumber, BigNumber, BigNumber, BigNumber, BigNumber]; -export interface LogEvent<ArgsType> extends LogWithDecodedArgs<ArgsType> { - removed: boolean; -} -export type EventCallbackAsync<ArgsType> = (err: null|Error, log?: LogEvent<ArgsType>) => Promise<void>; -export type EventCallbackSync<ArgsType> = (err: null|Error, log?: LogEvent<ArgsType>) => void; +export type LogEvent = Web3.LogEntryEvent; +export type DecodedLogEvent<ArgsType> = Web3.DecodedLogEntryEvent<ArgsType>; + +export type EventCallbackAsync<ArgsType> = (err: null|Error, log?: DecodedLogEvent<ArgsType>) => Promise<void>; +export type EventCallbackSync<ArgsType> = (err: null|Error, log?: DecodedLogEvent<ArgsType>) => void; export type EventCallback<ArgsType> = EventCallbackSync<ArgsType>|EventCallbackAsync<ArgsType>; + +export type EventWatcherCallbackSync = (log: LogEvent) => void; +export type EventWatcherCallbackAsync = (log: LogEvent) => Promise<void>; +export type EventWatcherCallback = EventWatcherCallbackSync|EventWatcherCallbackAsync; + export interface ExchangeContract extends Web3.ContractInstance { isValidSignature: { callAsync: (signerAddressHex: string, dataHex: string, v: number, r: string, s: string, @@ -391,16 +397,27 @@ export interface JSONRPCPayload { } /* + * 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; + numConfirmations: 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 + * 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; + orderWatcherConfig?: OrderStateWatcherConfig; } export type TransactionReceipt = Web3.TransactionReceipt; @@ -416,12 +433,7 @@ export interface DecodedLogArgs { [argName: string]: ContractEventArg; } -export interface DecodedArgs<ArgsType> { - args: ArgsType; - event: string; -} - -export interface LogWithDecodedArgs<ArgsType> extends Web3.LogEntry, DecodedArgs<ArgsType> {} +export interface LogWithDecodedArgs<ArgsType> extends Web3.DecodedLogEntry<ArgsType> {} export interface TransactionReceiptWithDecodedLogs extends Web3.TransactionReceipt { logs: Array<LogWithDecodedArgs<DecodedLogArgs>|Web3.LogEntry>; @@ -473,3 +485,31 @@ export enum TransferType { Trade = 'trade', Fee = 'fee', } + +export interface OrderRelevantState { + makerBalance: BigNumber; + makerProxyAllowance: BigNumber; + makerFeeBalance: BigNumber; + makerFeeProxyAllowance: BigNumber; + filledTakerTokenAmount: BigNumber; + canceledTakerTokenAmount: BigNumber; + remainingFillableMakerTokenAmount: BigNumber; +} + +export interface OrderStateValid { + isValid: true; + orderHash: string; + orderRelevantState: OrderRelevantState; +} + +export interface OrderStateInvalid { + isValid: false; + orderHash: string; + error: ExchangeContractErrs; +} + +export type OrderState = OrderStateValid|OrderStateInvalid; + +export type OnOrderStateChangeCallbackSync = (orderState: OrderState) => void; +export type OnOrderStateChangeCallbackAsync = (orderState: OrderState) => Promise<void>; +export type OnOrderStateChangeCallback = OnOrderStateChangeCallbackAsync|OnOrderStateChangeCallbackSync; 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<ArgsType extends ContractEventArgs>( log: Web3.LogEntry): LogWithDecodedArgs<ArgsType>|RawLog { const methodId = log.topics[0]; diff --git a/src/utils/assert.ts b/src/utils/assert.ts index 286105345..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; @@ -11,6 +13,17 @@ export const assert = { const isBigNumber = _.isObject(value) && (value as any).isBigNumber; 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()}`, + ); + }, + 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)); }, diff --git a/src/utils/order_state_utils.ts b/src/utils/order_state_utils.ts new file mode 100644 index 000000000..36a4b68d6 --- /dev/null +++ b/src/utils/order_state_utils.ts @@ -0,0 +1,110 @@ +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<OrderState> { + 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<OrderRelevantState> { + 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 unavailableTakerTokenAmount = + await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash, methodOpts); + const totalMakerTokenAmount = signedOrder.makerTokenAmount; + const totalTakerTokenAmount = signedOrder.takerTokenAmount; + const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount); + const remainingMakerTokenAmount = remainingTakerTokenAmount.times(totalMakerTokenAmount) + .dividedToIntegerBy(totalTakerTokenAmount); + const fillableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]); + const remainingFillableMakerTokenAmount = BigNumber.min(fillableMakerTokenAmount, remainingMakerTokenAmount); + // TODO: Handle edge case where maker token is ZRX with fee + const orderRelevantState = { + makerBalance, + makerProxyAllowance, + makerFeeBalance, + makerFeeProxyAllowance, + filledTakerTokenAmount, + canceledTakerTokenAmount, + remainingFillableMakerTokenAmount, + }; + 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/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]; diff --git a/src/web3_wrapper.ts b/src/web3_wrapper.ts index 3b1e4477b..d03c102b2 100644 --- a/src/web3_wrapper.ts +++ b/src/web3_wrapper.ts @@ -10,10 +10,16 @@ export class Web3Wrapper { private defaults: Partial<Web3.TxData>; private networkIdIfExists?: number; private jsonRpcRequestId: number; - constructor(provider: Web3.Provider, defaults: Partial<Web3.TxData>) { + constructor(provider: Web3.Provider, defaults?: Partial<Web3.TxData>) { + 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) { @@ -94,6 +100,10 @@ export class Web3Wrapper { const signData = await promisify(this.web3.eth.sign)(address, message); return signData; } + public async getBlockNumberAsync(): Promise<number> { + const blockNumber = await promisify(this.web3.eth.getBlockNumber)(); + return blockNumber; + } public async getBlockAsync(blockParam: string|Web3.BlockParam): Promise<Web3.BlockWithoutTransactionData> { const block = await promisify(this.web3.eth.getBlock)(blockParam); return block; diff --git a/test/event_watcher_test.ts b/test/event_watcher_test.ts new file mode 100644 index 000000000..98dab93b5 --- /dev/null +++ b/test/event_watcher_test.ts @@ -0,0 +1,127 @@ +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/order_watcher/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 numConfirmations = 0; + const logA: Web3.LogEntry = { + address: '0x71d271f8b14adef568f8f28f1587ce7271ac4ca5', + blockHash: null, + blockNumber: null, + data: '', + logIndex: null, + topics: [], + transactionHash: '0x004881d38cd4a8f72f1a0d68c8b9b8124504706041ff37019c1d1ed6bfda8e17', + transactionIndex: 0, + }; + const logB: Web3.LogEntry = { + address: '0x8d12a197cb00d4747a1fe03395095ce2a5cc6819', + blockHash: null, + blockNumber: null, + data: '', + logIndex: null, + topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ], + transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25', + transactionIndex: 0, + }; + const logC: Web3.LogEntry = { + address: '0x1d271f8b174adef58f1587ce68f8f27271ac4ca5', + blockHash: null, + blockNumber: null, + data: '', + logIndex: null, + topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ], + transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25', + transactionIndex: 0, + }; + before(async () => { + web3 = web3Factory.create(); + const pollingIntervalMs = 10; + web3Wrapper = new Web3Wrapper(web3.currentProvider); + eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalMs, numConfirmations); + }); + 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/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index 654f626c6..26b8c1e0e 100644 --- a/test/exchange_wrapper_test.ts +++ b/test/exchange_wrapper_test.ts @@ -11,13 +11,13 @@ import { SignedOrder, SubscriptionOpts, ExchangeEvents, - ContractEvent, ExchangeContractErrs, OrderCancellationRequest, OrderFillRequest, LogFillContractEventArgs, LogCancelContractEventArgs, LogEvent, + DecodedLogEvent, } from '../src'; import {DoneCallback, BlockParamLiteral} from '../src/types'; import {FillScenarios} from './utils/fill_scenarios'; @@ -70,7 +70,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( @@ -647,7 +647,8 @@ 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 = (err: Error, logEvent: LogEvent<LogFillContractEventArgs>) => { + + const callback = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill); done(); }; @@ -662,7 +663,8 @@ describe('ExchangeWrapper', () => { }); it('Should receive the LogCancel event when an order is cancelled', (done: DoneCallback) => { (async () => { - const callback = (err: Error, logEvent: LogEvent<LogCancelContractEventArgs>) => { + + const callback = (err: Error, logEvent: DecodedLogEvent<LogCancelContractEventArgs>) => { expect(logEvent.event).to.be.equal(ExchangeEvents.LogCancel); done(); }; @@ -674,7 +676,8 @@ describe('ExchangeWrapper', () => { }); it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => { (async () => { - const callbackNeverToBeCalled = (err: Error, logEvent: LogEvent<LogFillContractEventArgs>) => { + + const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { done(new Error('Expected this subscription to have been cancelled')); }; await zeroEx.exchange.subscribeAsync( @@ -684,7 +687,7 @@ describe('ExchangeWrapper', () => { const newProvider = web3Factory.getRpcProvider(); await zeroEx.setProviderAsync(newProvider); - const callback = (err: Error, logEvent: LogEvent<LogFillContractEventArgs>) => { + const callback = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill); done(); }; @@ -699,7 +702,7 @@ describe('ExchangeWrapper', () => { }); it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { (async () => { - const callbackNeverToBeCalled = (err: Error, logEvent: LogEvent<LogFillContractEventArgs>) => { + const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => { done(new Error('Expected this subscription to have been cancelled')); }; const subscriptionToken = await zeroEx.exchange.subscribeAsync( diff --git a/test/order_state_watcher_test.ts b/test/order_state_watcher_test.ts new file mode 100644 index 000000000..41f938584 --- /dev/null +++ b/test/order_state_watcher_test.ts @@ -0,0 +1,421 @@ +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/order_watcher/order_state_watcher'; +import { + Token, + ZeroEx, + LogEvent, + DecodedLogEvent, + ZeroExConfig, + 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 {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; + 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; + }); + 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); + 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)._orderByOrderHash).to.not.include({ + [orderHash]: signedOrder, + }); + dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes; + expect(dependentOrderHashes[signedOrder.maker]).to.be.undefined(); + }); + 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.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(); + 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 () => { + 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.InsufficientMakerAllowance); + done(); + }); + zeroEx.orderStateWatcher.subscribe(callback); + 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); + setTimeout(() => { + done(); + }, TIMEOUT_MS); + })().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 = 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); + })().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 = reportCallbackErrors(done)((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 = 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; + const remainingMakerBalance = makerBalance.sub(fillAmountInBaseUnits); + const remainingFillable = fillableAmount.minus(fillAmountInBaseUnits); + expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal( + remainingFillable); + 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); + }); + describe('remainingFillableMakerTokenAmount', () => { + it('should calculate correct remaining 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('should equal approved amount when approved amount is lowest', (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('should equal balance amount when balance amount is lowest', (done: DoneCallback) => { + (async () => { + signedOrder = await fillScenarios.createFillableSignedOrderAsync( + makerToken.address, takerToken.address, maker, taker, fillableAmount, + ); + + const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker); + + 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( + remainingAmount); + done(); + }); + zeroEx.orderStateWatcher.subscribe(callback); + await zeroEx.token.transferAsync( + makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferAmount); + })().catch(done); + }); + }); + it('should emit orderStateInvalid 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); + + const shouldThrowOnInsufficientBalanceOrAllowance = true; + await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount); + })().catch(done); + }); + it('should emit orderStateValid when watched order partially cancelled', (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); + 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/token_wrapper_test.ts b/test/token_wrapper_test.ts index d9d0fb9ad..40652d03c 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 = (err: Error, logEvent: LogEvent<TransferContractEventArgs>) => { + const callback = (err: Error, logEvent: DecodedLogEvent<TransferContractEventArgs>) => { 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 = (err: Error, logEvent: LogEvent<ApprovalContractEventArgs>) => { + const callback = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => { 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 = (err: Error, logEvent: LogEvent<TransferContractEventArgs>) => { + const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => { done(new Error('Expected this subscription to have been cancelled')); }; zeroEx.token.subscribe( tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackNeverToBeCalled, ); - const callbackToBeCalled = (err: Error, logEvent: LogEvent<TransferContractEventArgs>) => { + const callbackToBeCalled = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => { done(); }; const newProvider = web3Factory.getRpcProvider(); @@ -407,7 +408,7 @@ describe('TokenWrapper', () => { }); it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { (async () => { - const callbackNeverToBeCalled = (err: Error, logEvent: LogEvent<TokenContractEventArgs>) => { + const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => { done(new Error('Expected this subscription to have been cancelled')); }; const subscriptionToken = zeroEx.token.subscribe( 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<void> { + await this.rpc.mineBlockAsync(); + } } 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; + }; +}; diff --git a/test/utils/rpc.ts b/test/utils/rpc.ts index f28a85340..299e72e79 100644 --- a/test/utils/rpc.ts +++ b/test/utils/rpc.ts @@ -26,6 +26,12 @@ export class RPC { const didRevert = await this.sendAsync(payload); return didRevert; } + public async mineBlockAsync(): Promise<void> { + const method = 'evm_mine'; + const params: any[] = []; + const payload = this.toPayload(method, params); + await this.sendAsync(payload); + } private toPayload(method: string, params: any[] = []): string { const payload = JSON.stringify({ id: this.id, @@ -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.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" |