diff options
author | Fabio Berger <me@fabioberger.com> | 2017-12-09 01:22:25 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-12-09 01:22:25 +0800 |
commit | 828dffffedf64e7ab5301284e2586082b6bc8f9c (patch) | |
tree | 6f429e6f2b3050de6dd7ce16e42c7f387bd5018a | |
parent | 5eea829be9f5e8669c40dac965231051b668ed37 (diff) | |
parent | af8d24d0eb5af781f4731b5559979f2b02579785 (diff) | |
download | dexon-0x-contracts-828dffffedf64e7ab5301284e2586082b6bc8f9c.tar.gz dexon-0x-contracts-828dffffedf64e7ab5301284e2586082b6bc8f9c.tar.zst dexon-0x-contracts-828dffffedf64e7ab5301284e2586082b6bc8f9c.zip |
Merge pull request #252 from 0xProject/feature/addSubproviders
Add Subproviders Subpackage
25 files changed, 1270 insertions, 233 deletions
diff --git a/package.json b/package.json index dcaf34eba..54a2148d2 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "lerna": "^2.5.1", "async-child-process": "^1.1.1", "semver-sort": "^0.0.4", - "publish-release": "0xproject/publish-release" + "publish-release": "0xproject/publish-release", + "ethereumjs-testrpc": "6.0.3" } } diff --git a/packages/0x.js/package.json b/packages/0x.js/package.json index 4468bae09..7b267fa8e 100644 --- a/packages/0x.js/package.json +++ b/packages/0x.js/package.json @@ -65,7 +65,6 @@ "copyfiles": "^1.2.0", "coveralls": "^3.0.0", "dirty-chai": "^2.0.1", - "ethereumjs-testrpc": "6.0.3", "json-loader": "^0.5.4", "mocha": "^4.0.1", "npm-run-all": "^4.1.2", diff --git a/packages/abi-gen/package.json b/packages/abi-gen/package.json index 0d61891e0..9ce64f818 100644 --- a/packages/abi-gen/package.json +++ b/packages/abi-gen/package.json @@ -22,7 +22,7 @@ }, "homepage": "https://github.com/0xProject/0x.js/packages/abi-gen/README.md", "dependencies": { - "bignumber.js": "^5.0.0", + "bignumber.js": "~4.1.0", "chalk": "^2.3.0", "glob": "^7.1.2", "handlebars": "^4.0.11", @@ -39,7 +39,7 @@ "@types/mkdirp": "^0.5.1", "@types/node": "^8.0.53", "@types/yargs": "^8.0.2", - "npm-run-all": "^4.1.1", + "npm-run-all": "^4.1.2", "shx": "^0.2.2", "tslint": "5.8.0", "typescript": "~2.6.1", diff --git a/packages/subproviders/README.md b/packages/subproviders/README.md new file mode 100644 index 000000000..5fa31611a --- /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 on-device 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..c3ecfd2f0 --- /dev/null +++ b/packages/subproviders/package.json @@ -0,0 +1,53 @@ +{ + "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" + } +} diff --git a/packages/subproviders/src/globals.d.ts b/packages/subproviders/src/globals.d.ts new file mode 100644 index 000000000..520ca9232 --- /dev/null +++ b/packages/subproviders/src/globals.d.ts @@ -0,0 +1,97 @@ +/// <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:completed-docs + +// Ethereumjs-tx declarations +declare module 'ethereumjs-tx' { + class EthereumTx { + public raw: Buffer[]; + public r: Buffer; + public s: Buffer; + public v: Buffer; + public serialize(): Buffer; + constructor(txParams: any); + } + export = EthereumTx; +} + +// Ledgerco declarations +interface ECSignatureString { + v: string; + r: string; + s: string; +} +interface ECSignature { + v: number; + r: string; + s: string; +} +declare module 'ledgerco' { + interface comm { + close_async(): Promise<void>; + } + export class comm_node implements comm { + public static create_async(timeoutMilliseconds?: number): Promise<comm_node>; + public close_async(): Promise<void>; + } + export class comm_u2f implements comm { + public static create_async(): Promise<comm_u2f>; + public close_async(): Promise<void>; + } + export class eth { + public comm: comm; + constructor(comm: comm); + public getAddress_async(path: string, display?: boolean, chaincode?: boolean): + Promise<{publicKey: string; address: string}>; + public signTransaction_async(path: string, rawTxHex: string): Promise<ECSignatureString>; + 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 { + constructor(permits: number); + public wait(): Promise<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; +} diff --git a/packages/subproviders/src/index.ts b/packages/subproviders/src/index.ts new file mode 100644 index 000000000..9b7cab3fa --- /dev/null +++ b/packages/subproviders/src/index.ts @@ -0,0 +1,38 @@ +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'; + +/** + * A factory method for creating a LedgerEthereumClient usable in a browser context. + * @return LedgerEthereumClient A browser client + */ +export async function ledgerEthereumBrowserClientFactoryAsync(): Promise<LedgerEthereumClient> { + const ledgerConnection = await LedgerBrowserCommunication.create_async(); + const ledgerEthClient = new LedgerEthereumClientFn(ledgerConnection); + return ledgerEthClient; +} + +/** + * A factory for creating a LedgerEthereumClient usable in a Node.js context. + * @return LedgerEthereumClient A Node.js client + */ +export async function ledgerEthereumNodeJsClientFactoryAsync(): Promise<LedgerEthereumClient> { + const ledgerConnection = await LedgerNodeCommunication.create_async(); + const ledgerEthClient = new LedgerEthereumClientFn(ledgerConnection); + return ledgerEthClient; +} diff --git a/packages/website/ts/subproviders/injected_web3_subprovider.ts b/packages/subproviders/src/subproviders/injected_web3.ts index 910fe3cdf..25d747a62 100644 --- a/packages/website/ts/subproviders/injected_web3_subprovider.ts +++ b/packages/subproviders/src/subproviders/injected_web3.ts @@ -1,6 +1,6 @@ import * as _ from 'lodash'; -import {constants} from 'ts/utils/constants'; import Web3 = require('web3'); +import Web3ProviderEngine = require('web3-provider-engine'); /* * This class implements the web3-provider-engine subprovider interface and forwards @@ -8,12 +8,14 @@ import Web3 = require('web3'); * web3 instance in their browser. * Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js */ -export class InjectedWeb3SubProvider { +export class InjectedWeb3Subprovider { private injectedWeb3: Web3; constructor(injectedWeb3: Web3) { this.injectedWeb3 = injectedWeb3; } - public handleRequest(payload: any, next: () => void, end: (err: Error, result: any) => void) { + public handleRequest( + payload: Web3.JSONRPCRequestPayload, next: () => void, end: (err: Error|null, result: any) => void, + ) { switch (payload.method) { case 'web3_clientVersion': this.injectedWeb3.version.getNode(end); @@ -39,7 +41,7 @@ export class InjectedWeb3SubProvider { } // Required to implement this method despite not needing it for this subprovider // tslint:disable-next-line:prefer-function-over-method - public setEngine(engine: any) { + 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..e0a08f792 --- /dev/null +++ b/packages/subproviders/src/subproviders/ledger.ts @@ -0,0 +1,306 @@ +import {assert} from '@0xproject/assert'; +import promisify = require('es6-promisify'); +import {isAddress} from 'ethereum-address'; +import EthereumTx = require('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, +} 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 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': + const data = payload.params[0]; + try { + if (_.isUndefined(data)) { + throw new Error(LedgerSubproviderErrors.DataMissingForSignPersonalMessage); + } + assert.isHexString('data', data); + const ecSignatureHex = await this.signPersonalMessageAsync(data); + end(null, ecSignatureHex); + } catch (err) { + end(err); + } + return; + + default: + next(); + return; + } + } + public async getAccountsAsync(): Promise<string[]> { + this._ledgerClientIfExists = await this.createLedgerClientAsync(); + + // 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; + } + } + 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(data: string): Promise<string> { + this._ledgerClientIfExists = await this.createLedgerClientAsync(); + try { + const derivationPath = this.getDerivationPath(); + const result = await this._ledgerClientIfExists.signPersonalMessage_async( + 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(); + 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<Web3.JSONRPCResponsePayload> { + 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/website/ts/subproviders/redundant_rpc_subprovider.ts b/packages/subproviders/src/subproviders/redundant_rpc.ts index d540e6e7b..80462bbfb 100644 --- a/packages/website/ts/subproviders/redundant_rpc_subprovider.ts +++ b/packages/subproviders/src/subproviders/redundant_rpc.ts @@ -1,15 +1,17 @@ import {promisify} from '@0xproject/utils'; import * as _ from 'lodash'; -import {JSONRPCPayload} from 'ts/types'; import RpcSubprovider = require('web3-provider-engine/subproviders/rpc'); -import Subprovider = require('web3-provider-engine/subproviders/subprovider'); + +import {JSONRPCPayload} from '../types'; + +import {Subprovider} from './subprovider'; export class RedundantRPCSubprovider extends Subprovider { private rpcs: RpcSubprovider[]; private static async firstSuccessAsync( rpcs: RpcSubprovider[], payload: JSONRPCPayload, next: () => void, ): Promise<any> { - let lastErr; + let lastErr: Error|undefined; for (const rpc of rpcs) { try { const data = await promisify(rpc.handleRequest.bind(rpc))(payload, next); @@ -19,7 +21,9 @@ export class RedundantRPCSubprovider extends Subprovider { continue; } } - throw Error(lastErr); + if (!_.isUndefined(lastErr)) { + throw lastErr; + } } constructor(endpoints: string[]) { super(); @@ -30,7 +34,7 @@ export class RedundantRPCSubprovider extends Subprovider { }); } public async handleRequest(payload: JSONRPCPayload, next: () => void, - end: (err?: Error, data?: any) => void): Promise<void> { + end: (err: Error|null, data?: any) => void): Promise<void> { const rpcsCopy = this.rpcs.slice(); try { const data = await RedundantRPCSubprovider.firstSuccessAsync(rpcsCopy, payload, next); diff --git a/packages/subproviders/src/subproviders/subprovider.ts b/packages/subproviders/src/subproviders/subprovider.ts new file mode 100644 index 000000000..64d97b958 --- /dev/null +++ b/packages/subproviders/src/subproviders/subprovider.ts @@ -0,0 +1,46 @@ +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; + // Ported from: https://github.com/MetaMask/provider-engine/blob/master/util/random-id.js + 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..38dc1e67e --- /dev/null +++ b/packages/subproviders/src/types.ts @@ -0,0 +1,108 @@ +import * as _ from 'lodash'; +import * as Web3 from 'web3'; + +export interface LedgerCommunicationClient { + 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 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', + 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..75f6d47fe --- /dev/null +++ b/packages/subproviders/test/integration/ledger_subprovider_test.ts @@ -0,0 +1,172 @@ +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'; + +chaiSetup.configure(); +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..964df5db9 --- /dev/null +++ b/packages/subproviders/test/unit/ledger_subprovider_test.ts @@ -0,0 +1,212 @@ +import * as chai from 'chai'; +import * as ethUtils from 'ethereumjs-util'; +import * as _ from 'lodash'; +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'; + +chaiSetup.configure(); +const expect = chai.expect; +const FAKE_ADDRESS = '0x9901c66f2d4b95f7074b553da78084d708beca70'; + +describe('LedgerSubprovider', () => { + const networkId: number = 42; + let ledgerSubprovider: LedgerSubprovider; + before(async () => { + const ledgerEthereumClientFactoryAsync = async () => { + // tslint:disable:no-object-literal-type-assertion + const ledgerEthClient = { + getAddress_async: async () => { + return { + address: FAKE_ADDRESS, + }; + }, + signPersonalMessage_async: async () => { + const ecSignature = { + v: 28, + r: 'a6cc284bff14b42bdf5e9286730c152be91719d478605ec46b3bebcd0ae49148', + s: '0652a1a7b742ceb0213d1e744316e285f41f878d8af0b8e632cbca4c279132d0', + }; + return ecSignature; + }, + signTransaction_async: async (derivationPath: string, txHex: string) => { + const ecSignature = { + v: '77', + r: '88a95ef1378487bc82be558e82c8478baf840c545d5b887536bb1da63673a98b', + s: '019f4a4b9a107d1e6752bf7f701e275f28c13791d6e76af895b07373462cefaa', + }; + return ecSignature; + }, + comm: { + close_async: _.noop, + } as LedgerCommunicationClient, + }; + // tslint:enable:no-object-literal-type-assertion + 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 ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync(data); + // 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')); + return expect(Promise.all([ + ledgerSubprovider.getAccountsAsync(), + ledgerSubprovider.signPersonalMessageAsync(data), + ])).to.be.rejectedWith(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', + gasPrice: '0x00', + nonce: '0x00', + gas: '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(192); + expect(response.result.raw.substr(0, 2)).to.be.equal('0x'); + done(); + }); + provider.sendAsync(payload, callback); + }); + }); + describe('failure cases', () => { + 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('Expected data to be of type HexString, encountered: hello world'); + 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/unit/redundant_rpc_subprovider_test.ts b/packages/subproviders/test/unit/redundant_rpc_subprovider_test.ts new file mode 100644 index 000000000..edeb1d5a2 --- /dev/null +++ b/packages/subproviders/test/unit/redundant_rpc_subprovider_test.ts @@ -0,0 +1,62 @@ +import * as chai from 'chai'; +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'; + +const expect = chai.expect; + +describe('RedundantRpcSubprovider', () => { + let provider: Web3ProviderEngine; + it('succeeds when supplied a healthy endpoint', (done: DoneCallback) => { + provider = new Web3ProviderEngine(); + const endpoints = [ + 'http://localhost:8545', + ]; + const redundantSubprovider = new RedundantRPCSubprovider(endpoints); + provider.addProvider(redundantSubprovider); + provider.start(); + + 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(); + }); + provider.sendAsync(payload, callback); + }); + 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 redundantSubprovider = new RedundantRPCSubprovider(endpoints); + provider.addProvider(redundantSubprovider); + provider.start(); + + 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(); + }); + 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/website/package.json b/packages/website/package.json index fdc5de5d7..9234ad601 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -18,6 +18,7 @@ "author": "Fabio Berger", "license": "Apache-2.0", "dependencies": { + "@0xproject/subproviders": "0.0.1", "0x.js": "0xproject/0x.js/packages/0x.js#0x.js@0.27.1", "accounting": "^0.4.1", "basscss": "^8.0.3", @@ -61,7 +62,6 @@ "thenby": "^1.2.3", "truffle-contract": "2.0.1", "tslint-config-0xproject": "^0.0.2", - "typescript": "^2.4.1", "web3": "^0.20.0", "web3-provider-engine": "^13.0.1", "whatwg-fetch": "^2.0.3", diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index 24205802d..76640a072 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -16,6 +16,13 @@ import { ZeroEx, ZeroExError, } from '0x.js'; +import { + InjectedWeb3Subprovider, + ledgerEthereumBrowserClientFactoryAsync, + LedgerSubprovider, + LedgerWalletSubprovider, + RedundantRPCSubprovider, +} from '@0xproject/subproviders'; import {promisify} from '@0xproject/utils'; import BigNumber from 'bignumber.js'; import compareVersions = require('compare-versions'); @@ -25,20 +32,16 @@ import * as _ from 'lodash'; import * as React from 'react'; import contract = require('truffle-contract'); import {TokenSendCompleted} from 'ts/components/flash_messages/token_send_completed'; -import { TransactionSubmitted } from 'ts/components/flash_messages/transaction_submitted'; +import {TransactionSubmitted} from 'ts/components/flash_messages/transaction_submitted'; import {trackedTokenStorage} from 'ts/local_storage/tracked_token_storage'; import {tradeHistoryStorage} from 'ts/local_storage/trade_history_storage'; import {Dispatcher} from 'ts/redux/dispatcher'; -import {InjectedWeb3SubProvider} from 'ts/subproviders/injected_web3_subprovider'; -import {ledgerWalletSubproviderFactory} from 'ts/subproviders/ledger_wallet_subprovider_factory'; -import {RedundantRPCSubprovider} from 'ts/subproviders/redundant_rpc_subprovider'; import { BlockchainCallErrs, BlockchainErrs, ContractInstance, ContractResponse, EtherscanLinkSuffixes, - LedgerWalletSubprovider, ProviderType, Side, SignatureData, @@ -71,7 +74,7 @@ export class Blockchain { private tokenRegistry: ContractInstance; private userAddress: string; private cachedProvider: Web3.Provider; - private ledgerSubProvider: LedgerWalletSubprovider; + private ledgerSubprovider: LedgerWalletSubprovider; private zrxPollIntervalId: number; private static async onPageLoadAsync() { if (document.readyState === 'complete') { @@ -105,7 +108,7 @@ export class Blockchain { // We catch all requests involving a users account and send it to the injectedWeb3 // instance. All other requests go to the public hosted node. provider = new ProviderEngine(); - provider.addProvider(new InjectedWeb3SubProvider(injectedWeb3)); + provider.addProvider(new InjectedWeb3Subprovider(injectedWeb3)); provider.addProvider(new FilterSubprovider()); provider.addProvider(new RedundantRPCSubprovider( publicNodeUrlsIfExistsForNetworkId, @@ -168,23 +171,23 @@ export class Blockchain { return !_.isUndefined(tokenIfExists); } public getLedgerDerivationPathIfExists(): string { - if (_.isUndefined(this.ledgerSubProvider)) { + if (_.isUndefined(this.ledgerSubprovider)) { return undefined; } - const path = this.ledgerSubProvider.getPath(); + const path = this.ledgerSubprovider.getPath(); return path; } public updateLedgerDerivationPathIfExists(path: string) { - if (_.isUndefined(this.ledgerSubProvider)) { + if (_.isUndefined(this.ledgerSubprovider)) { return; // noop } - this.ledgerSubProvider.setPath(path); + this.ledgerSubprovider.setPath(path); } public updateLedgerDerivationIndex(pathIndex: number) { - if (_.isUndefined(this.ledgerSubProvider)) { + if (_.isUndefined(this.ledgerSubprovider)) { return; // noop } - this.ledgerSubProvider.setPathIndex(pathIndex); + this.ledgerSubprovider.setPathIndex(pathIndex); } public async providerTypeUpdatedFireAndForgetAsync(providerType: ProviderType) { utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.'); @@ -204,8 +207,12 @@ export class Blockchain { this.dispatcher.updateUserAddress(''); // Clear old userAddress provider = new ProviderEngine(); - this.ledgerSubProvider = ledgerWalletSubproviderFactory(this.getBlockchainNetworkId.bind(this)); - provider.addProvider(this.ledgerSubProvider); + const ledgerWalletConfigs = { + networkId: this.networkId, + ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync, + }; + this.ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs); + provider.addProvider(this.ledgerSubprovider); provider.addProvider(new FilterSubprovider()); const networkId = configs.isMainnetEnabled ? constants.MAINNET_NETWORK_ID : @@ -231,7 +238,7 @@ export class Blockchain { this.web3Wrapper = new Web3Wrapper(this.dispatcher, provider, this.networkId, shouldPollUserAddress); this.zeroEx.setProvider(provider, this.networkId); await this.postInstantiationOrUpdatingProviderZeroExAsync(); - delete this.ledgerSubProvider; + delete this.ledgerSubprovider; delete this.cachedProvider; break; } @@ -657,11 +664,6 @@ export class Blockchain { constants.PUBLIC_PROVIDER_NAME; this.dispatcher.updateInjectedProviderName(providerName); } - // This is only ever called by the LedgerWallet subprovider in order to retrieve - // the current networkId without this value going stale. - private getBlockchainNetworkId() { - return this.networkId; - } private async fetchTokenInformationAsync() { utils.assert(!_.isUndefined(this.networkId), 'Cannot call fetchTokenInformationAsync if disconnected from Ethereum node'); diff --git a/packages/website/ts/globals.d.ts b/packages/website/ts/globals.d.ts index 7bbbb3a98..b4611f583 100644 --- a/packages/website/ts/globals.d.ts +++ b/packages/website/ts/globals.d.ts @@ -3,7 +3,6 @@ declare module 'react-router-hash-link'; declare module 'truffle-contract'; declare module 'ethereumjs-util'; declare module 'keccak'; -declare module 'web3-provider-engine'; declare module 'whatwg-fetch'; declare module 'react-html5video'; declare module 'web3-provider-engine/subproviders/filters'; @@ -21,6 +20,8 @@ declare module '*.json' { /* tslint:enable */ } +// tslint:disable:max-classes-per-file + // find-version declarations declare function findVersions(version: string): string[]; declare module 'find-versions' { @@ -131,21 +132,26 @@ declare class Subprovider {} declare module 'web3-provider-engine/subproviders/subprovider' { export = Subprovider; } - -// tslint:disable-next-line:max-classes-per-file -declare class RpcSubprovider { - constructor(options: {rpcUrl: string}); - public handleRequest(payload: any, next: any, end: (err?: Error, data?: any) => void): void; -} 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; } -// tslint:disable-next-line:max-classes-per-file -declare class HookedWalletSubprovider { - constructor(wallet: any); -} -declare module 'web3-provider-engine/subproviders/hooked-wallet' { - export = HookedWalletSubprovider; +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; } declare interface Artifact { diff --git a/packages/website/ts/subproviders/ledger_wallet_subprovider_factory.ts b/packages/website/ts/subproviders/ledger_wallet_subprovider_factory.ts deleted file mode 100644 index bfabc90ae..000000000 --- a/packages/website/ts/subproviders/ledger_wallet_subprovider_factory.ts +++ /dev/null @@ -1,172 +0,0 @@ -import * as EthereumTx from 'ethereumjs-tx'; -import ethUtil = require('ethereumjs-util'); -import * as ledger from 'ledgerco'; -import * as _ from 'lodash'; -import {LedgerEthConnection, SignPersonalMessageParams, TxParams} from 'ts/types'; -import {constants} from 'ts/utils/constants'; -import Web3 = require('web3'); -import HookedWalletSubprovider = require('web3-provider-engine/subproviders/hooked-wallet'); - -const NUM_ADDRESSES_TO_FETCH = 10; -const ASK_FOR_ON_DEVICE_CONFIRMATION = false; -const SHOULD_GET_CHAIN_CODE = false; - -export class LedgerWallet { - public isU2FSupported: boolean; - public getAccounts: (callback: (err: Error, accounts: string[]) => void) => void; - public signMessage: (msgParams: SignPersonalMessageParams, - callback: (err: Error, result?: string) => void) => void; - public signTransaction: (txParams: TxParams, - callback: (err: Error, result?: string) => void) => void; - private getNetworkId: () => number; - private path: string; - private pathIndex: number; - private ledgerEthConnection: LedgerEthConnection; - private accounts: string[]; - constructor(getNetworkIdFn: () => number) { - this.path = constants.DEFAULT_DERIVATION_PATH; - this.pathIndex = 0; - this.isU2FSupported = false; - this.getNetworkId = getNetworkIdFn; - this.getAccounts = this.getAccountsAsync.bind(this); - this.signMessage = this.signPersonalMessageAsync.bind(this); - this.signTransaction = this.signTransactionAsync.bind(this); - } - public getPath(): string { - return this.path; - } - public setPath(derivationPath: string) { - this.path = derivationPath; - // HACK: Must re-assign getAccounts, signMessage and signTransaction since they were - // previously bound to old values of this.path - this.getAccounts = this.getAccountsAsync.bind(this); - this.signMessage = this.signPersonalMessageAsync.bind(this); - this.signTransaction = this.signTransactionAsync.bind(this); - } - public setPathIndex(pathIndex: number) { - this.pathIndex = pathIndex; - // HACK: Must re-assign signMessage & signTransaction since they it was previously bound to - // old values of this.path - this.signMessage = this.signPersonalMessageAsync.bind(this); - this.signTransaction = this.signTransactionAsync.bind(this); - } - public async getAccountsAsync(callback: (err: Error, accounts: string[]) => void) { - if (!_.isUndefined(this.ledgerEthConnection)) { - callback(null, []); - return; - } - this.ledgerEthConnection = await this.createLedgerConnectionAsync(); - - const accounts = []; - for (let i = 0; i < NUM_ADDRESSES_TO_FETCH; i++) { - try { - const derivationPath = `${this.path}/${i}`; - const result = await this.ledgerEthConnection.getAddress_async( - derivationPath, ASK_FOR_ON_DEVICE_CONFIRMATION, SHOULD_GET_CHAIN_CODE, - ); - accounts.push(result.address.toLowerCase()); - } catch (err) { - await this.closeLedgerConnectionAsync(); - callback(err, null); - return; - } - } - - await this.closeLedgerConnectionAsync(); - callback(null, accounts); - } - public async signTransactionAsync(txParams: TxParams, callback: (err: Error, result?: string) => void) { - const tx = new EthereumTx(txParams); - - const networkId = this.getNetworkId(); - const chainId = networkId; // Same thing - - // Set the EIP155 bits - tx.raw[6] = Buffer.from([chainId]); // v - tx.raw[7] = Buffer.from([]); // r - tx.raw[8] = Buffer.from([]); // s - - const txHex = tx.serialize().toString('hex'); - - this.ledgerEthConnection = await this.createLedgerConnectionAsync(); - - try { - const derivationPath = this.getDerivationPath(); - const result = await this.ledgerEthConnection.signTransaction_async(derivationPath, txHex); - - // Store signature in transaction - tx.v = new Buffer(result.v, 'hex'); - tx.r = new Buffer(result.r, 'hex'); - tx.s = new Buffer(result.s, 'hex'); - - // EIP155: v should be chain_id * 2 + {35, 36} - const signedChainId = Math.floor((tx.v[0] - 35) / 2); - if (signedChainId !== chainId) { - const err = new Error('TOO_OLD_LEDGER_FIRMWARE'); - callback(err, null); - return; - } - - const signedTxHex = `0x${tx.serialize().toString('hex')}`; - await this.closeLedgerConnectionAsync(); - callback(null, signedTxHex); - } catch (err) { - await this.closeLedgerConnectionAsync(); - callback(err, null); - } - } - public async signPersonalMessageAsync(msgParams: SignPersonalMessageParams, - callback: (err: Error, result?: string) => void) { - if (!_.isUndefined(this.ledgerEthConnection)) { - callback(new Error('Another request is in progress.')); - return; - } - this.ledgerEthConnection = await this.createLedgerConnectionAsync(); - - try { - const derivationPath = this.getDerivationPath(); - const result = await this.ledgerEthConnection.signPersonalMessage_async( - derivationPath, ethUtil.stripHexPrefix(msgParams.data), - ); - const v = _.parseInt(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.closeLedgerConnectionAsync(); - callback(null, signature); - } catch (err) { - await this.closeLedgerConnectionAsync(); - callback(err, null); - } - } - private async createLedgerConnectionAsync() { - if (!_.isUndefined(this.ledgerEthConnection)) { - throw new Error('Multiple open connections to the Ledger disallowed.'); - } - const ledgerConnection = await ledger.comm_u2f.create_async(); - const ledgerEthConnection = new ledger.eth(ledgerConnection); - return ledgerEthConnection; - } - private async closeLedgerConnectionAsync() { - if (_.isUndefined(this.ledgerEthConnection)) { - return; - } - await this.ledgerEthConnection.comm.close_async(); - this.ledgerEthConnection = undefined; - } - private getDerivationPath() { - const derivationPath = `${this.path}/${this.pathIndex}`; - return derivationPath; - } -} - -export const ledgerWalletSubproviderFactory = (getNetworkIdFn: () => number): LedgerWallet => { - const ledgerWallet = new LedgerWallet(getNetworkIdFn); - const ledgerWalletSubprovider = new HookedWalletSubprovider(ledgerWallet) as LedgerWallet; - ledgerWalletSubprovider.getPath = ledgerWallet.getPath.bind(ledgerWallet); - ledgerWalletSubprovider.setPath = ledgerWallet.setPath.bind(ledgerWallet); - ledgerWalletSubprovider.setPathIndex = ledgerWallet.setPathIndex.bind(ledgerWallet); - return ledgerWalletSubprovider; -}; diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index d2c690ce1..d225e7784 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -521,12 +521,6 @@ export interface SignPersonalMessageParams { data: string; } -export interface LedgerWalletSubprovider { - getPath: () => string; - setPath: (path: string) => void; - setPathIndex: (pathIndex: number) => void; -} - export interface TxParams { nonce: string; gasPrice?: number; @@ -2183,10 +2183,14 @@ conventional-recommended-bump@^1.0.1: meow "^3.3.0" object-assign "^4.0.1" -convert-source-map@^1.1.0, convert-source-map@^1.3.0, convert-source-map@^1.5.0: +convert-source-map@^1.1.0, convert-source-map@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" +convert-source-map@^1.3.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -4737,7 +4741,7 @@ lcov-parse@^0.0.10: ledgerco@0xProject/ledger-node-js-api: version "1.1.3" - resolved "https://codeload.github.com/0xProject/ledger-node-js-api/tar.gz/dc2024bac997bf023f12203f118d10ba84d15ded" + resolved "https://codeload.github.com/0xProject/ledger-node-js-api/tar.gz/24aed21b8b362f2afc86faa578f38955ae2319ba" dependencies: async "2.1.4" node-hid "0.5.4" @@ -5443,11 +5447,11 @@ mute-stream@0.0.7, mute-stream@~0.0.4: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -nan@^2.0.5, nan@^2.0.8, nan@^2.2.1, nan@^2.3.0, nan@^2.3.3: +nan@^2.0.5, nan@^2.2.1, nan@^2.3.0, nan@^2.3.3: version "2.7.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" -nan@^2.4.0: +nan@^2.0.8, nan@^2.4.0: version "2.8.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" @@ -7298,6 +7302,10 @@ selfsigned@^1.9.1: dependencies: node-forge "0.6.33" +semaphore-async-await@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/semaphore-async-await/-/semaphore-async-await-1.5.1.tgz#857bef5e3644601ca4b9570b87e9df5ca12974fa" + semaphore@>=1.0.1, semaphore@^1.0.3: version "1.1.0" resolved "https://registry.yarnpkg.com/semaphore/-/semaphore-1.1.0.tgz#aaad8b86b20fe8e9b32b16dc2ee682a8cd26a8aa" @@ -8385,8 +8393,16 @@ types-bn@^0.0.1: bn.js "4.11.7" types-ethereumjs-util@0xProject/types-ethereumjs-util: - version "0.0.5" - resolved "https://codeload.github.com/0xProject/types-ethereumjs-util/tar.gz/b9ae55d2c2711d89f63f7fc53a78579f2d4fbd74" + version "0.0.6" + resolved "https://codeload.github.com/0xProject/types-ethereumjs-util/tar.gz/a3b236df39d9fbfcb3b832a1fea7110649eeb616" + dependencies: + bn.js "^4.11.7" + buffer "^5.0.6" + rlp "^2.0.0" + +types-ethereumjs-util@0xproject/types-ethereumjs-util: + version "0.0.6" + resolved "https://codeload.github.com/0xproject/types-ethereumjs-util/tar.gz/a3b236df39d9fbfcb3b832a1fea7110649eeb616" dependencies: bn.js "^4.11.7" buffer "^5.0.6" @@ -8396,7 +8412,7 @@ typescript@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc" -typescript@^2.4.1, typescript@~2.6.1: +typescript@~2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.1.tgz#ef39cdea27abac0b500242d6726ab90e0c846631" |