aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/abi-gen/CHANGELOG.json9
-rwxr-xr-xpackages/abi-gen/bin/abi-gen.js2
-rw-r--r--packages/abi-gen/coverage/.gitkeep0
-rw-r--r--packages/abi-gen/package.json15
-rw-r--r--packages/abi-gen/src/index.ts26
-rw-r--r--packages/abi-gen/src/utils.ts24
-rw-r--r--packages/abi-gen/test/utils_test.ts86
-rw-r--r--packages/connect/CHANGELOG.json9
-rw-r--r--packages/connect/package.json4
-rw-r--r--packages/connect/src/index.ts3
-rw-r--r--packages/connect/src/orderbook_channel_factory.ts32
-rw-r--r--packages/connect/src/schemas/schemas.ts2
-rw-r--r--packages/connect/src/schemas/websocket_orderbook_channel_config_schema.ts10
-rw-r--r--packages/connect/src/types.ts13
-rw-r--r--packages/connect/src/utils/assert.ts26
-rw-r--r--packages/connect/src/utils/orderbook_channel_message_parser.ts8
-rw-r--r--packages/connect/src/ws_orderbook_channel.ts185
-rw-r--r--packages/connect/test/orderbook_channel_factory_test.ts45
-rw-r--r--packages/connect/test/ws_orderbook_channel_test.ts60
-rw-r--r--packages/contract-wrappers/src/utils/assert.ts2
-rw-r--r--packages/contract-wrappers/test/ether_token_wrapper_test.ts14
-rw-r--r--packages/contracts/src/1.0.0/MultiSigWalletWithTImeLockExceptRemoveAuthorizedAddress/MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress.sol4
-rw-r--r--packages/contracts/src/2.0.0/forwarder/Forwarder.sol1
-rw-r--r--packages/contracts/test/forwarder/forwarder.ts3
-rw-r--r--packages/migrations/artifacts/2.0.0/Forwarder.json14
-rw-r--r--packages/migrations/compiler.json2
-rw-r--r--packages/migrations/src/2.0.0/migration.ts14
-rw-r--r--packages/order-utils/src/assert.ts2
-rw-r--r--packages/order-watcher/src/utils/assert.ts2
29 files changed, 398 insertions, 219 deletions
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';