aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/0x.ts33
-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.ts88
-rw-r--r--src/order_watcher/order_state_watcher.ts232
-rw-r--r--src/schemas/zero_ex_config_schema.ts13
-rw-r--r--src/stores/balance_proxy_allowance_lazy_store.ts82
-rw-r--r--src/stores/order_filled_cancelled_lazy_store.ts61
-rw-r--r--src/types.ts60
-rw-r--r--src/utils/abi_decoder.ts2
-rw-r--r--src/utils/assert.ts17
-rw-r--r--src/utils/exchange_transfer_simulator.ts76
-rw-r--r--src/utils/order_state_utils.ts119
-rw-r--r--src/utils/signature_utils.ts15
-rw-r--r--src/web3_wrapper.ts14
17 files changed, 739 insertions, 112 deletions
diff --git a/src/0x.ts b/src/0x.ts
index 43430e544..fe765bbbe 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,10 @@ 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;
+ this.orderStateWatcher = new OrderStateWatcher(
+ this._web3Wrapper, this._abiDecoder, this.token, this.exchange, 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..81529a98c
--- /dev/null
+++ b/src/order_watcher/event_watcher.ts
@@ -0,0 +1,88 @@
+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[] = [];
+ constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) {
+ this._web3Wrapper = web3Wrapper;
+ 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[]> {
+ const eventFilter = {
+ fromBlock: BlockParamLiteral.Pending,
+ toBlock: BlockParamLiteral.Pending,
+ };
+ 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..139f13fdf
--- /dev/null
+++ b/src/order_watcher/order_state_watcher.ts
@@ -0,0 +1,232 @@
+import * as _ from 'lodash';
+import {schemas} from '0x-json-schemas';
+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,
+ ContractEventArgs,
+ OnOrderStateChangeCallback,
+ OrderStateWatcherConfig,
+ ApprovalContractEventArgs,
+ TransferContractEventArgs,
+ LogFillContractEventArgs,
+ LogCancelContractEventArgs,
+ ExchangeEvents,
+ TokenEvents,
+ ZeroExError,
+} from '../types';
+import {Web3Wrapper} from '../web3_wrapper';
+import {TokenWrapper} from '../contract_wrappers/token_wrapper';
+import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
+import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store';
+import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
+
+const DEFAULT_NUM_CONFIRMATIONS = 0;
+
+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 _callbackIfExistsAsync?: OnOrderStateChangeCallback;
+ private _eventWatcher: EventWatcher;
+ private _web3Wrapper: Web3Wrapper;
+ private _abiDecoder: AbiDecoder;
+ private _orderStateUtils: OrderStateUtils;
+ private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore;
+ private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore;
+ constructor(
+ web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, token: TokenWrapper, exchange: ExchangeWrapper,
+ config?: OrderStateWatcherConfig,
+ ) {
+ this._abiDecoder = abiDecoder;
+ this._web3Wrapper = web3Wrapper;
+ const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs;
+ this._eventWatcher = new EventWatcher(web3Wrapper, eventPollingIntervalMs);
+ this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore(token);
+ this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore(exchange);
+ this._orderStateUtils = new OrderStateUtils(
+ this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore,
+ );
+ }
+ /**
+ * 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 {
+ if (_.isUndefined(this._callbackIfExistsAsync)) {
+ throw new Error(ZeroExError.SubscriptionNotFound);
+ }
+ this._balanceAndProxyAllowanceLazyStore.deleteAll();
+ this._orderFilledCancelledLazyStore.deleteAll();
+ 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
+ }
+ const decodedLog = maybeDecodedLog as LogWithDecodedArgs<ContractEventArgs>;
+ let makerToken: string;
+ let makerAddress: string;
+ let orderHashesSet: Set<string>;
+ switch (decodedLog.event) {
+ case TokenEvents.Approval:
+ {
+ // Invalidate cache
+ const args = decodedLog.args as ApprovalContractEventArgs;
+ this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(decodedLog.address, args._owner);
+ // Revalidate orders
+ makerToken = decodedLog.address;
+ makerAddress = args._owner;
+ orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]);
+ if (!_.isUndefined(orderHashesSet)) {
+ const orderHashes = Array.from(orderHashesSet);
+ await this._emitRevalidateOrdersAsync(orderHashes);
+ }
+ break;
+ }
+ case TokenEvents.Transfer:
+ {
+ // Invalidate cache
+ const args = decodedLog.args as TransferContractEventArgs;
+ this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._from);
+ this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._to);
+ // Revalidate orders
+ makerToken = decodedLog.address;
+ makerAddress = args._from;
+ orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]);
+ if (!_.isUndefined(orderHashesSet)) {
+ const orderHashes = Array.from(orderHashesSet);
+ await this._emitRevalidateOrdersAsync(orderHashes);
+ }
+ break;
+ }
+ case ExchangeEvents.LogFill:
+ {
+ // Invalidate cache
+ const args = decodedLog.args as LogFillContractEventArgs;
+ this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(args.orderHash);
+ // Revalidate orders
+ const orderHash = args.orderHash;
+ const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]);
+ if (isOrderWatched) {
+ await this._emitRevalidateOrdersAsync([orderHash]);
+ }
+ break;
+ }
+ case ExchangeEvents.LogCancel:
+ {
+ // Invalidate cache
+ const args = decodedLog.args as LogCancelContractEventArgs;
+ this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(args.orderHash);
+ // Revalidate orders
+ const orderHash = args.orderHash;
+ const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]);
+ if (isOrderWatched) {
+ await this._emitRevalidateOrdersAsync([orderHash]);
+ }
+ break;
+ }
+ case ExchangeEvents.LogError:
+ return; // noop
+
+ default:
+ throw utils.spawnSwitchErr('decodedLog.event', decodedLog.event);
+ }
+ }
+ private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise<void> {
+ for (const orderHash of orderHashes) {
+ const signedOrder = this._orderByOrderHash[orderHash] as SignedOrder;
+ // Most of these calls will never reach the network because the data is fetched from stores
+ // and only updated when cache is invalidated
+ const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder);
+ if (_.isUndefined(this._callbackIfExistsAsync)) {
+ break; // Unsubscribe was called
+ }
+ await this._callbackIfExistsAsync(orderState);
+ }
+ }
+ 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/stores/balance_proxy_allowance_lazy_store.ts b/src/stores/balance_proxy_allowance_lazy_store.ts
new file mode 100644
index 000000000..c83e61606
--- /dev/null
+++ b/src/stores/balance_proxy_allowance_lazy_store.ts
@@ -0,0 +1,82 @@
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+import {BigNumber} from 'bignumber.js';
+import {TokenWrapper} from '../contract_wrappers/token_wrapper';
+import {BlockParamLiteral} from '../types';
+
+/**
+ * Copy on read store for balances/proxyAllowances of tokens/accounts
+ */
+export class BalanceAndProxyAllowanceLazyStore {
+ private token: TokenWrapper;
+ private balance: {
+ [tokenAddress: string]: {
+ [userAddress: string]: BigNumber,
+ },
+ };
+ private proxyAllowance: {
+ [tokenAddress: string]: {
+ [userAddress: string]: BigNumber,
+ },
+ };
+ constructor(token: TokenWrapper) {
+ this.token = token;
+ this.balance = {};
+ this.proxyAllowance = {};
+ }
+ public async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
+ if (_.isUndefined(this.balance[tokenAddress]) || _.isUndefined(this.balance[tokenAddress][userAddress])) {
+ const methodOpts = {
+ defaultBlock: BlockParamLiteral.Pending,
+ };
+ const balance = await this.token.getBalanceAsync(tokenAddress, userAddress, methodOpts);
+ this.setBalance(tokenAddress, userAddress, balance);
+ }
+ const cachedBalance = this.balance[tokenAddress][userAddress];
+ return cachedBalance;
+ }
+ public setBalance(tokenAddress: string, userAddress: string, balance: BigNumber): void {
+ if (_.isUndefined(this.balance[tokenAddress])) {
+ this.balance[tokenAddress] = {};
+ }
+ this.balance[tokenAddress][userAddress] = balance;
+ }
+ public deleteBalance(tokenAddress: string, userAddress: string): void {
+ if (!_.isUndefined(this.balance[tokenAddress])) {
+ delete this.balance[tokenAddress][userAddress];
+ if (_.isEmpty(this.balance[tokenAddress])) {
+ delete this.balance[tokenAddress];
+ }
+ }
+ }
+ public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
+ if (_.isUndefined(this.proxyAllowance[tokenAddress]) ||
+ _.isUndefined(this.proxyAllowance[tokenAddress][userAddress])) {
+ const methodOpts = {
+ defaultBlock: BlockParamLiteral.Pending,
+ };
+ const proxyAllowance = await this.token.getProxyAllowanceAsync(tokenAddress, userAddress, methodOpts);
+ this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance);
+ }
+ const cachedProxyAllowance = this.proxyAllowance[tokenAddress][userAddress];
+ return cachedProxyAllowance;
+ }
+ public setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber): void {
+ if (_.isUndefined(this.proxyAllowance[tokenAddress])) {
+ this.proxyAllowance[tokenAddress] = {};
+ }
+ this.proxyAllowance[tokenAddress][userAddress] = proxyAllowance;
+ }
+ public deleteProxyAllowance(tokenAddress: string, userAddress: string): void {
+ if (!_.isUndefined(this.proxyAllowance[tokenAddress])) {
+ delete this.proxyAllowance[tokenAddress][userAddress];
+ if (_.isEmpty(this.proxyAllowance[tokenAddress])) {
+ delete this.proxyAllowance[tokenAddress];
+ }
+ }
+ }
+ public deleteAll(): void {
+ this.balance = {};
+ this.proxyAllowance = {};
+ }
+}
diff --git a/src/stores/order_filled_cancelled_lazy_store.ts b/src/stores/order_filled_cancelled_lazy_store.ts
new file mode 100644
index 000000000..9d74da096
--- /dev/null
+++ b/src/stores/order_filled_cancelled_lazy_store.ts
@@ -0,0 +1,61 @@
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+import {BigNumber} from 'bignumber.js';
+import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
+import {BlockParamLiteral} from '../types';
+
+/**
+ * Copy on read store for filled/cancelled taker amounts
+ */
+export class OrderFilledCancelledLazyStore {
+ private exchange: ExchangeWrapper;
+ private filledTakerAmount: {
+ [orderHash: string]: BigNumber,
+ };
+ private cancelledTakerAmount: {
+ [orderHash: string]: BigNumber,
+ };
+ constructor(exchange: ExchangeWrapper) {
+ this.exchange = exchange;
+ this.filledTakerAmount = {};
+ this.cancelledTakerAmount = {};
+ }
+ public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
+ if (_.isUndefined(this.filledTakerAmount[orderHash])) {
+ const methodOpts = {
+ defaultBlock: BlockParamLiteral.Pending,
+ };
+ const filledTakerAmount = await this.exchange.getFilledTakerAmountAsync(orderHash, methodOpts);
+ this.setFilledTakerAmount(orderHash, filledTakerAmount);
+ }
+ const cachedFilled = this.filledTakerAmount[orderHash];
+ return cachedFilled;
+ }
+ public setFilledTakerAmount(orderHash: string, filledTakerAmount: BigNumber): void {
+ this.filledTakerAmount[orderHash] = filledTakerAmount;
+ }
+ public deleteFilledTakerAmount(orderHash: string): void {
+ delete this.filledTakerAmount[orderHash];
+ }
+ public async getCancelledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
+ if (_.isUndefined(this.cancelledTakerAmount[orderHash])) {
+ const methodOpts = {
+ defaultBlock: BlockParamLiteral.Pending,
+ };
+ const cancelledTakerAmount = await this.exchange.getCanceledTakerAmountAsync(orderHash, methodOpts);
+ this.setCancelledTakerAmount(orderHash, cancelledTakerAmount);
+ }
+ const cachedCancelled = this.cancelledTakerAmount[orderHash];
+ return cachedCancelled;
+ }
+ public setCancelledTakerAmount(orderHash: string, cancelledTakerAmount: BigNumber): void {
+ this.cancelledTakerAmount[orderHash] = cancelledTakerAmount;
+ }
+ public deleteCancelledTakerAmount(orderHash: string): void {
+ delete this.cancelledTakerAmount[orderHash];
+ }
+ public deleteAll(): void {
+ this.filledTakerAmount = {};
+ this.cancelledTakerAmount = {};
+ }
+}
diff --git a/src/types.ts b/src/types.ts
index af3c36736..20f994ac7 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,25 @@ export interface JSONRPCPayload {
}
/*
+ * eventPollingIntervalMs: How often to poll the Ethereum node for new events
+ */
+export interface OrderStateWatcherConfig {
+ eventPollingIntervalMs?: 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 +431,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 +483,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/exchange_transfer_simulator.ts b/src/utils/exchange_transfer_simulator.ts
index 89b23c8ab..308ef06db 100644
--- a/src/utils/exchange_transfer_simulator.ts
+++ b/src/utils/exchange_transfer_simulator.ts
@@ -1,7 +1,8 @@
import * as _ from 'lodash';
import BigNumber from 'bignumber.js';
-import {ExchangeContractErrs, TradeSide, TransferType} from '../types';
+import {ExchangeContractErrs, TradeSide, TransferType, BlockParamLiteral} from '../types';
import {TokenWrapper} from '../contract_wrappers/token_wrapper';
+import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
enum FailureReason {
Balance = 'balance',
@@ -31,58 +32,13 @@ const ERR_MSG_MAPPING = {
},
};
-/**
- * Copy on read store for balances/proxyAllowances of tokens/accounts touched in trades
- */
-export class BalanceAndProxyAllowanceLazyStore {
- protected _token: TokenWrapper;
- private _balance: {
- [tokenAddress: string]: {
- [userAddress: string]: BigNumber,
- },
- };
- private _proxyAllowance: {
- [tokenAddress: string]: {
- [userAddress: string]: BigNumber,
- },
- };
+export class ExchangeTransferSimulator {
+ private store: BalanceAndProxyAllowanceLazyStore;
+ private UNLIMITED_ALLOWANCE_IN_BASE_UNITS: BigNumber;
constructor(token: TokenWrapper) {
- this._token = token;
- this._balance = {};
- this._proxyAllowance = {};
- }
- protected async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
- if (_.isUndefined(this._balance[tokenAddress]) || _.isUndefined(this._balance[tokenAddress][userAddress])) {
- const balance = await this._token.getBalanceAsync(tokenAddress, userAddress);
- this.setBalance(tokenAddress, userAddress, balance);
- }
- const cachedBalance = this._balance[tokenAddress][userAddress];
- return cachedBalance;
- }
- protected setBalance(tokenAddress: string, userAddress: string, balance: BigNumber): void {
- if (_.isUndefined(this._balance[tokenAddress])) {
- this._balance[tokenAddress] = {};
- }
- this._balance[tokenAddress][userAddress] = balance;
+ this.store = new BalanceAndProxyAllowanceLazyStore(token);
+ this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS = token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
}
- protected async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
- if (_.isUndefined(this._proxyAllowance[tokenAddress]) ||
- _.isUndefined(this._proxyAllowance[tokenAddress][userAddress])) {
- const proxyAllowance = await this._token.getProxyAllowanceAsync(tokenAddress, userAddress);
- this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance);
- }
- const cachedProxyAllowance = this._proxyAllowance[tokenAddress][userAddress];
- return cachedProxyAllowance;
- }
- protected setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber): void {
- if (_.isUndefined(this._proxyAllowance[tokenAddress])) {
- this._proxyAllowance[tokenAddress] = {};
- }
- this._proxyAllowance[tokenAddress][userAddress] = proxyAllowance;
- }
-}
-
-export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore {
/**
* Simulates transferFrom call performed by a proxy
* @param tokenAddress Address of the token to be transferred
@@ -95,8 +51,8 @@ export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore
public async transferFromAsync(tokenAddress: string, from: string, to: string,
amountInBaseUnits: BigNumber, tradeSide: TradeSide,
transferType: TransferType): Promise<void> {
- const balance = await this.getBalanceAsync(tokenAddress, from);
- const proxyAllowance = await this.getProxyAllowanceAsync(tokenAddress, from);
+ const balance = await this.store.getBalanceAsync(tokenAddress, from);
+ const proxyAllowance = await this.store.getProxyAllowanceAsync(tokenAddress, from);
if (proxyAllowance.lessThan(amountInBaseUnits)) {
this.throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType);
}
@@ -109,20 +65,20 @@ export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore
}
private async decreaseProxyAllowanceAsync(tokenAddress: string, userAddress: string,
amountInBaseUnits: BigNumber): Promise<void> {
- const proxyAllowance = await this.getProxyAllowanceAsync(tokenAddress, userAddress);
- if (!proxyAllowance.eq(this._token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
- this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits));
+ const proxyAllowance = await this.store.getProxyAllowanceAsync(tokenAddress, userAddress);
+ if (!proxyAllowance.eq(this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
+ this.store.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits));
}
}
private async increaseBalanceAsync(tokenAddress: string, userAddress: string,
amountInBaseUnits: BigNumber): Promise<void> {
- const balance = await this.getBalanceAsync(tokenAddress, userAddress);
- this.setBalance(tokenAddress, userAddress, balance.plus(amountInBaseUnits));
+ const balance = await this.store.getBalanceAsync(tokenAddress, userAddress);
+ this.store.setBalance(tokenAddress, userAddress, balance.plus(amountInBaseUnits));
}
private async decreaseBalanceAsync(tokenAddress: string, userAddress: string,
amountInBaseUnits: BigNumber): Promise<void> {
- const balance = await this.getBalanceAsync(tokenAddress, userAddress);
- this.setBalance(tokenAddress, userAddress, balance.minus(amountInBaseUnits));
+ const balance = await this.store.getBalanceAsync(tokenAddress, userAddress);
+ this.store.setBalance(tokenAddress, userAddress, balance.minus(amountInBaseUnits));
}
private throwValidationError(failureReason: FailureReason, tradeSide: TradeSide,
transferType: TransferType): Promise<never> {
diff --git a/src/utils/order_state_utils.ts b/src/utils/order_state_utils.ts
new file mode 100644
index 000000000..f82601cae
--- /dev/null
+++ b/src/utils/order_state_utils.ts
@@ -0,0 +1,119 @@
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+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';
+import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store';
+import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
+
+export class OrderStateUtils {
+ private balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore;
+ private orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore;
+ constructor(balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore,
+ orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore) {
+ this.balanceAndProxyAllowanceLazyStore = balanceAndProxyAllowanceLazyStore;
+ this.orderFilledCancelledLazyStore = orderFilledCancelledLazyStore;
+ }
+ public async getOrderStateAsync(signedOrder: SignedOrder): Promise<OrderState> {
+ const orderRelevantState = await this.getOrderRelevantStateAsync(signedOrder);
+ 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): Promise<OrderRelevantState> {
+ // HACK: We access the private property here but otherwise the interface will be less nice.
+ // If we pass it from the instantiator - there is no opportunity to get it there
+ // because JS doesn't support async constructors.
+ // Moreover - it's cached under the hood so it's equivalent to an async constructor.
+ const exchange = (this.orderFilledCancelledLazyStore as any).exchange as ExchangeWrapper;
+ const zrxTokenAddress = await exchange.getZRXTokenAddressAsync();
+ const orderHash = ZeroEx.getOrderHashHex(signedOrder);
+ const makerBalance = await this.balanceAndProxyAllowanceLazyStore.getBalanceAsync(
+ signedOrder.makerTokenAddress, signedOrder.maker,
+ );
+ const makerProxyAllowance = await this.balanceAndProxyAllowanceLazyStore.getProxyAllowanceAsync(
+ signedOrder.makerTokenAddress, signedOrder.maker,
+ );
+ const makerFeeBalance = await this.balanceAndProxyAllowanceLazyStore.getBalanceAsync(
+ zrxTokenAddress, signedOrder.maker,
+ );
+ const makerFeeProxyAllowance = await this.balanceAndProxyAllowanceLazyStore.getProxyAllowanceAsync(
+ zrxTokenAddress, signedOrder.maker,
+ );
+ const filledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getFilledTakerAmountAsync(orderHash);
+ const canceledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getCancelledTakerAmountAsync(
+ orderHash,
+ );
+ const unavailableTakerTokenAmount = await exchange.getUnavailableTakerAmountAsync(orderHash);
+ const totalMakerTokenAmount = signedOrder.makerTokenAmount;
+ const totalTakerTokenAmount = signedOrder.takerTokenAmount;
+ const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount);
+ 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;