aboutsummaryrefslogtreecommitdiffstats
path: root/packages/subproviders
diff options
context:
space:
mode:
Diffstat (limited to 'packages/subproviders')
-rw-r--r--packages/subproviders/CHANGELOG.md13
-rw-r--r--packages/subproviders/README.md87
-rw-r--r--packages/subproviders/package.json104
-rw-r--r--packages/subproviders/scripts/postpublish.js14
-rw-r--r--packages/subproviders/src/globals.d.ts55
-rw-r--r--packages/subproviders/src/index.ts16
-rw-r--r--packages/subproviders/src/subproviders/injected_web3.ts16
-rw-r--r--packages/subproviders/src/subproviders/ledger.ts129
-rw-r--r--packages/subproviders/src/subproviders/redundant_rpc.ts31
-rw-r--r--packages/subproviders/src/subproviders/subprovider.ts22
-rw-r--r--packages/subproviders/src/types.ts12
-rw-r--r--packages/subproviders/test/integration/ledger_subprovider_test.ts44
-rw-r--r--packages/subproviders/test/unit/ledger_subprovider_test.ts84
-rw-r--r--packages/subproviders/test/unit/redundant_rpc_subprovider_test.ts20
-rw-r--r--packages/subproviders/tsconfig.json33
-rw-r--r--packages/subproviders/tslint.json4
16 files changed, 412 insertions, 272 deletions
diff --git a/packages/subproviders/CHANGELOG.md b/packages/subproviders/CHANGELOG.md
new file mode 100644
index 000000000..0469150c0
--- /dev/null
+++ b/packages/subproviders/CHANGELOG.md
@@ -0,0 +1,13 @@
+# CHANGELOG
+
+## v0.4.0 - _January 28, 2017_
+
+ * Return a transaction hash from `_sendTransactionAsync` (#303)
+
+## v0.3.0 - _December 28, 2017_
+
+ * Allow LedgerSubprovider to handle `eth_sign` in addition to `personal_sign` RPC requests
+
+## v0.2.0 - _December 20, 2017_
+
+ * Improve the performance of address fetching (#271)
diff --git a/packages/subproviders/README.md b/packages/subproviders/README.md
index 5fa31611a..d7b80f7ee 100644
--- a/packages/subproviders/README.md
+++ b/packages/subproviders/README.md
@@ -1,15 +1,32 @@
-Subproviders
------------
+## @0xproject/subproviders
-A few useful subproviders.
+A few useful web3 subproviders including a LedgerSubprovider useful for adding Ledger Nano S support.
## Installation
```
-npm install @0xproject/subproviders --save
+yarn add @0xproject/subproviders
```
-## Subproviders
+## Usage
+
+Simply import the subprovider you are interested in using:
+
+```javascript
+import {
+ ledgerEthereumBrowserClientFactoryAsync as ledgerEthereumClientFactoryAsync,
+ LedgerSubprovider,
+} from '@0xproject/subproviders';
+
+const ledgerSubprovider = new LedgerSubprovider(
+ networkId,
+ ledgerEthereumClientFactoryAsync,
+);
+
+const accounts = await ledgerSubprovider.getAccountsAsync();
+```
+
+### Subproviders
#### Ledger Nano S subprovider
@@ -23,17 +40,67 @@ A subprovider which attempts to send an RPC call to a list of RPC endpoints sequ
A subprovider that relays all signing related requests to a particular provider (in our case the provider injected onto the web page), while sending all other requests to a different provider (perhaps your own backing Ethereum node or Infura).
-### Integration tests
+## Contributing
+
+We strongly recommend that the community help us make improvements and determine the future direction of the protocol. To report bugs within this package, please create an issue in this repository.
+
+Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting started.
+
+### Install Dependencies
+
+If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them:
+
+```bash
+yarn config set workspaces-experimental true
+```
+
+```bash
+yarn install
+```
+
+### Build
+
+```bash
+yarn build
+```
+
+### Clean
+
+```bash
+yarn clean
+```
+
+### Lint
+
+```bash
+yarn lint
+```
+
+### Run tests
+
+#### Unit tests
+
+```bash
+yarn run test:unit
+```
+
+#### Integration tests
In order to run the integration tests, make sure you have a Ledger Nano S available.
-- Plug it into your computer
-- Unlock the device
-- Open the on-device Ethereum app
-- Make sure "browser support" is disabled
+* Plug it into your computer
+* Unlock the device
+* Open the on-device Ethereum app
+* Make sure "browser support" is disabled
Then run:
```
yarn test:integration
```
+
+#### All tests
+
+```bash
+yarn run test:all
+```
diff --git a/packages/subproviders/package.json b/packages/subproviders/package.json
index c3ecfd2f0..2735f8280 100644
--- a/packages/subproviders/package.json
+++ b/packages/subproviders/package.json
@@ -1,53 +1,55 @@
{
- "name": "@0xproject/subproviders",
- "version": "0.0.1",
- "main": "lib/src/index.js",
- "types": "lib/src/index.d.ts",
- "license": "Apache-2.0",
- "scripts": {
- "clean": "shx rm -rf lib",
- "build": "tsc",
- "lint": "tslint --project . 'src/**/*.ts' 'test/**/*.ts'",
- "run_mocha_unit": "mocha lib/test/unit/**/*_test.js --timeout 10000 --bail --exit",
- "run_mocha_integration": "mocha lib/test/integration/**/*_test.js --timeout 10000 --bail --exit",
- "test": "npm run test:unit",
- "test:circleci": "npm run test:unit",
- "test:all": "run-s test:unit test:integration",
- "test:unit": "run-s clean build run_mocha_unit",
- "test:integration": "run-s clean build run_mocha_integration"
- },
- "dependencies": {
- "@0xproject/assert": "^0.0.6",
- "bn.js": "^4.11.8",
- "es6-promisify": "^5.0.0",
- "ethereum-address": "^0.0.4",
- "ethereumjs-tx": "^1.3.3",
- "ethereumjs-util": "^5.1.1",
- "ledgerco": "0xProject/ledger-node-js-api",
- "lodash": "^4.17.4",
- "semaphore-async-await": "^1.5.1",
- "web3": "^0.20.0",
- "web3-provider-engine": "^13.0.1"
- },
- "devDependencies": {
- "@0xproject/tslint-config": "^0.2.0",
- "@types/lodash": "^4.14.86",
- "@types/mocha": "^2.2.42",
- "@types/node": "^8.0.53",
- "awesome-typescript-loader": "^3.1.3",
- "chai": "^4.0.1",
- "chai-as-promised": "^7.1.0",
- "chai-as-promised-typescript-typings": "^0.0.3",
- "chai-typescript-typings": "^0.0.1",
- "dirty-chai": "^2.0.1",
- "mocha": "^4.0.1",
- "npm-run-all": "^4.1.2",
- "shx": "^0.2.2",
- "tslint": "5.8.0",
- "types-bn": "^0.0.1",
- "types-ethereumjs-util": "0xproject/types-ethereumjs-util",
- "typescript": "~2.6.1",
- "web3-typescript-typings": "^0.7.2",
- "webpack": "^3.1.0"
- }
+ "name": "@0xproject/subproviders",
+ "version": "0.3.2",
+ "main": "lib/src/index.js",
+ "types": "lib/src/index.d.ts",
+ "license": "Apache-2.0",
+ "scripts": {
+ "clean": "shx rm -rf lib",
+ "build": "tsc",
+ "lint": "tslint --project . 'src/**/*.ts' 'test/**/*.ts'",
+ "run_mocha_unit": "mocha lib/test/unit/**/*_test.js --timeout 10000 --bail --exit",
+ "run_mocha_integration": "mocha lib/test/integration/**/*_test.js --timeout 10000 --bail --exit",
+ "test": "npm run test:unit",
+ "test:circleci": "npm run test:unit",
+ "test:all": "run-s test:unit test:integration",
+ "test:unit": "run-s clean build run_mocha_unit",
+ "test:integration": "run-s clean build run_mocha_integration"
+ },
+ "dependencies": {
+ "@0xproject/assert": "^0.0.12",
+ "@0xproject/utils": "^0.2.1",
+ "bn.js": "^4.11.8",
+ "es6-promisify": "^5.0.0",
+ "ethereumjs-tx": "^1.3.3",
+ "ethereumjs-util": "^5.1.1",
+ "hdkey": "^0.7.1",
+ "ledgerco": "0xProject/ledger-node-js-api",
+ "lodash": "^4.17.4",
+ "semaphore-async-await": "^1.5.1",
+ "web3": "^0.20.0",
+ "web3-provider-engine": "^13.0.1"
+ },
+ "devDependencies": {
+ "@0xproject/tslint-config": "^0.4.3",
+ "@0xproject/utils": "^0.2.1",
+ "@types/lodash": "^4.14.86",
+ "@types/mocha": "^2.2.42",
+ "@types/node": "^8.0.53",
+ "awesome-typescript-loader": "^3.1.3",
+ "chai": "^4.0.1",
+ "chai-as-promised": "^7.1.0",
+ "chai-as-promised-typescript-typings": "^0.0.5",
+ "chai-typescript-typings": "^0.0.2",
+ "dirty-chai": "^2.0.1",
+ "mocha": "^4.0.1",
+ "npm-run-all": "^4.1.2",
+ "shx": "^0.2.2",
+ "tslint": "5.8.0",
+ "types-bn": "^0.0.1",
+ "types-ethereumjs-util": "0xproject/types-ethereumjs-util",
+ "typescript": "~2.6.1",
+ "web3-typescript-typings": "^0.7.2",
+ "webpack": "^3.1.0"
+ }
}
diff --git a/packages/subproviders/scripts/postpublish.js b/packages/subproviders/scripts/postpublish.js
new file mode 100644
index 000000000..7fa452b08
--- /dev/null
+++ b/packages/subproviders/scripts/postpublish.js
@@ -0,0 +1,14 @@
+const postpublish_utils = require('../../../scripts/postpublish_utils');
+const packageJSON = require('../package.json');
+
+const subPackageName = packageJSON.name;
+
+postpublish_utils.getLatestTagAndVersionAsync(subPackageName)
+ .then(function(result) {
+ const releaseName = postpublish_utils.getReleaseName(subPackageName, result.version);
+ const assets = [];
+ return postpublish_utils.publishReleaseNotes(result.tag, releaseName, assets);
+ })
+ .catch (function(err) {
+ throw err;
+ });
diff --git a/packages/subproviders/src/globals.d.ts b/packages/subproviders/src/globals.d.ts
index 520ca9232..53457fa24 100644
--- a/packages/subproviders/src/globals.d.ts
+++ b/packages/subproviders/src/globals.d.ts
@@ -1,10 +1,9 @@
-/// <reference types='chai-typescript-typings' />
-/// <reference types='chai-as-promised-typescript-typings' />
declare module 'dirty-chai';
declare module 'es6-promisify';
// tslint:disable:max-classes-per-file
// tslint:disable:class-name
+// tslint:disable:async-suffix
// tslint:disable:completed-docs
// Ethereumjs-tx declarations
@@ -46,19 +45,20 @@ declare module 'ledgerco' {
export class eth {
public comm: comm;
constructor(comm: comm);
- public getAddress_async(path: string, display?: boolean, chaincode?: boolean):
- Promise<{publicKey: string; address: string}>;
+ public getAddress_async(
+ path: string,
+ display?: boolean,
+ chaincode?: boolean,
+ ): Promise<{ publicKey: string; address: string; chainCode: string }>;
public signTransaction_async(path: string, rawTxHex: string): Promise<ECSignatureString>;
- public getAppConfiguration_async(): Promise<{ arbitraryDataEnabled: number; version: string }>;
+ public getAppConfiguration_async(): Promise<{
+ arbitraryDataEnabled: number;
+ version: string;
+ }>;
public signPersonalMessage_async(path: string, messageHex: string): Promise<ECSignature>;
}
}
-// ethereum-address declarations
-declare module 'ethereum-address' {
- export const isAddress: (address: string) => boolean;
-}
-
// Semaphore-async-await declarations
declare module 'semaphore-async-await' {
class Semaphore {
@@ -77,21 +77,34 @@ declare module 'web3-provider-engine/subproviders/subprovider' {
declare module 'web3-provider-engine/subproviders/rpc' {
import * as Web3 from 'web3';
class RpcSubprovider {
- constructor(options: {rpcUrl: string});
+ constructor(options: { rpcUrl: string });
public handleRequest(
- payload: Web3.JSONRPCRequestPayload, next: () => void, end: (err: Error|null, data?: any) => void,
+ payload: Web3.JSONRPCRequestPayload,
+ next: () => void,
+ end: (err: Error | null, data?: any) => void,
): void;
}
export = RpcSubprovider;
}
declare module 'web3-provider-engine' {
- class Web3ProviderEngine {
- public on(event: string, handler: () => void): void;
- public send(payload: any): void;
- public sendAsync(payload: any, callback: (error: any, response: any) => void): void;
- public addProvider(provider: any): void;
- public start(): void;
- public stop(): void;
- }
- export = Web3ProviderEngine;
+ class Web3ProviderEngine {
+ public on(event: string, handler: () => void): void;
+ public send(payload: any): void;
+ public sendAsync(payload: any, callback: (error: any, response: any) => void): void;
+ public addProvider(provider: any): void;
+ public start(): void;
+ public stop(): void;
+ }
+ export = Web3ProviderEngine;
+}
+
+// hdkey declarations
+declare module 'hdkey' {
+ class HDNode {
+ public publicKey: Buffer;
+ public chainCode: Buffer;
+ public constructor();
+ public derive(path: string): HDNode;
+ }
+ export = HDNode;
}
diff --git a/packages/subproviders/src/index.ts b/packages/subproviders/src/index.ts
index 9b7cab3fa..720c4362f 100644
--- a/packages/subproviders/src/index.ts
+++ b/packages/subproviders/src/index.ts
@@ -4,18 +4,12 @@ import {
eth as LedgerEthereumClientFn,
} from 'ledgerco';
-import {LedgerEthereumClient} from './types';
+import { LedgerEthereumClient } from './types';
-export {InjectedWeb3Subprovider} from './subproviders/injected_web3';
-export {RedundantRPCSubprovider} from './subproviders/redundant_rpc';
-export {
- LedgerSubprovider,
-} from './subproviders/ledger';
-export {
- ECSignature,
- LedgerWalletSubprovider,
- LedgerCommunicationClient,
-} from './types';
+export { InjectedWeb3Subprovider } from './subproviders/injected_web3';
+export { RedundantRPCSubprovider } from './subproviders/redundant_rpc';
+export { LedgerSubprovider } from './subproviders/ledger';
+export { ECSignature, LedgerWalletSubprovider, LedgerCommunicationClient } from './types';
/**
* A factory method for creating a LedgerEthereumClient usable in a browser context.
diff --git a/packages/subproviders/src/subproviders/injected_web3.ts b/packages/subproviders/src/subproviders/injected_web3.ts
index 25d747a62..bd29acb22 100644
--- a/packages/subproviders/src/subproviders/injected_web3.ts
+++ b/packages/subproviders/src/subproviders/injected_web3.ts
@@ -9,29 +9,31 @@ import Web3ProviderEngine = require('web3-provider-engine');
* Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
*/
export class InjectedWeb3Subprovider {
- private injectedWeb3: Web3;
+ private _injectedWeb3: Web3;
constructor(injectedWeb3: Web3) {
- this.injectedWeb3 = injectedWeb3;
+ this._injectedWeb3 = injectedWeb3;
}
public handleRequest(
- payload: Web3.JSONRPCRequestPayload, next: () => void, end: (err: Error|null, result: any) => void,
+ payload: Web3.JSONRPCRequestPayload,
+ next: () => void,
+ end: (err: Error | null, result: any) => void,
) {
switch (payload.method) {
case 'web3_clientVersion':
- this.injectedWeb3.version.getNode(end);
+ this._injectedWeb3.version.getNode(end);
return;
case 'eth_accounts':
- this.injectedWeb3.eth.getAccounts(end);
+ this._injectedWeb3.eth.getAccounts(end);
return;
case 'eth_sendTransaction':
const [txParams] = payload.params;
- this.injectedWeb3.eth.sendTransaction(txParams, end);
+ this._injectedWeb3.eth.sendTransaction(txParams, end);
return;
case 'eth_sign':
const [address, message] = payload.params;
- this.injectedWeb3.eth.sign(address, message, end);
+ this._injectedWeb3.eth.sign(address, message, end);
return;
default:
diff --git a/packages/subproviders/src/subproviders/ledger.ts b/packages/subproviders/src/subproviders/ledger.ts
index e0a08f792..7267a793e 100644
--- a/packages/subproviders/src/subproviders/ledger.ts
+++ b/packages/subproviders/src/subproviders/ledger.ts
@@ -1,9 +1,8 @@
-import {assert} from '@0xproject/assert';
-import promisify = require('es6-promisify');
-import {isAddress} from 'ethereum-address';
+import { assert } from '@0xproject/assert';
+import { addressUtils } from '@0xproject/utils';
import EthereumTx = require('ethereumjs-tx');
import ethUtil = require('ethereumjs-util');
-import * as ledger from 'ledgerco';
+import HDNode = require('hdkey');
import * as _ from 'lodash';
import Semaphore from 'semaphore-async-await';
import Web3 = require('web3');
@@ -17,13 +16,12 @@ import {
ResponseWithTxParams,
} from '../types';
-import {Subprovider} from './subprovider';
+import { Subprovider } from './subprovider';
const DEFAULT_DERIVATION_PATH = `44'/60'/0'`;
const NUM_ADDRESSES_TO_FETCH = 10;
const ASK_FOR_ON_DEVICE_CONFIRMATION = false;
-const SHOULD_GET_CHAIN_CODE = false;
-const HEX_REGEX = /^[0-9A-Fa-f]+$/g;
+const SHOULD_GET_CHAIN_CODE = true;
export class LedgerSubprovider extends Subprovider {
private _nonceLock: Semaphore;
@@ -34,20 +32,8 @@ export class LedgerSubprovider extends Subprovider {
private _ledgerEthereumClientFactoryAsync: LedgerEthereumClientFactoryAsync;
private _ledgerClientIfExists?: LedgerEthereumClient;
private _shouldAlwaysAskForConfirmation: boolean;
- private static isValidHex(data: string) {
- if (!_.isString(data)) {
- return false;
- }
- const isHexPrefixed = data.slice(0, 2) === '0x';
- if (!isHexPrefixed) {
- return false;
- }
- const nonPrefixed = data.slice(2);
- const isValid = nonPrefixed.match(HEX_REGEX);
- return isValid;
- }
- private static validateSender(sender: string) {
- if (_.isUndefined(sender) || !isAddress(sender)) {
+ private static _validateSender(sender: string) {
+ if (_.isUndefined(sender) || !addressUtils.isAddress(sender)) {
throw new Error(LedgerSubproviderErrors.SenderInvalidOrNotSupplied);
}
}
@@ -58,12 +44,11 @@ export class LedgerSubprovider extends Subprovider {
this._networkId = config.networkId;
this._ledgerEthereumClientFactoryAsync = config.ledgerEthereumClientFactoryAsync;
this._derivationPath = config.derivationPath || DEFAULT_DERIVATION_PATH;
- this._shouldAlwaysAskForConfirmation = !_.isUndefined(config.accountFetchingConfigs) &&
- !_.isUndefined(
- config.accountFetchingConfigs.shouldAskForOnDeviceConfirmation,
- ) ?
- config.accountFetchingConfigs.shouldAskForOnDeviceConfirmation :
- ASK_FOR_ON_DEVICE_CONFIRMATION;
+ this._shouldAlwaysAskForConfirmation =
+ !_.isUndefined(config.accountFetchingConfigs) &&
+ !_.isUndefined(config.accountFetchingConfigs.shouldAskForOnDeviceConfirmation)
+ ? config.accountFetchingConfigs.shouldAskForOnDeviceConfirmation
+ : ASK_FOR_ON_DEVICE_CONFIRMATION;
this._derivationPathIndex = 0;
}
public getPath(): string {
@@ -76,7 +61,9 @@ export class LedgerSubprovider extends Subprovider {
this._derivationPathIndex = pathIndex;
}
public async handleRequest(
- payload: Web3.JSONRPCRequestPayload, next: () => void, end: (err: Error|null, result?: any) => void,
+ payload: Web3.JSONRPCRequestPayload,
+ next: () => void,
+ end: (err: Error | null, result?: any) => void,
) {
let accounts;
let txParams;
@@ -102,8 +89,8 @@ export class LedgerSubprovider extends Subprovider {
case 'eth_sendTransaction':
txParams = payload.params[0];
try {
- LedgerSubprovider.validateSender(txParams.from);
- const result = await this.sendTransactionAsync(txParams);
+ LedgerSubprovider._validateSender(txParams.from);
+ const result = await this._sendTransactionAsync(txParams);
end(null, result);
} catch (err) {
end(err);
@@ -113,15 +100,16 @@ export class LedgerSubprovider extends Subprovider {
case 'eth_signTransaction':
txParams = payload.params[0];
try {
- const result = await this.signTransactionWithoutSendingAsync(txParams);
+ const result = await this._signTransactionWithoutSendingAsync(txParams);
end(null, result);
} catch (err) {
end(err);
}
return;
+ case 'eth_sign':
case 'personal_sign':
- const data = payload.params[0];
+ const data = payload.method === 'eth_sign' ? payload.params[1] : payload.params[0];
try {
if (_.isUndefined(data)) {
throw new Error(LedgerSubproviderErrors.DataMissingForSignPersonalMessage);
@@ -140,27 +128,38 @@ export class LedgerSubprovider extends Subprovider {
}
}
public async getAccountsAsync(): Promise<string[]> {
- this._ledgerClientIfExists = await this.createLedgerClientAsync();
+ this._ledgerClientIfExists = await this._createLedgerClientAsync();
+
+ let ledgerResponse;
+ try {
+ ledgerResponse = await this._ledgerClientIfExists.getAddress_async(
+ this._derivationPath,
+ this._shouldAlwaysAskForConfirmation,
+ SHOULD_GET_CHAIN_CODE,
+ );
+ } finally {
+ await this._destroyLedgerClientAsync();
+ }
+
+ const hdKey = new HDNode();
+ hdKey.publicKey = new Buffer(ledgerResponse.publicKey, 'hex');
+ hdKey.chainCode = new Buffer(ledgerResponse.chainCode, 'hex');
- // TODO: replace with generating addresses without hitting Ledger
const accounts = [];
for (let i = 0; i < NUM_ADDRESSES_TO_FETCH; i++) {
- try {
- const derivationPath = `${this._derivationPath}/${i + this._derivationPathIndex}`;
- const result = await this._ledgerClientIfExists.getAddress_async(
- derivationPath, this._shouldAlwaysAskForConfirmation, SHOULD_GET_CHAIN_CODE,
- );
- accounts.push(result.address.toLowerCase());
- } catch (err) {
- await this.destoryLedgerClientAsync();
- throw err;
- }
+ const derivedHDNode = hdKey.derive(`m/${i + this._derivationPathIndex}`);
+ const derivedPublicKey = derivedHDNode.publicKey;
+ const shouldSanitizePublicKey = true;
+ const ethereumAddressUnprefixed = ethUtil
+ .publicToAddress(derivedPublicKey, shouldSanitizePublicKey)
+ .toString('hex');
+ const ethereumAddressPrefixed = ethUtil.addHexPrefix(ethereumAddressUnprefixed);
+ accounts.push(ethereumAddressPrefixed.toLowerCase());
}
- await this.destoryLedgerClientAsync();
return accounts;
}
public async signTransactionAsync(txParams: PartialTxParams): Promise<string> {
- this._ledgerClientIfExists = await this.createLedgerClientAsync();
+ this._ledgerClientIfExists = await this._createLedgerClientAsync();
const tx = new EthereumTx(txParams);
@@ -171,7 +170,7 @@ export class LedgerSubprovider extends Subprovider {
const txHex = tx.serialize().toString('hex');
try {
- const derivationPath = this.getDerivationPath();
+ const derivationPath = this._getDerivationPath();
const result = await this._ledgerClientIfExists.signTransaction_async(derivationPath, txHex);
// Store signature in transaction
tx.r = Buffer.from(result.r, 'hex');
@@ -181,43 +180,45 @@ export class LedgerSubprovider extends Subprovider {
// EIP155: v should be chain_id * 2 + {35, 36}
const signedChainId = Math.floor((tx.v[0] - 35) / 2);
if (signedChainId !== this._networkId) {
- await this.destoryLedgerClientAsync();
+ await this._destroyLedgerClientAsync();
const err = new Error(LedgerSubproviderErrors.TooOldLedgerFirmware);
throw err;
}
const signedTxHex = `0x${tx.serialize().toString('hex')}`;
- await this.destoryLedgerClientAsync();
+ await this._destroyLedgerClientAsync();
return signedTxHex;
} catch (err) {
- await this.destoryLedgerClientAsync();
+ await this._destroyLedgerClientAsync();
throw err;
}
}
public async signPersonalMessageAsync(data: string): Promise<string> {
- this._ledgerClientIfExists = await this.createLedgerClientAsync();
+ this._ledgerClientIfExists = await this._createLedgerClientAsync();
try {
- const derivationPath = this.getDerivationPath();
+ const derivationPath = this._getDerivationPath();
const result = await this._ledgerClientIfExists.signPersonalMessage_async(
- derivationPath, ethUtil.stripHexPrefix(data));
+ derivationPath,
+ ethUtil.stripHexPrefix(data),
+ );
const v = result.v - 27;
let vHex = v.toString(16);
if (vHex.length < 2) {
vHex = `0${v}`;
}
const signature = `0x${result.r}${result.s}${vHex}`;
- await this.destoryLedgerClientAsync();
+ await this._destroyLedgerClientAsync();
return signature;
} catch (err) {
- await this.destoryLedgerClientAsync();
+ await this._destroyLedgerClientAsync();
throw err;
}
}
- private getDerivationPath() {
+ private _getDerivationPath() {
const derivationPath = `${this.getPath()}/${this._derivationPathIndex}`;
return derivationPath;
}
- private async createLedgerClientAsync(): Promise<LedgerEthereumClient> {
+ private async _createLedgerClientAsync(): Promise<LedgerEthereumClient> {
await this._connectionLock.wait();
if (!_.isUndefined(this._ledgerClientIfExists)) {
this._connectionLock.signal();
@@ -227,7 +228,7 @@ export class LedgerSubprovider extends Subprovider {
this._connectionLock.signal();
return ledgerEthereumClient;
}
- private async destoryLedgerClientAsync() {
+ private async _destroyLedgerClientAsync() {
await this._connectionLock.wait();
if (_.isUndefined(this._ledgerClientIfExists)) {
this._connectionLock.signal();
@@ -237,11 +238,11 @@ export class LedgerSubprovider extends Subprovider {
this._ledgerClientIfExists = undefined;
this._connectionLock.signal();
}
- private async sendTransactionAsync(txParams: PartialTxParams): Promise<Web3.JSONRPCResponsePayload> {
+ private async _sendTransactionAsync(txParams: PartialTxParams): Promise<string> {
await this._nonceLock.wait();
try {
// fill in the extras
- const filledParams = await this.populateMissingTxParamsAsync(txParams);
+ const filledParams = await this._populateMissingTxParamsAsync(txParams);
// sign it
const signedTx = await this.signTransactionAsync(filledParams);
// emit a submit
@@ -251,17 +252,17 @@ export class LedgerSubprovider extends Subprovider {
};
const result = await this.emitPayloadAsync(payload);
this._nonceLock.signal();
- return result;
+ return result.result;
} catch (err) {
this._nonceLock.signal();
throw err;
}
}
- private async signTransactionWithoutSendingAsync(txParams: PartialTxParams): Promise<ResponseWithTxParams> {
+ private async _signTransactionWithoutSendingAsync(txParams: PartialTxParams): Promise<ResponseWithTxParams> {
await this._nonceLock.wait();
try {
// fill in the extras
- const filledParams = await this.populateMissingTxParamsAsync(txParams);
+ const filledParams = await this._populateMissingTxParamsAsync(txParams);
// sign it
const signedTx = await this.signTransactionAsync(filledParams);
@@ -276,7 +277,7 @@ export class LedgerSubprovider extends Subprovider {
throw err;
}
}
- private async populateMissingTxParamsAsync(txParams: PartialTxParams): Promise<PartialTxParams> {
+ private async _populateMissingTxParamsAsync(txParams: PartialTxParams): Promise<PartialTxParams> {
if (_.isUndefined(txParams.gasPrice)) {
const gasPriceResult = await this.emitPayloadAsync({
method: 'eth_gasPrice',
diff --git a/packages/subproviders/src/subproviders/redundant_rpc.ts b/packages/subproviders/src/subproviders/redundant_rpc.ts
index 80462bbfb..a3cb463a8 100644
--- a/packages/subproviders/src/subproviders/redundant_rpc.ts
+++ b/packages/subproviders/src/subproviders/redundant_rpc.ts
@@ -1,17 +1,19 @@
-import {promisify} from '@0xproject/utils';
+import { promisify } from '@0xproject/utils';
import * as _ from 'lodash';
import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
-import {JSONRPCPayload} from '../types';
+import { JSONRPCPayload } from '../types';
-import {Subprovider} from './subprovider';
+import { Subprovider } from './subprovider';
export class RedundantRPCSubprovider extends Subprovider {
- private rpcs: RpcSubprovider[];
- private static async firstSuccessAsync(
- rpcs: RpcSubprovider[], payload: JSONRPCPayload, next: () => void,
+ private _rpcs: RpcSubprovider[];
+ private static async _firstSuccessAsync(
+ rpcs: RpcSubprovider[],
+ payload: JSONRPCPayload,
+ next: () => void,
): Promise<any> {
- let lastErr: Error|undefined;
+ let lastErr: Error | undefined;
for (const rpc of rpcs) {
try {
const data = await promisify(rpc.handleRequest.bind(rpc))(payload, next);
@@ -27,21 +29,24 @@ export class RedundantRPCSubprovider extends Subprovider {
}
constructor(endpoints: string[]) {
super();
- this.rpcs = _.map(endpoints, endpoint => {
+ this._rpcs = _.map(endpoints, endpoint => {
return new RpcSubprovider({
rpcUrl: endpoint,
});
});
}
- public async handleRequest(payload: JSONRPCPayload, next: () => void,
- end: (err: Error|null, data?: any) => void): Promise<void> {
- const rpcsCopy = this.rpcs.slice();
+ // tslint:disable-next-line:async-suffix
+ public async handleRequest(
+ payload: JSONRPCPayload,
+ next: () => void,
+ end: (err: Error | null, data?: any) => void,
+ ): Promise<void> {
+ const rpcsCopy = this._rpcs.slice();
try {
- const data = await RedundantRPCSubprovider.firstSuccessAsync(rpcsCopy, payload, next);
+ const data = await RedundantRPCSubprovider._firstSuccessAsync(rpcsCopy, payload, next);
end(null, data);
} catch (err) {
end(err);
}
-
}
}
diff --git a/packages/subproviders/src/subproviders/subprovider.ts b/packages/subproviders/src/subproviders/subprovider.ts
index 64d97b958..6435c9f65 100644
--- a/packages/subproviders/src/subproviders/subprovider.ts
+++ b/packages/subproviders/src/subproviders/subprovider.ts
@@ -1,19 +1,16 @@
import promisify = require('es6-promisify');
import Web3 = require('web3');
-import {
- JSONRPCPayload,
-} from '../types';
+import { JSONRPCPayload } from '../types';
/*
* A version of the base class Subprovider found in providerEngine
* This one has an async/await `emitPayloadAsync` and also defined types.
* Altered version of: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
*/
export class Subprovider {
- private engine: any;
- private currentBlock: any;
+ private _engine: any;
// Ported from: https://github.com/MetaMask/provider-engine/blob/master/util/random-id.js
- private static getRandomId() {
+ private static _getRandomId() {
const extraDigits = 3;
// 13 time digits
const datePart = new Date().getTime() * Math.pow(10, extraDigits);
@@ -22,10 +19,10 @@ export class Subprovider {
// 16 digits
return datePart + extraPart;
}
- private static createFinalPayload(payload: JSONRPCPayload): Web3.JSONRPCRequestPayload {
+ private static _createFinalPayload(payload: JSONRPCPayload): Web3.JSONRPCRequestPayload {
const finalPayload = {
// defaults
- id: Subprovider.getRandomId(),
+ id: Subprovider._getRandomId(),
jsonrpc: '2.0',
params: [],
...payload,
@@ -33,14 +30,11 @@ export class Subprovider {
return finalPayload;
}
public setEngine(engine: any): void {
- this.engine = engine;
- engine.on('block', (block: any) => {
- this.currentBlock = block;
- });
+ this._engine = engine;
}
public async emitPayloadAsync(payload: JSONRPCPayload): Promise<any> {
- const finalPayload = Subprovider.createFinalPayload(payload);
- const response = await promisify(this.engine.sendAsync, this.engine)(finalPayload);
+ const finalPayload = Subprovider._createFinalPayload(payload);
+ const response = await promisify(this._engine.sendAsync, this._engine)(finalPayload);
return response;
}
}
diff --git a/packages/subproviders/src/types.ts b/packages/subproviders/src/types.ts
index 38dc1e67e..3db8be943 100644
--- a/packages/subproviders/src/types.ts
+++ b/packages/subproviders/src/types.ts
@@ -1,5 +1,4 @@
import * as _ from 'lodash';
-import * as Web3 from 'web3';
export interface LedgerCommunicationClient {
close_async: () => Promise<void>;
@@ -11,8 +10,13 @@ export interface LedgerCommunicationClient {
* NodeJs and Browser communication are supported.
*/
export interface LedgerEthereumClient {
- getAddress_async: (derivationPath: string, askForDeviceConfirmation: boolean,
- shouldGetChainCode: boolean) => Promise<LedgerGetAddressResult>;
+ // shouldGetChainCode is defined as `true` instead of `boolean` because other types rely on the assumption
+ // that we get back the chain code and we don't have dependent types to express it properly
+ getAddress_async: (
+ derivationPath: string,
+ askForDeviceConfirmation: boolean,
+ shouldGetChainCode: true,
+ ) => Promise<LedgerGetAddressResult>;
signPersonalMessage_async: (derivationPath: string, messageHex: string) => Promise<ECSignature>;
signTransaction_async: (derivationPath: string, txHex: string) => Promise<ECSignatureString>;
comm: LedgerCommunicationClient;
@@ -64,6 +68,8 @@ export interface SignatureData {
export interface LedgerGetAddressResult {
address: string;
+ publicKey: string;
+ chainCode: string;
}
export interface LedgerWalletSubprovider {
diff --git a/packages/subproviders/test/integration/ledger_subprovider_test.ts b/packages/subproviders/test/integration/ledger_subprovider_test.ts
index 75f6d47fe..628b532d7 100644
--- a/packages/subproviders/test/integration/ledger_subprovider_test.ts
+++ b/packages/subproviders/test/integration/ledger_subprovider_test.ts
@@ -2,23 +2,14 @@ import * as chai from 'chai';
import promisify = require('es6-promisify');
import * as ethUtils from 'ethereumjs-util';
import * as _ from 'lodash';
-import * as mocha from 'mocha';
import Web3 = require('web3');
import Web3ProviderEngine = require('web3-provider-engine');
import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
-import {
- ECSignature,
- ledgerEthereumNodeJsClientFactoryAsync,
- LedgerSubprovider,
-} from '../../src';
-import {
- DoneCallback,
- LedgerGetAddressResult,
- PartialTxParams,
-} from '../../src/types';
-import {chaiSetup} from '../chai_setup';
-import {reportCallbackErrors} from '../utils/report_callback_errors';
+import { ledgerEthereumNodeJsClientFactoryAsync, LedgerSubprovider } from '../../src';
+import { DoneCallback } from '../../src/types';
+import { chaiSetup } from '../chai_setup';
+import { reportCallbackErrors } from '../utils/report_callback_errors';
chaiSetup.configure();
const expect = chai.expect;
@@ -55,8 +46,9 @@ describe('LedgerSubprovider', () => {
chainId: 3,
};
const txHex = await ledgerSubprovider.signTransactionAsync(tx);
- // tslint:disable-next-line:max-line-length
- expect(txHex).to.be.equal('0xf85f8080822710940000000000000000000000000000000000000000808077a088a95ef1378487bc82be558e82c8478baf840c545d5b887536bb1da63673a98ba0019f4a4b9a107d1e6752bf7f701e275f28c13791d6e76af895b07373462cefaa');
+ expect(txHex).to.be.equal(
+ '0xf85f8080822710940000000000000000000000000000000000000000808077a088a95ef1378487bc82be558e82c8478baf840c545d5b887536bb1da63673a98ba0019f4a4b9a107d1e6752bf7f701e275f28c13791d6e76af895b07373462cefaa',
+ );
});
});
describe('calls through a provider', () => {
@@ -89,7 +81,27 @@ describe('LedgerSubprovider', () => {
});
ledgerProvider.sendAsync(payload, callback);
});
- it('signs a personal message', (done: DoneCallback) => {
+ it('signs a personal message with eth_sign', (done: DoneCallback) => {
+ (async () => {
+ const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
+ const accounts = await ledgerSubprovider.getAccountsAsync();
+ const signer = accounts[0];
+ const payload = {
+ jsonrpc: '2.0',
+ method: 'eth_sign',
+ params: [signer, messageHex],
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
+ expect(err).to.be.a('null');
+ expect(response.result.length).to.be.equal(132);
+ expect(response.result.substr(0, 2)).to.be.equal('0x');
+ done();
+ });
+ ledgerProvider.sendAsync(payload, callback);
+ })().catch(done);
+ });
+ it('signs a personal message with personal_sign', (done: DoneCallback) => {
(async () => {
const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
const accounts = await ledgerSubprovider.getAccountsAsync();
diff --git a/packages/subproviders/test/unit/ledger_subprovider_test.ts b/packages/subproviders/test/unit/ledger_subprovider_test.ts
index 964df5db9..1c70dd3a6 100644
--- a/packages/subproviders/test/unit/ledger_subprovider_test.ts
+++ b/packages/subproviders/test/unit/ledger_subprovider_test.ts
@@ -5,23 +5,14 @@ import Web3 = require('web3');
import Web3ProviderEngine = require('web3-provider-engine');
import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
-import {
- ECSignature,
- LedgerSubprovider,
-} from '../../src';
-import {
- DoneCallback,
- ECSignatureString,
- LedgerCommunicationClient,
- LedgerGetAddressResult,
- LedgerSubproviderErrors,
-} from '../../src/types';
-import {chaiSetup} from '../chai_setup';
-import {reportCallbackErrors} from '../utils/report_callback_errors';
+import { LedgerSubprovider } from '../../src';
+import { DoneCallback, LedgerCommunicationClient, LedgerSubproviderErrors } from '../../src/types';
+import { chaiSetup } from '../chai_setup';
+import { reportCallbackErrors } from '../utils/report_callback_errors';
chaiSetup.configure();
const expect = chai.expect;
-const FAKE_ADDRESS = '0x9901c66f2d4b95f7074b553da78084d708beca70';
+const FAKE_ADDRESS = '0xb088a3bc93f71b4de97b9de773e9647645983688';
describe('LedgerSubprovider', () => {
const networkId: number = 42;
@@ -31,8 +22,14 @@ describe('LedgerSubprovider', () => {
// tslint:disable:no-object-literal-type-assertion
const ledgerEthClient = {
getAddress_async: async () => {
+ const publicKey =
+ '04f428290f4c5ed6a198f71b8205f488141dbb3f0840c923bbfa798ecbee6370986c03b5575d94d506772fb48a6a44e345e4ebd4f028a6f609c44b655d6d3e71a1';
+ const chainCode = 'ac055a5537c0c7e9e02d14a197cad6b857836da2a12043b46912a37d959b5ae8';
+ const address = '0xBa388BA5e5EEF2c6cE42d831c2B3A28D3c99bdB1';
return {
- address: FAKE_ADDRESS,
+ publicKey,
+ address,
+ chainCode,
};
},
signPersonalMessage_async: async () => {
@@ -73,17 +70,20 @@ describe('LedgerSubprovider', () => {
it('signs a personal message', async () => {
const data = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
const ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync(data);
- // tslint:disable-next-line:max-line-length
- expect(ecSignatureHex).to.be.equal('0xa6cc284bff14b42bdf5e9286730c152be91719d478605ec46b3bebcd0ae491480652a1a7b742ceb0213d1e744316e285f41f878d8af0b8e632cbca4c279132d001');
+ expect(ecSignatureHex).to.be.equal(
+ '0xa6cc284bff14b42bdf5e9286730c152be91719d478605ec46b3bebcd0ae491480652a1a7b742ceb0213d1e744316e285f41f878d8af0b8e632cbca4c279132d001',
+ );
});
});
describe('failure cases', () => {
it('cannot open multiple simultaneous connections to the Ledger device', async () => {
const data = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
- return expect(Promise.all([
- ledgerSubprovider.getAccountsAsync(),
- ledgerSubprovider.signPersonalMessageAsync(data),
- ])).to.be.rejectedWith(LedgerSubproviderErrors.MultipleOpenConnectionsDisallowed);
+ return expect(
+ Promise.all([
+ ledgerSubprovider.getAccountsAsync(),
+ ledgerSubprovider.signPersonalMessageAsync(data),
+ ]),
+ ).to.be.rejectedWith(LedgerSubproviderErrors.MultipleOpenConnectionsDisallowed);
});
});
});
@@ -114,7 +114,24 @@ describe('LedgerSubprovider', () => {
});
provider.sendAsync(payload, callback);
});
- it('signs a personal message', (done: DoneCallback) => {
+ it('signs a personal message with eth_sign', (done: DoneCallback) => {
+ const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
+ const payload = {
+ jsonrpc: '2.0',
+ method: 'eth_sign',
+ params: ['0x0000000000000000000000000000000000000000', messageHex],
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
+ expect(err).to.be.a('null');
+ expect(response.result).to.be.equal(
+ '0xa6cc284bff14b42bdf5e9286730c152be91719d478605ec46b3bebcd0ae491480652a1a7b742ceb0213d1e744316e285f41f878d8af0b8e632cbca4c279132d001',
+ );
+ done();
+ });
+ provider.sendAsync(payload, callback);
+ });
+ it('signs a personal message with personal_sign', (done: DoneCallback) => {
const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
const payload = {
jsonrpc: '2.0',
@@ -124,8 +141,9 @@ describe('LedgerSubprovider', () => {
};
const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
expect(err).to.be.a('null');
- // tslint:disable-next-line:max-line-length
- expect(response.result).to.be.equal('0xa6cc284bff14b42bdf5e9286730c152be91719d478605ec46b3bebcd0ae491480652a1a7b742ceb0213d1e744316e285f41f878d8af0b8e632cbca4c279132d001');
+ expect(response.result).to.be.equal(
+ '0xa6cc284bff14b42bdf5e9286730c152be91719d478605ec46b3bebcd0ae491480652a1a7b742ceb0213d1e744316e285f41f878d8af0b8e632cbca4c279132d001',
+ );
done();
});
provider.sendAsync(payload, callback);
@@ -154,6 +172,21 @@ describe('LedgerSubprovider', () => {
});
});
describe('failure cases', () => {
+ it('should throw if `data` param not hex when calling eth_sign', (done: DoneCallback) => {
+ const nonHexMessage = 'hello world';
+ const payload = {
+ jsonrpc: '2.0',
+ method: 'eth_sign',
+ params: ['0x0000000000000000000000000000000000000000', nonHexMessage],
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
+ expect(err).to.not.be.a('null');
+ expect(err.message).to.be.equal('Expected data to be of type HexString, encountered: hello world');
+ done();
+ });
+ provider.sendAsync(payload, callback);
+ });
it('should throw if `data` param not hex when calling personal_sign', (done: DoneCallback) => {
const nonHexMessage = 'hello world';
const payload = {
@@ -187,8 +220,7 @@ describe('LedgerSubprovider', () => {
});
provider.sendAsync(payload, callback);
});
- it('should throw if `from` param invalid address when calling eth_sendTransaction',
- (done: DoneCallback) => {
+ it('should throw if `from` param invalid address when calling eth_sendTransaction', (done: DoneCallback) => {
const tx = {
to: '0xafa3f8684e54059998bc3a7b0d2b0da075154d66',
from: '0xIncorrectEthereumAddress',
diff --git a/packages/subproviders/test/unit/redundant_rpc_subprovider_test.ts b/packages/subproviders/test/unit/redundant_rpc_subprovider_test.ts
index edeb1d5a2..c3170745c 100644
--- a/packages/subproviders/test/unit/redundant_rpc_subprovider_test.ts
+++ b/packages/subproviders/test/unit/redundant_rpc_subprovider_test.ts
@@ -3,22 +3,19 @@ import * as _ from 'lodash';
import Web3 = require('web3');
import Web3ProviderEngine = require('web3-provider-engine');
-import {RedundantRPCSubprovider} from '../../src';
-import {
- DoneCallback,
-} from '../../src/types';
-import {chaiSetup} from '../chai_setup';
-import {reportCallbackErrors} from '../utils/report_callback_errors';
+import { RedundantRPCSubprovider } from '../../src';
+import { DoneCallback } from '../../src/types';
+import { chaiSetup } from '../chai_setup';
+import { reportCallbackErrors } from '../utils/report_callback_errors';
const expect = chai.expect;
+chaiSetup.configure();
describe('RedundantRpcSubprovider', () => {
let provider: Web3ProviderEngine;
it('succeeds when supplied a healthy endpoint', (done: DoneCallback) => {
provider = new Web3ProviderEngine();
- const endpoints = [
- 'http://localhost:8545',
- ];
+ const endpoints = ['http://localhost:8545'];
const redundantSubprovider = new RedundantRPCSubprovider(endpoints);
provider.addProvider(redundantSubprovider);
provider.start();
@@ -38,10 +35,7 @@ describe('RedundantRpcSubprovider', () => {
});
it('succeeds when supplied at least one healthy endpoint', (done: DoneCallback) => {
provider = new Web3ProviderEngine();
- const endpoints = [
- 'http://does-not-exist:3000',
- 'http://localhost:8545',
- ];
+ const endpoints = ['http://does-not-exist:3000', 'http://localhost:8545'];
const redundantSubprovider = new RedundantRPCSubprovider(endpoints);
provider.addProvider(redundantSubprovider);
provider.start();
diff --git a/packages/subproviders/tsconfig.json b/packages/subproviders/tsconfig.json
index 24adf4637..9a65a0a97 100644
--- a/packages/subproviders/tsconfig.json
+++ b/packages/subproviders/tsconfig.json
@@ -1,22 +1,15 @@
{
- "compilerOptions": {
- "module": "commonjs",
- "target": "es5",
- "lib": [ "es2015", "dom" ],
- "outDir": "lib",
- "sourceMap": true,
- "declaration": true,
- "noImplicitAny": true,
- "experimentalDecorators": true,
- "strictNullChecks": true
- },
- "include": [
- "./src/**/*",
- "./test/**/*",
- "../../node_modules/web3-typescript-typings/index.d.ts",
- "../../node_modules/chai-typescript-typings/index.d.ts",
- "../../node_modules/types-bn/index.d.ts",
- "../../node_modules/types-ethereumjs-util/index.d.ts",
- "../../node_modules/chai-as-promised-typescript-typings/index.d.ts"
- ]
+ "extends": "../../tsconfig",
+ "compilerOptions": {
+ "outDir": "lib"
+ },
+ "include": [
+ "./src/**/*",
+ "./test/**/*",
+ "../../node_modules/chai-typescript-typings/index.d.ts",
+ "../../node_modules/web3-typescript-typings/index.d.ts",
+ "../../node_modules/types-bn/index.d.ts",
+ "../../node_modules/types-ethereumjs-util/index.d.ts",
+ "../../node_modules/chai-as-promised-typescript-typings/index.d.ts"
+ ]
}
diff --git a/packages/subproviders/tslint.json b/packages/subproviders/tslint.json
index a07795151..ffaefe83a 100644
--- a/packages/subproviders/tslint.json
+++ b/packages/subproviders/tslint.json
@@ -1,5 +1,3 @@
{
- "extends": [
- "@0xproject/tslint-config"
- ]
+ "extends": ["@0xproject/tslint-config"]
}