aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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.ts61
-rw-r--r--src/utils/abi_decoder.ts2
-rw-r--r--src/utils/assert.ts17
-rw-r--r--src/utils/order_state_utils.ts99
-rw-r--r--src/utils/signature_utils.ts15
-rw-r--r--src/web3_wrapper.ts14
14 files changed, 541 insertions, 52 deletions
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 b8704e72c..f68e80b5d 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,
@@ -167,7 +166,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);
@@ -240,7 +239,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);
@@ -409,7 +408,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();
@@ -544,7 +543,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();
@@ -739,7 +738,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);
@@ -755,7 +754,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(
@@ -773,7 +772,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);
@@ -792,9 +791,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 5d6d61cef..dd87ebc9f 100644
--- a/src/contract_wrappers/token_wrapper.ts
+++ b/src/contract_wrappers/token_wrapper.ts
@@ -72,7 +72,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
@@ -152,7 +152,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);
@@ -187,7 +187,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);
@@ -221,7 +221,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 a9eac56d8..160b71fda 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> = (log: LogEvent<ArgsType>) => Promise<void>;
-export type EventCallbackSync<ArgsType> = (log: LogEvent<ArgsType>) => void;
+export type LogEvent = Web3.LogEntryEvent;
+export type DecodedLogEvent<ArgsType> = Web3.DecodedLogEntryEvent<ArgsType>;
+
+export type EventCallbackAsync<ArgsType> = (log: DecodedLogEvent<ArgsType>) => Promise<void>;
+export type EventCallbackSync<ArgsType> = (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,30 @@ export enum TransferType {
Trade = 'trade',
Fee = 'fee',
}
+
+export interface OrderRelevantState {
+ makerBalance: BigNumber;
+ makerProxyAllowance: BigNumber;
+ makerFeeBalance: BigNumber;
+ makerFeeProxyAllowance: BigNumber;
+ filledTakerTokenAmount: BigNumber;
+ canceledTakerTokenAmount: 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..2a5becf9a
--- /dev/null
+++ b/src/utils/order_state_utils.ts
@@ -0,0 +1,99 @@
+import * as _ from 'lodash';
+import BigNumber from 'bignumber.js';
+import {
+ ExchangeContractErrs,
+ SignedOrder,
+ OrderRelevantState,
+ MethodOpts,
+ OrderState,
+ OrderStateValid,
+ OrderStateInvalid,
+} from '../types';
+import {ZeroEx} from '../0x';
+import {TokenWrapper} from '../contract_wrappers/token_wrapper';
+import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
+import {utils} from '../utils/utils';
+import {constants} from '../utils/constants';
+
+export class OrderStateUtils {
+ private tokenWrapper: TokenWrapper;
+ private exchangeWrapper: ExchangeWrapper;
+ constructor(tokenWrapper: TokenWrapper, exchangeWrapper: ExchangeWrapper) {
+ this.tokenWrapper = tokenWrapper;
+ this.exchangeWrapper = exchangeWrapper;
+ }
+ public async getOrderStateAsync(signedOrder: SignedOrder, methodOpts?: MethodOpts): Promise<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 orderRelevantState = {
+ makerBalance,
+ makerProxyAllowance,
+ makerFeeBalance,
+ makerFeeProxyAllowance,
+ filledTakerTokenAmount,
+ canceledTakerTokenAmount,
+ };
+ return orderRelevantState;
+ }
+ private validateIfOrderIsValid(signedOrder: SignedOrder, orderRelevantState: OrderRelevantState): void {
+ const unavailableTakerTokenAmount = orderRelevantState.canceledTakerTokenAmount.add(
+ orderRelevantState.filledTakerTokenAmount,
+ );
+ const availableTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount);
+ if (availableTakerTokenAmount.eq(0)) {
+ throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero);
+ }
+
+ if (orderRelevantState.makerBalance.eq(0)) {
+ throw new Error(ExchangeContractErrs.InsufficientMakerBalance);
+ }
+ if (orderRelevantState.makerProxyAllowance.eq(0)) {
+ throw new Error(ExchangeContractErrs.InsufficientMakerAllowance);
+ }
+ if (!signedOrder.makerFee.eq(0)) {
+ if (orderRelevantState.makerFeeBalance.eq(0)) {
+ throw new Error(ExchangeContractErrs.InsufficientMakerFeeBalance);
+ }
+ if (orderRelevantState.makerFeeProxyAllowance.eq(0)) {
+ throw new Error(ExchangeContractErrs.InsufficientMakerFeeAllowance);
+ }
+ }
+ // TODO Add linear function solver when maker token is ZRX #badass
+ // Return the max amount that's fillable
+ }
+}
diff --git a/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;