From 7dd63523939822203d938511472c84b8ff418aaf Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Thu, 5 Oct 2017 14:34:30 +0300 Subject: Implement subscriptions based on ethereumjs-blockstream --- package.json | 2 + src/contract_wrappers/contract_wrapper.ts | 122 +++++++++++++------- src/contract_wrappers/exchange_wrapper.ts | 82 ++++++-------- src/contract_wrappers/token_wrapper.ts | 60 +++++----- src/index.ts | 2 +- src/types.ts | 24 +--- src/utils/constants.ts | 1 + src/utils/event_utils.ts | 41 ------- src/utils/filter_utils.ts | 80 +++++++++++++ test/exchange_wrapper_test.ts | 95 ++++++---------- test/token_wrapper_test.ts | 88 ++++++--------- yarn.lock | 180 +++++++++++++----------------- 12 files changed, 376 insertions(+), 401 deletions(-) delete mode 100644 src/utils/event_utils.ts create mode 100644 src/utils/filter_utils.ts diff --git a/package.json b/package.json index 011e974c0..59b0c9cf9 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@types/mocha": "^2.2.41", "@types/node": "^8.0.1", "@types/sinon": "^2.2.2", + "@types/uuid": "^3.4.2", "awesome-typescript-loader": "^3.1.3", "bignumber.js": "^4.0.2", "chai": "^4.0.1", @@ -100,6 +101,7 @@ "find-versions": "^2.0.0", "lodash": "^4.17.4", "publish-release": "^1.3.3", + "uuid": "^3.1.0", "web3": "^0.20.0" } } diff --git a/src/contract_wrappers/contract_wrapper.ts b/src/contract_wrappers/contract_wrapper.ts index 927a09b52..6f22f5bdb 100644 --- a/src/contract_wrappers/contract_wrapper.ts +++ b/src/contract_wrappers/contract_wrapper.ts @@ -1,10 +1,10 @@ import * as _ from 'lodash'; import * as Web3 from 'web3'; -import * as ethUtil from 'ethereumjs-util'; -import {BlockAndLogStreamer} from 'ethereumjs-blockstream'; +import {BlockAndLogStreamer, Block} from 'ethereumjs-blockstream'; import {Web3Wrapper} from '../web3_wrapper'; import {AbiDecoder} from '../utils/abi_decoder'; import { + ZeroExError, InternalZeroExError, Artifact, LogWithDecodedArgs, @@ -12,38 +12,73 @@ import { ContractEvents, SubscriptionOpts, IndexedFilterValues, + EventCallback, } from '../types'; import {utils} from '../utils/utils'; - -const TOPIC_LENGTH = 32; +import {constants} from '../utils/constants'; +import {intervalUtils} from '../utils/interval_utils'; +import {filterUtils} from '../utils/filter_utils'; export class ContractWrapper { protected _web3Wrapper: Web3Wrapper; private _abiDecoder?: AbiDecoder; private _blockAndLogStreamer: BlockAndLogStreamer; + private _blockAndLogStreamInterval: NodeJS.Timer; + private _activeFilters: number; + private _filters: {[filterToken: string]: Web3.FilterObject}; + private _filterCallbacks: {[filterToken: string]: EventCallback}; + private _onLogAddedSubscriptionToken: string|undefined; + private _onLogRemovedSubscriptionToken: string|undefined; constructor(web3Wrapper: Web3Wrapper, abiDecoder?: AbiDecoder) { this._web3Wrapper = web3Wrapper; this._abiDecoder = abiDecoder; - const getBlockAsync = async (hash: string) => this._web3Wrapper.getBlockAsync(hash); - this._blockAndLogStreamer = new BlockAndLogStreamer( - this._web3Wrapper.getBlockAsync.bind(this._web3Wrapper), - this._web3Wrapper.getLogsAsync.bind(this._web3Wrapper), + this._activeFilters = 0; + this._filters = {}; + this._filterCallbacks = {}; + this._onLogAddedSubscriptionToken = undefined; + this._onLogRemovedSubscriptionToken = undefined; + } + protected _subscribe(address: string, eventName: ContractEvents, + indexFilterValues: IndexedFilterValues, abi: Web3.ContractAbi, + callback: EventCallback): string { + const filter = filterUtils.getFilter( + this._web3Wrapper.keccak256.bind(this._web3Wrapper), address, eventName, indexFilterValues, abi, ); + if (_.isEmpty(this._filters)) { + this._startBlockAndLogStream(); + let removed = false; + this._onLogAddedSubscriptionToken = this._blockAndLogStreamer.subscribeToOnLogAdded( + this._onLogStateChanged.bind(this, removed), + ); + removed = true; + this._onLogRemovedSubscriptionToken = this._blockAndLogStreamer.subscribeToOnLogRemoved( + this._onLogStateChanged.bind(this, removed), + ); + } + const filterToken = filterUtils.generateUUID(); + this._filters[filterToken] = filter; + this._filterCallbacks[filterToken] = callback; + return filterToken; + } + protected _unsubscribe(filterToken: string): void { + if (_.isUndefined(this._filters[filterToken])) { + throw new Error(ZeroExError.SubscriptionNotFound); + } + delete this._filters[filterToken]; + delete this._filterCallbacks[filterToken]; + if (_.isEmpty(this._filters)) { + this._blockAndLogStreamer.unsubscribeFromOnLogAdded(this._onLogAddedSubscriptionToken as string); + this._blockAndLogStreamer.unsubscribeFromOnLogRemoved(this._onLogRemovedSubscriptionToken as string); + this._stopBlockAndLogStream(); + } } protected async _getLogsAsync(address: string, eventName: ContractEvents, subscriptionOpts: SubscriptionOpts, indexFilterValues: IndexedFilterValues, abi: Web3.ContractAbi): Promise { - const eventAbi = _.find(abi, {name: eventName}) as Web3.EventAbi; - const eventSignature = this._getEventSignatureFromAbiByName(eventAbi, eventName); - const topicForEventSignature = this._web3Wrapper.keccak256(eventSignature); - const topicsForIndexedArgs = this._getTopicsForIndexedArgs(eventAbi, indexFilterValues); - const topics = [topicForEventSignature, ...topicsForIndexedArgs]; - const filter = { - fromBlock: subscriptionOpts.fromBlock, - toBlock: subscriptionOpts.toBlock, - address, - topics, - }; + const filter = filterUtils.getFilter( + this._web3Wrapper.keccak256.bind(this._web3Wrapper), address, eventName, indexFilterValues, abi, + subscriptionOpts, + ); const logs = await this._web3Wrapper.getLogsAsync(filter); const logsWithDecodedArguments = _.map(logs, this._tryToDecodeLogOrNoop.bind(this)); return logsWithDecodedArguments; @@ -62,27 +97,34 @@ export class ContractWrapper { await this._web3Wrapper.getContractInstanceFromArtifactAsync(artifact, addressIfExists); return contractInstance; } - protected _getEventSignatureFromAbiByName(eventAbi: Web3.EventAbi, eventName: ContractEvents): string { - const types = _.map(eventAbi.inputs, 'type'); - const signature = `${eventAbi.name}(${types.join(',')})`; - return signature; - } - private _getTopicsForIndexedArgs(abi: Web3.EventAbi, indexFilterValues: IndexedFilterValues): Array { - const topics: Array = []; - for (const eventInput of abi.inputs) { - if (!eventInput.indexed) { - continue; + private _onLogStateChanged(removed: boolean, log: Web3.LogEntry): void { + _.forEach(this._filters, (filter: Web3.FilterObject, filterToken: string) => { + if (filterUtils.matchesFilter(log, filter)) { + const decodedLog = this._tryToDecodeLogOrNoop(log) as LogWithDecodedArgs; + const logEvent = { + ...decodedLog, + removed, + }; + this._filterCallbacks[filterToken](logEvent); } - if (_.isUndefined(indexFilterValues[eventInput.name])) { - topics.push(null); - } else { - const value = indexFilterValues[eventInput.name] as string; - const buffer = ethUtil.toBuffer(value); - const paddedBuffer = ethUtil.setLengthLeft(buffer, TOPIC_LENGTH); - const topic = ethUtil.bufferToHex(paddedBuffer); - topics.push(topic); - } - } - return topics; + }); + } + private _startBlockAndLogStream(): void { + this._blockAndLogStreamer = new BlockAndLogStreamer( + this._web3Wrapper.getBlockAsync.bind(this._web3Wrapper), + this._web3Wrapper.getLogsAsync.bind(this._web3Wrapper), + ); + this._blockAndLogStreamer.addLogFilter({}); + this._blockAndLogStreamInterval = intervalUtils.setAsyncExcludingInterval( + this._reconcileBlockAsync.bind(this), constants.DEFAULT_BLOCK_POLLING_INTERVAL, + ); + } + private _stopBlockAndLogStream(): void { + intervalUtils.clearAsyncExcludingInterval(this._blockAndLogStreamInterval); + delete this._blockAndLogStreamer; + } + private async _reconcileBlockAsync(): Promise { + const latestBlock = await this._web3Wrapper.getBlockAsync('latest'); + this._blockAndLogStreamer.reconcileNewBlock(latestBlock as any as Block); } } diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index 32eaa590c..e5f190864 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -16,11 +16,8 @@ import { SignedOrder, ContractEvent, ExchangeEvents, - ContractEventEmitter, SubscriptionOpts, IndexedFilterValues, - CreateContractEvent, - ContractEventObj, OrderCancellationRequest, OrderFillRequest, LogErrorContractEventArgs, @@ -31,10 +28,10 @@ import { ValidateOrderFillableOpts, OrderTransactionOpts, RawLog, + EventCallback, } from '../types'; import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; -import {eventUtils} from '../utils/event_utils'; import {OrderValidationUtils} from '../utils/order_validation_utils'; import {ContractWrapper} from './contract_wrapper'; import {constants} from '../utils/constants'; @@ -51,7 +48,7 @@ const SHOULD_VALIDATE_BY_DEFAULT = true; */ export class ExchangeWrapper extends ContractWrapper { private _exchangeContractIfExists?: ExchangeContract; - private _exchangeLogEventEmitters: ContractEventEmitter[]; + private _activeSubscriptions: string[]; private _orderValidationUtils: OrderValidationUtils; private _tokenWrapper: TokenWrapper; private _exchangeContractErrCodesToMsg = { @@ -86,7 +83,7 @@ export class ExchangeWrapper extends ContractWrapper { super(web3Wrapper, abiDecoder); this._tokenWrapper = tokenWrapper; this._orderValidationUtils = new OrderValidationUtils(tokenWrapper, this); - this._exchangeLogEventEmitters = []; + this._activeSubscriptions = []; this._contractAddressIfExists = contractAddressIfExists; } /** @@ -622,41 +619,32 @@ export class ExchangeWrapper extends ContractWrapper { return txHash; } /** - * Subscribe to an event type emitted by the Exchange smart contract - * @param eventName The exchange contract event you would like to subscribe to. - * @param subscriptionOpts Subscriptions options that let you configure the subscription. - * @param indexFilterValues An object where the keys are indexed args returned by the event and - * the value is the value you are interested in. E.g `{maker: aUserAddressHex}` - * @param exchangeContractAddress The hex encoded address of the Exchange contract to call. - * @return ContractEventEmitter object + * Subscribe to an event type emitted by the Exchange contract. + * @param tokenAddress The hex encoded address where the ERC20 token is deployed. + * @param eventName The exchange contract event you would like to subscribe to. + * @param indexFilterValues An object where the keys are indexed args returned by the event and + * the value is the value you are interested in. E.g `{maker: aUserAddressHex}` + * @param callback Callback that gets called when a log is added/removed + * @return ContractEventEmitter object */ - public async subscribeAsync(eventName: ExchangeEvents, subscriptionOpts: SubscriptionOpts, - indexFilterValues: IndexedFilterValues, exchangeContractAddress: string): - Promise { - assert.isETHAddressHex('exchangeContractAddress', exchangeContractAddress); + public async subscribeAsync(eventName: ExchangeEvents, indexFilterValues: IndexedFilterValues, + callback: EventCallback): Promise { assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents); - assert.doesConformToSchema('subscriptionOpts', subscriptionOpts, schemas.subscriptionOptsSchema); assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); - const exchangeContract = await this._getExchangeContractAsync(); - let createLogEvent: CreateContractEvent; - switch (eventName) { - case ExchangeEvents.LogFill: - createLogEvent = exchangeContract.LogFill; - break; - case ExchangeEvents.LogError: - createLogEvent = exchangeContract.LogError; - break; - case ExchangeEvents.LogCancel: - createLogEvent = exchangeContract.LogCancel; - break; - default: - throw utils.spawnSwitchErr('ExchangeEvents', eventName); - } - - const logEventObj: ContractEventObj = createLogEvent(indexFilterValues, subscriptionOpts); - const eventEmitter = eventUtils.wrapEventEmitter(logEventObj); - this._exchangeLogEventEmitters.push(eventEmitter); - return eventEmitter; + const exchangeContractAddress = await this.getContractAddressAsync(); + const subscriptionToken = this._subscribe( + exchangeContractAddress, eventName, indexFilterValues, artifacts.ExchangeArtifact.abi, callback, + ); + this._activeSubscriptions.push(subscriptionToken); + return subscriptionToken; + } + /** + * Cancel a subscription + * @param subscriptionToken Subscription token returned by `subscribe()` + */ + public unsubscribe(subscriptionToken: string): void { + _.pull(this._activeSubscriptions, subscriptionToken); + this._unsubscribe(subscriptionToken); } /** * Gets historical logs without creating a subscription @@ -677,15 +665,6 @@ export class ExchangeWrapper extends ContractWrapper { ); return logs; } - /** - * Stops watching for all exchange events - */ - public async stopWatchingAllEventsAsync(): Promise { - const stopWatchingPromises = _.map(this._exchangeLogEventEmitters, - logEventObj => logEventObj.stopWatchingAsync()); - await Promise.all(stopWatchingPromises); - this._exchangeLogEventEmitters = []; - } /** * Retrieves the Ethereum address of the Exchange contract deployed on the network * that the user-passed web3 provider is connected to. @@ -809,8 +788,15 @@ export class ExchangeWrapper extends ContractWrapper { const ZRXtokenAddress = await exchangeInstance.ZRX_TOKEN_CONTRACT.callAsync(); return ZRXtokenAddress; } + /** + * Cancels all existing subscriptions + */ + public unsubscribeAll(): void { + _.forEach(this._activeSubscriptions, this._unsubscribe.bind(this)); + this._activeSubscriptions = []; + } private async _invalidateContractInstancesAsync(): Promise { - await this.stopWatchingAllEventsAsync(); + this.unsubscribeAll(); delete this._exchangeContractIfExists; } private async _isValidSignatureUsingContractCallAsync(dataHex: string, ecSignature: ECSignature, diff --git a/src/contract_wrappers/token_wrapper.ts b/src/contract_wrappers/token_wrapper.ts index f988e6ece..d2b1ccf96 100644 --- a/src/contract_wrappers/token_wrapper.ts +++ b/src/contract_wrappers/token_wrapper.ts @@ -4,7 +4,6 @@ import {SchemaValidator, schemas} from '0x-json-schemas'; import {Web3Wrapper} from '../web3_wrapper'; import {assert} from '../utils/assert'; import {utils} from '../utils/utils'; -import {eventUtils} from '../utils/event_utils'; import {constants} from '../utils/constants'; import {ContractWrapper} from './contract_wrapper'; import {AbiDecoder} from '../utils/abi_decoder'; @@ -15,12 +14,10 @@ import { TokenEvents, IndexedFilterValues, SubscriptionOpts, - CreateContractEvent, - ContractEventEmitter, - ContractEventObj, MethodOpts, LogWithDecodedArgs, RawLog, + EventCallback, } from '../types'; const ALLOWANCE_TO_ZERO_GAS_AMOUNT = 47155; @@ -33,13 +30,13 @@ const ALLOWANCE_TO_ZERO_GAS_AMOUNT = 47155; export class TokenWrapper extends ContractWrapper { public UNLIMITED_ALLOWANCE_IN_BASE_UNITS = constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; private _tokenContractsByAddress: {[address: string]: TokenContract}; - private _tokenLogEventEmitters: ContractEventEmitter[]; + private _activeSubscriptions: string[]; private _tokenTransferProxyContractAddressFetcher: () => Promise; constructor(web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, tokenTransferProxyContractAddressFetcher: () => Promise) { super(web3Wrapper, abiDecoder); this._tokenContractsByAddress = {}; - this._tokenLogEventEmitters = []; + this._activeSubscriptions = []; this._tokenTransferProxyContractAddressFetcher = tokenTransferProxyContractAddressFetcher; } /** @@ -251,34 +248,29 @@ export class TokenWrapper extends ContractWrapper { * Subscribe to an event type emitted by the Token contract. * @param tokenAddress The hex encoded address where the ERC20 token is deployed. * @param eventName The token contract event you would like to subscribe to. - * @param subscriptionOpts Subscriptions options that let you configure the subscription. * @param indexFilterValues An object where the keys are indexed args returned by the event and * the value is the value you are interested in. E.g `{maker: aUserAddressHex}` + * @param callback Callback that gets called when a log is added/removed * @return ContractEventEmitter object */ - public async subscribeAsync(tokenAddress: string, eventName: TokenEvents, subscriptionOpts: SubscriptionOpts, - indexFilterValues: IndexedFilterValues): Promise { + public subscribe(tokenAddress: string, eventName: TokenEvents, indexFilterValues: IndexedFilterValues, + callback: EventCallback): string { assert.isETHAddressHex('tokenAddress', tokenAddress); assert.doesBelongToStringEnum('eventName', eventName, TokenEvents); - assert.doesConformToSchema('subscriptionOpts', subscriptionOpts, schemas.subscriptionOptsSchema); assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); - const tokenContract = await this._getTokenContractAsync(tokenAddress); - let createLogEvent: CreateContractEvent; - switch (eventName) { - case TokenEvents.Approval: - createLogEvent = tokenContract.Approval; - break; - case TokenEvents.Transfer: - createLogEvent = tokenContract.Transfer; - break; - default: - throw utils.spawnSwitchErr('TokenEvents', eventName); - } - - const logEventObj: ContractEventObj = createLogEvent(indexFilterValues, subscriptionOpts); - const eventEmitter = eventUtils.wrapEventEmitter(logEventObj); - this._tokenLogEventEmitters.push(eventEmitter); - return eventEmitter; + const subscriptionToken = this._subscribe( + tokenAddress, eventName, indexFilterValues, artifacts.TokenArtifact.abi, callback, + ); + this._activeSubscriptions.push(subscriptionToken); + return subscriptionToken; + } + /** + * Cancel a subscription + * @param subscriptionToken Subscription token returned by `subscribe()` + */ + public unsubscribe(subscriptionToken: string): void { + _.pull(this._activeSubscriptions, subscriptionToken); + this._unsubscribe(subscriptionToken); } /** * Gets historical logs without creating a subscription @@ -301,16 +293,14 @@ export class TokenWrapper extends ContractWrapper { return logs; } /** - * Stops watching for all token events + * Cancels all existing subscriptions */ - public async stopWatchingAllEventsAsync(): Promise { - const stopWatchingPromises = _.map(this._tokenLogEventEmitters, - logEventObj => logEventObj.stopWatchingAsync()); - await Promise.all(stopWatchingPromises); - this._tokenLogEventEmitters = []; + public unsubscribeAll(): void { + _.forEach(this._activeSubscriptions, this._unsubscribe.bind(this)); + this._activeSubscriptions = []; } - private async _invalidateContractInstancesAsync(): Promise { - await this.stopWatchingAllEventsAsync(); + private _invalidateContractInstancesAsync(): void { + this.unsubscribeAll(); this._tokenContractsByAddress = {}; } private async _getTokenContractAsync(tokenAddress: string): Promise { diff --git a/src/index.ts b/src/index.ts index 3359743e9..97ab084b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,6 @@ export { OrderFillOrKillRequest, OrderCancellationRequest, OrderFillRequest, - ContractEventEmitter, LogErrorContractEventArgs, LogCancelContractEventArgs, LogFillContractEventArgs, @@ -36,4 +35,5 @@ export { MethodOpts, OrderTransactionOpts, FilterObject, + LogEvent, } from './types'; diff --git a/src/types.ts b/src/types.ts index 35bb6af78..f0f37bfca 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,7 @@ export enum ZeroExError { InvalidJump = 'INVALID_JUMP', OutOfGas = 'OUT_OF_GAS', NoNetworkId = 'NO_NETWORK_ID', + SubscriptionNotFound = 'SUBSCRIPTION_NOT_FOUND', } export enum InternalZeroExError { @@ -35,23 +36,17 @@ export type OrderAddresses = [string, string, string, string, string]; export type OrderValues = [BigNumber.BigNumber, BigNumber.BigNumber, BigNumber.BigNumber, BigNumber.BigNumber, BigNumber.BigNumber, BigNumber.BigNumber]; -export type EventCallbackAsync = (err: Error, event: ContractEvent) => Promise; -export type EventCallbackSync = (err: Error, event: ContractEvent) => void; -export type EventCallback = EventCallbackSync|EventCallbackAsync; -export interface ContractEventObj { - watch: (eventWatch: EventCallback) => void; - stopWatching: () => void; +export interface LogEvent extends LogWithDecodedArgs { + removed: boolean; } -export type CreateContractEvent = (indexFilterValues: IndexedFilterValues, - subscriptionOpts: SubscriptionOpts) => ContractEventObj; +export type EventCallbackAsync = (log: LogEvent) => Promise; +export type EventCallbackSync = (log: LogEvent) => void; +export type EventCallback = EventCallbackSync|EventCallbackAsync; export interface ExchangeContract extends Web3.ContractInstance { isValidSignature: { callAsync: (signerAddressHex: string, dataHex: string, v: number, r: string, s: string, txOpts?: TxOpts) => Promise; }; - LogFill: CreateContractEvent; - LogCancel: CreateContractEvent; - LogError: CreateContractEvent; ZRX_TOKEN_CONTRACT: { callAsync: () => Promise; }; @@ -137,8 +132,6 @@ export interface ExchangeContract extends Web3.ContractInstance { } export interface TokenContract extends Web3.ContractInstance { - Transfer: CreateContractEvent; - Approval: CreateContractEvent; balanceOf: { callAsync: (address: string, defaultBlock?: Web3.BlockParam) => Promise; }; @@ -378,11 +371,6 @@ export interface OrderFillRequest { export type AsyncMethod = (...args: any[]) => Promise; -export interface ContractEventEmitter { - watch: (eventCallback: EventCallback) => void; - stopWatchingAsync: () => Promise; -} - /** * We re-export the `Web3.Provider` type specified in the Web3 Typescript typings * since it is the type of the `provider` argument to the `ZeroEx` constructor. diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 1b11d7055..a066fe869 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -7,4 +7,5 @@ export const constants = { INVALID_JUMP_PATTERN: 'invalid JUMP at', OUT_OF_GAS_PATTERN: 'out of gas', UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), + DEFAULT_BLOCK_POLLING_INTERVAL: 1000, }; diff --git a/src/utils/event_utils.ts b/src/utils/event_utils.ts deleted file mode 100644 index e8f30e1a8..000000000 --- a/src/utils/event_utils.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as _ from 'lodash'; -import {EventCallback, ContractEventArg, ContractEvent, ContractEventObj, ContractEventEmitter} from '../types'; -import * as BigNumber from 'bignumber.js'; -import promisify = require('es6-promisify'); - -export const eventUtils = { - wrapEventEmitter(event: ContractEventObj): ContractEventEmitter { - const watch = (eventCallback: EventCallback) => { - const bignumberWrappingEventCallback = eventUtils._getBigNumberWrappingEventCallback(eventCallback); - event.watch(bignumberWrappingEventCallback); - }; - const zeroExEvent = { - watch, - stopWatchingAsync: async () => { - await promisify(event.stopWatching, event)(); - }, - }; - return zeroExEvent; - }, - /** - * Wraps eventCallback function so that all the BigNumber arguments are wrapped in a newer version of BigNumber. - * @param eventCallback Event callback function to be wrapped - * @return Wrapped event callback function - */ - _getBigNumberWrappingEventCallback(eventCallback: EventCallback): EventCallback { - const bignumberWrappingEventCallback = (err: Error, event: ContractEvent) => { - if (_.isNull(err)) { - const wrapIfBigNumber = (value: ContractEventArg): ContractEventArg => { - // HACK: The old version of BigNumber used by Web3@0.19.0 does not support the `isBigNumber` - // and checking for a BigNumber instance using `instanceof` does not work either. We therefore - // check if the value constructor is a bignumber constructor. - const isWeb3BigNumber = _.startsWith(value.constructor.toString(), 'function BigNumber('); - return isWeb3BigNumber ? new BigNumber(value) : value; - }; - event.args = _.mapValues(event.args, wrapIfBigNumber); - } - eventCallback(err, event); - }; - return bignumberWrappingEventCallback; - }, -}; diff --git a/src/utils/filter_utils.ts b/src/utils/filter_utils.ts new file mode 100644 index 000000000..ee39b6836 --- /dev/null +++ b/src/utils/filter_utils.ts @@ -0,0 +1,80 @@ +import * as _ from 'lodash'; +import * as Web3 from 'web3'; +import * as uuid from 'uuid/v4'; +import * as ethUtil from 'ethereumjs-util'; +import {ContractEvents, IndexedFilterValues, SubscriptionOpts} from '../types'; + +const TOPIC_LENGTH = 32; + +export const filterUtils = { + generateUUID(): string { + return uuid(); + }, + getFilter(keccak256: (data: string) => string, address: string, eventName: ContractEvents, + indexFilterValues: IndexedFilterValues, abi: Web3.ContractAbi, + subscriptionOpts?: SubscriptionOpts): Web3.FilterObject { + const eventAbi = _.find(abi, {name: eventName}) as Web3.EventAbi; + const eventSignature = filterUtils.getEventSignatureFromAbiByName(eventAbi, eventName); + const topicForEventSignature = keccak256(eventSignature); + const topicsForIndexedArgs = filterUtils.getTopicsForIndexedArgs(eventAbi, indexFilterValues); + const topics = [topicForEventSignature, ...topicsForIndexedArgs]; + let filter: Web3.FilterObject = { + address, + topics, + }; + if (!_.isUndefined(subscriptionOpts)) { + filter = { + ...subscriptionOpts, + ...filter, + }; + } + return filter; + }, + getEventSignatureFromAbiByName(eventAbi: Web3.EventAbi, eventName: ContractEvents): string { + const types = _.map(eventAbi.inputs, 'type'); + const signature = `${eventAbi.name}(${types.join(',')})`; + return signature; + }, + getTopicsForIndexedArgs(abi: Web3.EventAbi, indexFilterValues: IndexedFilterValues): Array { + const topics: Array = []; + for (const eventInput of abi.inputs) { + if (!eventInput.indexed) { + continue; + } + if (_.isUndefined(indexFilterValues[eventInput.name])) { + topics.push(null); + } else { + const value = indexFilterValues[eventInput.name] as string; + const buffer = ethUtil.toBuffer(value); + const paddedBuffer = ethUtil.setLengthLeft(buffer, TOPIC_LENGTH); + const topic = ethUtil.bufferToHex(paddedBuffer); + topics.push(topic); + } + } + return topics; + }, + matchesFilter(log: Web3.LogEntry, filter: Web3.FilterObject): boolean { + if (!_.isUndefined(filter.address) && log.address !== filter.address) { + return false; + } + if (!_.isUndefined(filter.topics)) { + return filterUtils.matchesTopics(log.topics, filter.topics); + } + return true; + }, + matchesTopics(logTopics: string[], filterTopics: Array): boolean { + const matchesTopic = _.zipWith(logTopics, filterTopics, filterUtils.matchesTopic.bind(filterUtils)); + const matchesTopics = _.every(matchesTopic); + return matchesTopics; + }, + matchesTopic(logTopic: string, filterTopic: string[]|string|null): boolean { + if (_.isArray(filterTopic)) { + return _.includes(filterTopic, logTopic); + } + if (_.isString(filterTopic)) { + return filterTopic === logTopic; + } + // null topic is a wildcard + return true; + }, +}; diff --git a/test/exchange_wrapper_test.ts b/test/exchange_wrapper_test.ts index 71c5713ad..51d6957d5 100644 --- a/test/exchange_wrapper_test.ts +++ b/test/exchange_wrapper_test.ts @@ -19,6 +19,7 @@ import { OrderFillRequest, LogFillContractEventArgs, OrderFillOrKillRequest, + LogEvent, } from '../src'; import {DoneCallback} from '../src/types'; import {FillScenarios} from './utils/fill_scenarios'; @@ -616,7 +617,7 @@ describe('ExchangeWrapper', () => { }); }); }); - describe('#subscribeAsync', () => { + describe('#subscribe', () => { const indexFilterValues = {}; const shouldThrowOnInsufficientBalanceOrAllowance = true; let makerTokenAddress: string; @@ -626,10 +627,6 @@ describe('ExchangeWrapper', () => { let makerAddress: string; let fillableAmount: BigNumber.BigNumber; let signedOrder: SignedOrder; - const subscriptionOpts: SubscriptionOpts = { - fromBlock: 0, - toBlock: 'latest', - }; const fillTakerAmountInBaseUnits = new BigNumber(1); const cancelTakerAmountInBaseUnits = new BigNumber(1); before(() => { @@ -645,24 +642,22 @@ describe('ExchangeWrapper', () => { ); }); afterEach(async () => { - await zeroEx.exchange.stopWatchingAllEventsAsync(); + zeroEx.exchange.unsubscribeAll(); }); // Hack: Mocha does not allow a test to be both async and have a `done` callback - // Since we need to await the receipt of the event in the `subscribeAsync` callback, + // Since we need to await the receipt of the event in the `subscribe` callback, // we do need both. A hack is to make the top-level a sync fn w/ a done callback and then // wrap the rest of the test in an async block // Source: https://github.com/mochajs/mocha/issues/2407 it('Should receive the LogFill event when an order is filled', (done: DoneCallback) => { (async () => { - const zeroExEvent = await zeroEx.exchange.subscribeAsync( - ExchangeEvents.LogFill, subscriptionOpts, indexFilterValues, exchangeContractAddress, - ); - zeroExEvent.watch((err: Error, event: ContractEvent) => { - expect(err).to.be.null(); - expect(event).to.not.be.undefined(); - expect(event.event).to.be.equal('LogFill'); + const callback = (logEvent: LogEvent) => { + expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill); done(); - }); + }; + await zeroEx.exchange.subscribeAsync( + ExchangeEvents.LogFill, indexFilterValues, callback, + ); await zeroEx.exchange.fillOrderAsync( signedOrder, fillTakerAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, ); @@ -670,75 +665,53 @@ describe('ExchangeWrapper', () => { }); it('Should receive the LogCancel event when an order is cancelled', (done: DoneCallback) => { (async () => { - const zeroExEvent = await zeroEx.exchange.subscribeAsync( - ExchangeEvents.LogCancel, subscriptionOpts, indexFilterValues, exchangeContractAddress, + const callback = (logEvent: LogEvent) => { + expect(logEvent.event).to.be.equal(ExchangeEvents.LogCancel); + done(); + }; + await zeroEx.exchange.subscribeAsync( + ExchangeEvents.LogCancel, indexFilterValues, callback, ); - zeroExEvent.watch((err: Error, event: ContractEvent) => { - expect(err).to.be.null(); - expect(event).to.not.be.undefined(); - expect(event.event).to.be.equal('LogCancel'); - done(); - }); await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelTakerAmountInBaseUnits); })().catch(done); }); it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => { (async () => { - const eventSubscriptionToBeCancelled = await zeroEx.exchange.subscribeAsync( - ExchangeEvents.LogFill, subscriptionOpts, indexFilterValues, exchangeContractAddress, - ); - eventSubscriptionToBeCancelled.watch((err: Error, event: ContractEvent) => { + const callbackNeverToBeCalled = (logEvent: LogEvent) => { done(new Error('Expected this subscription to have been cancelled')); - }); + }; + await zeroEx.exchange.subscribeAsync( + ExchangeEvents.LogFill, indexFilterValues, callbackNeverToBeCalled, + ); const newProvider = web3Factory.getRpcProvider(); await zeroEx.setProviderAsync(newProvider); - const eventSubscriptionToStay = await zeroEx.exchange.subscribeAsync( - ExchangeEvents.LogFill, subscriptionOpts, indexFilterValues, exchangeContractAddress, - ); - eventSubscriptionToStay.watch((err: Error, event: ContractEvent) => { - expect(err).to.be.null(); - expect(event).to.not.be.undefined(); - expect(event.event).to.be.equal('LogFill'); + const callback = (logEvent: LogEvent) => { + expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill); done(); - }); - await zeroEx.exchange.fillOrderAsync( - signedOrder, fillTakerAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, - ); - })().catch(done); - }); - it('Should stop watch for events when stopWatchingAsync called on the eventEmitter', (done: DoneCallback) => { - (async () => { - const eventSubscriptionToBeStopped = await zeroEx.exchange.subscribeAsync( - ExchangeEvents.LogFill, subscriptionOpts, indexFilterValues, exchangeContractAddress, + }; + await zeroEx.exchange.subscribeAsync( + ExchangeEvents.LogFill, indexFilterValues, callback, ); - eventSubscriptionToBeStopped.watch((err: Error, event: ContractEvent) => { - done(new Error('Expected this subscription to have been stopped')); - }); - await eventSubscriptionToBeStopped.stopWatchingAsync(); await zeroEx.exchange.fillOrderAsync( signedOrder, fillTakerAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, ); - done(); })().catch(done); }); - it('Should wrap all event args BigNumber instances in a newer version of BigNumber', (done: DoneCallback) => { + it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { (async () => { - const zeroExEvent = await zeroEx.exchange.subscribeAsync( - ExchangeEvents.LogFill, subscriptionOpts, indexFilterValues, exchangeContractAddress, + const callbackNeverToBeCalled = (logEvent: LogEvent) => { + done(new Error('Expected this subscription to have been cancelled')); + }; + const subscriptionToken = await zeroEx.exchange.subscribeAsync( + ExchangeEvents.LogFill, indexFilterValues, callbackNeverToBeCalled, ); - zeroExEvent.watch((err: Error, event: ContractEvent) => { - const args = event.args as LogFillContractEventArgs; - expect(args.filledMakerTokenAmount.isBigNumber).to.be.true(); - expect(args.filledTakerTokenAmount.isBigNumber).to.be.true(); - expect(args.paidMakerFee.isBigNumber).to.be.true(); - expect(args.paidTakerFee.isBigNumber).to.be.true(); - done(); - }); + zeroEx.exchange.unsubscribe(subscriptionToken); await zeroEx.exchange.fillOrderAsync( signedOrder, fillTakerAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, ); + done(); })().catch(done); }); }); diff --git a/test/token_wrapper_test.ts b/test/token_wrapper_test.ts index da020f714..be97496e0 100644 --- a/test/token_wrapper_test.ts +++ b/test/token_wrapper_test.ts @@ -15,6 +15,7 @@ import { TransferContractEventArgs, ApprovalContractEventArgs, LogWithDecodedArgs, + LogEvent, } from '../src'; import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; import {TokenUtils} from './utils/token_utils'; @@ -336,22 +337,18 @@ describe('TokenWrapper', () => { return expect(allowance).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS); }); }); - describe('#subscribeAsync', () => { + describe('#subscribe', () => { const indexFilterValues = {}; const shouldThrowOnInsufficientBalanceOrAllowance = true; let tokenAddress: string; - const subscriptionOpts: SubscriptionOpts = { - fromBlock: 0, - toBlock: 'latest', - }; const transferAmount = new BigNumber(42); const allowanceAmount = new BigNumber(42); before(() => { const token = tokens[0]; tokenAddress = token.address; }); - afterEach(async () => { - await zeroEx.token.stopWatchingAllEventsAsync(); + afterEach(() => { + zeroEx.token.unsubscribeAll(); }); // Hack: Mocha does not allow a test to be both async and have a `done` callback // Since we need to await the receipt of the event in the `subscribeAsync` callback, @@ -360,81 +357,66 @@ describe('TokenWrapper', () => { // Source: https://github.com/mochajs/mocha/issues/2407 it('Should receive the Transfer event when tokens are transfered', (done: DoneCallback) => { (async () => { - const zeroExEvent = await zeroEx.token.subscribeAsync( - tokenAddress, TokenEvents.Transfer, subscriptionOpts, indexFilterValues); - zeroExEvent.watch((err: Error, event: ContractEvent) => { - expect(err).to.be.null(); - expect(event).to.not.be.undefined(); - const args = event.args as TransferContractEventArgs; + const callback = (logEvent: LogEvent) => { + expect(logEvent).to.not.be.undefined(); + const args = logEvent.args as any as TransferContractEventArgs; expect(args._from).to.be.equal(coinbase); expect(args._to).to.be.equal(addressWithoutFunds); expect(args._value).to.be.bignumber.equal(transferAmount); done(); - }); + }; + zeroEx.token.subscribe( + tokenAddress, TokenEvents.Transfer, indexFilterValues, callback); await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount); })().catch(done); }); it('Should receive the Approval event when allowance is being set', (done: DoneCallback) => { (async () => { - const zeroExEvent = await zeroEx.token.subscribeAsync( - tokenAddress, TokenEvents.Approval, subscriptionOpts, indexFilterValues); - zeroExEvent.watch((err: Error, event: ContractEvent) => { - expect(err).to.be.null(); - expect(event).to.not.be.undefined(); - const args = event.args as ApprovalContractEventArgs; + const callback = (logEvent: LogEvent) => { + expect(logEvent).to.not.be.undefined(); + const args = logEvent.args as any as ApprovalContractEventArgs; expect(args._owner).to.be.equal(coinbase); expect(args._spender).to.be.equal(addressWithoutFunds); expect(args._value).to.be.bignumber.equal(allowanceAmount); done(); - }); + }; + zeroEx.token.subscribe( + tokenAddress, TokenEvents.Approval, indexFilterValues, callback); await zeroEx.token.setAllowanceAsync(tokenAddress, coinbase, addressWithoutFunds, allowanceAmount); })().catch(done); }); it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => { (async () => { - const eventSubscriptionToBeCancelled = await zeroEx.token.subscribeAsync( - tokenAddress, TokenEvents.Transfer, subscriptionOpts, indexFilterValues); - eventSubscriptionToBeCancelled.watch((err: Error, event: ContractEvent) => { + const callbackNeverToBeCalled = (logEvent: LogEvent) => { done(new Error('Expected this subscription to have been cancelled')); - }); - + }; + zeroEx.token.subscribe( + tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackNeverToBeCalled, + ); + const callbackToBeCalled = (logEvent: LogEvent) => { + done(); + }; const newProvider = web3Factory.getRpcProvider(); await zeroEx.setProviderAsync(newProvider); - - const eventSubscriptionToStay = await zeroEx.token.subscribeAsync( - tokenAddress, TokenEvents.Transfer, subscriptionOpts, indexFilterValues); - eventSubscriptionToStay.watch((err: Error, event: ContractEvent) => { - expect(err).to.be.null(); - expect(event).to.not.be.undefined(); - done(); - }); + zeroEx.token.subscribe( + tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackToBeCalled, + ); await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount); })().catch(done); }); - it('Should stop watch for events when stopWatchingAsync called on the eventEmitter', (done: DoneCallback) => { + it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { (async () => { - const eventSubscriptionToBeStopped = await zeroEx.token.subscribeAsync( - tokenAddress, TokenEvents.Transfer, subscriptionOpts, indexFilterValues); - eventSubscriptionToBeStopped.watch((err: Error, event: ContractEvent) => { - done(new Error('Expected this subscription to have been stopped')); - }); - await eventSubscriptionToBeStopped.stopWatchingAsync(); + const callbackNeverToBeCalled = (logEvent: LogEvent) => { + done(new Error('Expected this subscription to have been cancelled')); + }; + const subscriptionToken = zeroEx.token.subscribe( + tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackNeverToBeCalled); + zeroEx.token.unsubscribe(subscriptionToken); await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount); done(); })().catch(done); }); - it('Should wrap all event args BigNumber instances in a newer version of BigNumber', (done: DoneCallback) => { - (async () => { - const zeroExEvent = await zeroEx.token.subscribeAsync( - tokenAddress, TokenEvents.Transfer, subscriptionOpts, indexFilterValues); - zeroExEvent.watch((err: Error, event: ContractEvent) => { - const args = event.args as TransferContractEventArgs; - expect(args._value.isBigNumber).to.be.true(); - done(); - }); - await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount); - })().catch(done); - }); + // TODO test block reorgs }); describe('#getLogsAsync', () => { let tokenAddress: string; diff --git a/yarn.lock b/yarn.lock index 4d27d246c..577aa03c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -62,6 +62,12 @@ version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-2.3.2.tgz#1e1e99e67162d78e2db17816892bf93bf5209885" +"@types/uuid@^3.4.2": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.2.tgz#b8cde3c32c273d3fc658b96f810e8ff091e1b723" + dependencies: + "@types/node" "*" + abbrev@1: version "1.1.0" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" @@ -1155,11 +1161,9 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -commander@2.9.0, commander@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" - dependencies: - graceful-readlink ">= 1.0.0" +commander@2.11.0, commander@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" commondir@^1.0.1: version "1.0.1" @@ -1321,11 +1325,11 @@ debug-log@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debug-log/-/debug-log-1.0.1.tgz#2307632d4c04382b8df8a32f70b895046d52745f" -debug@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: - ms "0.7.2" + ms "2.0.0" debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.3: version "2.6.8" @@ -1407,9 +1411,9 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" -diff@3.2.0, diff@^3.1.0, diff@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" +diff@3.3.1, diff@^3.1.0, diff@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" diffie-hellman@^5.0.0: version "5.0.2" @@ -1604,6 +1608,15 @@ escope@^3.6.0: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-plugin-node@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-5.2.0.tgz#e1efca04a385516cff3f2f04027ce8c5ae6db749" + dependencies: + ignore "^3.3.3" + minimatch "^3.0.4" + resolve "^1.3.3" + semver "5.3.0" + esprima@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" @@ -2120,18 +2133,7 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" -glob@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.2" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.0, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@~7.1.2: +glob@7.1.2, glob@^7.0.0, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@~7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -2157,13 +2159,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" -"graceful-readlink@>= 1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" - -growl@1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" +growl@1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.2.tgz#8f16dfcd8fb7c04cfc1f4e0012e0ea858726979a" + dependencies: + eslint-plugin-node "^5.1.0" handlebars@^4.0.3, handlebars@^4.0.6: version "4.0.10" @@ -2196,6 +2196,10 @@ has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -2247,6 +2251,10 @@ hdkey@^0.7.0: coinstring "^2.0.0" secp256k1 "^3.0.1" +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + highlight.js@^9.0.0: version "9.12.0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" @@ -2301,6 +2309,10 @@ ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" +ignore@^3.3.3: + version "3.3.5" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.5.tgz#c4e715455f6073a8d7e5dae72d2fc9d71663dba6" + immediate@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.2.3.tgz#d140fa8f614659bd6541233097ddaac25cdd991c" @@ -2680,10 +2692,6 @@ json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" -json3@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" - json5@^0.5.0, json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" @@ -2852,61 +2860,14 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" -lodash._baseassign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" - dependencies: - lodash._basecopy "^3.0.0" - lodash.keys "^3.0.0" - -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" - -lodash._basecreate@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" - -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - -lodash._isiterateecall@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" - lodash.assign@^4.0.3, lodash.assign@^4.0.6: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" -lodash.create@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" - dependencies: - lodash._baseassign "^3.0.0" - lodash._basecreate "^3.0.0" - lodash._isiterateecall "^3.0.0" - lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" - lodash@^3.3.1, lodash@^3.6.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" @@ -3155,25 +3116,20 @@ mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: dependencies: minimist "0.0.8" -mocha@^3.4.1: - version "3.4.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.4.2.tgz#d0ef4d332126dbf18d0d640c9b382dd48be97594" +mocha@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.0.0.tgz#3da718ccd76e93b9d82afb065e17086bdbe352bf" dependencies: browser-stdout "1.3.0" - commander "2.9.0" - debug "2.6.0" - diff "3.2.0" + commander "2.11.0" + debug "3.1.0" + diff "3.3.1" escape-string-regexp "1.0.5" - glob "7.1.1" - growl "1.9.2" - json3 "3.3.2" - lodash.create "3.1.1" + glob "7.1.2" + growl "1.10.2" + he "1.1.1" mkdirp "0.5.1" - supports-color "3.1.2" - -ms@0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" + supports-color "4.4.0" ms@2.0.0: version "2.0.0" @@ -4017,7 +3973,7 @@ resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" -resolve@^1.1.6, resolve@^1.3.2, resolve@~1.3.3: +resolve@^1.1.6, resolve@^1.3.2, resolve@^1.3.3, resolve@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" dependencies: @@ -4105,7 +4061,7 @@ semver-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-1.0.0.tgz#92a4969065f9c70c694753d55248fc68f8f652c9" -"semver@2 || 3 || 4 || 5", semver@^5.3.0: +"semver@2 || 3 || 4 || 5", semver@5.3.0, semver@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -4294,6 +4250,12 @@ source-map-support@^0.4.15, source-map-support@^0.4.2: dependencies: source-map "^0.5.6" +source-map-support@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.0.tgz#2018a7ad2bdf8faf2691e5fddab26bed5a2bacab" + dependencies: + source-map "^0.6.0" + source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" @@ -4308,6 +4270,10 @@ source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, sour version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + spawn-wrap@^1.3.7: version "1.3.8" resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.3.8.tgz#fa2a79b990cbb0bb0018dca6748d88367b19ec31" @@ -4500,16 +4466,22 @@ strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" -supports-color@3.1.2, supports-color@^3.1.0, supports-color@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" +supports-color@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" dependencies: - has-flag "^1.0.0" + has-flag "^2.0.0" supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" +supports-color@^3.1.0, supports-color@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" + dependencies: + has-flag "^1.0.0" + tapable@^0.2.5, tapable@~0.2.5: version "0.2.6" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d" @@ -4886,7 +4858,7 @@ uuid@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" -uuid@^3.0.0: +uuid@^3.0.0, uuid@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" -- cgit