diff options
author | Leonid Logvinov <logvinov.leon@gmail.com> | 2018-07-13 23:45:25 +0800 |
---|---|---|
committer | Leonid Logvinov <logvinov.leon@gmail.com> | 2018-07-13 23:45:25 +0800 |
commit | 95e9f33f6aa5fa7849279062b008afa763a465d8 (patch) | |
tree | 085b1303c0576a1bafe471191253a416cbd939d2 /packages/order-watcher/src/order_watcher | |
parent | c599a20b34331c6481318d3cd3b01855444f339d (diff) | |
download | dexon-sol-tools-95e9f33f6aa5fa7849279062b008afa763a465d8.tar dexon-sol-tools-95e9f33f6aa5fa7849279062b008afa763a465d8.tar.gz dexon-sol-tools-95e9f33f6aa5fa7849279062b008afa763a465d8.tar.bz2 dexon-sol-tools-95e9f33f6aa5fa7849279062b008afa763a465d8.tar.lz dexon-sol-tools-95e9f33f6aa5fa7849279062b008afa763a465d8.tar.xz dexon-sol-tools-95e9f33f6aa5fa7849279062b008afa763a465d8.tar.zst dexon-sol-tools-95e9f33f6aa5fa7849279062b008afa763a465d8.zip |
Migrate order-watcher to v2
Diffstat (limited to 'packages/order-watcher/src/order_watcher')
5 files changed, 527 insertions, 212 deletions
diff --git a/packages/order-watcher/src/order_watcher/collision_resistant_abi_decoder.ts b/packages/order-watcher/src/order_watcher/collision_resistant_abi_decoder.ts new file mode 100644 index 000000000..e13663c7a --- /dev/null +++ b/packages/order-watcher/src/order_watcher/collision_resistant_abi_decoder.ts @@ -0,0 +1,54 @@ +import { AbiDecoder } from '@0xproject/utils'; +import { ContractAbi, DecodedLogArgs, LogEntry, LogWithDecodedArgs, RawLog } from 'ethereum-types'; + +const TOKEN_TYPE_COLLISION = `Token can't be marked as ERC20 and ERC721 at the same time`; + +/** + * ERC20 and ERC721 have some events with different args but colliding signature. + * For exmaple: + * Transfer(_from address, _to address, _value uint256) + * Transfer(_from address, _to address, _tokenId uint256) + * Both have the signature: + * Transfer(address,address,uint256) + * + * In order to correctly decode those events we need to know the token type by address in advance. + * You can pass it by calling `this.addERC20Token(address)` or `this.addERC721Token(address)` + */ +export class CollisionResistanceAbiDecoder { + private readonly _erc20AbiDecoder: AbiDecoder; + private readonly _erc721AbiDecoder: AbiDecoder; + private readonly _restAbiDecoder: AbiDecoder; + private readonly _knownERC20Tokens = new Set(); + private readonly _knownERC721Tokens = new Set(); + constructor(erc20Abi: ContractAbi, erc721Abi: ContractAbi, abis: ContractAbi[]) { + this._erc20AbiDecoder = new AbiDecoder([erc20Abi]); + this._erc721AbiDecoder = new AbiDecoder([erc721Abi]); + this._restAbiDecoder = new AbiDecoder(abis); + } + public tryToDecodeLogOrNoop<ArgsType extends DecodedLogArgs>(log: LogEntry): LogWithDecodedArgs<ArgsType> | RawLog { + if (this._knownERC20Tokens.has(log.address)) { + const maybeDecodedERC20Log = this._erc20AbiDecoder.tryToDecodeLogOrNoop(log); + return maybeDecodedERC20Log; + } else if (this._knownERC721Tokens.has(log.address)) { + const maybeDecodedERC721Log = this._erc721AbiDecoder.tryToDecodeLogOrNoop(log); + return maybeDecodedERC721Log; + } else { + const maybeDecodedLog = this._restAbiDecoder.tryToDecodeLogOrNoop(log); + return maybeDecodedLog; + } + } + // Hints the ABI decoder that a particular token address is ERC20 and events from it should be decoded as ERC20 events + public addERC20Token(address: string): void { + if (this._knownERC721Tokens.has(address)) { + throw new Error(TOKEN_TYPE_COLLISION); + } + this._knownERC20Tokens.add(address); + } + // Hints the ABI decoder that a particular token address is ERC721 and events from it should be decoded as ERC721 events + public addERC721Token(address: string): void { + if (this._knownERC20Tokens.has(address)) { + throw new Error(TOKEN_TYPE_COLLISION); + } + this._knownERC721Tokens.add(address); + } +} diff --git a/packages/order-watcher/src/order_watcher/dependent_order_hashes_tracker.ts b/packages/order-watcher/src/order_watcher/dependent_order_hashes_tracker.ts new file mode 100644 index 000000000..ae7d5078c --- /dev/null +++ b/packages/order-watcher/src/order_watcher/dependent_order_hashes_tracker.ts @@ -0,0 +1,230 @@ +// tslint:disable:no-unnecessary-type-assertion +import { assetProxyUtils, orderHashUtils } from '@0xproject/order-utils'; +import { AssetProxyId, ERC20AssetData, ERC721AssetData, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +export interface OrderHashesByMakerAddress { + [makerAddress: string]: Set<string>; +} + +export interface OrderHashesByERC20ByMakerAddress { + [makerAddress: string]: { + [erc20TokenAddress: string]: Set<string>; + }; +} + +export interface OrderHashesByERC721AddressByTokenIdByMakerAddress { + [makerAddress: string]: { + [erc721TokenAddress: string]: { + // Ideally erc721TokenId should be a BigNumber, but it's not a valid index type so we just convert it to a string before using it as an index + [erc721TokenId: string]: Set<string>; + }; + }; +} + +/** + */ +export class DependentOrderHashesTracker { + private readonly _zrxTokenAddress: string; + // `_orderHashesByMakerAddress` is redundant and could be generated from + // `_orderHashesByERC20ByMakerAddress` and `_orderHashesByERC721AddressByTokenIdByMakerAddress` + // on the fly by merging all the entries together but it's more complex and computationally heavy. + // We might change that in future if we're move memory-constrained. + private readonly _orderHashesByMakerAddress: OrderHashesByMakerAddress = {}; + private readonly _orderHashesByERC20ByMakerAddress: OrderHashesByERC20ByMakerAddress = {}; + private readonly _orderHashesByERC721AddressByTokenIdByMakerAddress: OrderHashesByERC721AddressByTokenIdByMakerAddress = {}; + constructor(zrxTokenAddress: string) { + this._zrxTokenAddress = zrxTokenAddress; + } + public getDependentOrderHashesByERC721ByMaker(makerAddress: string, tokenAddress: string): string[] { + const orderHashSets = _.values( + this._orderHashesByERC721AddressByTokenIdByMakerAddress[makerAddress][tokenAddress], + ); + const orderHashList = _.reduce( + orderHashSets, + (accumulator, orderHashSet) => [...accumulator, ...orderHashSet], + [] as string[], + ); + const uniqueOrderHashList = _.uniq(orderHashList); + return uniqueOrderHashList; + } + public getDependentOrderHashesByMaker(makerAddress: string): string[] { + const dependentOrderHashes = Array.from(this._orderHashesByMakerAddress[makerAddress]); + return dependentOrderHashes; + } + public getDependentOrderHashesByAssetDataByMaker(makerAddress: string, assetData: string): string[] { + const decodedAssetData = assetProxyUtils.decodeAssetData(assetData); + const dependentOrderHashes = + decodedAssetData.assetProxyId === AssetProxyId.ERC20 + ? this._getDependentOrderHashesByERC20AssetData(makerAddress, assetData) + : this._getDependentOrderHashesByERC721AssetData(makerAddress, assetData); + return dependentOrderHashes; + } + public addToDependentOrderHashes(signedOrder: SignedOrder): void { + const decodedMakerAssetData = assetProxyUtils.decodeAssetData(signedOrder.makerAssetData); + if (decodedMakerAssetData.assetProxyId === AssetProxyId.ERC20) { + this._addToERC20DependentOrderHashes(signedOrder, (decodedMakerAssetData as ERC20AssetData).tokenAddress); + } else { + this._addToERC721DependentOrderHashes( + signedOrder, + (decodedMakerAssetData as ERC721AssetData).tokenAddress, + (decodedMakerAssetData as ERC721AssetData).tokenId, + ); + } + this._addToERC20DependentOrderHashes(signedOrder, this._zrxTokenAddress); + this._addToMakerDependentOrderHashes(signedOrder); + } + public removeFromDependentOrderHashes(signedOrder: SignedOrder): void { + const decodedMakerAssetData = assetProxyUtils.decodeAssetData(signedOrder.makerAssetData); + if (decodedMakerAssetData.assetProxyId === AssetProxyId.ERC20) { + this._removeFromERC20DependentOrderhashes( + signedOrder, + (decodedMakerAssetData as ERC20AssetData).tokenAddress, + ); + } else { + this._removeFromERC721DependentOrderhashes( + signedOrder, + (decodedMakerAssetData as ERC721AssetData).tokenAddress, + (decodedMakerAssetData as ERC721AssetData).tokenId, + ); + } + this._removeFromERC20DependentOrderhashes(signedOrder, this._zrxTokenAddress); + this._removeFromMakerDependentOrderhashes(signedOrder); + } + private _getDependentOrderHashesByERC20AssetData(makerAddress: string, erc20AssetData: string): string[] { + const tokenAddress = assetProxyUtils.decodeERC20AssetData(erc20AssetData).tokenAddress; + let dependentOrderHashes: string[] = []; + if ( + !_.isUndefined(this._orderHashesByERC20ByMakerAddress[makerAddress]) && + !_.isUndefined(this._orderHashesByERC20ByMakerAddress[makerAddress][tokenAddress]) + ) { + dependentOrderHashes = Array.from(this._orderHashesByERC20ByMakerAddress[makerAddress][tokenAddress]); + } + return dependentOrderHashes; + } + private _getDependentOrderHashesByERC721AssetData(makerAddress: string, erc721AssetData: string): string[] { + const tokenAddress = assetProxyUtils.decodeERC721AssetData(erc721AssetData).tokenAddress; + const tokenId = assetProxyUtils.decodeERC721AssetData(erc721AssetData).tokenId; + let dependentOrderHashes: string[] = []; + if ( + !_.isUndefined(this._orderHashesByERC721AddressByTokenIdByMakerAddress[makerAddress]) && + !_.isUndefined(this._orderHashesByERC721AddressByTokenIdByMakerAddress[makerAddress][tokenAddress]) && + !_.isUndefined( + this._orderHashesByERC721AddressByTokenIdByMakerAddress[makerAddress][tokenAddress][tokenId.toString()], + ) + ) { + dependentOrderHashes = Array.from( + this._orderHashesByERC721AddressByTokenIdByMakerAddress[makerAddress][tokenAddress][tokenId.toString()], + ); + } + return dependentOrderHashes; + } + private _addToERC20DependentOrderHashes(signedOrder: SignedOrder, erc20TokenAddress: string): void { + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + if (_.isUndefined(this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress])) { + this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress] = {}; + } + if (_.isUndefined(this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress][erc20TokenAddress])) { + this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress][erc20TokenAddress] = new Set(); + } + this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress][erc20TokenAddress].add(orderHash); + } + private _addToERC721DependentOrderHashes( + signedOrder: SignedOrder, + erc721TokenAddress: string, + tokenId: BigNumber, + ): void { + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + if (_.isUndefined(this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress])) { + this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress] = {}; + } + + if ( + _.isUndefined( + this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress], + ) + ) { + this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress] = {}; + } + + if ( + _.isUndefined( + this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress][ + tokenId.toString() + ], + ) + ) { + this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress][ + tokenId.toString() + ] = new Set(); + } + + this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress][ + tokenId.toString() + ].add(orderHash); + } + private _addToMakerDependentOrderHashes(signedOrder: SignedOrder): void { + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + if (_.isUndefined(this._orderHashesByMakerAddress[signedOrder.makerAddress])) { + this._orderHashesByMakerAddress[signedOrder.makerAddress] = new Set(); + } + this._orderHashesByMakerAddress[signedOrder.makerAddress].add(orderHash); + } + private _removeFromERC20DependentOrderhashes(signedOrder: SignedOrder, erc20TokenAddress: string): void { + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress][erc20TokenAddress].delete(orderHash); + + if (_.isEmpty(this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress][erc20TokenAddress])) { + delete this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress][erc20TokenAddress]; + } + + if (_.isEmpty(this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress])) { + delete this._orderHashesByERC20ByMakerAddress[signedOrder.makerAddress]; + } + } + private _removeFromERC721DependentOrderhashes( + signedOrder: SignedOrder, + erc721TokenAddress: string, + tokenId: BigNumber, + ): void { + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress][ + tokenId.toString() + ].delete(orderHash); + + if ( + _.isEmpty( + this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress][ + tokenId.toString() + ], + ) + ) { + delete this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][ + erc721TokenAddress + ][tokenId.toString()]; + } + + if ( + _.isEmpty( + this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][erc721TokenAddress], + ) + ) { + delete this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress][ + erc721TokenAddress + ]; + } + + if (_.isEmpty(this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress])) { + delete this._orderHashesByERC721AddressByTokenIdByMakerAddress[signedOrder.makerAddress]; + } + } + private _removeFromMakerDependentOrderhashes(signedOrder: SignedOrder): void { + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + this._orderHashesByMakerAddress[signedOrder.makerAddress].delete(orderHash); + + if (_.isEmpty(this._orderHashesByMakerAddress[signedOrder.makerAddress])) { + delete this._orderHashesByMakerAddress[signedOrder.makerAddress]; + } + } +} diff --git a/packages/order-watcher/src/order_watcher/event_watcher.ts b/packages/order-watcher/src/order_watcher/event_watcher.ts index 08ecf81cb..68c043dfe 100644 --- a/packages/order-watcher/src/order_watcher/event_watcher.ts +++ b/packages/order-watcher/src/order_watcher/event_watcher.ts @@ -1,6 +1,6 @@ -import { BlockParamLiteral, LogEntry } from '@0xproject/types'; import { intervalUtils, logUtils } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { BlockParamLiteral, LogEntry, Provider } from 'ethereum-types'; import { Block, BlockAndLogStreamer, Log } from 'ethereumjs-blockstream'; import * as _ from 'lodash'; @@ -19,22 +19,22 @@ enum LogEventState { * depth. */ export class EventWatcher { - private _web3Wrapper: Web3Wrapper; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _pollingIntervalMs: number; + private readonly _stateLayer: BlockParamLiteral; + private readonly _isVerbose: boolean; private _blockAndLogStreamerIfExists: BlockAndLogStreamer<Block, Log> | undefined; private _blockAndLogStreamIntervalIfExists?: NodeJS.Timer; private _onLogAddedSubscriptionToken: string | undefined; private _onLogRemovedSubscriptionToken: string | undefined; - private _pollingIntervalMs: number; - private _stateLayer: BlockParamLiteral; - private _isVerbose: boolean; constructor( - web3Wrapper: Web3Wrapper, + provider: Provider, pollingIntervalIfExistsMs: undefined | number, stateLayer: BlockParamLiteral = BlockParamLiteral.Latest, isVerbose: boolean, ) { this._isVerbose = isVerbose; - this._web3Wrapper = web3Wrapper; + this._web3Wrapper = new Web3Wrapper(provider); this._stateLayer = stateLayer; this._pollingIntervalMs = _.isUndefined(pollingIntervalIfExistsMs) ? DEFAULT_EVENT_POLLING_INTERVAL_MS diff --git a/packages/order-watcher/src/order_watcher/expiration_watcher.ts b/packages/order-watcher/src/order_watcher/expiration_watcher.ts index 31fda7dca..c4c94a015 100644 --- a/packages/order-watcher/src/order_watcher/expiration_watcher.ts +++ b/packages/order-watcher/src/order_watcher/expiration_watcher.ts @@ -13,10 +13,10 @@ const DEFAULT_ORDER_EXPIRATION_CHECKING_INTERVAL_MS = 50; * It stores them in a min heap by expiration time and checks for expired ones every `orderExpirationCheckingIntervalMs` */ export class ExpirationWatcher { - private _orderHashByExpirationRBTree: RBTree<string>; - private _expiration: { [orderHash: string]: BigNumber } = {}; - private _orderExpirationCheckingIntervalMs: number; - private _expirationMarginMs: number; + private readonly _orderHashByExpirationRBTree: RBTree<string>; + private readonly _expiration: { [orderHash: string]: BigNumber } = {}; + private readonly _orderExpirationCheckingIntervalMs: number; + private readonly _expirationMarginMs: number; private _orderExpirationCheckingIntervalIdIfExists?: NodeJS.Timer; constructor(expirationMarginIfExistsMs?: number, orderExpirationCheckingIntervalIfExistsMs?: number) { this._orderExpirationCheckingIntervalMs = @@ -68,8 +68,8 @@ export class ExpirationWatcher { private _pruneExpiredOrders(callback: (orderHash: string) => void): void { const currentUnixTimestampMs = utils.getCurrentUnixTimestampMs(); while (true) { - const hasTrakedOrders = this._orderHashByExpirationRBTree.size === 0; - if (hasTrakedOrders) { + const hasNoTrackedOrders = this._orderHashByExpirationRBTree.size === 0; + if (hasNoTrackedOrders) { break; } const nextOrderHashToExpire = this._orderHashByExpirationRBTree.min(); diff --git a/packages/order-watcher/src/order_watcher/order_watcher.ts b/packages/order-watcher/src/order_watcher/order_watcher.ts index b09ba8d9d..af479f32d 100644 --- a/packages/order-watcher/src/order_watcher/order_watcher.ts +++ b/packages/order-watcher/src/order_watcher/order_watcher.ts @@ -1,55 +1,53 @@ +// tslint:disable:no-unnecessary-type-assertion import { - BalanceAndProxyAllowanceLazyStore, ContractWrappers, - OrderFilledCancelledLazyStore, + ERC20TokenApprovalEventArgs, + ERC20TokenEventArgs, + ERC20TokenEvents, + ERC20TokenTransferEventArgs, + ERC721TokenApprovalEventArgs, + ERC721TokenApprovalForAllEventArgs, + ERC721TokenEventArgs, + ERC721TokenEvents, + ERC721TokenTransferEventArgs, + ExchangeCancelEventArgs, + ExchangeCancelUpToEventArgs, + ExchangeEventArgs, + ExchangeEvents, + ExchangeFillEventArgs, + WETH9DepositEventArgs, + WETH9EventArgs, + WETH9Events, + WETH9WithdrawalEventArgs, } from '@0xproject/contract-wrappers'; import { schemas } from '@0xproject/json-schemas'; -import { getOrderHashHex, OrderStateUtils } from '@0xproject/order-utils'; import { - BlockParamLiteral, - ExchangeContractErrs, - LogEntryEvent, - LogWithDecodedArgs, - OrderState, - Provider, - SignedOrder, -} from '@0xproject/types'; + assetProxyUtils, + BalanceAndProxyAllowanceLazyStore, + OrderFilledCancelledLazyStore, + orderHashUtils, + OrderStateUtils, +} from '@0xproject/order-utils'; +import { ExchangeContractErrs, OrderState, SignedOrder } from '@0xproject/types'; import { errorUtils, intervalUtils } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { BlockParamLiteral, LogEntryEvent, LogWithDecodedArgs, Provider } from 'ethereum-types'; import * as _ from 'lodash'; import { artifacts } from '../artifacts'; -import { - EtherTokenDepositEventArgs, - EtherTokenEventArgs, - EtherTokenEvents, - EtherTokenWithdrawalEventArgs, -} from '../generated_contract_wrappers/ether_token'; -import { - ExchangeEventArgs, - ExchangeEvents, - ExchangeLogCancelEventArgs, - ExchangeLogFillEventArgs, -} from '../generated_contract_wrappers/exchange'; -import { - TokenApprovalEventArgs, - TokenEventArgs, - TokenEvents, - TokenTransferEventArgs, -} from '../generated_contract_wrappers/token'; +import { AssetBalanceAndProxyAllowanceFetcher } from '../fetchers/asset_balance_and_proxy_allowance_fetcher'; +import { OrderFilledCancelledFetcher } from '../fetchers/order_filled_cancelled_fetcher'; +import { orderWatcherPartialConfigSchema } from '../schemas/order_watcher_partial_config_schema'; import { OnOrderStateChangeCallback, OrderWatcherConfig, OrderWatcherError } from '../types'; import { assert } from '../utils/assert'; +import { CollisionResistanceAbiDecoder } from './collision_resistant_abi_decoder'; +import { DependentOrderHashesTracker } from './dependent_order_hashes_tracker'; import { EventWatcher } from './event_watcher'; import { ExpirationWatcher } from './expiration_watcher'; -type ContractEventArgs = EtherTokenEventArgs | ExchangeEventArgs | TokenEventArgs; +const MILLISECONDS_IN_A_SECOND = 1000; -interface DependentOrderHashes { - [makerAddress: string]: { - [makerToken: string]: Set<string>; - }; -} +type ContractEventArgs = WETH9EventArgs | ExchangeEventArgs | ERC20TokenEventArgs | ERC721TokenEventArgs; interface OrderByOrderHash { [orderHash: string]: SignedOrder; @@ -59,8 +57,15 @@ interface OrderStateByOrderHash { [orderHash: string]: OrderState; } -// tslint:disable-next-line:custom-no-magic-numbers -const DEFAULT_CLEANUP_JOB_INTERVAL_MS = 1000 * 60 * 60; // 1h +const DEFAULT_ORDER_WATCHER_CONFIG: OrderWatcherConfig = { + stateLayer: BlockParamLiteral.Latest, + orderExpirationCheckingIntervalMs: 50, + eventPollingIntervalMs: 200, + expirationMarginMs: 0, + // tslint:disable-next-line:custom-no-magic-numbers + cleanupJobIntervalMs: 1000 * 60 * 60, // 1h + isVerbose: true, +}; /** * This class includes all the functionality related to watching a set of orders @@ -69,56 +74,68 @@ const DEFAULT_CLEANUP_JOB_INTERVAL_MS = 1000 * 60 * 60; // 1h * the order should be deemed invalid. */ export class OrderWatcher { - private _contractWrappers: ContractWrappers; - private _orderStateByOrderHashCache: OrderStateByOrderHash = {}; - private _orderByOrderHash: OrderByOrderHash = {}; - private _dependentOrderHashes: DependentOrderHashes = {}; - private _callbackIfExists?: OnOrderStateChangeCallback; - private _eventWatcher: EventWatcher; - private _web3Wrapper: Web3Wrapper; - private _expirationWatcher: ExpirationWatcher; - private _orderStateUtils: OrderStateUtils; - private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; - private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; - private _cleanupJobInterval: number; + private readonly _dependentOrderHashesTracker: DependentOrderHashesTracker; + private readonly _orderStateByOrderHashCache: OrderStateByOrderHash = {}; + private readonly _orderByOrderHash: OrderByOrderHash = {}; + private readonly _eventWatcher: EventWatcher; + private readonly _provider: Provider; + private readonly _collisionResistantAbiDecoder: CollisionResistanceAbiDecoder; + private readonly _expirationWatcher: ExpirationWatcher; + private readonly _orderStateUtils: OrderStateUtils; + private readonly _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore; + private readonly _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore; + private readonly _cleanupJobInterval: number; private _cleanupJobIntervalIdIfExists?: NodeJS.Timer; - constructor(provider: Provider, networkId: number, config?: OrderWatcherConfig) { - this._web3Wrapper = new Web3Wrapper(provider); - const artifactJSONs = _.values(artifacts); - const abiArrays = _.map(artifactJSONs, artifact => artifact.abi); - _.forEach(abiArrays, abi => { - this._web3Wrapper.abiDecoder.addABI(abi); - }); - this._contractWrappers = new ContractWrappers(provider, { networkId }); - const pollingIntervalIfExistsMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs; - const stateLayer = - _.isUndefined(config) || _.isUndefined(config.stateLayer) ? BlockParamLiteral.Latest : config.stateLayer; - const isVerbose = !_.isUndefined(config) && !_.isUndefined(config.isVerbose) ? config.isVerbose : false; - this._eventWatcher = new EventWatcher(this._web3Wrapper, pollingIntervalIfExistsMs, stateLayer, isVerbose); - this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( - this._contractWrappers.token, - stateLayer, + private _callbackIfExists?: OnOrderStateChangeCallback; + constructor( + provider: Provider, + networkId: number, + partialConfig: Partial<OrderWatcherConfig> = DEFAULT_ORDER_WATCHER_CONFIG, + ) { + assert.isWeb3Provider('provider', provider); + assert.isNumber('networkId', networkId); + assert.doesConformToSchema('partialConfig', partialConfig, orderWatcherPartialConfigSchema); + const config = { + ...DEFAULT_ORDER_WATCHER_CONFIG, + ...partialConfig, + }; + + this._provider = provider; + this._collisionResistantAbiDecoder = new CollisionResistanceAbiDecoder( + artifacts.ERC20Token.abi, + artifacts.ERC721Token.abi, + [artifacts.EtherToken.abi, artifacts.Exchange.abi], ); - this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore( - this._contractWrappers.exchange, - stateLayer, + const contractWrappers = new ContractWrappers(provider, { networkId }); + this._eventWatcher = new EventWatcher( + provider, + config.eventPollingIntervalMs, + config.stateLayer, + config.isVerbose, ); - this._orderStateUtils = new OrderStateUtils( - this._balanceAndProxyAllowanceLazyStore, - this._orderFilledCancelledLazyStore, + const balanceAndProxyAllowanceFetcher = new AssetBalanceAndProxyAllowanceFetcher( + contractWrappers.erc20Token, + contractWrappers.erc721Token, + config.stateLayer, + ); + this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore( + balanceAndProxyAllowanceFetcher, ); - const orderExpirationCheckingIntervalMsIfExists = _.isUndefined(config) - ? undefined - : config.orderExpirationCheckingIntervalMs; + const orderFilledCancelledFetcher = new OrderFilledCancelledFetcher( + contractWrappers.exchange, + config.stateLayer, + ); + this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore(orderFilledCancelledFetcher); + this._orderStateUtils = new OrderStateUtils(balanceAndProxyAllowanceFetcher, orderFilledCancelledFetcher); const expirationMarginIfExistsMs = _.isUndefined(config) ? undefined : config.expirationMarginMs; this._expirationWatcher = new ExpirationWatcher( expirationMarginIfExistsMs, - orderExpirationCheckingIntervalMsIfExists, + config.orderExpirationCheckingIntervalMs, ); - this._cleanupJobInterval = - _.isUndefined(config) || _.isUndefined(config.cleanupJobIntervalMs) - ? DEFAULT_CLEANUP_JOB_INTERVAL_MS - : config.cleanupJobIntervalMs; + this._cleanupJobInterval = config.cleanupJobIntervalMs; + const zrxTokenAddress = assetProxyUtils.decodeERC20AssetData(orderFilledCancelledFetcher.getZRXAssetData()) + .tokenAddress; + this._dependentOrderHashesTracker = new DependentOrderHashesTracker(zrxTokenAddress); } /** * Add an order to the orderWatcher. Before the order is added, it's @@ -127,13 +144,14 @@ export class OrderWatcher { */ public addOrder(signedOrder: SignedOrder): void { assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); - const orderHash = getOrderHashHex(signedOrder); - assert.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker); - this._orderByOrderHash[orderHash] = signedOrder; - this._addToDependentOrderHashes(signedOrder, orderHash); - const milisecondsInASecond = 1000; - const expirationUnixTimestampMs = signedOrder.expirationUnixTimestampSec.times(milisecondsInASecond); + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + assert.isValidSignatureAsync(this._provider, orderHash, signedOrder.signature, signedOrder.makerAddress); + + const expirationUnixTimestampMs = signedOrder.expirationTimeSeconds.times(MILLISECONDS_IN_A_SECOND); this._expirationWatcher.addOrder(orderHash, expirationUnixTimestampMs); + + this._orderByOrderHash[orderHash] = signedOrder; + this._dependentOrderHashesTracker.addToDependentOrderHashes(signedOrder); } /** * Removes an order from the orderWatcher @@ -145,16 +163,10 @@ export class OrderWatcher { if (_.isUndefined(signedOrder)) { return; // noop } + this._dependentOrderHashesTracker.removeFromDependentOrderHashes(signedOrder); delete this._orderByOrderHash[orderHash]; - delete this._orderStateByOrderHashCache[orderHash]; - const zrxTokenAddress = this._orderFilledCancelledLazyStore.getZRXTokenAddress(); - - this._removeFromDependentOrderHashes(signedOrder.maker, zrxTokenAddress, orderHash); - if (zrxTokenAddress !== signedOrder.makerTokenAddress) { - this._removeFromDependentOrderHashes(signedOrder.maker, signedOrder.makerTokenAddress, orderHash); - } - this._expirationWatcher.removeOrder(orderHash); + delete this._orderStateByOrderHashCache[orderHash]; } /** * Starts an orderWatcher subscription. The callback will be called every time a watched order's @@ -203,21 +215,27 @@ export class OrderWatcher { const signedOrder = this._orderByOrderHash[orderHash]; this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(orderHash); - this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(orderHash); + this._orderFilledCancelledLazyStore.deleteIsCancelled(orderHash); - this._balanceAndProxyAllowanceLazyStore.deleteBalance(signedOrder.makerTokenAddress, signedOrder.maker); - this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(signedOrder.makerTokenAddress, signedOrder.maker); - this._balanceAndProxyAllowanceLazyStore.deleteBalance(signedOrder.takerTokenAddress, signedOrder.taker); - this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(signedOrder.takerTokenAddress, signedOrder.taker); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(signedOrder.makerAssetData, signedOrder.makerAddress); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance( + signedOrder.makerAssetData, + signedOrder.makerAddress, + ); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(signedOrder.takerAssetData, signedOrder.takerAddress); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance( + signedOrder.takerAssetData, + signedOrder.takerAddress, + ); - const zrxTokenAddress = this._getZRXTokenAddress(); + const zrxAssetData = this._orderFilledCancelledLazyStore.getZRXAssetData(); if (!signedOrder.makerFee.isZero()) { - this._balanceAndProxyAllowanceLazyStore.deleteBalance(zrxTokenAddress, signedOrder.maker); - this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(zrxTokenAddress, signedOrder.maker); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(zrxAssetData, signedOrder.makerAddress); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(zrxAssetData, signedOrder.makerAddress); } if (!signedOrder.takerFee.isZero()) { - this._balanceAndProxyAllowanceLazyStore.deleteBalance(zrxTokenAddress, signedOrder.taker); - this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(zrxTokenAddress, signedOrder.taker); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(zrxAssetData, signedOrder.takerAddress); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(zrxAssetData, signedOrder.takerAddress); } } private _onOrderExpired(orderHash: string): void { @@ -240,89 +258,122 @@ export class OrderWatcher { } return; } - const log = logIfExists as LogEntryEvent; // At this moment we are sure that no error occured and log is defined. - const maybeDecodedLog = this._web3Wrapper.abiDecoder.tryToDecodeLogOrNoop<ContractEventArgs>(log); + const maybeDecodedLog = this._collisionResistantAbiDecoder.tryToDecodeLogOrNoop<ContractEventArgs>( + // At this moment we are sure that no error occured and log is defined. + logIfExists as LogEntryEvent, + ); const isLogDecoded = !_.isUndefined(((maybeDecodedLog as any) as LogWithDecodedArgs<ContractEventArgs>).event); if (!isLogDecoded) { return; // noop } const decodedLog = (maybeDecodedLog as any) as LogWithDecodedArgs<ContractEventArgs>; - let makerToken: string; - let makerAddress: string; switch (decodedLog.event) { - case TokenEvents.Approval: { - // Invalidate cache - // tslint:disable-next-line:no-unnecessary-type-assertion - const args = decodedLog.args as TokenApprovalEventArgs; - this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(decodedLog.address, args._owner); - // Revalidate orders - makerToken = decodedLog.address; - makerAddress = args._owner; - if ( - !_.isUndefined(this._dependentOrderHashes[makerAddress]) && - !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken]) - ) { - const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]); + case ERC20TokenEvents.Approval: + case ERC721TokenEvents.Approval: { + // ERC20 and ERC721 Transfer events have the same name so we need to distinguish them by args + if (!_.isUndefined(decodedLog.args._value)) { + // ERC20 + // Invalidate cache + const args = decodedLog.args as ERC20TokenApprovalEventArgs; + const tokenAssetData = assetProxyUtils.encodeERC20AssetData(decodedLog.address); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(tokenAssetData, args._owner); + // Revalidate orders + const orderHashes = this._dependentOrderHashesTracker.getDependentOrderHashesByAssetDataByMaker( + args._owner, + tokenAssetData, + ); + await this._emitRevalidateOrdersAsync(orderHashes); + break; + } else { + // ERC721 + // Invalidate cache + const args = decodedLog.args as ERC721TokenApprovalEventArgs; + const tokenAssetData = assetProxyUtils.encodeERC721AssetData(decodedLog.address, args._tokenId); + this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(tokenAssetData, args._owner); + // Revalidate orders + const orderHashes = this._dependentOrderHashesTracker.getDependentOrderHashesByAssetDataByMaker( + args._owner, + tokenAssetData, + ); await this._emitRevalidateOrdersAsync(orderHashes); + break; } - break; } - case TokenEvents.Transfer: { - // Invalidate cache - // tslint:disable-next-line:no-unnecessary-type-assertion - const args = decodedLog.args as TokenTransferEventArgs; - this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._from); - this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._to); - // Revalidate orders - makerToken = decodedLog.address; - makerAddress = args._from; - if ( - !_.isUndefined(this._dependentOrderHashes[makerAddress]) && - !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken]) - ) { - const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]); + case ERC20TokenEvents.Transfer: + case ERC721TokenEvents.Transfer: { + // ERC20 and ERC721 Transfer events have the same name so we need to distinguish them by args + if (!_.isUndefined(decodedLog.args._value)) { + // ERC20 + // Invalidate cache + const args = decodedLog.args as ERC20TokenTransferEventArgs; + const tokenAssetData = assetProxyUtils.encodeERC20AssetData(decodedLog.address); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(tokenAssetData, args._from); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(tokenAssetData, args._to); + // Revalidate orders + const orderHashes = this._dependentOrderHashesTracker.getDependentOrderHashesByAssetDataByMaker( + args._from, + tokenAssetData, + ); + await this._emitRevalidateOrdersAsync(orderHashes); + break; + } else { + // ERC721 + // Invalidate cache + const args = decodedLog.args as ERC721TokenTransferEventArgs; + const tokenAssetData = assetProxyUtils.encodeERC721AssetData(decodedLog.address, args._tokenId); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(tokenAssetData, args._from); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(tokenAssetData, args._to); + // Revalidate orders + const orderHashes = this._dependentOrderHashesTracker.getDependentOrderHashesByAssetDataByMaker( + args._from, + tokenAssetData, + ); await this._emitRevalidateOrdersAsync(orderHashes); + break; } + } + case ERC721TokenEvents.ApprovalForAll: { + // Invalidate cache + const args = decodedLog.args as ERC721TokenApprovalForAllEventArgs; + const tokenAddress = decodedLog.address; + this._balanceAndProxyAllowanceLazyStore.deleteAllERC721ProxyAllowance(tokenAddress, args._owner); + // Revalidate orders + const orderHashes = this._dependentOrderHashesTracker.getDependentOrderHashesByERC721ByMaker( + args._owner, + tokenAddress, + ); + await this._emitRevalidateOrdersAsync(orderHashes); break; } - case EtherTokenEvents.Deposit: { + case WETH9Events.Deposit: { // Invalidate cache - // tslint:disable-next-line:no-unnecessary-type-assertion - const args = decodedLog.args as EtherTokenDepositEventArgs; - this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._owner); + const args = decodedLog.args as WETH9DepositEventArgs; + const tokenAssetData = assetProxyUtils.encodeERC20AssetData(decodedLog.address); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(tokenAssetData, args._owner); // Revalidate orders - makerToken = decodedLog.address; - makerAddress = args._owner; - if ( - !_.isUndefined(this._dependentOrderHashes[makerAddress]) && - !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken]) - ) { - const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]); - await this._emitRevalidateOrdersAsync(orderHashes); - } + const orderHashes = this._dependentOrderHashesTracker.getDependentOrderHashesByAssetDataByMaker( + args._owner, + tokenAssetData, + ); + await this._emitRevalidateOrdersAsync(orderHashes); break; } - case EtherTokenEvents.Withdrawal: { + case WETH9Events.Withdrawal: { // Invalidate cache - // tslint:disable-next-line:no-unnecessary-type-assertion - const args = decodedLog.args as EtherTokenWithdrawalEventArgs; - this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._owner); + const args = decodedLog.args as WETH9WithdrawalEventArgs; + const tokenAssetData = assetProxyUtils.encodeERC20AssetData(decodedLog.address); + this._balanceAndProxyAllowanceLazyStore.deleteBalance(tokenAssetData, args._owner); // Revalidate orders - makerToken = decodedLog.address; - makerAddress = args._owner; - if ( - !_.isUndefined(this._dependentOrderHashes[makerAddress]) && - !_.isUndefined(this._dependentOrderHashes[makerAddress][makerToken]) - ) { - const orderHashes = Array.from(this._dependentOrderHashes[makerAddress][makerToken]); - await this._emitRevalidateOrdersAsync(orderHashes); - } + const orderHashes = this._dependentOrderHashesTracker.getDependentOrderHashesByAssetDataByMaker( + args._owner, + tokenAssetData, + ); + await this._emitRevalidateOrdersAsync(orderHashes); break; } - case ExchangeEvents.LogFill: { + case ExchangeEvents.Fill: { // Invalidate cache - // tslint:disable-next-line:no-unnecessary-type-assertion - const args = decodedLog.args as ExchangeLogFillEventArgs; + const args = decodedLog.args as ExchangeFillEventArgs; this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(args.orderHash); // Revalidate orders const orderHash = args.orderHash; @@ -332,11 +383,10 @@ export class OrderWatcher { } break; } - case ExchangeEvents.LogCancel: { + case ExchangeEvents.Cancel: { // Invalidate cache - // tslint:disable-next-line:no-unnecessary-type-assertion - const args = decodedLog.args as ExchangeLogCancelEventArgs; - this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(args.orderHash); + const args = decodedLog.args as ExchangeCancelEventArgs; + this._orderFilledCancelledLazyStore.deleteIsCancelled(args.orderHash); // Revalidate orders const orderHash = args.orderHash; const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]); @@ -345,8 +395,16 @@ export class OrderWatcher { } break; } - case ExchangeEvents.LogError: - return; // noop + case ExchangeEvents.CancelUpTo: { + // TODO(logvinov): Do it smarter and actually look at the salt and order epoch + // Invalidate cache + const args = decodedLog.args as ExchangeCancelUpToEventArgs; + this._orderFilledCancelledLazyStore.deleteAllIsCancelled(); + // Revalidate orders + const orderHashes = this._dependentOrderHashesTracker.getDependentOrderHashesByMaker(args.makerAddress); + await this._emitRevalidateOrdersAsync(orderHashes); + break; + } default: throw errorUtils.spawnSwitchErr('decodedLog.event', decodedLog.event); @@ -357,7 +415,7 @@ export class OrderWatcher { const signedOrder = this._orderByOrderHash[orderHash]; // 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); + const orderState = await this._orderStateUtils.getOpenOrderStateAsync(signedOrder); if (_.isUndefined(this._callbackIfExists)) { break; // Unsubscribe was called } @@ -370,31 +428,4 @@ export class OrderWatcher { this._callbackIfExists(null, orderState); } } - private _addToDependentOrderHashes(signedOrder: SignedOrder, orderHash: string): void { - 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); - const zrxTokenAddress = this._getZRXTokenAddress(); - if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker][zrxTokenAddress])) { - this._dependentOrderHashes[signedOrder.maker][zrxTokenAddress] = new Set(); - } - this._dependentOrderHashes[signedOrder.maker][zrxTokenAddress].add(orderHash); - } - private _removeFromDependentOrderHashes(makerAddress: string, tokenAddress: string, orderHash: string): void { - this._dependentOrderHashes[makerAddress][tokenAddress].delete(orderHash); - if (this._dependentOrderHashes[makerAddress][tokenAddress].size === 0) { - delete this._dependentOrderHashes[makerAddress][tokenAddress]; - } - if (_.isEmpty(this._dependentOrderHashes[makerAddress])) { - delete this._dependentOrderHashes[makerAddress]; - } - } - private _getZRXTokenAddress(): string { - const zrxTokenAddress = this._orderFilledCancelledLazyStore.getZRXTokenAddress(); - return zrxTokenAddress; - } } |