diff options
author | Fabio Berger <me@fabioberger.com> | 2017-11-13 10:12:37 +0800 |
---|---|---|
committer | Fabio Berger <me@fabioberger.com> | 2017-11-13 10:12:37 +0800 |
commit | e33027c6244b99a1fb181404668bd2a259d923e2 (patch) | |
tree | 24438eda01318a80b5cbc7f1939d1640764a9859 /src | |
parent | 5d2b6585c66fc17a36bb9841a3b3fb009e23024c (diff) | |
parent | b0be323e899ea7be42b6c695b4fd6d526070b213 (diff) | |
download | dexon-sol-tools-e33027c6244b99a1fb181404668bd2a259d923e2.tar dexon-sol-tools-e33027c6244b99a1fb181404668bd2a259d923e2.tar.gz dexon-sol-tools-e33027c6244b99a1fb181404668bd2a259d923e2.tar.bz2 dexon-sol-tools-e33027c6244b99a1fb181404668bd2a259d923e2.tar.lz dexon-sol-tools-e33027c6244b99a1fb181404668bd2a259d923e2.tar.xz dexon-sol-tools-e33027c6244b99a1fb181404668bd2a259d923e2.tar.zst dexon-sol-tools-e33027c6244b99a1fb181404668bd2a259d923e2.zip |
Merge branch 'development' into feature/receipt-status
* development: (164 commits)
Remove old tests
Remove unused code
Fix tests
Remove redundant spaces
Don't store empty objects
Fix a typo
Remove duplicate operations
Remove redundant instance variables
Fix tests
Remove blockStore and default to numConfirmations === 0
Add a comment
Store number of confirmations in a blockStore
Remove tautology check
Pass blockStore to eventWatcher
Fix last merge conflicts
Clear cache on unsubscribe
Clear store cache on events
Add more configs for order watcher
Make subscribe function async and make blockStore operational
Adjust tests to new interface
...
# Conflicts:
# package.json
# src/types.ts
# yarn.lock
Diffstat (limited to 'src')
-rw-r--r-- | src/0x.ts | 33 | ||||
-rw-r--r-- | src/contract_wrappers/ether_token_wrapper.ts | 4 | ||||
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 21 | ||||
-rw-r--r-- | src/contract_wrappers/token_wrapper.ts | 8 | ||||
-rw-r--r-- | src/index.ts | 6 | ||||
-rw-r--r-- | src/order_watcher/event_watcher.ts | 88 | ||||
-rw-r--r-- | src/order_watcher/order_state_watcher.ts | 232 | ||||
-rw-r--r-- | src/schemas/zero_ex_config_schema.ts | 13 | ||||
-rw-r--r-- | src/stores/balance_proxy_allowance_lazy_store.ts | 82 | ||||
-rw-r--r-- | src/stores/order_filled_cancelled_lazy_store.ts | 61 | ||||
-rw-r--r-- | src/types.ts | 60 | ||||
-rw-r--r-- | src/utils/abi_decoder.ts | 2 | ||||
-rw-r--r-- | src/utils/assert.ts | 17 | ||||
-rw-r--r-- | src/utils/exchange_transfer_simulator.ts | 76 | ||||
-rw-r--r-- | src/utils/order_state_utils.ts | 119 | ||||
-rw-r--r-- | src/utils/signature_utils.ts | 15 | ||||
-rw-r--r-- | src/web3_wrapper.ts | 14 |
17 files changed, 739 insertions, 112 deletions
@@ -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 a69b7c141..1b3e893ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,4 +36,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 a18164e20..11683378f 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 enum AbiType { @@ -414,12 +429,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 TransactionReceipt { logs: Array<LogWithDecodedArgs<DecodedLogArgs>|Web3.LogEntry>; @@ -472,6 +482,34 @@ export enum TransferType { 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; + export interface TransactionReceipt { blockHash: string; blockNumber: number; 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 be81fe78c..c937f9288 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) { @@ -95,6 +101,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; |