aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--package.json4
-rw-r--r--src/0x.ts34
-rw-r--r--src/contract_wrappers/ether_token_wrapper.ts4
-rw-r--r--src/contract_wrappers/exchange_wrapper.ts21
-rw-r--r--src/contract_wrappers/token_wrapper.ts8
-rw-r--r--src/index.ts6
-rw-r--r--src/order_watcher/event_watcher.ts97
-rw-r--r--src/order_watcher/order_state_watcher.ts202
-rw-r--r--src/schemas/zero_ex_config_schema.ts13
-rw-r--r--src/types.ts62
-rw-r--r--src/utils/abi_decoder.ts2
-rw-r--r--src/utils/assert.ts17
-rw-r--r--src/utils/order_state_utils.ts110
-rw-r--r--src/utils/signature_utils.ts15
-rw-r--r--src/web3_wrapper.ts14
-rw-r--r--test/event_watcher_test.ts127
-rw-r--r--test/exchange_wrapper_test.ts17
-rw-r--r--test/order_state_watcher_test.ts421
-rw-r--r--test/token_wrapper_test.ts11
-rw-r--r--test/utils/blockchain_lifecycle.ts3
-rw-r--r--test/utils/report_callback_errors.ts14
-rw-r--r--test/utils/rpc.ts6
-rw-r--r--yarn.lock8
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": {
diff --git a/src/0x.ts b/src/0x.ts
index 43430e544..a1841eaa8 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 './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,
diff --git a/yarn.lock b/yarn.lock
index eefd9a429..55f3f7d1b 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.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"