aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorFabio Berger <me@fabioberger.com>2017-12-06 05:45:35 +0800
committerFabio Berger <me@fabioberger.com>2017-12-06 05:45:35 +0800
commit038668efdfdd2eac85c30206e17128b0af2b48ce (patch)
treedd89073024f6fc89ac6306629d4bd14cd1ac5e5e /packages
parent47789d770d08c20f33dbd839fcbd7bfa23d252a3 (diff)
downloaddexon-sol-tools-038668efdfdd2eac85c30206e17128b0af2b48ce.tar
dexon-sol-tools-038668efdfdd2eac85c30206e17128b0af2b48ce.tar.gz
dexon-sol-tools-038668efdfdd2eac85c30206e17128b0af2b48ce.tar.bz2
dexon-sol-tools-038668efdfdd2eac85c30206e17128b0af2b48ce.tar.lz
dexon-sol-tools-038668efdfdd2eac85c30206e17128b0af2b48ce.tar.xz
dexon-sol-tools-038668efdfdd2eac85c30206e17128b0af2b48ce.tar.zst
dexon-sol-tools-038668efdfdd2eac85c30206e17128b0af2b48ce.zip
Port subproviders over to mono repo, refactor LedgerSubprovider to no longer rely on hookedWalletSubprovider. Added unit and integration tests.
Diffstat (limited to 'packages')
-rw-r--r--packages/subproviders/README.md39
-rw-r--r--packages/subproviders/package.json60
-rw-r--r--packages/subproviders/src/globals.d.ts68
-rw-r--r--packages/subproviders/src/index.ts30
-rw-r--r--packages/subproviders/src/subproviders/injected_web3.ts47
-rw-r--r--packages/subproviders/src/subproviders/ledger.ts320
-rw-r--r--packages/subproviders/src/subproviders/redundant_rpc.ts46
-rw-r--r--packages/subproviders/src/subproviders/subprovider.ts45
-rw-r--r--packages/subproviders/src/types.ts115
-rw-r--r--packages/subproviders/test/chai_setup.ts11
-rw-r--r--packages/subproviders/test/integration/ledger_subprovider_test.ts171
-rw-r--r--packages/subproviders/test/unit/ledger_subprovider_test.ts229
-rw-r--r--packages/subproviders/test/utils/report_callback_errors.ts14
-rw-r--r--packages/subproviders/tsconfig.json22
-rw-r--r--packages/subproviders/tslint.json5
-rw-r--r--packages/subproviders/webpack.config.js56
16 files changed, 1278 insertions, 0 deletions
diff --git a/packages/subproviders/README.md b/packages/subproviders/README.md
new file mode 100644
index 000000000..72f18a962
--- /dev/null
+++ b/packages/subproviders/README.md
@@ -0,0 +1,39 @@
+Subproviders
+-----------
+
+A few useful subproviders.
+
+## Installation
+
+```
+npm install @0xproject/subproviders --save
+```
+
+## Subproviders
+
+#### Ledger Nano S subprovider
+
+A subprovider that enables your dApp to send signing requests to a user's Ledger Nano S hardware wallet. These can be requests to sign transactions or messages.
+
+#### Redundant RPC subprovider
+
+A subprovider which attempts to send an RPC call to a list of RPC endpoints sequentially, until one of them returns a successful response.
+
+#### Injected Web3 subprovider
+
+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
+
+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 Ethereum app
+- Make sure "browser support" is disabled
+
+Then run:
+
+```
+yarn test:integration
+```
diff --git a/packages/subproviders/package.json b/packages/subproviders/package.json
new file mode 100644
index 000000000..2b1637286
--- /dev/null
+++ b/packages/subproviders/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "@0xproject/subproviders",
+ "version": "0.0.0",
+ "main": "lib/src/index.js",
+ "types": "lib/src/index.d.ts",
+ "license": "Apache-2.0",
+ "scripts": {
+ "prebuild": "npm run clean",
+ "build": "run-p build:umd:dev build:commonjs; exit 0;",
+ "clean": "shx rm -rf lib",
+ "build:umd:dev": "webpack",
+ "build:umd:prod": "NODE_ENV=production webpack",
+ "build:commonjs": "tsc",
+ "lint": "tslint --project . 'src/**/*.ts' 'test/**/*.ts'",
+ "pretest:umd": "run-s clean build:umd:dev build:commonjs",
+ "substitute_umd_bundle": "shx mv _bundles/* lib/src",
+ "run_mocha_all": "mocha lib/test/**/*_test.js --timeout 10000 --bail --exit",
+ "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": "run-s clean build:commonjs run_mocha_all",
+ "test:unit": "run-s clean build:commonjs run_mocha_unit",
+ "test:integration": "run-s clean build:commonjs 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",
+ "sinon": "^4.0.0",
+ "web3": "^0.20.0",
+ "web3-provider-engine": "^13.0.1"
+ },
+ "devDependencies": {
+ "@0xproject/tslint-config": "^0.2.0",
+ "@types/lodash": "^4.14.64",
+ "@types/mocha": "^2.2.44",
+ "@types/node": "^8.0.1",
+ "@types/sinon": "^2.2.2",
+ "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.0",
+ "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/src/globals.d.ts b/packages/subproviders/src/globals.d.ts
new file mode 100644
index 000000000..1f9c6e8a6
--- /dev/null
+++ b/packages/subproviders/src/globals.d.ts
@@ -0,0 +1,68 @@
+/// <reference types='chai-typescript-typings' />
+/// <reference types='chai-as-promised-typescript-typings' />
+declare module 'bn.js';
+declare module 'dirty-chai';
+declare module 'ledgerco';
+declare module 'ethereumjs-tx';
+declare module 'es6-promisify';
+declare module 'ethereum-address';
+declare module 'debug';
+
+// tslint:disable:max-classes-per-file
+// tslint:disable:class-name
+// tslint:disable:completed-docs
+declare module 'ledgerco' {
+ interface comm {
+ close_async: Promise<void>;
+ create_async: Promise<void>;
+ }
+ export class comm_node implements comm {
+ public create_async: Promise<void>;
+ public close_async: Promise<void>;
+ }
+ export class comm_u2f implements comm {
+ public create_async: Promise<void>;
+ public close_async: Promise<void>;
+ }
+}
+
+// Semaphore-async-await declarations
+declare module 'semaphore-async-await' {
+ class Semaphore {
+ constructor(permits: number);
+ public wait(): void;
+ public signal(): void;
+ }
+ export default Semaphore;
+}
+
+// web3-provider-engine declarations
+declare module 'web3-provider-engine/subproviders/subprovider' {
+ class Subprovider {}
+ export = Subprovider;
+}
+declare module 'web3-provider-engine/subproviders/rpc' {
+ import * as Web3 from 'web3';
+ class RpcSubprovider {
+ constructor(options: {rpcUrl: string});
+ public handleRequest(
+ 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;
+}
+// tslint:enable:max-classes-per-file
+// tslint:enable:class-name
+// tslint:enable:completed-docs
diff --git a/packages/subproviders/src/index.ts b/packages/subproviders/src/index.ts
new file mode 100644
index 000000000..9560c3597
--- /dev/null
+++ b/packages/subproviders/src/index.ts
@@ -0,0 +1,30 @@
+import {
+ comm_node as LedgerNodeCommunication,
+ comm_u2f as LedgerBrowserCommunication,
+ eth as LedgerEthereumClientFn,
+} from 'ledgerco';
+
+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 async function ledgerEthereumBrowserClientFactoryAsync(): Promise<LedgerEthereumClient> {
+ const ledgerConnection = await LedgerBrowserCommunication.create_async();
+ const ledgerEthClient = new LedgerEthereumClientFn(ledgerConnection);
+ return ledgerEthClient;
+}
+
+export async function ledgerEthereumNodeJsClientFactoryAsync(): Promise<LedgerEthereumClient> {
+ const ledgerConnection = await LedgerNodeCommunication.create_async();
+ const ledgerEthClient = new LedgerEthereumClientFn(ledgerConnection);
+ return ledgerEthClient;
+}
diff --git a/packages/subproviders/src/subproviders/injected_web3.ts b/packages/subproviders/src/subproviders/injected_web3.ts
new file mode 100644
index 000000000..a3308d142
--- /dev/null
+++ b/packages/subproviders/src/subproviders/injected_web3.ts
@@ -0,0 +1,47 @@
+import * as _ from 'lodash';
+import Web3 = require('web3');
+import Web3ProviderEngine = require('web3-provider-engine');
+
+/*
+ * This class implements the web3-provider-engine subprovider interface and forwards
+ * requests involving user accounts (getAccounts, sendTransaction, etc...) to the injected
+ * web3 instance in their browser.
+ * Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
+ */
+export class InjectedWeb3Subprovider {
+ private injectedWeb3: Web3;
+ constructor(injectedWeb3: Web3) {
+ this.injectedWeb3 = injectedWeb3;
+ }
+ public handleRequest(
+ payload: Web3.JSONRPCRequestPayload, next: () => void, end: (err: Error, result: any) => void,
+ ) {
+ switch (payload.method) {
+ case 'web3_clientVersion':
+ this.injectedWeb3.version.getNode(end);
+ return;
+ case 'eth_accounts':
+ this.injectedWeb3.eth.getAccounts(end);
+ return;
+
+ case 'eth_sendTransaction':
+ const [txParams] = payload.params;
+ this.injectedWeb3.eth.sendTransaction(txParams, end);
+ return;
+
+ case 'eth_sign':
+ const [address, message] = payload.params;
+ this.injectedWeb3.eth.sign(address, message, end);
+ return;
+
+ default:
+ next();
+ return;
+ }
+ }
+ // Required to implement this method despite not needing it for this subprovider
+ // tslint:disable-next-line:prefer-function-over-method
+ public setEngine(engine: Web3ProviderEngine) {
+ // noop
+ }
+}
diff --git a/packages/subproviders/src/subproviders/ledger.ts b/packages/subproviders/src/subproviders/ledger.ts
new file mode 100644
index 000000000..931199ed1
--- /dev/null
+++ b/packages/subproviders/src/subproviders/ledger.ts
@@ -0,0 +1,320 @@
+import promisify = require('es6-promisify');
+import {isAddress} from 'ethereum-address';
+import * as EthereumTx from 'ethereumjs-tx';
+import ethUtil = require('ethereumjs-util');
+import * as ledger from 'ledgerco';
+import * as _ from 'lodash';
+import Semaphore from 'semaphore-async-await';
+import Web3 = require('web3');
+
+import {
+ LedgerEthereumClient,
+ LedgerEthereumClientFactoryAsync,
+ LedgerSubproviderConfigs,
+ LedgerSubproviderErrors,
+ PartialTxParams,
+ ResponseWithTxParams,
+ SignPersonalMessageParams,
+} from '../types';
+
+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;
+
+export class LedgerSubprovider extends Subprovider {
+ private _nonceLock: Semaphore;
+ private _connectionLock: Semaphore;
+ private _networkId: number;
+ private _derivationPath: string;
+ private _derivationPathIndex: number;
+ 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 validatePersonalMessage(msgParams: PartialTxParams) {
+ if (_.isUndefined(msgParams.from) || !isAddress(msgParams.from)) {
+ throw new Error(LedgerSubproviderErrors.FromAddressMissingOrInvalid);
+ }
+ if (_.isUndefined(msgParams.data)) {
+ throw new Error(LedgerSubproviderErrors.DataMissingForSignPersonalMessage);
+ }
+ if (!LedgerSubprovider.isValidHex(msgParams.data)) {
+ throw new Error(LedgerSubproviderErrors.DataNotValidHexForSignPersonalMessage);
+ }
+ }
+ private static validateSender(sender: string) {
+ if (_.isUndefined(sender) || !isAddress(sender)) {
+ throw new Error(LedgerSubproviderErrors.SenderInvalidOrNotSupplied);
+ }
+ }
+ constructor(config: LedgerSubproviderConfigs) {
+ super();
+ this._nonceLock = new Semaphore(1);
+ this._connectionLock = new Semaphore(1);
+ 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._derivationPathIndex = 0;
+ }
+ public getPath(): string {
+ return this._derivationPath;
+ }
+ public setPath(derivationPath: string) {
+ this._derivationPath = derivationPath;
+ }
+ public setPathIndex(pathIndex: number) {
+ this._derivationPathIndex = pathIndex;
+ }
+ public async handleRequest(
+ payload: Web3.JSONRPCRequestPayload, next: () => void, end: (err: Error|null, result?: any) => void,
+ ) {
+ let accounts;
+ let txParams;
+ switch (payload.method) {
+ case 'eth_coinbase':
+ try {
+ accounts = await this.getAccountsAsync();
+ end(null, accounts[0]);
+ } catch (err) {
+ end(err);
+ }
+ return;
+
+ case 'eth_accounts':
+ try {
+ accounts = await this.getAccountsAsync();
+ end(null, accounts);
+ } catch (err) {
+ end(err);
+ }
+ return;
+
+ case 'eth_sendTransaction':
+ txParams = payload.params[0];
+ try {
+ LedgerSubprovider.validateSender(txParams.from);
+ const result = await this.sendTransactionAsync(txParams);
+ end(null, result);
+ } catch (err) {
+ end(err);
+ }
+ return;
+
+ case 'eth_signTransaction':
+ txParams = payload.params[0];
+ try {
+ const result = await this.signTransactionWithoutSendingAsync(txParams);
+ end(null, result);
+ } catch (err) {
+ end(err);
+ }
+ return;
+
+ case 'personal_sign':
+ // non-standard "extraParams" to be appended to our "msgParams" obj
+ // good place for metadata
+ const extraParams = payload.params[2] || {};
+ const msgParams = _.assign({}, extraParams, {
+ from: payload.params[1],
+ data: payload.params[0],
+ });
+
+ try {
+ LedgerSubprovider.validatePersonalMessage(msgParams);
+ const ecSignatureHex = await this.signPersonalMessageAsync(msgParams);
+ end(null, ecSignatureHex);
+ } catch (err) {
+ end(err);
+ }
+ return;
+
+ default:
+ next();
+ return;
+ }
+ }
+ public async getAccountsAsync(): Promise<string[]> {
+ this._ledgerClientIfExists = await this.createLedgerClientAsync();
+
+ 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;
+ }
+ }
+ await this.destoryLedgerClientAsync();
+ return accounts;
+ }
+ public async signTransactionAsync(txParams: PartialTxParams): Promise<string> {
+ this._ledgerClientIfExists = await this.createLedgerClientAsync();
+
+ const tx = new EthereumTx(txParams);
+
+ // Set the EIP155 bits
+ tx.raw[6] = Buffer.from([this._networkId]); // v
+ tx.raw[7] = Buffer.from([]); // r
+ tx.raw[8] = Buffer.from([]); // s
+
+ const txHex = tx.serialize().toString('hex');
+ try {
+ const derivationPath = this.getDerivationPath();
+ const result = await this._ledgerClientIfExists.signTransaction_async(derivationPath, txHex);
+ // Store signature in transaction
+ tx.r = Buffer.from(result.r, 'hex');
+ tx.s = Buffer.from(result.s, 'hex');
+ tx.v = Buffer.from(result.v, 'hex');
+
+ // 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();
+ const err = new Error(LedgerSubproviderErrors.TooOldLedgerFirmware);
+ throw err;
+ }
+
+ const signedTxHex = `0x${tx.serialize().toString('hex')}`;
+ await this.destoryLedgerClientAsync();
+ return signedTxHex;
+ } catch (err) {
+ await this.destoryLedgerClientAsync();
+ throw err;
+ }
+ }
+ public async signPersonalMessageAsync(msgParams: SignPersonalMessageParams): Promise<string> {
+ this._ledgerClientIfExists = await this.createLedgerClientAsync();
+ try {
+ const derivationPath = this.getDerivationPath();
+ const result = await this._ledgerClientIfExists.signPersonalMessage_async(
+ derivationPath, ethUtil.stripHexPrefix(msgParams.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();
+ return signature;
+ } catch (err) {
+ await this.destoryLedgerClientAsync();
+ throw err;
+ }
+ }
+ private getDerivationPath() {
+ const derivationPath = `${this.getPath()}/${this._derivationPathIndex}`;
+ return derivationPath;
+ }
+ private async createLedgerClientAsync(): Promise<LedgerEthereumClient> {
+ await this._connectionLock.wait();
+ if (!_.isUndefined(this._ledgerClientIfExists)) {
+ this._connectionLock.signal();
+ throw new Error(LedgerSubproviderErrors.MultipleOpenConnectionsDisallowed);
+ }
+ const ledgerEthereumClient = await this._ledgerEthereumClientFactoryAsync();
+ this._connectionLock.signal();
+ return ledgerEthereumClient;
+ }
+ private async destoryLedgerClientAsync() {
+ await this._connectionLock.wait();
+ if (_.isUndefined(this._ledgerClientIfExists)) {
+ this._connectionLock.signal();
+ return;
+ }
+ await this._ledgerClientIfExists.comm.close_async();
+ this._ledgerClientIfExists = undefined;
+ this._connectionLock.signal();
+ }
+ private async sendTransactionAsync(txParams: PartialTxParams): Promise<any> {
+ await this._nonceLock.wait();
+ try {
+ // fill in the extras
+ const filledParams = await this.populateMissingTxParamsAsync(txParams);
+ // sign it
+ const signedTx = await this.signTransactionAsync(filledParams);
+ // emit a submit
+ const payload = {
+ method: 'eth_sendRawTransaction',
+ params: [signedTx],
+ };
+ const result = await this.emitPayloadAsync(payload);
+ this._nonceLock.signal();
+ return result;
+ } catch (err) {
+ this._nonceLock.signal();
+ throw err;
+ }
+ }
+ private async signTransactionWithoutSendingAsync(txParams: PartialTxParams): Promise<ResponseWithTxParams> {
+ await this._nonceLock.wait();
+ try {
+ // fill in the extras
+ const filledParams = await this.populateMissingTxParamsAsync(txParams);
+ // sign it
+ const signedTx = await this.signTransactionAsync(filledParams);
+
+ this._nonceLock.signal();
+ const result = {
+ raw: signedTx,
+ tx: txParams,
+ };
+ return result;
+ } catch (err) {
+ this._nonceLock.signal();
+ throw err;
+ }
+ }
+ private async populateMissingTxParamsAsync(txParams: PartialTxParams): Promise<PartialTxParams> {
+ if (_.isUndefined(txParams.gasPrice)) {
+ const gasPriceResult = await this.emitPayloadAsync({
+ method: 'eth_gasPrice',
+ params: [],
+ });
+ const gasPrice = gasPriceResult.result.toString();
+ txParams.gasPrice = gasPrice;
+ }
+ if (_.isUndefined(txParams.nonce)) {
+ const nonceResult = await this.emitPayloadAsync({
+ method: 'eth_getTransactionCount',
+ params: [txParams.from, 'pending'],
+ });
+ const nonce = nonceResult.result;
+ txParams.nonce = nonce;
+ }
+ if (_.isUndefined(txParams.gas)) {
+ const gasResult = await this.emitPayloadAsync({
+ method: 'eth_estimateGas',
+ params: [txParams],
+ });
+ const gas = gasResult.result.toString();
+ txParams.gas = gas;
+ }
+ return txParams;
+ }
+}
diff --git a/packages/subproviders/src/subproviders/redundant_rpc.ts b/packages/subproviders/src/subproviders/redundant_rpc.ts
new file mode 100644
index 000000000..43d711ee6
--- /dev/null
+++ b/packages/subproviders/src/subproviders/redundant_rpc.ts
@@ -0,0 +1,46 @@
+import promisify = require('es6-promisify');
+import * as _ from 'lodash';
+import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
+import Subprovider = require('web3-provider-engine/subproviders/subprovider');
+
+import {JSONRPCPayload} from '../types';
+
+export class RedundantRPCSubprovider extends Subprovider {
+ private rpcs: RpcSubprovider[];
+ private static async firstSuccessAsync(
+ rpcs: RpcSubprovider[], payload: JSONRPCPayload, next: () => void,
+ ): Promise<any> {
+ let lastErr: Error|undefined;
+ for (const rpc of rpcs) {
+ try {
+ const data = await promisify(rpc.handleRequest.bind(rpc))(payload, next);
+ return data;
+ } catch (err) {
+ lastErr = err;
+ continue;
+ }
+ }
+ if (!_.isUndefined(lastErr)) {
+ throw lastErr;
+ }
+ }
+ constructor(endpoints: string[]) {
+ super();
+ this.rpcs = _.map(endpoints, endpoint => {
+ return new RpcSubprovider({
+ rpcUrl: endpoint,
+ });
+ });
+ }
+ public async handleRequest(payload: JSONRPCPayload, next: () => void,
+ end: (err?: Error, data?: any) => void): Promise<void> {
+ const rpcsCopy = this.rpcs.slice();
+ try {
+ const data = await RedundantRPCSubprovider.firstSuccessAsync(rpcsCopy, payload, next);
+ end(undefined, data);
+ } catch (err) {
+ end(err);
+ }
+
+ }
+}
diff --git a/packages/subproviders/src/subproviders/subprovider.ts b/packages/subproviders/src/subproviders/subprovider.ts
new file mode 100644
index 000000000..07f4d6353
--- /dev/null
+++ b/packages/subproviders/src/subproviders/subprovider.ts
@@ -0,0 +1,45 @@
+import promisify = require('es6-promisify');
+import Web3 = require('web3');
+
+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 static getRandomId() {
+ const extraDigits = 3;
+ // 13 time digits
+ const datePart = new Date().getTime() * Math.pow(10, extraDigits);
+ // 3 random digits
+ const extraPart = Math.floor(Math.random() * Math.pow(10, extraDigits));
+ // 16 digits
+ return datePart + extraPart;
+ }
+ private static createFinalPayload(payload: JSONRPCPayload): Web3.JSONRPCRequestPayload {
+ const finalPayload = {
+ // defaults
+ id: Subprovider.getRandomId(),
+ jsonrpc: '2.0',
+ params: [],
+ ...payload,
+ };
+ return finalPayload;
+ }
+ public setEngine(engine: any): void {
+ this.engine = engine;
+ engine.on('block', (block: any) => {
+ this.currentBlock = block;
+ });
+ }
+ public async emitPayloadAsync(payload: JSONRPCPayload): Promise<any> {
+ 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
new file mode 100644
index 000000000..4564c5229
--- /dev/null
+++ b/packages/subproviders/src/types.ts
@@ -0,0 +1,115 @@
+import * as _ from 'lodash';
+import * as Web3 from 'web3';
+
+export interface LedgerCommunicationClient {
+ exchange: (apduHex: string, statusList: number[]) => Promise<any[]>;
+ setScrambleKey: (key: string) => void;
+ close_async: () => Promise<void>;
+}
+
+/*
+ * The LedgerEthereumClient sends Ethereum-specific requests to the Ledger Nano S
+ * It uses an internal LedgerCommunicationClient to relay these requests. Currently
+ * NodeJs and Browser communication are supported.
+ */
+export interface LedgerEthereumClient {
+ getAddress_async: (derivationPath: string, askForDeviceConfirmation: boolean,
+ shouldGetChainCode: boolean) => Promise<LedgerGetAddressResult>;
+ signPersonalMessage_async: (derivationPath: string, messageHex: string) => Promise<ECSignature>;
+ signTransaction_async: (derivationPath: string, txHex: string) => Promise<ECSignatureString>;
+ comm: LedgerCommunicationClient;
+}
+
+export interface ECSignatureString {
+ v: string;
+ r: string;
+ s: string;
+}
+
+export interface ECSignature {
+ v: number;
+ r: string;
+ s: string;
+}
+
+export type LedgerEthereumClientFactoryAsync = () => Promise<LedgerEthereumClient>;
+
+/*
+ * networkId: The ethereum networkId to set as the chainId from EIP155
+ * ledgerConnectionType: Environment in which you wish to connect to Ledger (nodejs or browser)
+ * derivationPath: Initial derivation path to use e.g 44'/60'/0'
+ * accountFetchingConfigs: configs related to fetching accounts from a Ledger
+ */
+export interface LedgerSubproviderConfigs {
+ networkId: number;
+ ledgerEthereumClientFactoryAsync: LedgerEthereumClientFactoryAsync;
+ derivationPath?: string;
+ accountFetchingConfigs?: AccountFetchingConfigs;
+}
+
+/*
+ * numAddressesToReturn: Number of addresses to return from 'eth_accounts' call
+ * shouldAskForOnDeviceConfirmation: Whether you wish to prompt the user on their Ledger
+ * before fetching their addresses
+ */
+export interface AccountFetchingConfigs {
+ numAddressesToReturn?: number;
+ shouldAskForOnDeviceConfirmation?: boolean;
+}
+
+export interface SignatureData {
+ hash: string;
+ r: string;
+ s: string;
+ v: number;
+}
+
+export interface LedgerGetAddressResult {
+ address: string;
+}
+
+export interface LedgerWalletSubprovider {
+ getPath: () => string;
+ setPath: (path: string) => void;
+ setPathIndex: (pathIndex: number) => void;
+}
+
+export interface SignPersonalMessageParams {
+ data: string;
+}
+
+export interface PartialTxParams {
+ nonce: string;
+ gasPrice?: string;
+ gas: string;
+ to: string;
+ from?: string;
+ value?: string;
+ data?: string;
+ chainId: number; // EIP 155 chainId - mainnet: 1, ropsten: 3
+}
+
+export type DoneCallback = (err?: Error) => void;
+
+export interface JSONRPCPayload {
+ params: any[];
+ method: string;
+}
+
+export interface LedgerCommunication {
+ close_async: () => Promise<void>;
+}
+
+export interface ResponseWithTxParams {
+ raw: string;
+ tx: PartialTxParams;
+}
+
+export enum LedgerSubproviderErrors {
+ TooOldLedgerFirmware = 'TOO_OLD_LEDGER_FIRMWARE',
+ FromAddressMissingOrInvalid = 'FROM_ADDRESS_MISSING_OR_INVALID',
+ DataMissingForSignPersonalMessage = 'DATA_MISSING_FOR_SIGN_PERSONAL_MESSAGE',
+ DataNotValidHexForSignPersonalMessage = 'DATA_NOT_VALID_HEX_FOR_SIGN_PERSONAL_MESSAGE',
+ SenderInvalidOrNotSupplied = 'SENDER_INVALID_OR_NOT_SUPPLIED',
+ MultipleOpenConnectionsDisallowed = 'MULTIPLE_OPEN_CONNECTIONS_DISALLOWED',
+}
diff --git a/packages/subproviders/test/chai_setup.ts b/packages/subproviders/test/chai_setup.ts
new file mode 100644
index 000000000..a281bab6c
--- /dev/null
+++ b/packages/subproviders/test/chai_setup.ts
@@ -0,0 +1,11 @@
+import * as chai from 'chai';
+import chaiAsPromised = require('chai-as-promised');
+import * as dirtyChai from 'dirty-chai';
+
+export const chaiSetup = {
+ configure() {
+ chai.config.includeStack = true;
+ chai.use(dirtyChai);
+ chai.use(chaiAsPromised);
+ },
+};
diff --git a/packages/subproviders/test/integration/ledger_subprovider_test.ts b/packages/subproviders/test/integration/ledger_subprovider_test.ts
new file mode 100644
index 000000000..be5393ea5
--- /dev/null
+++ b/packages/subproviders/test/integration/ledger_subprovider_test.ts
@@ -0,0 +1,171 @@
+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';
+
+const expect = chai.expect;
+
+const TEST_RPC_ACCOUNT_0 = '0x5409ed021d9299bf6814279a6a1411a7e866a631';
+
+describe('LedgerSubprovider', () => {
+ let ledgerSubprovider: LedgerSubprovider;
+ const networkId: number = 42;
+ before(async () => {
+ ledgerSubprovider = new LedgerSubprovider({
+ networkId,
+ ledgerEthereumClientFactoryAsync: ledgerEthereumNodeJsClientFactoryAsync,
+ });
+ });
+ describe('direct method calls', () => {
+ it('returns a list of accounts', async () => {
+ const accounts = await ledgerSubprovider.getAccountsAsync();
+ expect(accounts[0]).to.not.be.an('undefined');
+ expect(accounts.length).to.be.equal(10);
+ });
+ it('signs a personal message', async () => {
+ const data = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
+ const ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync({data});
+ expect(ecSignatureHex.length).to.be.equal(132);
+ expect(ecSignatureHex.substr(0, 2)).to.be.equal('0x');
+ });
+ it('signs a transaction', async () => {
+ const tx = {
+ nonce: '0x00',
+ gas: '0x2710',
+ to: '0x0000000000000000000000000000000000000000',
+ value: '0x00',
+ chainId: 3,
+ };
+ const txHex = await ledgerSubprovider.signTransactionAsync(tx);
+ // tslint:disable-next-line:max-line-length
+ expect(txHex).to.be.equal('0xf85f8080822710940000000000000000000000000000000000000000808077a088a95ef1378487bc82be558e82c8478baf840c545d5b887536bb1da63673a98ba0019f4a4b9a107d1e6752bf7f701e275f28c13791d6e76af895b07373462cefaa');
+ });
+ });
+ describe('calls through a provider', () => {
+ let defaultProvider: Web3ProviderEngine;
+ let ledgerProvider: Web3ProviderEngine;
+ before(() => {
+ ledgerProvider = new Web3ProviderEngine();
+ ledgerProvider.addProvider(ledgerSubprovider);
+ const httpProvider = new RpcSubprovider({
+ rpcUrl: 'http://localhost:8545',
+ });
+ ledgerProvider.addProvider(httpProvider);
+ ledgerProvider.start();
+
+ defaultProvider = new Web3ProviderEngine();
+ defaultProvider.addProvider(httpProvider);
+ defaultProvider.start();
+ });
+ it('returns a list of accounts', (done: DoneCallback) => {
+ const payload = {
+ jsonrpc: '2.0',
+ method: 'eth_accounts',
+ params: [],
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
+ expect(err).to.be.a('null');
+ expect(response.result.length).to.be.equal(10);
+ done();
+ });
+ ledgerProvider.sendAsync(payload, callback);
+ });
+ it('signs a personal message', (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: 'personal_sign',
+ params: [messageHex, signer],
+ 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 transaction', (done: DoneCallback) => {
+ const tx = {
+ to: '0xafa3f8684e54059998bc3a7b0d2b0da075154d66',
+ value: '0x00',
+ };
+ const payload = {
+ jsonrpc: '2.0',
+ method: 'eth_signTransaction',
+ params: [tx],
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
+ expect(err).to.be.a('null');
+ expect(response.result.raw.length).to.be.equal(206);
+ expect(response.result.raw.substr(0, 2)).to.be.equal('0x');
+ done();
+ });
+ ledgerProvider.sendAsync(payload, callback);
+ });
+ it('signs and sends a transaction', (done: DoneCallback) => {
+ (async () => {
+ const accounts = await ledgerSubprovider.getAccountsAsync();
+
+ // Give first account on Ledger sufficient ETH to complete tx send
+ let tx = {
+ to: accounts[0],
+ from: TEST_RPC_ACCOUNT_0,
+ value: '0x8ac7230489e80000', // 10 ETH
+ };
+ let payload = {
+ jsonrpc: '2.0',
+ method: 'eth_sendTransaction',
+ params: [tx],
+ id: 1,
+ };
+ await promisify(defaultProvider.sendAsync, defaultProvider)(payload);
+
+ // Send transaction from Ledger
+ tx = {
+ to: '0xafa3f8684e54059998bc3a7b0d2b0da075154d66',
+ from: accounts[0],
+ value: '0xde0b6b3a7640000',
+ };
+ payload = {
+ jsonrpc: '2.0',
+ method: 'eth_sendTransaction',
+ params: [tx],
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
+ expect(err).to.be.a('null');
+ const result = response.result.result;
+ expect(result.length).to.be.equal(66);
+ expect(result.substr(0, 2)).to.be.equal('0x');
+ done();
+ });
+ ledgerProvider.sendAsync(payload, callback);
+ })().catch(done);
+ });
+ });
+});
diff --git a/packages/subproviders/test/unit/ledger_subprovider_test.ts b/packages/subproviders/test/unit/ledger_subprovider_test.ts
new file mode 100644
index 000000000..ad7e76efd
--- /dev/null
+++ b/packages/subproviders/test/unit/ledger_subprovider_test.ts
@@ -0,0 +1,229 @@
+import * as chai from 'chai';
+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,
+ LedgerSubprovider,
+} from '../../src';
+import {
+ DoneCallback,
+ ECSignatureString,
+ LedgerCommunicationClient,
+ LedgerGetAddressResult,
+ LedgerSubproviderErrors,
+} from '../../src/types';
+import { chaiSetup } from '../chai_setup';
+import {reportCallbackErrors} from '../utils/report_callback_errors';
+
+const expect = chai.expect;
+const FAKE_ADDRESS = '0x9901c66f2d4b95f7074b553da78084d708beca70';
+
+describe('LedgerSubprovider', () => {
+ const networkId: number = 42;
+ let ledgerSubprovider: LedgerSubprovider;
+ before(async () => {
+ const ledgerEthereumClientFactoryAsync = async () => {
+ const ledgerEthClient = {
+ getAddress_async: async () => {
+ return {
+ address: FAKE_ADDRESS,
+ };
+ },
+ signPersonalMessage_async: async () => {
+ const ecSignature: ECSignature = {
+ v: 28,
+ r: 'a6cc284bff14b42bdf5e9286730c152be91719d478605ec46b3bebcd0ae49148',
+ s: '0652a1a7b742ceb0213d1e744316e285f41f878d8af0b8e632cbca4c279132d0',
+ };
+ return ecSignature;
+ },
+ signTransaction_async: async (derivationPath: string, txHex: string) => {
+ const ecSignature: ECSignatureString = {
+ v: '77',
+ r: '88a95ef1378487bc82be558e82c8478baf840c545d5b887536bb1da63673a98b',
+ s: '019f4a4b9a107d1e6752bf7f701e275f28c13791d6e76af895b07373462cefaa',
+ };
+ return ecSignature;
+ },
+ comm: {
+ close_async: _.noop,
+ } as LedgerCommunicationClient,
+ };
+ return ledgerEthClient;
+ };
+ ledgerSubprovider = new LedgerSubprovider({
+ networkId,
+ ledgerEthereumClientFactoryAsync,
+ });
+ });
+ describe('direct method calls', () => {
+ describe('success cases', () => {
+ it('returns a list of accounts', async () => {
+ const accounts = await ledgerSubprovider.getAccountsAsync();
+ expect(accounts[0]).to.be.equal(FAKE_ADDRESS);
+ expect(accounts.length).to.be.equal(10);
+ });
+ it('signs a personal message', async () => {
+ const data = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
+ const msgParams = {data};
+ const ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync(msgParams);
+ // tslint:disable-next-line:max-line-length
+ 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'));
+ const msgParams = {data};
+ try {
+ const result = await Promise.all([
+ ledgerSubprovider.getAccountsAsync(),
+ ledgerSubprovider.signPersonalMessageAsync(msgParams),
+ ]);
+ throw new Error('Multiple simultaneous calls succeeded when they should have failed');
+ } catch (err) {
+ expect(err.message).to.be.equal(LedgerSubproviderErrors.MultipleOpenConnectionsDisallowed);
+ }
+ });
+ });
+ });
+ describe('calls through a provider', () => {
+ let provider: Web3ProviderEngine;
+ before(() => {
+ provider = new Web3ProviderEngine();
+ provider.addProvider(ledgerSubprovider);
+ const httpProvider = new RpcSubprovider({
+ rpcUrl: 'http://localhost:8545',
+ });
+ provider.addProvider(httpProvider);
+ provider.start();
+ });
+ describe('success cases', () => {
+ it('returns a list of accounts', (done: DoneCallback) => {
+ const payload = {
+ jsonrpc: '2.0',
+ method: 'eth_accounts',
+ params: [],
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
+ expect(err).to.be.a('null');
+ expect(response.result.length).to.be.equal(10);
+ expect(response.result[0]).to.be.equal(FAKE_ADDRESS);
+ done();
+ });
+ provider.sendAsync(payload, callback);
+ });
+ it('signs a personal message', (done: DoneCallback) => {
+ const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
+ const payload = {
+ jsonrpc: '2.0',
+ method: 'personal_sign',
+ params: [messageHex, '0x0000000000000000000000000000000000000000'],
+ id: 1,
+ };
+ 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');
+ done();
+ });
+ provider.sendAsync(payload, callback);
+ });
+ it('signs a transaction', (done: DoneCallback) => {
+ const tx = {
+ to: '0xafa3f8684e54059998bc3a7b0d2b0da075154d66',
+ value: '0x00',
+ };
+ const payload = {
+ jsonrpc: '2.0',
+ method: 'eth_signTransaction',
+ params: [tx],
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
+ expect(err).to.be.a('null');
+ expect(response.result.raw.length).to.be.equal(206);
+ expect(response.result.raw.substr(0, 2)).to.be.equal('0x');
+ done();
+ });
+ provider.sendAsync(payload, callback);
+ });
+ });
+ describe('failure cases', () => {
+ it('should throw if `from` param missing when calling personal_sign', (done: DoneCallback) => {
+ const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer('hello world'));
+ const payload = {
+ jsonrpc: '2.0',
+ method: 'personal_sign',
+ params: [messageHex], // Missing from param
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
+ expect(err).to.not.be.a('null');
+ expect(err.message).to.be.equal(LedgerSubproviderErrors.FromAddressMissingOrInvalid);
+ 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 = {
+ jsonrpc: '2.0',
+ method: 'personal_sign',
+ params: [nonHexMessage, '0x0000000000000000000000000000000000000000'],
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
+ expect(err).to.not.be.a('null');
+ expect(err.message).to.be.equal(LedgerSubproviderErrors.DataNotValidHexForSignPersonalMessage);
+ done();
+ });
+ provider.sendAsync(payload, callback);
+ });
+ it('should throw if `from` param missing when calling eth_sendTransaction', (done: DoneCallback) => {
+ const tx = {
+ to: '0xafa3f8684e54059998bc3a7b0d2b0da075154d66',
+ value: '0xde0b6b3a7640000',
+ };
+ const payload = {
+ jsonrpc: '2.0',
+ method: 'eth_sendTransaction',
+ params: [tx],
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
+ expect(err).to.not.be.a('null');
+ expect(err.message).to.be.equal(LedgerSubproviderErrors.SenderInvalidOrNotSupplied);
+ done();
+ });
+ provider.sendAsync(payload, callback);
+ });
+ it('should throw if `from` param invalid address when calling eth_sendTransaction',
+ (done: DoneCallback) => {
+ const tx = {
+ to: '0xafa3f8684e54059998bc3a7b0d2b0da075154d66',
+ from: '0xIncorrectEthereumAddress',
+ value: '0xde0b6b3a7640000',
+ };
+ const payload = {
+ jsonrpc: '2.0',
+ method: 'eth_sendTransaction',
+ params: [tx],
+ id: 1,
+ };
+ const callback = reportCallbackErrors(done)((err: Error, response: Web3.JSONRPCResponsePayload) => {
+ expect(err).to.not.be.a('null');
+ expect(err.message).to.be.equal(LedgerSubproviderErrors.SenderInvalidOrNotSupplied);
+ done();
+ });
+ provider.sendAsync(payload, callback);
+ });
+ });
+ });
+});
diff --git a/packages/subproviders/test/utils/report_callback_errors.ts b/packages/subproviders/test/utils/report_callback_errors.ts
new file mode 100644
index 000000000..8a8f4d966
--- /dev/null
+++ b/packages/subproviders/test/utils/report_callback_errors.ts
@@ -0,0 +1,14 @@
+import { DoneCallback } from '../../src/types';
+
+export const reportCallbackErrors = (done: DoneCallback) => {
+ return (f: (...args: any[]) => void) => {
+ const wrapped = async (...args: any[]) => {
+ try {
+ f(...args);
+ } catch (err) {
+ done(err);
+ }
+ };
+ return wrapped;
+ };
+};
diff --git a/packages/subproviders/tsconfig.json b/packages/subproviders/tsconfig.json
new file mode 100644
index 000000000..24adf4637
--- /dev/null
+++ b/packages/subproviders/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "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"
+ ]
+}
diff --git a/packages/subproviders/tslint.json b/packages/subproviders/tslint.json
new file mode 100644
index 000000000..a07795151
--- /dev/null
+++ b/packages/subproviders/tslint.json
@@ -0,0 +1,5 @@
+{
+ "extends": [
+ "@0xproject/tslint-config"
+ ]
+}
diff --git a/packages/subproviders/webpack.config.js b/packages/subproviders/webpack.config.js
new file mode 100644
index 000000000..a73489704
--- /dev/null
+++ b/packages/subproviders/webpack.config.js
@@ -0,0 +1,56 @@
+/**
+ * This is to generate the umd bundle only
+ */
+const _ = require('lodash');
+const webpack = require('webpack');
+const path = require('path');
+const production = process.env.NODE_ENV === 'production';
+
+let entry = {
+ 'index': './src/index.ts',
+};
+if (production) {
+ entry = _.assign({}, entry, {'index.min': './src/index.ts'});
+}
+
+module.exports = {
+ entry,
+ output: {
+ path: path.resolve(__dirname, '_bundles'),
+ filename: '[name].js',
+ libraryTarget: 'umd',
+ library: '0x Subproviders',
+ umdNamedDefine: true,
+ },
+ resolve: {
+ extensions: ['.ts', '.js', '.json'],
+ },
+ devtool: 'source-map',
+ plugins: [
+ new webpack.optimize.UglifyJsPlugin({
+ minimize: true,
+ sourceMap: true,
+ include: /\.min\.js$/,
+ }),
+ ],
+ module: {
+ rules: [
+ {
+ test: /\.ts$/,
+ use: [
+ {
+ loader: 'awesome-typescript-loader',
+ query: {
+ declaration: false,
+ },
+ },
+ ],
+ exclude: /node_modules/,
+ },
+ {
+ test: /\.json$/,
+ loader: 'json-loader',
+ },
+ ],
+ },
+};