diff options
author | Fabio Berger <me@fabioberger.com> | 2017-11-13 09:50:42 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-11-13 09:50:42 +0800 |
commit | b0be323e899ea7be42b6c695b4fd6d526070b213 (patch) | |
tree | 86042b06c407b388e39be690d9a40db218f82675 /src | |
parent | 1392a855bb17981f7680548a23062842fb6dc4e0 (diff) | |
parent | a22661670f105a2bf527aca0e803689e0302ed17 (diff) | |
download | dexon-sol-tools-b0be323e899ea7be42b6c695b4fd6d526070b213.tar dexon-sol-tools-b0be323e899ea7be42b6c695b4fd6d526070b213.tar.gz dexon-sol-tools-b0be323e899ea7be42b6c695b4fd6d526070b213.tar.bz2 dexon-sol-tools-b0be323e899ea7be42b6c695b4fd6d526070b213.tar.lz dexon-sol-tools-b0be323e899ea7be42b6c695b4fd6d526070b213.tar.xz dexon-sol-tools-b0be323e899ea7be42b6c695b4fd6d526070b213.tar.zst dexon-sol-tools-b0be323e899ea7be42b6c695b4fd6d526070b213.zip |
Merge pull request #205 from 0xProject/orderWatcher
Order watcher
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 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; |