diff options
31 files changed, 461 insertions, 235 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml index 7027e3999..3393da379 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -65,6 +65,7 @@ jobs: keys: - repo-{{ .Environment.CIRCLE_SHA1 }} - run: yarn wsrun test:circleci @0xproject/0x.js + - run: yarn wsrun test:circleci @0xproject/abi-gen - run: yarn wsrun test:circleci @0xproject/assert - run: yarn wsrun test:circleci @0xproject/base-contract - run: yarn wsrun test:circleci @0xproject/connect @@ -84,6 +85,10 @@ jobs: paths: - ~/repo/packages/0x.js/coverage/lcov.info - save_cache: + key: coverage-abi-gen-{{ .Environment.CIRCLE_SHA1 }} + paths: + - ~/repo/packages/abi-gen/coverage/lcov.info + - save_cache: key: coverage-assert-{{ .Environment.CIRCLE_SHA1 }} paths: - ~/repo/packages/assert/coverage/lcov.info @@ -162,6 +167,9 @@ jobs: - coverage-0xjs-{{ .Environment.CIRCLE_SHA1 }} - restore_cache: keys: + - coverage-abi-gen-{{ .Environment.CIRCLE_SHA1 }} + - restore_cache: + keys: - coverage-assert-{{ .Environment.CIRCLE_SHA1 }} - restore_cache: keys: diff --git a/packages/abi-gen/CHANGELOG.json b/packages/abi-gen/CHANGELOG.json index 38b72dbf3..ef8f5dd39 100644 --- a/packages/abi-gen/CHANGELOG.json +++ b/packages/abi-gen/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "0.4.1", + "changes": [ + { + "note": "skip generation of wrappers that are already up to date", + "pr": 788 + } + ] + }, + { "version": "0.4.0", "changes": [ { diff --git a/packages/abi-gen/bin/abi-gen.js b/packages/abi-gen/bin/abi-gen.js index c46eb9b66..8d6bdccf8 100755 --- a/packages/abi-gen/bin/abi-gen.js +++ b/packages/abi-gen/bin/abi-gen.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('../lib/index.js') +require('../lib/src/index.js') diff --git a/packages/abi-gen/coverage/.gitkeep b/packages/abi-gen/coverage/.gitkeep new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/packages/abi-gen/coverage/.gitkeep diff --git a/packages/abi-gen/package.json b/packages/abi-gen/package.json index 7d7a20e68..4fb335545 100644 --- a/packages/abi-gen/package.json +++ b/packages/abi-gen/package.json @@ -12,6 +12,11 @@ "lint": "tslint --project .", "clean": "shx rm -rf lib scripts", "build": "tsc && copyfiles -u 2 './lib/monorepo_scripts/**/*' ./scripts", + "test": "yarn run_mocha", + "run_mocha": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js --bail --exit", + "test:circleci": "yarn test:coverage", + "test:coverage": "nyc npm run test --all && yarn coverage:report:lcov", + "coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info", "manual:postpublish": "yarn build; node ./scripts/postpublish.js" }, "bin": { @@ -31,11 +36,13 @@ "@0xproject/utils": "^0.7.2", "ethereum-types": "^0.0.2", "chalk": "^2.3.0", + "ethereum-types": "^0.0.2", "glob": "^7.1.2", "handlebars": "^4.0.11", "lodash": "^4.17.4", - "rimraf": "^2.6.2", "mkdirp": "^0.5.1", + "sleep": "^5.1.1", + "tmp": "^0.0.33", "to-snake-case": "^1.0.0", "yargs": "^10.0.3" }, @@ -43,13 +50,17 @@ "@0xproject/monorepo-scripts": "^0.2.2", "@0xproject/tslint-config": "^0.4.21", "@types/glob": "5.0.35", - "@types/rimraf": "^2.0.2", "@types/handlebars": "^4.0.36", "@types/mkdirp": "^0.5.1", "@types/node": "^8.0.53", + "@types/sleep": "^0.0.7", + "@types/tmp": "^0.0.33", "@types/yargs": "^10.0.0", + "chai": "^4.1.2", "copyfiles": "^1.2.0", + "dirty-chai": "^2.0.1", "make-promises-safe": "^1.1.0", + "mocha": "^5.2.0", "npm-run-all": "^4.1.2", "shx": "^0.2.2", "tslint": "5.8.0", diff --git a/packages/abi-gen/src/index.ts b/packages/abi-gen/src/index.ts index 47f2c404b..753bb2cce 100644 --- a/packages/abi-gen/src/index.ts +++ b/packages/abi-gen/src/index.ts @@ -3,16 +3,12 @@ import { abiUtils, logUtils } from '@0xproject/utils'; import chalk from 'chalk'; import { AbiDefinition, ConstructorAbi, EventAbi, MethodAbi } from 'ethereum-types'; -import * as fs from 'fs'; import { sync as globSync } from 'glob'; import * as Handlebars from 'handlebars'; import * as _ from 'lodash'; import * as mkdirp from 'mkdirp'; -import * as rimraf from 'rimraf'; import * as yargs from 'yargs'; -import toSnakeCase = require('to-snake-case'); - import { ContextData, ContractsBackend, ParamKind } from './types'; import { utils } from './utils'; @@ -71,16 +67,6 @@ function registerPartials(partialsGlob: string): void { } } -function writeOutputFile(name: string, renderedTsCode: string): void { - let fileName = toSnakeCase(name); - // HACK: Snake case doesn't make a lot of sense for abbreviated names but we can't reliably detect abbreviations - // so we special-case the abbreviations we use. - fileName = fileName.replace('z_r_x', 'zrx').replace('e_r_c', 'erc'); - const filePath = `${args.output}/${fileName}.ts`; - fs.writeFileSync(filePath, renderedTsCode); - logUtils.log(`Created: ${chalk.bold(filePath)}`); -} - Handlebars.registerHelper('parameterType', utils.solTypeToTsType.bind(utils, ParamKind.Input, args.backend)); Handlebars.registerHelper('returnType', utils.solTypeToTsType.bind(utils, ParamKind.Output, args.backend)); if (args.partials) { @@ -97,7 +83,6 @@ if (_.isEmpty(abiFileNames)) { process.exit(1); } else { logUtils.log(`Found ${chalk.green(`${abiFileNames.length}`)} ${chalk.bold('ABI')} files`); - rimraf.sync(args.output); mkdirp.sync(args.output); } for (const abiFileName of abiFileNames) { @@ -120,6 +105,14 @@ for (const abiFileName of abiFileNames) { process.exit(1); } + const outFileName = utils.makeOutputFileName(namedContent.name); + const outFilePath = `${args.output}/${outFileName}.ts`; + + if (utils.isOutputFileUpToDate(abiFileName, outFilePath)) { + logUtils.log(`Aready up to date: ${chalk.bold(outFilePath)}`); + continue; + } + let ctor = ABI.find((abi: AbiDefinition) => abi.type === ABI_TYPE_CONSTRUCTOR) as ConstructorAbi; if (_.isUndefined(ctor)) { ctor = utils.getEmptyConstructor(); // The constructor exists, but it's implicit in JSON's ABI definition @@ -154,5 +147,6 @@ for (const abiFileName of abiFileNames) { events: eventAbis, }; const renderedTsCode = template(contextData); - writeOutputFile(namedContent.name, renderedTsCode); + utils.writeOutputFile(outFilePath, renderedTsCode); + logUtils.log(`Created: ${chalk.bold(outFilePath)}`); } diff --git a/packages/abi-gen/src/utils.ts b/packages/abi-gen/src/utils.ts index 66390174c..56b996ce3 100644 --- a/packages/abi-gen/src/utils.ts +++ b/packages/abi-gen/src/utils.ts @@ -2,6 +2,7 @@ import { AbiType, ConstructorAbi, DataItem } from 'ethereum-types'; import * as fs from 'fs'; import * as _ from 'lodash'; import * as path from 'path'; +import toSnakeCase = require('to-snake-case'); import { ContractsBackend, ParamKind } from './types'; @@ -92,4 +93,27 @@ export const utils = { inputs: [], }; }, + makeOutputFileName(name: string): string { + let fileName = toSnakeCase(name); + // HACK: Snake case doesn't make a lot of sense for abbreviated names but we can't reliably detect abbreviations + // so we special-case the abbreviations we use. + fileName = fileName.replace('z_r_x', 'zrx').replace('e_r_c', 'erc'); + return fileName; + }, + writeOutputFile(filePath: string, renderedTsCode: string): void { + fs.writeFileSync(filePath, renderedTsCode); + }, + isOutputFileUpToDate(abiFile: string, outputFile: string): boolean { + const abiFileModTimeMs = fs.statSync(abiFile).mtimeMs; + try { + const outFileModTimeMs = fs.statSync(outputFile).mtimeMs; + return outFileModTimeMs > abiFileModTimeMs; + } catch (err) { + if (err.code === 'ENOENT') { + return false; + } else { + throw err; + } + } + }, }; diff --git a/packages/abi-gen/test/utils_test.ts b/packages/abi-gen/test/utils_test.ts new file mode 100644 index 000000000..c6147df38 --- /dev/null +++ b/packages/abi-gen/test/utils_test.ts @@ -0,0 +1,86 @@ +import * as chai from 'chai'; +import * as dirtyChai from 'dirty-chai'; +import * as fs from 'fs'; +import 'mocha'; +import * as sleep from 'sleep'; +import * as tmp from 'tmp'; + +import { utils } from '../src/utils'; + +tmp.setGracefulCleanup(); // remove tmp files even if there are failures + +chai.use(dirtyChai); + +const expect = chai.expect; + +const SLEEP_MS = 10; // time to wait before re-timestamping a file + +describe('makeOutputFileName()', () => { + it('should handle Metacoin usage', () => { + expect(utils.makeOutputFileName('Metacoin')).to.equal('metacoin'); + }); + it('should handle special zrx_token case', () => { + expect(utils.makeOutputFileName('ZRXToken')).to.equal('zrx_token'); + }); + it('should handle special erc_token case', () => { + expect(utils.makeOutputFileName('ERC20Token')).to.equal('erc20_token'); + }); +}); + +describe('writeOutputFile()', () => { + let tempFilePath: string; + before(() => { + tempFilePath = tmp.fileSync( + { discardDescriptor: true }, // close file (so we can update it) + ).name; + }); + it('should write content to output file', () => { + const content = 'hello world'; + + utils.writeOutputFile(tempFilePath, content); + + expect(fs.readFileSync(tempFilePath).toString()).to.equal(content); + }); +}); + +describe('isOutputFileUpToDate()', () => { + it('should throw ENOENT when there is no abi file', () => { + expect(utils.isOutputFileUpToDate.bind('nonexistant1', 'nonexistant2')).to.throw('ENOENT'); + }); + + describe('when the abi input file exists', () => { + let abiFile: string; + before(() => { + abiFile = tmp.fileSync( + { discardDescriptor: true }, // close file (set timestamp) + ).name; + }); + + describe('without an existing output file', () => { + it('should return false', () => { + expect(utils.isOutputFileUpToDate(abiFile, 'nonexistant_file')).to.be.false(); + }); + }); + + describe('with an existing output file', () => { + let outputFile: string; + before(() => { + sleep.msleep(SLEEP_MS); // to ensure different timestamp + outputFile = tmp.fileSync( + { discardDescriptor: true }, // close file (set timestamp) + ).name; + }); + + it('should return true when output file and is newer than abi file', async () => { + expect(utils.isOutputFileUpToDate(abiFile, outputFile)).to.be.true(); + }); + + it('should return false when output file exists but is older than abi file', () => { + sleep.msleep(SLEEP_MS); // to ensure different timestamp + fs.closeSync(fs.openSync(abiFile, 'w')); // touch abi file + + expect(utils.isOutputFileUpToDate(abiFile, outputFile)).to.be.false(); + }); + }); + }); +}); diff --git a/packages/connect/CHANGELOG.json b/packages/connect/CHANGELOG.json index c426f974b..7b747f167 100644 --- a/packages/connect/CHANGELOG.json +++ b/packages/connect/CHANGELOG.json @@ -1,5 +1,14 @@ [ { + "version": "1.0.0", + "changes": [ + { + "note": + "Remove WebSocketOrderbookChannel from the public interface and replace with orderbookChannelFactory" + } + ] + }, + { "timestamp": 1531149657, "version": "0.6.16", "changes": [ diff --git a/packages/connect/package.json b/packages/connect/package.json index 469d47d33..cc68d34f4 100644 --- a/packages/connect/package.json +++ b/packages/connect/package.json @@ -59,6 +59,7 @@ "isomorphic-fetch": "^2.2.1", "lodash": "^4.17.4", "query-string": "^5.0.1", + "sinon": "^4.0.0", "websocket": "^1.0.25" }, "devDependencies": { @@ -68,7 +69,8 @@ "@types/lodash": "4.14.104", "@types/mocha": "^2.2.42", "@types/query-string": "^5.0.1", - "@types/websocket": "^0.0.34", + "@types/sinon": "^2.2.2", + "@types/websocket": "^0.0.39", "async-child-process": "^1.1.1", "chai": "^4.0.1", "chai-as-promised": "^7.1.0", diff --git a/packages/connect/src/index.ts b/packages/connect/src/index.ts index ef5d8683e..7f5eb8ed3 100644 --- a/packages/connect/src/index.ts +++ b/packages/connect/src/index.ts @@ -1,5 +1,5 @@ export { HttpClient } from './http_client'; -export { WebSocketOrderbookChannel } from './ws_orderbook_channel'; +export { orderbookChannelFactory } from './orderbook_channel_factory'; export { Client, FeesRequest, @@ -14,7 +14,6 @@ export { TokenPairsItem, TokenPairsRequestOpts, TokenTradeInfo, - WebSocketOrderbookChannelConfig, } from './types'; export { Order, SignedOrder } from '@0xproject/types'; diff --git a/packages/connect/src/orderbook_channel_factory.ts b/packages/connect/src/orderbook_channel_factory.ts new file mode 100644 index 000000000..5134af323 --- /dev/null +++ b/packages/connect/src/orderbook_channel_factory.ts @@ -0,0 +1,32 @@ +import * as WebSocket from 'websocket'; + +import { OrderbookChannel, OrderbookChannelHandler } from './types'; +import { assert } from './utils/assert'; +import { WebSocketOrderbookChannel } from './ws_orderbook_channel'; + +export const orderbookChannelFactory = { + /** + * Instantiates a new WebSocketOrderbookChannel instance + * @param url The relayer API base WS url you would like to interact with + * @param handler An OrderbookChannelHandler instance that responds to various + * channel updates + * @return An OrderbookChannel Promise + */ + async createWebSocketOrderbookChannelAsync( + url: string, + handler: OrderbookChannelHandler, + ): Promise<OrderbookChannel> { + assert.isUri('url', url); + assert.isOrderbookChannelHandler('handler', handler); + return new Promise<OrderbookChannel>((resolve, reject) => { + const client = new WebSocket.w3cwebsocket(url); + client.onopen = () => { + const orderbookChannel = new WebSocketOrderbookChannel(client, handler); + resolve(orderbookChannel); + }; + client.onerror = err => { + reject(err); + }; + }); + }, +}; diff --git a/packages/connect/src/schemas/schemas.ts b/packages/connect/src/schemas/schemas.ts index b9a8472fb..0b8b798a9 100644 --- a/packages/connect/src/schemas/schemas.ts +++ b/packages/connect/src/schemas/schemas.ts @@ -3,7 +3,6 @@ import { orderBookRequestSchema } from './orderbook_request_schema'; import { ordersRequestOptsSchema } from './orders_request_opts_schema'; import { pagedRequestOptsSchema } from './paged_request_opts_schema'; import { tokenPairsRequestOptsSchema } from './token_pairs_request_opts_schema'; -import { webSocketOrderbookChannelConfigSchema } from './websocket_orderbook_channel_config_schema'; export const schemas = { feesRequestSchema, @@ -11,5 +10,4 @@ export const schemas = { ordersRequestOptsSchema, pagedRequestOptsSchema, tokenPairsRequestOptsSchema, - webSocketOrderbookChannelConfigSchema, }; diff --git a/packages/connect/src/schemas/websocket_orderbook_channel_config_schema.ts b/packages/connect/src/schemas/websocket_orderbook_channel_config_schema.ts deleted file mode 100644 index 81c0cac9c..000000000 --- a/packages/connect/src/schemas/websocket_orderbook_channel_config_schema.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const webSocketOrderbookChannelConfigSchema = { - id: '/WebSocketOrderbookChannelConfig', - type: 'object', - properties: { - heartbeatIntervalMs: { - type: 'number', - minimum: 10, - }, - }, -}; diff --git a/packages/connect/src/types.ts b/packages/connect/src/types.ts index f5e52f50d..fc7a4b24d 100644 --- a/packages/connect/src/types.ts +++ b/packages/connect/src/types.ts @@ -11,18 +11,11 @@ export interface Client { } export interface OrderbookChannel { - subscribe: (subscriptionOpts: OrderbookChannelSubscriptionOpts, handler: OrderbookChannelHandler) => void; + subscribe: (subscriptionOpts: OrderbookChannelSubscriptionOpts) => void; close: () => void; } /** - * heartbeatInterval: Interval in milliseconds that the orderbook channel should ping the underlying websocket. Default: 15000 - */ -export interface WebSocketOrderbookChannelConfig { - heartbeatIntervalMs?: number; -} - -/** * baseTokenAddress: The address of token designated as the baseToken in the currency pair calculation of price * quoteTokenAddress: The address of token designated as the quoteToken in the currency pair calculation of price * snapshot: If true, a snapshot of the orderbook will be sent before the updates to the orderbook @@ -46,8 +39,8 @@ export interface OrderbookChannelHandler { subscriptionOpts: OrderbookChannelSubscriptionOpts, order: SignedOrder, ) => void; - onError: (channel: OrderbookChannel, subscriptionOpts: OrderbookChannelSubscriptionOpts, err: Error) => void; - onClose: (channel: OrderbookChannel, subscriptionOpts: OrderbookChannelSubscriptionOpts) => void; + onError: (channel: OrderbookChannel, err: Error, subscriptionOpts?: OrderbookChannelSubscriptionOpts) => void; + onClose: (channel: OrderbookChannel) => void; } export type OrderbookChannelMessage = diff --git a/packages/connect/src/utils/assert.ts b/packages/connect/src/utils/assert.ts new file mode 100644 index 000000000..a0fd12fbd --- /dev/null +++ b/packages/connect/src/utils/assert.ts @@ -0,0 +1,26 @@ +import { assert as sharedAssert } from '@0xproject/assert'; +// HACK: We need those two unused imports because they're actually used by sharedAssert which gets injected here +// tslint:disable-next-line:no-unused-variable +import { Schema, schemas } from '@0xproject/json-schemas'; +// tslint:disable-next-line:no-unused-variable +import { ECSignature } from '@0xproject/types'; +// tslint:disable-next-line:no-unused-variable +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +export const assert = { + ...sharedAssert, + isOrderbookChannelSubscriptionOpts(variableName: string, subscriptionOpts: any): void { + sharedAssert.doesConformToSchema( + variableName, + subscriptionOpts, + schemas.relayerApiOrderbookChannelSubscribePayload, + ); + }, + isOrderbookChannelHandler(variableName: string, handler: any): void { + sharedAssert.isFunction(`${variableName}.onSnapshot`, _.get(handler, 'onSnapshot')); + sharedAssert.isFunction(`${variableName}.onUpdate`, _.get(handler, 'onUpdate')); + sharedAssert.isFunction(`${variableName}.onError`, _.get(handler, 'onError')); + sharedAssert.isFunction(`${variableName}.onClose`, _.get(handler, 'onClose')); + }, +}; diff --git a/packages/connect/src/utils/orderbook_channel_message_parser.ts b/packages/connect/src/utils/orderbook_channel_message_parser.ts index 9a9ca8901..593288078 100644 --- a/packages/connect/src/utils/orderbook_channel_message_parser.ts +++ b/packages/connect/src/utils/orderbook_channel_message_parser.ts @@ -8,10 +8,16 @@ import { relayerResponseJsonParsers } from './relayer_response_json_parsers'; export const orderbookChannelMessageParser = { parse(utf8Data: string): OrderbookChannelMessage { + // parse the message const messageObj = JSON.parse(utf8Data); + // ensure we have a type parameter to switch on const type: string = _.get(messageObj, 'type'); assert.assert(!_.isUndefined(type), `Message is missing a type parameter: ${utf8Data}`); assert.isString('type', type); + // ensure we have a request id for the resulting message + const requestId: number = _.get(messageObj, 'requestId'); + assert.assert(!_.isUndefined(requestId), `Message is missing a requestId parameter: ${utf8Data}`); + assert.isNumber('requestId', requestId); switch (type) { case OrderbookChannelMessageTypes.Snapshot: { assert.doesConformToSchema('message', messageObj, schemas.relayerApiOrderbookChannelSnapshotSchema); @@ -28,7 +34,7 @@ export const orderbookChannelMessageParser = { default: { return { type: OrderbookChannelMessageTypes.Unknown, - requestId: 0, + requestId, payload: undefined, }; } diff --git a/packages/connect/src/ws_orderbook_channel.ts b/packages/connect/src/ws_orderbook_channel.ts index bdcc8a75d..e1c55cce3 100644 --- a/packages/connect/src/ws_orderbook_channel.ts +++ b/packages/connect/src/ws_orderbook_channel.ts @@ -1,166 +1,105 @@ -import { assert } from '@0xproject/assert'; -import { schemas } from '@0xproject/json-schemas'; import * as _ from 'lodash'; import * as WebSocket from 'websocket'; -import { schemas as clientSchemas } from './schemas/schemas'; import { OrderbookChannel, OrderbookChannelHandler, OrderbookChannelMessageTypes, OrderbookChannelSubscriptionOpts, - WebsocketClientEventType, - WebsocketConnectionEventType, - WebSocketOrderbookChannelConfig, } from './types'; +import { assert } from './utils/assert'; import { orderbookChannelMessageParser } from './utils/orderbook_channel_message_parser'; -const DEFAULT_HEARTBEAT_INTERVAL_MS = 15000; -const MINIMUM_HEARTBEAT_INTERVAL_MS = 10; - /** * This class includes all the functionality related to interacting with a websocket endpoint * that implements the standard relayer API v0 */ export class WebSocketOrderbookChannel implements OrderbookChannel { - private _apiEndpointUrl: string; - private _client: WebSocket.client; - private _connectionIfExists?: WebSocket.connection; - private _heartbeatTimerIfExists?: NodeJS.Timer; - private _subscriptionCounter = 0; - private _heartbeatIntervalMs: number; + private _client: WebSocket.w3cwebsocket; + private _handler: OrderbookChannelHandler; + private _subscriptionOptsList: OrderbookChannelSubscriptionOpts[] = []; /** * Instantiates a new WebSocketOrderbookChannel instance - * @param url The relayer API base WS url you would like to interact with - * @param config The configuration object. Look up the type for the description. + * @param client A WebSocket client + * @param handler An OrderbookChannelHandler instance that responds to various + * channel updates * @return An instance of WebSocketOrderbookChannel */ - constructor(url: string, config?: WebSocketOrderbookChannelConfig) { - assert.isUri('url', url); - if (!_.isUndefined(config)) { - assert.doesConformToSchema('config', config, clientSchemas.webSocketOrderbookChannelConfigSchema); - } - this._apiEndpointUrl = url; - this._heartbeatIntervalMs = - _.isUndefined(config) || _.isUndefined(config.heartbeatIntervalMs) - ? DEFAULT_HEARTBEAT_INTERVAL_MS - : config.heartbeatIntervalMs; - this._client = new WebSocket.client(); + constructor(client: WebSocket.w3cwebsocket, handler: OrderbookChannelHandler) { + assert.isOrderbookChannelHandler('handler', handler); + // set private members + this._client = client; + this._handler = handler; + // attach client callbacks + this._client.onerror = err => { + this._handler.onError(this, err); + }; + this._client.onclose = () => { + this._handler.onClose(this); + }; + this._client.onmessage = message => { + this._handleWebSocketMessage(message); + }; } /** * Subscribe to orderbook snapshots and updates from the websocket * @param subscriptionOpts An OrderbookChannelSubscriptionOpts instance describing which * token pair to subscribe to - * @param handler An OrderbookChannelHandler instance that responds to various - * channel updates */ - public subscribe(subscriptionOpts: OrderbookChannelSubscriptionOpts, handler: OrderbookChannelHandler): void { - assert.doesConformToSchema( - 'subscriptionOpts', - subscriptionOpts, - schemas.relayerApiOrderbookChannelSubscribePayload, - ); - assert.isFunction('handler.onSnapshot', _.get(handler, 'onSnapshot')); - assert.isFunction('handler.onUpdate', _.get(handler, 'onUpdate')); - assert.isFunction('handler.onError', _.get(handler, 'onError')); - assert.isFunction('handler.onClose', _.get(handler, 'onClose')); - this._subscriptionCounter += 1; + public subscribe(subscriptionOpts: OrderbookChannelSubscriptionOpts): void { + assert.isOrderbookChannelSubscriptionOpts('subscriptionOpts', subscriptionOpts); + assert.assert(this._client.readyState === WebSocket.w3cwebsocket.OPEN, 'WebSocket connection is closed'); + this._subscriptionOptsList.push(subscriptionOpts); + // TODO: update requestId management to use UUIDs for v2 const subscribeMessage = { type: 'subscribe', channel: 'orderbook', - requestId: this._subscriptionCounter, + requestId: this._subscriptionOptsList.length - 1, payload: subscriptionOpts, }; - this._getConnection((error, connection) => { - if (!_.isUndefined(error)) { - handler.onError(this, subscriptionOpts, error); - } else if (!_.isUndefined(connection) && connection.connected) { - connection.on(WebsocketConnectionEventType.Error, wsError => { - handler.onError(this, subscriptionOpts, wsError); - }); - connection.on(WebsocketConnectionEventType.Close, (_code: number, _desc: string) => { - handler.onClose(this, subscriptionOpts); - }); - connection.on(WebsocketConnectionEventType.Message, message => { - this._handleWebSocketMessage(subscribeMessage.requestId, subscriptionOpts, message, handler); - }); - connection.sendUTF(JSON.stringify(subscribeMessage)); - } - }); + this._client.send(JSON.stringify(subscribeMessage)); } /** * Close the websocket and stop receiving updates */ public close(): void { - if (!_.isUndefined(this._connectionIfExists)) { - this._connectionIfExists.close(); - } - if (!_.isUndefined(this._heartbeatTimerIfExists)) { - clearInterval(this._heartbeatTimerIfExists); - } + this._client.close(); } - private _getConnection(callback: (error?: Error, connection?: WebSocket.connection) => void): void { - if (!_.isUndefined(this._connectionIfExists) && this._connectionIfExists.connected) { - callback(undefined, this._connectionIfExists); - } else { - this._client.on(WebsocketClientEventType.Connect, connection => { - this._connectionIfExists = connection; - if (this._heartbeatIntervalMs >= MINIMUM_HEARTBEAT_INTERVAL_MS) { - this._heartbeatTimerIfExists = setInterval(() => { - connection.ping(''); - }, this._heartbeatIntervalMs); - } else { - callback( - new Error( - `Heartbeat interval is ${ - this._heartbeatIntervalMs - }ms which is less than the required minimum of ${MINIMUM_HEARTBEAT_INTERVAL_MS}ms`, - ), - undefined, - ); - } - callback(undefined, this._connectionIfExists); - }); - this._client.on(WebsocketClientEventType.ConnectFailed, error => { - callback(error, undefined); - }); - this._client.connect(this._apiEndpointUrl); + private _handleWebSocketMessage(message: any): void { + if (_.isUndefined(message.data)) { + this._handler.onError(this, new Error(`Message does not contain data. Url: ${this._client.url}`)); + return; } - } - private _handleWebSocketMessage( - requestId: number, - subscriptionOpts: OrderbookChannelSubscriptionOpts, - message: WebSocket.IMessage, - handler: OrderbookChannelHandler, - ): void { - if (!_.isUndefined(message.utf8Data)) { - try { - const utf8Data = message.utf8Data; - const parserResult = orderbookChannelMessageParser.parse(utf8Data); - if (parserResult.requestId === requestId) { - switch (parserResult.type) { - case OrderbookChannelMessageTypes.Snapshot: { - handler.onSnapshot(this, subscriptionOpts, parserResult.payload); - break; - } - case OrderbookChannelMessageTypes.Update: { - handler.onUpdate(this, subscriptionOpts, parserResult.payload); - break; - } - default: { - handler.onError( - this, - subscriptionOpts, - new Error(`Message has missing a type parameter: ${utf8Data}`), - ); - } - } + try { + const data = message.data; + const parserResult = orderbookChannelMessageParser.parse(data); + const subscriptionOpts = this._subscriptionOptsList[parserResult.requestId]; + if (_.isUndefined(subscriptionOpts)) { + this._handler.onError( + this, + new Error(`Message has unknown requestId. Url: ${this._client.url} Message: ${data}`), + ); + return; + } + switch (parserResult.type) { + case OrderbookChannelMessageTypes.Snapshot: { + this._handler.onSnapshot(this, subscriptionOpts, parserResult.payload); + break; + } + case OrderbookChannelMessageTypes.Update: { + this._handler.onUpdate(this, subscriptionOpts, parserResult.payload); + break; + } + default: { + this._handler.onError( + this, + new Error(`Message has unknown type parameter. Url: ${this._client.url} Message: ${data}`), + subscriptionOpts, + ); } - } catch (error) { - handler.onError(this, subscriptionOpts, error); } - } else { - handler.onError(this, subscriptionOpts, new Error(`Message does not contain utf8Data`)); + } catch (error) { + this._handler.onError(this, error); } } } diff --git a/packages/connect/test/orderbook_channel_factory_test.ts b/packages/connect/test/orderbook_channel_factory_test.ts new file mode 100644 index 000000000..2ce361bd2 --- /dev/null +++ b/packages/connect/test/orderbook_channel_factory_test.ts @@ -0,0 +1,45 @@ +import * as chai from 'chai'; +import * as dirtyChai from 'dirty-chai'; +import * as _ from 'lodash'; +import 'mocha'; + +import { orderbookChannelFactory } from '../src/orderbook_channel_factory'; + +chai.config.includeStack = true; +chai.use(dirtyChai); +const expect = chai.expect; +const emptyOrderbookChannelHandler = { + onSnapshot: () => { + _.noop(); + }, + onUpdate: () => { + _.noop(); + }, + onError: () => { + _.noop(); + }, + onClose: () => { + _.noop(); + }, +}; + +describe('orderbookChannelFactory', () => { + const websocketUrl = 'ws://localhost:8080'; + describe('#createWebSocketOrderbookChannelAsync', () => { + it('throws when input is not a url', () => { + const badUrlInput = 54; + expect( + orderbookChannelFactory.createWebSocketOrderbookChannelAsync( + badUrlInput as any, + emptyOrderbookChannelHandler, + ), + ).to.be.rejected(); + }); + it('throws when handler has the incorrect members', () => { + const badHandlerInput = {}; + expect( + orderbookChannelFactory.createWebSocketOrderbookChannelAsync(websocketUrl, badHandlerInput as any), + ).to.be.rejected(); + }); + }); +}); diff --git a/packages/connect/test/ws_orderbook_channel_test.ts b/packages/connect/test/ws_orderbook_channel_test.ts index ce404d934..fed4f2217 100644 --- a/packages/connect/test/ws_orderbook_channel_test.ts +++ b/packages/connect/test/ws_orderbook_channel_test.ts @@ -2,60 +2,58 @@ import * as chai from 'chai'; import * as dirtyChai from 'dirty-chai'; import * as _ from 'lodash'; import 'mocha'; +import * as Sinon from 'sinon'; +import * as WebSocket from 'websocket'; import { WebSocketOrderbookChannel } from '../src/ws_orderbook_channel'; chai.config.includeStack = true; chai.use(dirtyChai); const expect = chai.expect; +const emptyOrderbookChannelHandler = { + onSnapshot: () => { + _.noop(); + }, + onUpdate: () => { + _.noop(); + }, + onError: () => { + _.noop(); + }, + onClose: () => { + _.noop(); + }, +}; describe('WebSocketOrderbookChannel', () => { const websocketUrl = 'ws://localhost:8080'; - const orderbookChannel = new WebSocketOrderbookChannel(websocketUrl); + const openClient = new WebSocket.w3cwebsocket(websocketUrl); + Sinon.stub(openClient, 'readyState').get(() => WebSocket.w3cwebsocket.OPEN); + Sinon.stub(openClient, 'send').callsFake(_.noop); + const openOrderbookChannel = new WebSocketOrderbookChannel(openClient, emptyOrderbookChannelHandler); const subscriptionOpts = { baseTokenAddress: '0x323b5d4c32345ced77393b3530b1eed0f346429d', quoteTokenAddress: '0xef7fff64389b814a946f3e92105513705ca6b990', snapshot: true, limit: 100, }; - const emptyOrderbookChannelHandler = { - onSnapshot: () => { - _.noop(); - }, - onUpdate: () => { - _.noop(); - }, - onError: () => { - _.noop(); - }, - onClose: () => { - _.noop(); - }, - }; describe('#subscribe', () => { it('throws when subscriptionOpts does not conform to schema', () => { - const badSubscribeCall = orderbookChannel.subscribe.bind( - orderbookChannel, - {}, - emptyOrderbookChannelHandler, - ); + const badSubscribeCall = openOrderbookChannel.subscribe.bind(openOrderbookChannel, {}); expect(badSubscribeCall).throws( 'Expected subscriptionOpts to conform to schema /RelayerApiOrderbookChannelSubscribePayload\nEncountered: {}\nValidation errors: instance requires property "baseTokenAddress", instance requires property "quoteTokenAddress"', ); }); - it('throws when handler has the incorrect members', () => { - const badSubscribeCall = orderbookChannel.subscribe.bind(orderbookChannel, subscriptionOpts, {}); - expect(badSubscribeCall).throws( - 'Expected handler.onSnapshot to be of type function, encountered: undefined', - ); - }); it('does not throw when inputs are of correct types', () => { - const goodSubscribeCall = orderbookChannel.subscribe.bind( - orderbookChannel, - subscriptionOpts, - emptyOrderbookChannelHandler, - ); + const goodSubscribeCall = openOrderbookChannel.subscribe.bind(openOrderbookChannel, subscriptionOpts); expect(goodSubscribeCall).to.not.throw(); }); + it('throws when client is closed', () => { + const closedClient = new WebSocket.w3cwebsocket(websocketUrl); + Sinon.stub(closedClient, 'readyState').get(() => WebSocket.w3cwebsocket.CLOSED); + const closedOrderbookChannel = new WebSocketOrderbookChannel(closedClient, emptyOrderbookChannelHandler); + const badSubscribeCall = closedOrderbookChannel.subscribe.bind(closedOrderbookChannel, subscriptionOpts); + expect(badSubscribeCall).throws('WebSocket connection is closed'); + }); }); }); diff --git a/packages/contract-wrappers/src/utils/assert.ts b/packages/contract-wrappers/src/utils/assert.ts index da6697b08..842b16fa0 100644 --- a/packages/contract-wrappers/src/utils/assert.ts +++ b/packages/contract-wrappers/src/utils/assert.ts @@ -1,5 +1,5 @@ import { assert as sharedAssert } from '@0xproject/assert'; -// We need those two unused imports because they're actually used by sharedAssert which gets injected here +// HACK: We need those two unused imports because they're actually used by sharedAssert which gets injected here import { Schema } from '@0xproject/json-schemas'; // tslint:disable-line:no-unused-variable import { isValidSignatureAsync } from '@0xproject/order-utils'; import { ECSignature } from '@0xproject/types'; // tslint:disable-line:no-unused-variable diff --git a/packages/contract-wrappers/test/ether_token_wrapper_test.ts b/packages/contract-wrappers/test/ether_token_wrapper_test.ts index 0a860884a..48bd6d3f6 100644 --- a/packages/contract-wrappers/test/ether_token_wrapper_test.ts +++ b/packages/contract-wrappers/test/ether_token_wrapper_test.ts @@ -336,15 +336,19 @@ describe('EtherTokenWrapper', () => { describe('#getLogsAsync', () => { let etherTokenAddress: string; let erc20ProxyAddress: string; - const blockRange: BlockRange = { - fromBlock: 0, - toBlock: BlockParamLiteral.Latest, - }; + let blockRange: BlockRange; let txHash: string; - before(() => { + before(async () => { addressWithETH = userAddresses[0]; etherTokenAddress = tokenUtils.getWethTokenAddress(); erc20ProxyAddress = contractWrappers.erc20Proxy.getContractAddress(); + // Start the block range after all migrations to avoid unexpected logs + const currentBlock = await web3Wrapper.getBlockNumberAsync(); + const fromBlock = currentBlock + 1; + blockRange = { + fromBlock, + toBlock: BlockParamLiteral.Latest, + }; }); it('should get logs with decoded args emitted by Approval', async () => { txHash = await contractWrappers.erc20Token.setUnlimitedProxyAllowanceAsync( diff --git a/packages/contracts/src/1.0.0/MultiSigWalletWithTImeLockExceptRemoveAuthorizedAddress/MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress.sol b/packages/contracts/src/1.0.0/MultiSigWalletWithTImeLockExceptRemoveAuthorizedAddress/MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress.sol index 241e02d4a..aee722c53 100644 --- a/packages/contracts/src/1.0.0/MultiSigWalletWithTImeLockExceptRemoveAuthorizedAddress/MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress.sol +++ b/packages/contracts/src/1.0.0/MultiSigWalletWithTImeLockExceptRemoveAuthorizedAddress/MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress.sol @@ -18,7 +18,7 @@ pragma solidity ^0.4.10; -import "../../current/multisig/MultiSigWalletWithTimeLock.sol"; +import "../../2.0.0/multisig/MultiSigWalletWithTimeLock.sol"; contract MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress is MultiSigWalletWithTimeLock { @@ -79,4 +79,4 @@ contract MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress is MultiSigWall } return true; } -}
\ No newline at end of file +} diff --git a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol index fc17a4c72..546e7f22c 100644 --- a/packages/contracts/src/2.0.0/forwarder/Forwarder.sol +++ b/packages/contracts/src/2.0.0/forwarder/Forwarder.sol @@ -40,7 +40,6 @@ contract Forwarder is address _exchange, address _etherToken, address _zrxToken, - bytes4 _erc20AssetProxyId, bytes memory _zrxAssetData, bytes memory _wethAssetData ) diff --git a/packages/contracts/test/forwarder/forwarder.ts b/packages/contracts/test/forwarder/forwarder.ts index b4555d417..f0bf6ac03 100644 --- a/packages/contracts/test/forwarder/forwarder.ts +++ b/packages/contracts/test/forwarder/forwarder.ts @@ -1,6 +1,6 @@ import { BlockchainLifecycle } from '@0xproject/dev-utils'; import { assetProxyUtils } from '@0xproject/order-utils'; -import { AssetProxyId, RevertReason, SignedOrder } from '@0xproject/types'; +import { RevertReason, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as chai from 'chai'; @@ -131,7 +131,6 @@ describe(ContractName.Forwarder, () => { exchangeInstance.address, wethContract.address, zrxToken.address, - AssetProxyId.ERC20, zrxAssetData, wethAssetData, ); diff --git a/packages/migrations/artifacts/2.0.0/Forwarder.json b/packages/migrations/artifacts/2.0.0/Forwarder.json index da1f54eca..0bef9adbc 100644 --- a/packages/migrations/artifacts/2.0.0/Forwarder.json +++ b/packages/migrations/artifacts/2.0.0/Forwarder.json @@ -709,10 +709,6 @@ "type": "address" }, { - "name": "_erc20AssetProxyId", - "type": "bytes4" - }, - { "name": "_zrxAssetData", "type": "bytes" }, @@ -896,5 +892,11 @@ } } }, - "networks": {} -}
\ No newline at end of file + "networks": { + "50": { + "address": "0xb69e673309512a9d726f87304c6984054f87a93b", + "links": {}, + "constructorArgs": "[\"0x48bacb9266a570d521063ef5dd96e61686dbe788\",\"0x0b1ba0af832d7c05fd64161e0db78e85978e8082\",\"0x871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c\",\"0xf47261b0\",\"0xf47261b0000000000000000000000000871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c\",\"0xf47261b00000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082\"]" + } + } +} diff --git a/packages/migrations/compiler.json b/packages/migrations/compiler.json index eae22a73c..493a73d46 100644 --- a/packages/migrations/compiler.json +++ b/packages/migrations/compiler.json @@ -1,5 +1,5 @@ { - "contractsDir": "../contracts/src/contracts", + "contractsDir": "../contracts/src/", "compilerSettings": { "optimizer": { "enabled": true, diff --git a/packages/migrations/src/2.0.0/migration.ts b/packages/migrations/src/2.0.0/migration.ts index 75e60d3e2..226bf0a68 100644 --- a/packages/migrations/src/2.0.0/migration.ts +++ b/packages/migrations/src/2.0.0/migration.ts @@ -13,6 +13,7 @@ import { DummyERC721TokenContract } from './contract_wrappers/dummy_erc721_token import { ERC20ProxyContract } from './contract_wrappers/erc20_proxy'; import { ERC721ProxyContract } from './contract_wrappers/erc721_proxy'; import { ExchangeContract } from './contract_wrappers/exchange'; +import { ForwarderContract } from './contract_wrappers/forwarder'; import { WETH9Contract } from './contract_wrappers/weth9'; import { ZRXTokenContract } from './contract_wrappers/zrx_token'; @@ -127,4 +128,17 @@ export const runV2MigrationsAsync = async (provider: Provider, artifactsDir: str erc721TokenInfo[0].name, erc721TokenInfo[0].symbol, ); + + // Forwarder + const forwarder = await ForwarderContract.deployFrom0xArtifactAsync( + artifacts.Forwarder, + provider, + txDefaults, + exchange.address, + etherToken.address, + zrxToken.address, + assetProxyUtils.encodeERC20AssetData(zrxToken.address), + assetProxyUtils.encodeERC20AssetData(etherToken.address), + ); + artifactsWriter.saveArtifact(forwarder); }; diff --git a/packages/order-utils/src/assert.ts b/packages/order-utils/src/assert.ts index b4b57d02a..f8db7ac63 100644 --- a/packages/order-utils/src/assert.ts +++ b/packages/order-utils/src/assert.ts @@ -1,5 +1,5 @@ import { assert as sharedAssert } from '@0xproject/assert'; -// We need those two unused imports because they're actually used by sharedAssert which gets injected here +// HACK: We need those two unused imports because they're actually used by sharedAssert which gets injected here // tslint:disable:no-unused-variable import { Schema } from '@0xproject/json-schemas'; import { ECSignature, SignatureType } from '@0xproject/types'; diff --git a/packages/order-watcher/src/utils/assert.ts b/packages/order-watcher/src/utils/assert.ts index 5d7f72716..9c992d9b4 100644 --- a/packages/order-watcher/src/utils/assert.ts +++ b/packages/order-watcher/src/utils/assert.ts @@ -1,5 +1,5 @@ import { assert as sharedAssert } from '@0xproject/assert'; -// We need those two unused imports because they're actually used by sharedAssert which gets injected here +// HACK: We need those two unused imports because they're actually used by sharedAssert which gets injected here // tslint:disable:no-unused-variable import { Schema } from '@0xproject/json-schemas'; import { ECSignature } from '@0xproject/types'; @@ -534,10 +534,18 @@ version "2.3.7" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-2.3.7.tgz#e92c2fed3297eae078d78d1da032b26788b4af86" +"@types/sleep@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@types/sleep/-/sleep-0.0.7.tgz#0572b5d06978b4305671c614b0f68b4640ce7d43" + "@types/solidity-parser-antlr@^0.2.1": version "0.2.1" resolved "https://registry.yarnpkg.com/@types/solidity-parser-antlr/-/solidity-parser-antlr-0.2.1.tgz#29ff386773a72a06c8a9c40c666d2a1cea53c2ed" +"@types/tmp@^0.0.33": + version "0.0.33" + resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.0.33.tgz#1073c4bc824754ae3d10cfab88ab0237ba964e4d" + "@types/uuid@^3.4.2": version "3.4.3" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.3.tgz#121ace265f5569ce40f4f6d0ff78a338c732a754" @@ -548,10 +556,11 @@ version "1.0.2" resolved "https://registry.yarnpkg.com/@types/valid-url/-/valid-url-1.0.2.tgz#60fa435ce24bfd5ba107b8d2a80796aeaf3a8f45" -"@types/websocket@^0.0.34": - version "0.0.34" - resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-0.0.34.tgz#25596764cec885eda070fdb6d19cd76fe582747c" +"@types/websocket@^0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-0.0.39.tgz#aa971e24f9c1455fe2a57ee3e69c7d395016b12a" dependencies: + "@types/events" "*" "@types/node" "*" "@types/yargs@^10.0.0": @@ -2024,6 +2033,10 @@ browser-stdout@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.0.6: version "1.2.0" resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" @@ -2730,7 +2743,7 @@ commander@2.13.0, commander@~2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" -commander@^2.12.1, commander@^2.8.1, commander@^2.9.0: +commander@2.15.1, commander@^2.12.1, commander@^2.8.1, commander@^2.9.0: version "2.15.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" @@ -3691,7 +3704,7 @@ diff@3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" -diff@^3.1.0, diff@^3.2.0, diff@^3.3.1, diff@^3.5.0: +diff@3.5.0, diff@^3.1.0, diff@^3.2.0, diff@^3.3.1, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -4468,9 +4481,9 @@ ethereumjs-wallet@~0.6.0: utf8 "^2.1.1" uuid "^2.0.1" -ethers@0xproject/ethers.js#eip-838-reasons, ethers@3.0.22: - version "3.0.18" - resolved "https://codeload.github.com/0xproject/ethers.js/tar.gz/b91342bd200d142af0165d6befddf783c8ae8447" +ethers@3.0.22: + version "3.0.22" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-3.0.22.tgz#7fab1ea16521705837aa43c15831877b2716b436" dependencies: aes-js "3.0.0" bn.js "^4.4.0" @@ -5629,6 +5642,10 @@ growl@1.10.3: version "1.10.3" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + gud@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" @@ -7947,7 +7964,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" -"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: +"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -8038,6 +8055,22 @@ mocha@^4.0.1: mkdirp "0.5.1" supports-color "4.4.0" +mocha@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.5" + he "1.1.1" + minimatch "3.0.4" + mkdirp "0.5.1" + supports-color "5.4.0" + mock-fs@^4.1.0: version "4.4.2" resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.4.2.tgz#09dec5313f97095a450be6aa2ad8ab6738d63d6b" @@ -8119,7 +8152,7 @@ mz@^2.6.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nan@^2.0.5, nan@^2.0.8, nan@^2.2.1, nan@^2.3.0, nan@^2.3.3, nan@^2.6.2: +nan@>=2.5.1, nan@^2.0.5, nan@^2.0.8, nan@^2.2.1, nan@^2.3.0, nan@^2.3.3, nan@^2.6.2: version "2.10.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" @@ -10997,6 +11030,12 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" +sleep@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/sleep/-/sleep-5.1.1.tgz#878fa1d44d08eeb0f26fb2018ef8629eb1a3ab94" + dependencies: + nan ">=2.5.1" + slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" @@ -11612,6 +11651,12 @@ supports-color@4.4.0: dependencies: has-flag "^2.0.0" +supports-color@5.4.0, supports-color@^5.1.0, supports-color@^5.2.0, supports-color@^5.3.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + dependencies: + has-flag "^3.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" @@ -11628,12 +11673,6 @@ supports-color@^4.2.1: dependencies: has-flag "^2.0.0" -supports-color@^5.1.0, supports-color@^5.2.0, supports-color@^5.3.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" - dependencies: - has-flag "^3.0.0" - svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" |