diff --git a/packages/subproviders/src/index.ts b/packages/subproviders/src/index.ts index 0dc0532868..d88792fd02 100644 --- a/packages/subproviders/src/index.ts +++ b/packages/subproviders/src/index.ts @@ -24,7 +24,7 @@ export { /** * A factory method for creating a LedgerEthereumClient usable in a browser context. - * @return LedgerEthereumClient A browser client + * @return LedgerEthereumClient A browser client for the LedgerSubprovider */ export async function ledgerEthereumBrowserClientFactoryAsync(): Promise { const ledgerConnection = await TransportU2F.create(); diff --git a/packages/subproviders/src/subproviders/empty_wallet_subprovider.ts b/packages/subproviders/src/subproviders/empty_wallet_subprovider.ts index d5d03a4ac1..38972b6cff 100644 --- a/packages/subproviders/src/subproviders/empty_wallet_subprovider.ts +++ b/packages/subproviders/src/subproviders/empty_wallet_subprovider.ts @@ -4,15 +4,14 @@ import { Callback, ErrorCallback } from '../types'; import { Subprovider } from './subprovider'; -/* - * This class implements the web3-provider-engine subprovider interface and returns - * that the provider has no addresses when queried. - * Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js +/** + * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. + * It intercepts the `eth_accounts` JSON RPC requests and never returns any addresses when queried. */ export class EmptyWalletSubprovider extends Subprovider { // This method needs to be here to satisfy the interface but linter wants it to be static. - // tslint:disable-next-line:prefer-function-over-method - public handleRequest(payload: Web3.JSONRPCRequestPayload, next: Callback, end: ErrorCallback) { + // tslint:disable-next-line:prefer-function-over-method underscore-private-and-protected + private handleRequest(payload: Web3.JSONRPCRequestPayload, next: Callback, end: ErrorCallback) { switch (payload.method) { case 'eth_accounts': end(null, []); diff --git a/packages/subproviders/src/subproviders/fake_gas_estimate_subprovider.ts b/packages/subproviders/src/subproviders/fake_gas_estimate_subprovider.ts index faec2d9e5f..8241e58f28 100644 --- a/packages/subproviders/src/subproviders/fake_gas_estimate_subprovider.ts +++ b/packages/subproviders/src/subproviders/fake_gas_estimate_subprovider.ts @@ -4,23 +4,28 @@ import { Callback, ErrorCallback } from '../types'; import { Subprovider } from './subprovider'; -/* - * This class implements the web3-provider-engine subprovider interface and returns - * the constant gas estimate when queried. - * HACK: We need this so that our tests don't use testrpc gas estimation which sometimes kills the node. - * Source: https://github.com/trufflesuite/ganache-cli/issues/417 - * Source: https://github.com/trufflesuite/ganache-cli/issues/437 - * Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js +// HACK: We need this so that our tests don't use testrpc gas estimation which sometimes kills the node. +// Source: https://github.com/trufflesuite/ganache-cli/issues/417 +// Source: https://github.com/trufflesuite/ganache-cli/issues/437 +// Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js + +/** + * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. + * It intercepts the `eth_estimateGas` JSON RPC call and always returns a constant gas amount when queried. */ export class FakeGasEstimateSubprovider extends Subprovider { private _constantGasAmount: number; + /** + * Instantiates an instance of the FakeGasEstimateSubprovider + * @param constantGasAmount The constant gas amount you want returned + */ constructor(constantGasAmount: number) { super(); this._constantGasAmount = constantGasAmount; } // This method needs to be here to satisfy the interface but linter wants it to be static. - // tslint:disable-next-line:prefer-function-over-method - public handleRequest(payload: Web3.JSONRPCRequestPayload, next: Callback, end: ErrorCallback) { + // tslint:disable-next-line:prefer-function-over-method underscore-private-and-protected + private handleRequest(payload: Web3.JSONRPCRequestPayload, next: Callback, end: ErrorCallback) { switch (payload.method) { case 'eth_estimateGas': end(null, this._constantGasAmount); diff --git a/packages/subproviders/src/subproviders/ganache.ts b/packages/subproviders/src/subproviders/ganache.ts index 6ff9396747..22ac5e1e2f 100644 --- a/packages/subproviders/src/subproviders/ganache.ts +++ b/packages/subproviders/src/subproviders/ganache.ts @@ -5,20 +5,23 @@ import { Callback, ErrorCallback } from '../types'; import { Subprovider } from './subprovider'; -/* - * This class implements the web3-provider-engine subprovider interface and returns - * the provider connected to a in-process ganache. - * Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js +/** + * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. + * It intercepts all JSON RPC requests and relays them to an in-process ganache instance. */ export class GanacheSubprovider extends Subprovider { private _ganacheProvider: Web3.Provider; + /** + * Instantiates a GanacheSubprovider + * @param opts The desired opts with which to instantiate the Ganache provider + */ constructor(opts: any) { super(); this._ganacheProvider = Ganache.provider(opts); } // This method needs to be here to satisfy the interface but linter wants it to be static. - // tslint:disable-next-line:prefer-function-over-method - public handleRequest(payload: Web3.JSONRPCRequestPayload, next: Callback, end: ErrorCallback) { + // tslint:disable-next-line:prefer-function-over-method underscore-private-and-protected + private handleRequest(payload: Web3.JSONRPCRequestPayload, next: Callback, end: ErrorCallback) { this._ganacheProvider.sendAsync(payload, (err: Error | null, result: any) => { end(err, result && result.result); }); diff --git a/packages/subproviders/src/subproviders/injected_web3.ts b/packages/subproviders/src/subproviders/injected_web3.ts index ec7304cb7b..3e3744d5ca 100644 --- a/packages/subproviders/src/subproviders/injected_web3.ts +++ b/packages/subproviders/src/subproviders/injected_web3.ts @@ -5,19 +5,25 @@ import { Callback, ErrorCallback } from '../types'; import { Subprovider } from './subprovider'; -/* - * This class implements the web3-provider-engine subprovider interface and forwards - * requests involving user accounts (getAccounts, sendTransaction, etc...) to the injected - * provider instance in their browser. - * Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js +/** + * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) + * subprovider interface. It forwards JSON RPC requests involving user accounts (getAccounts, + * sendTransaction, etc...) to the provider instance supplied at instantiation. All other requests + * are passed onwards for subsequent subproviders to handle. */ export class InjectedWeb3Subprovider extends Subprovider { private _injectedWeb3: Web3; - constructor(subprovider: Web3.Provider) { + /** + * Instantiates a new InjectedWeb3Subprovider + * @param provider Web3 provider that should handle all user account related requests + */ + constructor(provider: Web3.Provider) { super(); - this._injectedWeb3 = new Web3(subprovider); + this._injectedWeb3 = new Web3(provider); } - public handleRequest(payload: Web3.JSONRPCRequestPayload, next: Callback, end: ErrorCallback) { + // This method needs to be here to satisfy the interface but linter wants it to be static. + // tslint:disable-next-line:prefer-function-over-method underscore-private-and-protected + private handleRequest(payload: Web3.JSONRPCRequestPayload, next: Callback, end: ErrorCallback) { switch (payload.method) { case 'web3_clientVersion': this._injectedWeb3.version.getNode(end); diff --git a/packages/subproviders/src/subproviders/ledger.ts b/packages/subproviders/src/subproviders/ledger.ts index 95217c84d4..75c73c4de6 100644 --- a/packages/subproviders/src/subproviders/ledger.ts +++ b/packages/subproviders/src/subproviders/ledger.ts @@ -24,6 +24,11 @@ const DEFAULT_NUM_ADDRESSES_TO_FETCH = 10; const ASK_FOR_ON_DEVICE_CONFIRMATION = false; const SHOULD_GET_CHAIN_CODE = true; +/** + * Subprovider for interfacing with a user's [Ledger Nano S](https://www.ledgerwallet.com/products/ledger-nano-s). + * This subprovider intercepts all account related RPC requests (e.g message/transaction signing, etc...) and + * re-routes them to a Ledger device plugged into the users computer. + */ export class LedgerSubprovider extends Subprovider { private _nonceLock = new Lock(); private _connectionLock = new Lock(); @@ -38,6 +43,11 @@ export class LedgerSubprovider extends Subprovider { throw new Error(LedgerSubproviderErrors.SenderInvalidOrNotSupplied); } } + /** + * Instantiates a LedgerSubprovider + * @param config Several available configurations + * @return LedgerSubprovider instance + */ constructor(config: LedgerSubproviderConfigs) { super(); this._networkId = config.networkId; @@ -50,18 +60,146 @@ export class LedgerSubprovider extends Subprovider { : ASK_FOR_ON_DEVICE_CONFIRMATION; this._derivationPathIndex = 0; } + /** + * Retrieve the set derivation path + * @returns derivation path + */ public getPath(): string { return this._derivationPath; } + /** + * Set a desired derivation path when computing the available user addresses + * @param derivationPath The desired derivation path (e.g `44'/60'/0'`) + */ public setPath(derivationPath: string) { this._derivationPath = derivationPath; } + /** + * Set the final derivation path index. If a user wishes to sign a message with the + * 6th address in a derivation path, before calling `signPersonalMessageAsync`, you must + * call this method with pathIndex `6`. + * @param pathIndex Desired derivation path index + */ public setPathIndex(pathIndex: number) { this._derivationPathIndex = pathIndex; } + /** + * Retrieve a users Ledger accounts. The accounts are derived from the derivationPath, + * master public key and chainCode. Because of this, you can request as many accounts + * as you wish and it only requires a single request to the Ledger device. This method + * is automatically called when issuing a `eth_accounts` JSON RPC request via your providerEngine + * instance. + * @param numberOfAccounts Number of accounts to retrieve (default: 10) + * @return An array of accounts + */ + public async getAccountsAsync(numberOfAccounts: number = DEFAULT_NUM_ADDRESSES_TO_FETCH): Promise { + this._ledgerClientIfExists = await this._createLedgerClientAsync(); + + let ledgerResponse; + try { + ledgerResponse = await this._ledgerClientIfExists.getAddress( + 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'); + + const accounts: string[] = []; + for (let i = 0; i < numberOfAccounts; i++) { + 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()); + } + return accounts; + } + /** + * Sign a transaction with the Ledger. If you've added the LedgerSubprovider to your + * app's provider, you can simply send an `eth_sendTransaction` JSON RPC request, and + * this method will be called auto-magically. If you are not using this via a ProviderEngine + * instance, you can call it directly. + * @param txParams Parameters of the transaction to sign + * @return Signed transaction hex string + */ + public async signTransactionAsync(txParams: PartialTxParams): Promise { + 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(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._destroyLedgerClientAsync(); + const err = new Error(LedgerSubproviderErrors.TooOldLedgerFirmware); + throw err; + } + + const signedTxHex = `0x${tx.serialize().toString('hex')}`; + await this._destroyLedgerClientAsync(); + return signedTxHex; + } catch (err) { + await this._destroyLedgerClientAsync(); + throw err; + } + } + /** + * Sign a personal Ethereum signed message. The signing address will be to one + * retrieved given a derivationPath and pathIndex set on the subprovider. + * The Ledger adds the Ethereum signed message prefix on-device. If you've added + * the LedgerSubprovider to your app's provider, you can simply send an `eth_sign` + * or `personal_sign` JSON RPC request, and this method will be called auto-magically. + * If you are not using this via a ProviderEngine instance, you can call it directly. + * @param data Message to sign + * @return Signature hex string (order: rsv) + */ + public async signPersonalMessageAsync(data: string): Promise { + this._ledgerClientIfExists = await this._createLedgerClientAsync(); + try { + const derivationPath = this._getDerivationPath(); + const result = await this._ledgerClientIfExists.signPersonalMessage( + 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._destroyLedgerClientAsync(); + return signature; + } catch (err) { + await this._destroyLedgerClientAsync(); + throw err; + } + } // Required to implement this public interface which doesn't conform to our linting rule. - // tslint:disable-next-line:async-suffix - public async handleRequest( + // tslint:disable-next-line:async-suffix underscore-private-and-protected + private async handleRequest( payload: Web3.JSONRPCRequestPayload, next: Callback, end: (err: Error | null, result?: any) => void, @@ -128,93 +266,6 @@ export class LedgerSubprovider extends Subprovider { return; } } - public async getAccountsAsync(numberOfAccounts: number = DEFAULT_NUM_ADDRESSES_TO_FETCH): Promise { - this._ledgerClientIfExists = await this._createLedgerClientAsync(); - - let ledgerResponse; - try { - ledgerResponse = await this._ledgerClientIfExists.getAddress( - 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'); - - const accounts: string[] = []; - for (let i = 0; i < numberOfAccounts; i++) { - 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()); - } - return accounts; - } - public async signTransactionAsync(txParams: PartialTxParams): Promise { - 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(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._destroyLedgerClientAsync(); - const err = new Error(LedgerSubproviderErrors.TooOldLedgerFirmware); - throw err; - } - - const signedTxHex = `0x${tx.serialize().toString('hex')}`; - await this._destroyLedgerClientAsync(); - return signedTxHex; - } catch (err) { - await this._destroyLedgerClientAsync(); - throw err; - } - } - public async signPersonalMessageAsync(data: string): Promise { - this._ledgerClientIfExists = await this._createLedgerClientAsync(); - try { - const derivationPath = this._getDerivationPath(); - const result = await this._ledgerClientIfExists.signPersonalMessage( - 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._destroyLedgerClientAsync(); - return signature; - } catch (err) { - await this._destroyLedgerClientAsync(); - throw err; - } - } private _getDerivationPath() { const derivationPath = `${this.getPath()}/${this._derivationPathIndex}`; return derivationPath; diff --git a/packages/subproviders/src/subproviders/nonce_tracker.ts b/packages/subproviders/src/subproviders/nonce_tracker.ts index 82eab4a9a6..b51ba4ac89 100644 --- a/packages/subproviders/src/subproviders/nonce_tracker.ts +++ b/packages/subproviders/src/subproviders/nonce_tracker.ts @@ -10,13 +10,13 @@ import { Callback, ErrorCallback, NextCallback, NonceSubproviderErrors } from '. import { Subprovider } from './subprovider'; -// We do not export this since this is not our error, and we do not throw this error const NONCE_TOO_LOW_ERROR_MESSAGE = 'Transaction nonce is too low'; -/* - This class is heavily inspiried by the Web3ProviderEngine NonceSubprovider - We have added the additional feature of clearing any nonce balues when an error message - describes a nonce value being too low. -*/ + +/** + * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. + * It is heavily inspired by the [NonceSubprovider](https://github.com/MetaMask/provider-engine/blob/master/subproviders/nonce-tracker.js). + * We added the additional feature of clearing the cached nonce value when a `nonce value too low` error occurs. + */ export class NonceTrackerSubprovider extends Subprovider { private _nonceCache: { [address: string]: string } = {}; private static _reconstructTransaction(payload: Web3.JSONRPCRequestPayload): EthereumTx { @@ -47,8 +47,8 @@ export class NonceTrackerSubprovider extends Subprovider { } } // Required to implement this public interface which doesn't conform to our linting rule. - // tslint:disable-next-line:async-suffix - public async handleRequest( + // tslint:disable-next-line:prefer-function-over-method underscore-private-and-protected + private async handleRequest( payload: Web3.JSONRPCRequestPayload, next: NextCallback, end: ErrorCallback, diff --git a/packages/subproviders/src/subproviders/redundant_rpc.ts b/packages/subproviders/src/subproviders/redundant_rpc.ts index dacd1c2c52..2b84d223b5 100644 --- a/packages/subproviders/src/subproviders/redundant_rpc.ts +++ b/packages/subproviders/src/subproviders/redundant_rpc.ts @@ -7,6 +7,11 @@ import { Callback } from '../types'; import { Subprovider } from './subprovider'; +/** + * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. + * It attempts to handle each JSON RPC request by sequentially attempting to receive a valid response from one of a + * set of JSON RPC endpoints. + */ export class RedundantRPCSubprovider extends Subprovider { private _rpcs: RpcSubprovider[]; private static async _firstSuccessAsync( @@ -28,6 +33,10 @@ export class RedundantRPCSubprovider extends Subprovider { throw lastErr; } } + /** + * Instantiates a new RedundantRPCSubprovider + * @param endpoints JSON RPC endpoints to attempt. Attempts are made in the order of the endpoints. + */ constructor(endpoints: string[]) { super(); this._rpcs = _.map(endpoints, endpoint => { @@ -37,8 +46,8 @@ export class RedundantRPCSubprovider extends Subprovider { }); } // Required to implement this public interface which doesn't conform to our linting rule. - // tslint:disable-next-line:async-suffix - public async handleRequest( + // tslint:disable-next-line:prefer-function-over-method underscore-private-and-protected + private async handleRequest( payload: Web3.JSONRPCRequestPayload, next: Callback, end: (err: Error | null, data?: any) => void, diff --git a/packages/subproviders/src/subproviders/subprovider.ts b/packages/subproviders/src/subproviders/subprovider.ts index 4a224fc4ee..0b30c6397a 100644 --- a/packages/subproviders/src/subproviders/subprovider.ts +++ b/packages/subproviders/src/subproviders/subprovider.ts @@ -2,10 +2,9 @@ import promisify = require('es6-promisify'); import * as Web3 from 'web3'; import { JSONRPCRequestPayloadWithMethod } from '../types'; -/* - * A version of the base class Subprovider found in providerEngine +/** + * A altered version of the base class Subprovider found in [web3-provider-engine](https://github.com/MetaMask/provider-engine). * 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; @@ -31,9 +30,13 @@ export class Subprovider { }; return finalPayload; } - public setEngine(engine: any): void { - this._engine = engine; - } + /** + * Emits a JSON RPC payload that will then be handled by the ProviderEngine instance + * this subprovider is a part of. The payload will cascade down the subprovider middleware + * stack until finding the responsible entity for handling the request. + * @param payload JSON RPC payload + * @returns JSON RPC response payload + */ public async emitPayloadAsync( payload: Partial, ): Promise { @@ -41,4 +44,12 @@ export class Subprovider { const response = await promisify(this._engine.sendAsync, this._engine)(finalPayload); return response; } + /** + * Set's the subprovider's engine to the ProviderEngine it is added to. + * This is only called within the ProviderEngine source code + */ + // tslint:disable-next-line:underscore-private-and-protected + private setEngine(engine: any): void { + this._engine = engine; + } }