diff options
Diffstat (limited to 'packages/subproviders')
-rw-r--r-- | packages/subproviders/CHANGELOG.md | 13 | ||||
-rw-r--r-- | packages/subproviders/README.md | 87 | ||||
-rw-r--r-- | packages/subproviders/package.json | 104 | ||||
-rw-r--r-- | packages/subproviders/scripts/postpublish.js | 14 | ||||
-rw-r--r-- | packages/subproviders/src/globals.d.ts | 55 | ||||
-rw-r--r-- | packages/subproviders/src/index.ts | 16 | ||||
-rw-r--r-- | packages/subproviders/src/subproviders/injected_web3.ts | 16 | ||||
-rw-r--r-- | packages/subproviders/src/subproviders/ledger.ts | 129 | ||||
-rw-r--r-- | packages/subproviders/src/subproviders/redundant_rpc.ts | 31 | ||||
-rw-r--r-- | packages/subproviders/src/subproviders/subprovider.ts | 22 | ||||
-rw-r--r-- | packages/subproviders/src/types.ts | 12 | ||||
-rw-r--r-- | packages/subproviders/test/integration/ledger_subprovider_test.ts | 44 | ||||
-rw-r--r-- | packages/subproviders/test/unit/ledger_subprovider_test.ts | 84 | ||||
-rw-r--r-- | packages/subproviders/test/unit/redundant_rpc_subprovider_test.ts | 20 | ||||
-rw-r--r-- | packages/subproviders/tsconfig.json | 33 | ||||
-rw-r--r-- | packages/subproviders/tslint.json | 4 |
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"] } |