diff options
Diffstat (limited to 'packages/subproviders')
27 files changed, 770 insertions, 174 deletions
diff --git a/packages/subproviders/CHANGELOG.json b/packages/subproviders/CHANGELOG.json index f0702cd5d..d3ba7a928 100644 --- a/packages/subproviders/CHANGELOG.json +++ b/packages/subproviders/CHANGELOG.json @@ -1,12 +1,35 @@ [ { - "version": "0.8.5", + "version": "0.9.0", "changes": [ { - "note": "Add private key subprovider and refactor shared functionality into a base wallet subprovider", + "note": + "Refactor RedundantRPCSubprovider into RedundantSubprovider where it now accepts an array of subproviders rather then an array of RPC endpoints", + "pr": 500 + }, + { + "note": "Add PrivateKeySubprovider and refactor shared functionality into a base wallet subprovider", "pr": 506 + }, + { + "note": "Add MnemonicWalletsubprovider, deprecating our truffle-hdwallet-provider fork", + "pr": 507 + }, + { + "note": "Support multiple addresses in ledger and mnemonic wallets", + "pr": 507 + }, + { + "note": + "Refactors LedgerSubprovider such that explicitly setting the `pathIndex` is no longer required. Simply set the request `from` address as desired", + "pr": 507 + }, + { + "note": "Renamed derivationPath to baseDerivationPath.", + "pr": 507 } - ] + ], + "timestamp": 1523462196 }, { "timestamp": 1522673609, diff --git a/packages/subproviders/CHANGELOG.md b/packages/subproviders/CHANGELOG.md index 79711e7b7..85cd420dc 100644 --- a/packages/subproviders/CHANGELOG.md +++ b/packages/subproviders/CHANGELOG.md @@ -5,6 +5,10 @@ Edit the package's CHANGELOG.json file only. CHANGELOG +## v0.8.5 - _April 11, 2018_ + + * Add private key subprovider and refactor shared functionality into a base wallet subprovider (#506) + ## v0.8.4 - _April 2, 2018_ * Dependencies updated diff --git a/packages/subproviders/README.md b/packages/subproviders/README.md index 2936be545..8c8807895 100644 --- a/packages/subproviders/README.md +++ b/packages/subproviders/README.md @@ -4,7 +4,7 @@ A few useful web3 subproviders including a LedgerSubprovider useful for adding L We have written up a [Wiki](https://0xproject.com/wiki#Web3-Provider-Examples) article detailing some use cases of this subprovider package. -### Read the [Documentation](0xproject.com/docs/subproviders). +### Read the [Documentation](https://0xproject.com/docs/subproviders). ## Installation diff --git a/packages/subproviders/package.json b/packages/subproviders/package.json index cadbac0e8..c27b1d2fc 100644 --- a/packages/subproviders/package.json +++ b/packages/subproviders/package.json @@ -1,6 +1,6 @@ { "name": "@0xproject/subproviders", - "version": "0.8.4", + "version": "0.8.5", "main": "lib/src/index.js", "types": "lib/src/index.d.ts", "license": "Apache-2.0", @@ -21,31 +21,31 @@ "manual:postpublish": "yarn build; node ./scripts/postpublish.js", "docs:stage": "yarn build && node ./scripts/stage_docs.js", "docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --json $JSON_FILE_PATH $PROJECT_FILES", - "upload_docs_json": "aws s3 cp generated_docs/index.json $S3_URL --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type application/json" + "upload_docs_json": + "aws s3 cp generated_docs/index.json $S3_URL --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type application/json" }, "config": { "postpublish": { "assets": [], "docPublishConfigs": { - "extraFileIncludes": [ - "../types/src/index.ts" - ], + "extraFileIncludes": ["../types/src/index.ts"], "s3BucketPath": "s3://doc-jsons/subproviders/", "s3StagingBucketPath": "s3://staging-doc-jsons/subproviders/" } } }, "dependencies": { - "@0xproject/assert": "^0.2.5", - "@0xproject/types": "^0.5.0", - "@0xproject/typescript-typings": "^0.0.3", - "@0xproject/utils": "^0.5.0", + "@0xproject/assert": "^0.2.6", + "@0xproject/types": "^0.6.0", + "@0xproject/typescript-typings": "^0.1.0", + "@0xproject/utils": "^0.5.1", "@ledgerhq/hw-app-eth": "^4.3.0", "@ledgerhq/hw-transport-u2f": "^4.3.0", "bn.js": "^4.11.8", "ethereumjs-tx": "^1.3.3", "ethereumjs-util": "^5.1.1", "ganache-core": "0xProject/ganache-core", + "bip39": "^2.5.0", "hdkey": "^0.7.1", "lodash": "^4.17.4", "semaphore-async-await": "^1.5.1", @@ -53,9 +53,10 @@ "web3-provider-engine": "^13.0.1" }, "devDependencies": { - "@0xproject/monorepo-scripts": "^0.1.16", - "@0xproject/tslint-config": "^0.4.14", - "@0xproject/utils": "^0.5.0", + "@0xproject/monorepo-scripts": "^0.1.17", + "@0xproject/tslint-config": "^0.4.15", + "@0xproject/utils": "^0.5.1", + "@types/bip39": "^2.4.0", "@types/lodash": "4.14.104", "@types/mocha": "^2.2.42", "@types/node": "^8.0.53", diff --git a/packages/subproviders/src/globals.d.ts b/packages/subproviders/src/globals.d.ts index c5ad26876..4b3ecdf3c 100644 --- a/packages/subproviders/src/globals.d.ts +++ b/packages/subproviders/src/globals.d.ts @@ -51,17 +51,6 @@ declare module '@ledgerhq/hw-transport-node-hid' { } } -// hdkey declarations -declare module 'hdkey' { - class HDNode { - public publicKey: Buffer; - public chainCode: Buffer; - public constructor(); - public derive(path: string): HDNode; - } - export = HDNode; -} - declare module '*.json' { const json: any; /* tslint:disable */ diff --git a/packages/subproviders/src/index.ts b/packages/subproviders/src/index.ts index dd553fde4..ff28b8a8d 100644 --- a/packages/subproviders/src/index.ts +++ b/packages/subproviders/src/index.ts @@ -7,17 +7,17 @@ import { LedgerEthereumClient } from './types'; export { EmptyWalletSubprovider } from './subproviders/empty_wallet_subprovider'; export { FakeGasEstimateSubprovider } from './subproviders/fake_gas_estimate_subprovider'; export { InjectedWeb3Subprovider } from './subproviders/injected_web3'; -export { RedundantRPCSubprovider } from './subproviders/redundant_rpc'; +export { RedundantSubprovider } from './subproviders/redundant_subprovider'; export { LedgerSubprovider } from './subproviders/ledger'; export { GanacheSubprovider } from './subproviders/ganache'; export { Subprovider } from './subproviders/subprovider'; export { NonceTrackerSubprovider } from './subproviders/nonce_tracker'; -export { PrivateKeyWalletSubprovider } from './subproviders/private_key_wallet_subprovider'; +export { PrivateKeyWalletSubprovider } from './subproviders/private_key_wallet'; +export { MnemonicWalletSubprovider } from './subproviders/mnemonic_wallet'; export { Callback, ErrorCallback, NextCallback, - LedgerWalletSubprovider, LedgerCommunicationClient, NonceSubproviderErrors, LedgerSubproviderConfigs, diff --git a/packages/subproviders/src/subproviders/base_wallet_subprovider.ts b/packages/subproviders/src/subproviders/base_wallet_subprovider.ts index 034f83e7f..0a9b99ae4 100644 --- a/packages/subproviders/src/subproviders/base_wallet_subprovider.ts +++ b/packages/subproviders/src/subproviders/base_wallet_subprovider.ts @@ -21,7 +21,7 @@ export abstract class BaseWalletSubprovider extends Subprovider { public abstract async getAccountsAsync(): Promise<string[]>; public abstract async signTransactionAsync(txParams: PartialTxParams): Promise<string>; - public abstract async signPersonalMessageAsync(data: string): Promise<string>; + public abstract async signPersonalMessageAsync(data: string, address: string): Promise<string>; /** * This method conforms to the web3-provider-engine interface. @@ -85,8 +85,9 @@ export abstract class BaseWalletSubprovider extends Subprovider { case 'eth_sign': case 'personal_sign': const data = payload.method === 'eth_sign' ? payload.params[1] : payload.params[0]; + const address = payload.method === 'eth_sign' ? payload.params[0] : payload.params[1]; try { - const ecSignatureHex = await this.signPersonalMessageAsync(data); + const ecSignatureHex = await this.signPersonalMessageAsync(data, address); end(null, ecSignatureHex); } catch (err) { end(err); diff --git a/packages/subproviders/src/subproviders/empty_wallet_subprovider.ts b/packages/subproviders/src/subproviders/empty_wallet_subprovider.ts index 595ec654d..8f152dc0a 100644 --- a/packages/subproviders/src/subproviders/empty_wallet_subprovider.ts +++ b/packages/subproviders/src/subproviders/empty_wallet_subprovider.ts @@ -17,8 +17,8 @@ export class EmptyWalletSubprovider extends Subprovider { * @param next Callback to call if this subprovider decides not to handle the request * @param end Callback to call if subprovider handled the request and wants to pass back the request. */ - // tslint:disable-next-line:prefer-function-over-method - public handleRequest(payload: JSONRPCRequestPayload, next: Callback, end: ErrorCallback) { + // tslint:disable-next-line:prefer-function-over-method async-suffix + public async handleRequest(payload: 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 966c37d8a..54fd7bcb9 100644 --- a/packages/subproviders/src/subproviders/fake_gas_estimate_subprovider.ts +++ b/packages/subproviders/src/subproviders/fake_gas_estimate_subprovider.ts @@ -31,8 +31,8 @@ export class FakeGasEstimateSubprovider extends Subprovider { * @param next Callback to call if this subprovider decides not to handle the request * @param end Callback to call if subprovider handled the request and wants to pass back the request. */ - // tslint:disable-next-line:prefer-function-over-method - public handleRequest(payload: JSONRPCRequestPayload, next: Callback, end: ErrorCallback) { + // tslint:disable-next-line:prefer-function-over-method async-suffix + public async handleRequest(payload: 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 8008d1b4b..debd7ecf0 100644 --- a/packages/subproviders/src/subproviders/ganache.ts +++ b/packages/subproviders/src/subproviders/ganache.ts @@ -27,8 +27,8 @@ export class GanacheSubprovider extends Subprovider { * @param next Callback to call if this subprovider decides not to handle the request * @param end Callback to call if subprovider handled the request and wants to pass back the request. */ - // tslint:disable-next-line:prefer-function-over-method - public handleRequest(payload: JSONRPCRequestPayload, next: Callback, end: ErrorCallback) { + // tslint:disable-next-line:prefer-function-over-method async-suffix + public async handleRequest(payload: 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 1d7b2ddfe..3ca0c4bd0 100644 --- a/packages/subproviders/src/subproviders/injected_web3.ts +++ b/packages/subproviders/src/subproviders/injected_web3.ts @@ -30,8 +30,8 @@ export class InjectedWeb3Subprovider extends Subprovider { * @param next Callback to call if this subprovider decides not to handle the request * @param end Callback to call if subprovider handled the request and wants to pass back the request. */ - // tslint:disable-next-line:prefer-function-over-method - public handleRequest(payload: JSONRPCRequestPayload, next: Callback, end: ErrorCallback) { + // tslint:disable-next-line:prefer-function-over-method async-suffix + public async handleRequest(payload: 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 aa86bf6c0..563e5a56a 100644 --- a/packages/subproviders/src/subproviders/ledger.ts +++ b/packages/subproviders/src/subproviders/ledger.ts @@ -1,5 +1,4 @@ import { assert } from '@0xproject/assert'; -import { JSONRPCRequestPayload } from '@0xproject/types'; import { addressUtils } from '@0xproject/utils'; import EthereumTx = require('ethereumjs-tx'); import ethUtil = require('ethereumjs-util'); @@ -9,6 +8,7 @@ import { Lock } from 'semaphore-async-await'; import { Callback, + DerivedHDKeyInfo, LedgerEthereumClient, LedgerEthereumClientFactoryAsync, LedgerSubproviderConfigs, @@ -17,13 +17,15 @@ import { ResponseWithTxParams, WalletSubproviderErrors, } from '../types'; +import { walletUtils } from '../utils/wallet_utils'; import { BaseWalletSubprovider } from './base_wallet_subprovider'; -const DEFAULT_DERIVATION_PATH = `44'/60'/0'`; -const DEFAULT_NUM_ADDRESSES_TO_FETCH = 10; +const DEFAULT_BASE_DERIVATION_PATH = `44'/60'/0'`; const ASK_FOR_ON_DEVICE_CONFIRMATION = false; const SHOULD_GET_CHAIN_CODE = true; +const DEFAULT_NUM_ADDRESSES_TO_FETCH = 10; +const DEFAULT_ADDRESS_SEARCH_LIMIT = 1000; /** * Subprovider for interfacing with a user's [Ledger Nano S](https://www.ledgerwallet.com/products/ledger-nano-s). @@ -34,11 +36,11 @@ export class LedgerSubprovider extends BaseWalletSubprovider { private _nonceLock = new Lock(); private _connectionLock = new Lock(); private _networkId: number; - private _derivationPath: string; - private _derivationPathIndex: number; + private _baseDerivationPath: string; private _ledgerEthereumClientFactoryAsync: LedgerEthereumClientFactoryAsync; private _ledgerClientIfExists?: LedgerEthereumClient; private _shouldAlwaysAskForConfirmation: boolean; + private _addressSearchLimit: number; /** * Instantiates a LedgerSubprovider. Defaults to derivationPath set to `44'/60'/0'`. * TestRPC/Ganache defaults to `m/44'/60'/0'/0`, so set this in the configs if desired. @@ -49,40 +51,35 @@ export class LedgerSubprovider extends BaseWalletSubprovider { super(); this._networkId = config.networkId; this._ledgerEthereumClientFactoryAsync = config.ledgerEthereumClientFactoryAsync; - this._derivationPath = config.derivationPath || DEFAULT_DERIVATION_PATH; + this._baseDerivationPath = config.baseDerivationPath || DEFAULT_BASE_DERIVATION_PATH; this._shouldAlwaysAskForConfirmation = !_.isUndefined(config.accountFetchingConfigs) && !_.isUndefined(config.accountFetchingConfigs.shouldAskForOnDeviceConfirmation) ? config.accountFetchingConfigs.shouldAskForOnDeviceConfirmation : ASK_FOR_ON_DEVICE_CONFIRMATION; - this._derivationPathIndex = 0; + this._addressSearchLimit = + !_.isUndefined(config.accountFetchingConfigs) && + !_.isUndefined(config.accountFetchingConfigs.addressSearchLimit) + ? config.accountFetchingConfigs.addressSearchLimit + : DEFAULT_ADDRESS_SEARCH_LIMIT; } /** * Retrieve the set derivation path * @returns derivation path */ public getPath(): string { - return this._derivationPath; + return this._baseDerivationPath; } /** * 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 + * @param basDerivationPath The desired derivation path (e.g `44'/60'/0'`) */ - public setPathIndex(pathIndex: number) { - this._derivationPathIndex = pathIndex; + public setPath(basDerivationPath: string) { + this._baseDerivationPath = basDerivationPath; } /** * 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 + * master public key and chain code. 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. @@ -90,46 +87,27 @@ export class LedgerSubprovider extends BaseWalletSubprovider { * @return An array of accounts */ public async getAccountsAsync(numberOfAccounts: number = DEFAULT_NUM_ADDRESSES_TO_FETCH): Promise<string[]> { - 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()); - } + const initialDerivedKeyInfo = await this._initialDerivedKeyInfoAsync(); + const derivedKeyInfos = walletUtils.calculateDerivedHDKeyInfos(initialDerivedKeyInfo, numberOfAccounts); + const accounts = _.map(derivedKeyInfos, k => k.address); 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 + * Signs a transaction on the Ledger with the account specificed by the `from` field in txParams. + * 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<string> { LedgerSubprovider._validateTxParams(txParams); + if (_.isUndefined(txParams.from) || !addressUtils.isAddress(txParams.from)) { + throw new Error(WalletSubproviderErrors.FromAddressMissingOrInvalid); + } + const initialDerivedKeyInfo = await this._initialDerivedKeyInfoAsync(); + const derivedKeyInfo = this._findDerivedKeyInfoForAddress(initialDerivedKeyInfo, txParams.from); + this._ledgerClientIfExists = await this._createLedgerClientAsync(); const tx = new EthereumTx(txParams); @@ -141,8 +119,8 @@ export class LedgerSubprovider extends BaseWalletSubprovider { const txHex = tx.serialize().toString('hex'); try { - const derivationPath = this._getDerivationPath(); - const result = await this._ledgerClientIfExists.signTransaction(derivationPath, txHex); + const fullDerivationPath = derivedKeyInfo.derivationPath; + const result = await this._ledgerClientIfExists.signTransaction(fullDerivationPath, txHex); // Store signature in transaction tx.r = Buffer.from(result.r, 'hex'); tx.s = Buffer.from(result.s, 'hex'); @@ -165,25 +143,30 @@ export class LedgerSubprovider extends BaseWalletSubprovider { } } /** - * Sign a personal Ethereum signed message. The signing address will be the one - * retrieved given a derivationPath and pathIndex set on the subprovider. + * Sign a personal Ethereum signed message. The signing account will be the account + * associated with the provided address. * 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 + * @param data Hex string message to sign + * @param address Address of the account to sign with * @return Signature hex string (order: rsv) */ - public async signPersonalMessageAsync(data: string): Promise<string> { + public async signPersonalMessageAsync(data: string, address: string): Promise<string> { if (_.isUndefined(data)) { throw new Error(WalletSubproviderErrors.DataMissingForSignPersonalMessage); } assert.isHexString('data', data); + assert.isETHAddressHex('address', address); + const initialDerivedKeyInfo = await this._initialDerivedKeyInfoAsync(); + const derivedKeyInfo = this._findDerivedKeyInfoForAddress(initialDerivedKeyInfo, address); + this._ledgerClientIfExists = await this._createLedgerClientAsync(); try { - const derivationPath = this._getDerivationPath(); + const fullDerivationPath = derivedKeyInfo.derivationPath; const result = await this._ledgerClientIfExists.signPersonalMessage( - derivationPath, + fullDerivationPath, ethUtil.stripHexPrefix(data), ); const v = result.v - 27; @@ -199,10 +182,6 @@ export class LedgerSubprovider extends BaseWalletSubprovider { throw err; } } - private _getDerivationPath() { - const derivationPath = `${this.getPath()}/${this._derivationPathIndex}`; - return derivationPath; - } private async _createLedgerClientAsync(): Promise<LedgerEthereumClient> { await this._connectionLock.acquire(); if (!_.isUndefined(this._ledgerClientIfExists)) { @@ -223,4 +202,41 @@ export class LedgerSubprovider extends BaseWalletSubprovider { this._ledgerClientIfExists = undefined; this._connectionLock.release(); } + private async _initialDerivedKeyInfoAsync(): Promise<DerivedHDKeyInfo> { + this._ledgerClientIfExists = await this._createLedgerClientAsync(); + + const parentKeyDerivationPath = `m/${this._baseDerivationPath}`; + let ledgerResponse; + try { + ledgerResponse = await this._ledgerClientIfExists.getAddress( + parentKeyDerivationPath, + 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 address = walletUtils.addressOfHDKey(hdKey); + const initialDerivedKeyInfo = { + hdKey, + address, + derivationPath: parentKeyDerivationPath, + baseDerivationPath: this._baseDerivationPath, + }; + return initialDerivedKeyInfo; + } + private _findDerivedKeyInfoForAddress(initalHDKey: DerivedHDKeyInfo, address: string): DerivedHDKeyInfo { + const matchedDerivedKeyInfo = walletUtils.findDerivedKeyInfoForAddressIfExists( + address, + initalHDKey, + this._addressSearchLimit, + ); + if (_.isUndefined(matchedDerivedKeyInfo)) { + throw new Error(`${WalletSubproviderErrors.AddressNotFound}: ${address}`); + } + return matchedDerivedKeyInfo; + } } diff --git a/packages/subproviders/src/subproviders/mnemonic_wallet.ts b/packages/subproviders/src/subproviders/mnemonic_wallet.ts new file mode 100644 index 000000000..080bfeb4c --- /dev/null +++ b/packages/subproviders/src/subproviders/mnemonic_wallet.ts @@ -0,0 +1,145 @@ +import { assert } from '@0xproject/assert'; +import { addressUtils } from '@0xproject/utils'; +import * as bip39 from 'bip39'; +import ethUtil = require('ethereumjs-util'); +import HDNode = require('hdkey'); +import * as _ from 'lodash'; + +import { DerivedHDKeyInfo, MnemonicWalletSubproviderConfigs, PartialTxParams, WalletSubproviderErrors } from '../types'; +import { walletUtils } from '../utils/wallet_utils'; + +import { BaseWalletSubprovider } from './base_wallet_subprovider'; +import { PrivateKeyWalletSubprovider } from './private_key_wallet'; + +const DEFAULT_BASE_DERIVATION_PATH = `44'/60'/0'/0`; +const DEFAULT_NUM_ADDRESSES_TO_FETCH = 10; +const DEFAULT_ADDRESS_SEARCH_LIMIT = 1000; + +/** + * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. + * This subprovider intercepts all account related RPC requests (e.g message/transaction signing, etc...) and handles + * all requests with accounts derived from the supplied mnemonic. + */ +export class MnemonicWalletSubprovider extends BaseWalletSubprovider { + private _addressSearchLimit: number; + private _baseDerivationPath: string; + private _derivedKeyInfo: DerivedHDKeyInfo; + private _mnemonic: string; + + /** + * Instantiates a MnemonicWalletSubprovider. Defaults to baseDerivationPath set to `44'/60'/0'/0`. + * This is the default in TestRPC/Ganache, it can be overridden if desired. + * @param config Configuration for the mnemonic wallet, must contain the mnemonic + * @return MnemonicWalletSubprovider instance + */ + constructor(config: MnemonicWalletSubproviderConfigs) { + assert.isString('mnemonic', config.mnemonic); + const baseDerivationPath = config.baseDerivationPath || DEFAULT_BASE_DERIVATION_PATH; + assert.isString('baseDerivationPath', baseDerivationPath); + const addressSearchLimit = config.addressSearchLimit || DEFAULT_ADDRESS_SEARCH_LIMIT; + assert.isNumber('addressSearchLimit', addressSearchLimit); + super(); + + this._mnemonic = config.mnemonic; + this._baseDerivationPath = baseDerivationPath; + this._addressSearchLimit = addressSearchLimit; + this._derivedKeyInfo = this._initialDerivedKeyInfo(this._baseDerivationPath); + } + /** + * Retrieve the set derivation path + * @returns derivation path + */ + public getPath(): string { + return this._baseDerivationPath; + } + /** + * Set a desired derivation path when computing the available user addresses + * @param baseDerivationPath The desired derivation path (e.g `44'/60'/0'`) + */ + public setPath(baseDerivationPath: string) { + this._baseDerivationPath = baseDerivationPath; + this._derivedKeyInfo = this._initialDerivedKeyInfo(this._baseDerivationPath); + } + /** + * Retrieve the accounts associated with the mnemonic. + * This method is implicitly 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<string[]> { + const derivedKeys = walletUtils.calculateDerivedHDKeyInfos(this._derivedKeyInfo, numberOfAccounts); + const accounts = _.map(derivedKeys, k => k.address); + return accounts; + } + + /** + * Signs a transaction with the account specificed by the `from` field in txParams. + * If you've added this Subprovider 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<string> { + if (_.isUndefined(txParams.from) || !addressUtils.isAddress(txParams.from)) { + throw new Error(WalletSubproviderErrors.FromAddressMissingOrInvalid); + } + const privateKeyWallet = this._privateKeyWalletForAddress(txParams.from); + const signedTx = privateKeyWallet.signTransactionAsync(txParams); + return signedTx; + } + /** + * Sign a personal Ethereum signed message. The signing account will be the account + * associated with the provided address. + * If you've added the MnemonicWalletSubprovider 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 Hex string message to sign + * @param address Address of the account to sign with + * @return Signature hex string (order: rsv) + */ + public async signPersonalMessageAsync(data: string, address: string): Promise<string> { + if (_.isUndefined(data)) { + throw new Error(WalletSubproviderErrors.DataMissingForSignPersonalMessage); + } + assert.isHexString('data', data); + assert.isETHAddressHex('address', address); + const privateKeyWallet = this._privateKeyWalletForAddress(address); + const sig = await privateKeyWallet.signPersonalMessageAsync(data, address); + return sig; + } + private _privateKeyWalletForAddress(address: string): PrivateKeyWalletSubprovider { + const derivedKeyInfo = this._findDerivedKeyInfoForAddress(address); + const privateKeyHex = derivedKeyInfo.hdKey.privateKey.toString('hex'); + const privateKeyWallet = new PrivateKeyWalletSubprovider(privateKeyHex); + return privateKeyWallet; + } + private _findDerivedKeyInfoForAddress(address: string): DerivedHDKeyInfo { + const matchedDerivedKeyInfo = walletUtils.findDerivedKeyInfoForAddressIfExists( + address, + this._derivedKeyInfo, + this._addressSearchLimit, + ); + if (_.isUndefined(matchedDerivedKeyInfo)) { + throw new Error(`${WalletSubproviderErrors.AddressNotFound}: ${address}`); + } + return matchedDerivedKeyInfo; + } + private _initialDerivedKeyInfo(baseDerivationPath: string): DerivedHDKeyInfo { + const seed = bip39.mnemonicToSeed(this._mnemonic); + const hdKey = HDNode.fromMasterSeed(seed); + // Walk down to base derivation level (i.e m/44'/60'/0') and create an initial key at that level + // all children will then be walked relative (i.e m/0) + const parentKeyDerivationPath = `m/${baseDerivationPath}`; + const parentHDKeyAtDerivationPath = hdKey.derive(parentKeyDerivationPath); + const address = walletUtils.addressOfHDKey(parentHDKeyAtDerivationPath); + const derivedKeyInfo = { + address, + baseDerivationPath, + derivationPath: parentKeyDerivationPath, + hdKey: parentHDKeyAtDerivationPath, + }; + return derivedKeyInfo; + } +} diff --git a/packages/subproviders/src/subproviders/private_key_wallet_subprovider.ts b/packages/subproviders/src/subproviders/private_key_wallet.ts index c3a53773a..b3f48fd90 100644 --- a/packages/subproviders/src/subproviders/private_key_wallet_subprovider.ts +++ b/packages/subproviders/src/subproviders/private_key_wallet.ts @@ -1,13 +1,11 @@ import { assert } from '@0xproject/assert'; -import { JSONRPCRequestPayload } from '@0xproject/types'; import EthereumTx = require('ethereumjs-tx'); import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; -import { Callback, ErrorCallback, PartialTxParams, ResponseWithTxParams, WalletSubproviderErrors } from '../types'; +import { PartialTxParams, WalletSubproviderErrors } from '../types'; import { BaseWalletSubprovider } from './base_wallet_subprovider'; -import { Subprovider } from './subprovider'; /** * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. @@ -17,6 +15,11 @@ import { Subprovider } from './subprovider'; export class PrivateKeyWalletSubprovider extends BaseWalletSubprovider { private _address: string; private _privateKeyBuffer: Buffer; + /** + * Instantiates a PrivateKeyWalletSubprovider. + * @param privateKey The corresponding private key to an Ethereum address + * @return PrivateKeyWalletSubprovider instance + */ constructor(privateKey: string) { assert.isString('privateKey', privateKey); super(); @@ -42,26 +45,40 @@ export class PrivateKeyWalletSubprovider extends BaseWalletSubprovider { */ public async signTransactionAsync(txParams: PartialTxParams): Promise<string> { PrivateKeyWalletSubprovider._validateTxParams(txParams); + if (!_.isUndefined(txParams.from) && txParams.from !== this._address) { + throw new Error( + `Requested to sign transaction with address: ${txParams.from}, instantiated with address: ${ + this._address + }`, + ); + } const tx = new EthereumTx(txParams); tx.sign(this._privateKeyBuffer); const rawTx = `0x${tx.serialize().toString('hex')}`; return rawTx; } /** - * Sign a personal Ethereum signed message. The signing address will be - * calculated from the private key. - * If you've added the PKWalletSubprovider to your app's provider, you can simply send an `eth_sign` + * Sign a personal Ethereum signed message. The signing address will be calculated from the private key. + * The address must be provided it must match the address calculated from the private key. + * If you've added this Subprovider 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 + * @param data Hex string message to sign + * @param address Address of the account to sign with * @return Signature hex string (order: rsv) */ - public async signPersonalMessageAsync(dataIfExists: string): Promise<string> { - if (_.isUndefined(dataIfExists)) { + public async signPersonalMessageAsync(data: string, address: string): Promise<string> { + if (_.isUndefined(data)) { throw new Error(WalletSubproviderErrors.DataMissingForSignPersonalMessage); } - assert.isHexString('data', dataIfExists); - const dataBuff = ethUtil.toBuffer(dataIfExists); + assert.isHexString('data', data); + assert.isETHAddressHex('address', address); + if (address !== this._address) { + throw new Error( + `Requested to sign message with address: ${address}, instantiated with address: ${this._address}`, + ); + } + const dataBuff = ethUtil.toBuffer(data); const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff); const sig = ethUtil.ecsign(msgHashBuff, this._privateKeyBuffer); const rpcSig = ethUtil.toRpcSig(sig.v, sig.r, sig.s); diff --git a/packages/subproviders/src/subproviders/redundant_rpc.ts b/packages/subproviders/src/subproviders/redundant_subprovider.ts index f8ff0915d..37c8bba5a 100644 --- a/packages/subproviders/src/subproviders/redundant_rpc.ts +++ b/packages/subproviders/src/subproviders/redundant_subprovider.ts @@ -1,7 +1,6 @@ import { JSONRPCRequestPayload } from '@0xproject/types'; import { promisify } from '@0xproject/utils'; import * as _ from 'lodash'; -import RpcSubprovider = require('web3-provider-engine/subproviders/rpc'); import { Callback } from '../types'; @@ -12,17 +11,17 @@ import { Subprovider } from './subprovider'; * 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[]; +export class RedundantSubprovider extends Subprovider { + private _subproviders: Subprovider[]; private static async _firstSuccessAsync( - rpcs: RpcSubprovider[], + subproviders: Subprovider[], payload: JSONRPCRequestPayload, next: Callback, ): Promise<any> { let lastErr: Error | undefined; - for (const rpc of rpcs) { + for (const subprovider of subproviders) { try { - const data = await promisify(rpc.handleRequest.bind(rpc))(payload, next); + const data = await promisify(subprovider.handleRequest.bind(subprovider))(payload, next); return data; } catch (err) { lastErr = err; @@ -34,16 +33,12 @@ export class RedundantRPCSubprovider extends Subprovider { } } /** - * Instantiates a new RedundantRPCSubprovider + * Instantiates a new RedundantSubprovider * @param endpoints JSON RPC endpoints to attempt. Attempts are made in the order of the endpoints. */ - constructor(endpoints: string[]) { + constructor(subproviders: Subprovider[]) { super(); - this._rpcs = _.map(endpoints, endpoint => { - return new RpcSubprovider({ - rpcUrl: endpoint, - }); - }); + this._subproviders = subproviders; } /** * This method conforms to the web3-provider-engine interface. @@ -59,9 +54,9 @@ export class RedundantRPCSubprovider extends Subprovider { next: Callback, end: (err: Error | null, data?: any) => void, ): Promise<void> { - const rpcsCopy = this._rpcs.slice(); + const subprovidersCopy = this._subproviders.slice(); try { - const data = await RedundantRPCSubprovider._firstSuccessAsync(rpcsCopy, payload, next); + const data = await RedundantSubprovider._firstSuccessAsync(subprovidersCopy, 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 c51462f07..2118f52c8 100644 --- a/packages/subproviders/src/subproviders/subprovider.ts +++ b/packages/subproviders/src/subproviders/subprovider.ts @@ -2,13 +2,14 @@ import { JSONRPCRequestPayload, JSONRPCResponsePayload } from '@0xproject/types' import { promisify } from '@0xproject/utils'; import * as Web3 from 'web3'; -import { JSONRPCRequestPayloadWithMethod } from '../types'; +import { Callback, ErrorCallback, JSONRPCRequestPayloadWithMethod } from '../types'; /** * 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. */ -export class Subprovider { - private _engine: any; +export abstract class Subprovider { + // tslint:disable-next-line:underscore-private-and-protected + private engine: any; // Ported from: https://github.com/MetaMask/provider-engine/blob/master/util/random-id.js private static _getRandomId() { const extraDigits = 3; @@ -31,6 +32,13 @@ export class Subprovider { }; return finalPayload; } + // tslint:disable-next-line:async-suffix + public abstract async handleRequest( + payload: JSONRPCRequestPayload, + next: Callback, + end: ErrorCallback, + ): Promise<void>; + /** * 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 @@ -40,7 +48,7 @@ export class Subprovider { */ public async emitPayloadAsync(payload: Partial<JSONRPCRequestPayloadWithMethod>): Promise<JSONRPCResponsePayload> { const finalPayload = Subprovider._createFinalPayload(payload); - const response = await promisify<JSONRPCResponsePayload>(this._engine.sendAsync, this._engine)(finalPayload); + const response = await promisify<JSONRPCResponsePayload>(this.engine.sendAsync, this.engine)(finalPayload); return response; } /** @@ -49,6 +57,6 @@ export class Subprovider { * directly. */ public setEngine(engine: any): void { - this._engine = engine; + this.engine = engine; } } diff --git a/packages/subproviders/src/types.ts b/packages/subproviders/src/types.ts index bacb7091b..74ecec23b 100644 --- a/packages/subproviders/src/types.ts +++ b/packages/subproviders/src/types.ts @@ -1,4 +1,5 @@ import { ECSignature, JSONRPCRequestPayload } from '@0xproject/types'; +import HDNode = require('hdkey'); import * as _ from 'lodash'; export interface LedgerCommunicationClient { @@ -40,20 +41,33 @@ export type LedgerEthereumClientFactoryAsync = () => Promise<LedgerEthereumClien export interface LedgerSubproviderConfigs { networkId: number; ledgerEthereumClientFactoryAsync: LedgerEthereumClientFactoryAsync; - derivationPath?: string; + baseDerivationPath?: string; accountFetchingConfigs?: AccountFetchingConfigs; } /* + * addressSearchLimit: The maximum number of addresses to search through, defaults to 1000 * 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 { + addressSearchLimit?: number; numAddressesToReturn?: number; shouldAskForOnDeviceConfirmation?: boolean; } +/* + * mnemonic: The string mnemonic seed + * addressSearchLimit: The maximum number of addresses to search through, defaults to 1000 + * baseDerivationPath: The base derivation path (e.g 44'/60'/0'/0) + */ +export interface MnemonicWalletSubproviderConfigs { + mnemonic: string; + addressSearchLimit?: number; + baseDerivationPath?: string; +} + export interface SignatureData { hash: string; r: string; @@ -67,18 +81,12 @@ export interface LedgerGetAddressResult { chainCode: 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; + from: string; value?: string; data?: string; chainId: number; // EIP 155 chainId - mainnet: 1, ropsten: 3 @@ -96,12 +104,13 @@ export interface ResponseWithTxParams { } export enum WalletSubproviderErrors { + AddressNotFound = 'ADDRESS_NOT_FOUND', DataMissingForSignPersonalMessage = 'DATA_MISSING_FOR_SIGN_PERSONAL_MESSAGE', SenderInvalidOrNotSupplied = 'SENDER_INVALID_OR_NOT_SUPPLIED', + FromAddressMissingOrInvalid = 'FROM_ADDRESS_MISSING_OR_INVALID', } export enum LedgerSubproviderErrors { TooOldLedgerFirmware = 'TOO_OLD_LEDGER_FIRMWARE', - FromAddressMissingOrInvalid = 'FROM_ADDRESS_MISSING_OR_INVALID', MultipleOpenConnectionsDisallowed = 'MULTIPLE_OPEN_CONNECTIONS_DISALLOWED', } @@ -109,6 +118,12 @@ export enum NonceSubproviderErrors { EmptyParametersFound = 'EMPTY_PARAMETERS_FOUND', CannotDetermineAddressFromPayload = 'CANNOT_DETERMINE_ADDRESS_FROM_PAYLOAD', } +export interface DerivedHDKeyInfo { + address: string; + baseDerivationPath: string; + derivationPath: string; + hdKey: HDNode; +} export type ErrorCallback = (err: Error | null, data?: any) => void; export type Callback = () => void; diff --git a/packages/subproviders/src/utils/wallet_utils.ts b/packages/subproviders/src/utils/wallet_utils.ts new file mode 100644 index 000000000..cd5cd9ba0 --- /dev/null +++ b/packages/subproviders/src/utils/wallet_utils.ts @@ -0,0 +1,79 @@ +import ethUtil = require('ethereumjs-util'); +import HDNode = require('hdkey'); +import * as _ from 'lodash'; + +import { DerivedHDKeyInfo, WalletSubproviderErrors } from '../types'; + +const DEFAULT_ADDRESS_SEARCH_LIMIT = 1000; + +class DerivedHDKeyInfoIterator implements IterableIterator<DerivedHDKeyInfo> { + private _parentDerivedKeyInfo: DerivedHDKeyInfo; + private _searchLimit: number; + private _index: number; + + constructor(initialDerivedKey: DerivedHDKeyInfo, searchLimit: number = DEFAULT_ADDRESS_SEARCH_LIMIT) { + this._searchLimit = searchLimit; + this._parentDerivedKeyInfo = initialDerivedKey; + this._index = 0; + } + + public next(): IteratorResult<DerivedHDKeyInfo> { + const baseDerivationPath = this._parentDerivedKeyInfo.baseDerivationPath; + const derivationIndex = this._index; + const fullDerivationPath = `m/${baseDerivationPath}/${derivationIndex}`; + const path = `m/${derivationIndex}`; + const hdKey = this._parentDerivedKeyInfo.hdKey.derive(path); + const address = walletUtils.addressOfHDKey(hdKey); + const derivedKey: DerivedHDKeyInfo = { + address, + hdKey, + baseDerivationPath, + derivationPath: fullDerivationPath, + }; + const done = this._index === this._searchLimit; + this._index++; + return { + done, + value: derivedKey, + }; + } + + public [Symbol.iterator](): IterableIterator<DerivedHDKeyInfo> { + return this; + } +} + +export const walletUtils = { + calculateDerivedHDKeyInfos(parentDerivedKeyInfo: DerivedHDKeyInfo, numberOfKeys: number): DerivedHDKeyInfo[] { + const derivedKeys: DerivedHDKeyInfo[] = []; + const derivedKeyIterator = new DerivedHDKeyInfoIterator(parentDerivedKeyInfo, numberOfKeys); + for (const key of derivedKeyIterator) { + derivedKeys.push(key); + } + return derivedKeys; + }, + findDerivedKeyInfoForAddressIfExists( + address: string, + parentDerivedKeyInfo: DerivedHDKeyInfo, + searchLimit: number, + ): DerivedHDKeyInfo | undefined { + let matchedKey: DerivedHDKeyInfo | undefined; + const derivedKeyIterator = new DerivedHDKeyInfoIterator(parentDerivedKeyInfo, searchLimit); + for (const key of derivedKeyIterator) { + if (key.address === address) { + matchedKey = key; + break; + } + } + return matchedKey; + }, + addressOfHDKey(hdKey: HDNode): string { + const shouldSanitizePublicKey = true; + const derivedPublicKey = hdKey.publicKey; + const ethereumAddressUnprefixed = ethUtil + .publicToAddress(derivedPublicKey, shouldSanitizePublicKey) + .toString('hex'); + const address = ethUtil.addHexPrefix(ethereumAddressUnprefixed).toLowerCase(); + return address; + }, +}; diff --git a/packages/subproviders/test/integration/ledger_subprovider_test.ts b/packages/subproviders/test/integration/ledger_subprovider_test.ts index 503618089..0d6e67bd1 100644 --- a/packages/subproviders/test/integration/ledger_subprovider_test.ts +++ b/packages/subproviders/test/integration/ledger_subprovider_test.ts @@ -1,7 +1,7 @@ import { JSONRPCResponsePayload } from '@0xproject/types'; import { promisify } from '@0xproject/utils'; import Eth from '@ledgerhq/hw-app-eth'; -// HACK: This depdency is optional and tslint skips optional depdencies +// HACK: This dependency is optional and tslint skips optional dependencies // tslint:disable-next-line:no-implicit-dependencies import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'; import * as chai from 'chai'; @@ -33,7 +33,7 @@ describe('LedgerSubprovider', () => { ledgerSubprovider = new LedgerSubprovider({ networkId, ledgerEthereumClientFactoryAsync: ledgerEthereumNodeJsClientFactoryAsync, - derivationPath: fixtureData.TESTRPC_DERIVATION_PATH, + baseDerivationPath: fixtureData.TESTRPC_BASE_DERIVATION_PATH, }); }); describe('direct method calls', () => { @@ -42,9 +42,10 @@ describe('LedgerSubprovider', () => { expect(accounts[0]).to.not.be.an('undefined'); expect(accounts.length).to.be.equal(10); }); - it('returns the expected first account from a ledger set up with the test mnemonic', async () => { + it('returns the expected accounts from a ledger set up with the test mnemonic', async () => { const accounts = await ledgerSubprovider.getAccountsAsync(); expect(accounts[0]).to.be.equal(fixtureData.TEST_RPC_ACCOUNT_0); + expect(accounts[1]).to.be.equal(fixtureData.TEST_RPC_ACCOUNT_1); }); it('returns requested number of accounts', async () => { const numberOfAccounts = 20; @@ -54,14 +55,29 @@ describe('LedgerSubprovider', () => { }); it('signs a personal message', async () => { const data = ethUtils.bufferToHex(ethUtils.toBuffer(fixtureData.PERSONAL_MESSAGE_STRING)); - const ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync(data); - expect(ecSignatureHex.length).to.be.equal(132); + const ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync( + data, + fixtureData.TEST_RPC_ACCOUNT_0, + ); expect(ecSignatureHex).to.be.equal(fixtureData.PERSONAL_MESSAGE_SIGNED_RESULT); }); + it('signs a personal message with second address', async () => { + const data = ethUtils.bufferToHex(ethUtils.toBuffer(fixtureData.PERSONAL_MESSAGE_STRING)); + const ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync( + data, + fixtureData.TEST_RPC_ACCOUNT_1, + ); + expect(ecSignatureHex).to.be.equal(fixtureData.PERSONAL_MESSAGE_ACCOUNT_1_SIGNED_RESULT); + }); it('signs a transaction', async () => { const txHex = await ledgerSubprovider.signTransactionAsync(fixtureData.TX_DATA); expect(txHex).to.be.equal(fixtureData.TX_DATA_SIGNED_RESULT); }); + it('signs a transaction with the second address', async () => { + const txData = { ...fixtureData.TX_DATA, from: fixtureData.TEST_RPC_ACCOUNT_1 }; + const txHex = await ledgerSubprovider.signTransactionAsync(txData); + expect(txHex).to.be.equal(fixtureData.TX_DATA_ACCOUNT_1_SIGNED_RESULT); + }); }); describe('calls through a provider', () => { let defaultProvider: Web3ProviderEngine; diff --git a/packages/subproviders/test/unit/ledger_subprovider_test.ts b/packages/subproviders/test/unit/ledger_subprovider_test.ts index c18506681..892c2acd0 100644 --- a/packages/subproviders/test/unit/ledger_subprovider_test.ts +++ b/packages/subproviders/test/unit/ledger_subprovider_test.ts @@ -4,7 +4,6 @@ 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 { LedgerSubprovider } from '../../src'; import { @@ -15,6 +14,7 @@ import { } from '../../src/types'; import { chaiSetup } from '../chai_setup'; import { fixtureData } from '../utils/fixture_data'; +import { ganacheSubprovider } from '../utils/ganache_subprovider'; import { reportCallbackErrors } from '../utils/report_callback_errors'; chaiSetup.configure(); @@ -82,7 +82,7 @@ describe('LedgerSubprovider', () => { }); it('signs a personal message', async () => { const data = ethUtils.bufferToHex(ethUtils.toBuffer(fixtureData.PERSONAL_MESSAGE_STRING)); - const ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync(data); + const ecSignatureHex = await ledgerSubprovider.signPersonalMessageAsync(data, FAKE_ADDRESS); expect(ecSignatureHex).to.be.equal( '0xa6cc284bff14b42bdf5e9286730c152be91719d478605ec46b3bebcd0ae491480652a1a7b742ceb0213d1e744316e285f41f878d8af0b8e632cbca4c279132d001', ); @@ -94,7 +94,7 @@ describe('LedgerSubprovider', () => { return expect( Promise.all([ ledgerSubprovider.getAccountsAsync(), - ledgerSubprovider.signPersonalMessageAsync(data), + ledgerSubprovider.signPersonalMessageAsync(data, FAKE_ADDRESS), ]), ).to.be.rejectedWith(LedgerSubproviderErrors.MultipleOpenConnectionsDisallowed); }); @@ -105,10 +105,7 @@ describe('LedgerSubprovider', () => { before(() => { provider = new Web3ProviderEngine(); provider.addProvider(ledgerSubprovider); - const httpProvider = new RpcSubprovider({ - rpcUrl: 'http://localhost:8545', - }); - provider.addProvider(httpProvider); + provider.addProvider(ganacheSubprovider); provider.start(); }); describe('success cases', () => { @@ -132,7 +129,7 @@ describe('LedgerSubprovider', () => { const payload = { jsonrpc: '2.0', method: 'eth_sign', - params: ['0x0000000000000000000000000000000000000000', messageHex], + params: [FAKE_ADDRESS, messageHex], id: 1, }; const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { @@ -149,7 +146,7 @@ describe('LedgerSubprovider', () => { const payload = { jsonrpc: '2.0', method: 'personal_sign', - params: [messageHex, '0x0000000000000000000000000000000000000000'], + params: [messageHex, FAKE_ADDRESS], id: 1, }; const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { @@ -168,6 +165,7 @@ describe('LedgerSubprovider', () => { gasPrice: '0x00', nonce: '0x00', gas: '0x00', + from: FAKE_ADDRESS, }; const payload = { jsonrpc: '2.0', @@ -190,7 +188,7 @@ describe('LedgerSubprovider', () => { const payload = { jsonrpc: '2.0', method: 'eth_sign', - params: ['0x0000000000000000000000000000000000000000', nonHexMessage], + params: [FAKE_ADDRESS, nonHexMessage], id: 1, }; const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { @@ -205,7 +203,7 @@ describe('LedgerSubprovider', () => { const payload = { jsonrpc: '2.0', method: 'personal_sign', - params: [nonHexMessage, '0x0000000000000000000000000000000000000000'], + params: [nonHexMessage, FAKE_ADDRESS], id: 1, }; const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { diff --git a/packages/subproviders/test/unit/mnemonic_wallet_subprovider_test.ts b/packages/subproviders/test/unit/mnemonic_wallet_subprovider_test.ts new file mode 100644 index 000000000..93300f47d --- /dev/null +++ b/packages/subproviders/test/unit/mnemonic_wallet_subprovider_test.ts @@ -0,0 +1,215 @@ +import { JSONRPCResponsePayload } from '@0xproject/types'; +import * as chai from 'chai'; +import * as ethUtils from 'ethereumjs-util'; +import * as _ from 'lodash'; +import Web3ProviderEngine = require('web3-provider-engine'); + +import { GanacheSubprovider, MnemonicWalletSubprovider } from '../../src/'; +import { + DoneCallback, + LedgerCommunicationClient, + LedgerSubproviderErrors, + WalletSubproviderErrors, +} from '../../src/types'; +import { chaiSetup } from '../chai_setup'; +import { fixtureData } from '../utils/fixture_data'; +import { reportCallbackErrors } from '../utils/report_callback_errors'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('MnemonicWalletSubprovider', () => { + let subprovider: MnemonicWalletSubprovider; + before(async () => { + subprovider = new MnemonicWalletSubprovider({ + mnemonic: fixtureData.TEST_RPC_MNEMONIC, + baseDerivationPath: fixtureData.TEST_RPC_MNEMONIC_BASE_DERIVATION_PATH, + }); + }); + describe('direct method calls', () => { + describe('success cases', () => { + it('returns the accounts', async () => { + const accounts = await subprovider.getAccountsAsync(); + expect(accounts[0]).to.be.equal(fixtureData.TEST_RPC_ACCOUNT_0); + expect(accounts[1]).to.be.equal(fixtureData.TEST_RPC_ACCOUNT_1); + expect(accounts.length).to.be.equal(10); + }); + it('signs a personal message', async () => { + const data = ethUtils.bufferToHex(ethUtils.toBuffer(fixtureData.PERSONAL_MESSAGE_STRING)); + const ecSignatureHex = await subprovider.signPersonalMessageAsync(data, fixtureData.TEST_RPC_ACCOUNT_0); + expect(ecSignatureHex).to.be.equal(fixtureData.PERSONAL_MESSAGE_SIGNED_RESULT); + }); + it('signs a personal message with second address', async () => { + const data = ethUtils.bufferToHex(ethUtils.toBuffer(fixtureData.PERSONAL_MESSAGE_STRING)); + const ecSignatureHex = await subprovider.signPersonalMessageAsync(data, fixtureData.TEST_RPC_ACCOUNT_1); + expect(ecSignatureHex).to.be.equal(fixtureData.PERSONAL_MESSAGE_ACCOUNT_1_SIGNED_RESULT); + }); + it('signs a transaction', async () => { + const txHex = await subprovider.signTransactionAsync(fixtureData.TX_DATA); + expect(txHex).to.be.equal(fixtureData.TX_DATA_SIGNED_RESULT); + }); + it('signs a transaction with the second address', async () => { + const txData = { ...fixtureData.TX_DATA, from: fixtureData.TEST_RPC_ACCOUNT_1 }; + const txHex = await subprovider.signTransactionAsync(txData); + expect(txHex).to.be.equal(fixtureData.TX_DATA_ACCOUNT_1_SIGNED_RESULT); + }); + }); + describe('failure cases', () => { + it('throws an error if address is invalid ', async () => { + const txData = { ...fixtureData.TX_DATA, from: '0x0' }; + return expect(subprovider.signTransactionAsync(txData)).to.be.rejectedWith( + WalletSubproviderErrors.FromAddressMissingOrInvalid, + ); + }); + it('throws an error if address is valid format but not found', async () => { + const txData = { ...fixtureData.TX_DATA, from: fixtureData.NULL_ADDRESS }; + return expect(subprovider.signTransactionAsync(txData)).to.be.rejectedWith( + `${WalletSubproviderErrors.AddressNotFound}: ${fixtureData.NULL_ADDRESS}`, + ); + }); + }); + }); + describe('calls through a provider', () => { + let provider: Web3ProviderEngine; + before(() => { + provider = new Web3ProviderEngine(); + provider.addProvider(subprovider); + const ganacheSubprovider = new GanacheSubprovider({}); + provider.addProvider(ganacheSubprovider); + 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: JSONRPCResponsePayload) => { + expect(err).to.be.a('null'); + expect(response.result[0]).to.be.equal(fixtureData.TEST_RPC_ACCOUNT_0); + expect(response.result.length).to.be.equal(10); + done(); + }); + provider.sendAsync(payload, callback); + }); + it('signs a personal message with eth_sign', (done: DoneCallback) => { + const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer(fixtureData.PERSONAL_MESSAGE_STRING)); + const payload = { + jsonrpc: '2.0', + method: 'eth_sign', + params: [fixtureData.TEST_RPC_ACCOUNT_0, messageHex], + id: 1, + }; + const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { + expect(err).to.be.a('null'); + expect(response.result).to.be.equal(fixtureData.PERSONAL_MESSAGE_SIGNED_RESULT); + done(); + }); + provider.sendAsync(payload, callback); + }); + it('signs a personal message with personal_sign', (done: DoneCallback) => { + const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer(fixtureData.PERSONAL_MESSAGE_STRING)); + const payload = { + jsonrpc: '2.0', + method: 'personal_sign', + params: [messageHex, fixtureData.TEST_RPC_ACCOUNT_0], + id: 1, + }; + const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { + expect(err).to.be.a('null'); + expect(response.result).to.be.equal(fixtureData.PERSONAL_MESSAGE_SIGNED_RESULT); + done(); + }); + provider.sendAsync(payload, callback); + }); + }); + 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: [fixtureData.TEST_RPC_ACCOUNT_0, nonHexMessage], + id: 1, + }; + const callback = reportCallbackErrors(done)((err: Error, response: 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 = { + jsonrpc: '2.0', + method: 'personal_sign', + params: [nonHexMessage, fixtureData.TEST_RPC_ACCOUNT_0], + id: 1, + }; + const callback = reportCallbackErrors(done)((err: Error, response: 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 `address` param not found when calling personal_sign', (done: DoneCallback) => { + const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer(fixtureData.PERSONAL_MESSAGE_STRING)); + const payload = { + jsonrpc: '2.0', + method: 'personal_sign', + params: [messageHex, fixtureData.NULL_ADDRESS], + id: 1, + }; + const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { + expect(err).to.not.be.a('null'); + expect(err.message).to.be.equal( + `${WalletSubproviderErrors.AddressNotFound}: ${fixtureData.NULL_ADDRESS}`, + ); + 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: JSONRPCResponsePayload) => { + expect(err).to.not.be.a('null'); + expect(err.message).to.be.equal(WalletSubproviderErrors.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: JSONRPCResponsePayload) => { + expect(err).to.not.be.a('null'); + expect(err.message).to.be.equal(WalletSubproviderErrors.SenderInvalidOrNotSupplied); + done(); + }); + provider.sendAsync(payload, callback); + }); + }); + }); +}); diff --git a/packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts b/packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts index ca0665871..5c1b5cd25 100644 --- a/packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts +++ b/packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts @@ -32,7 +32,7 @@ describe('PrivateKeyWalletSubprovider', () => { }); it('signs a personal message', async () => { const data = ethUtils.bufferToHex(ethUtils.toBuffer(fixtureData.PERSONAL_MESSAGE_STRING)); - const ecSignatureHex = await subprovider.signPersonalMessageAsync(data); + const ecSignatureHex = await subprovider.signPersonalMessageAsync(data, fixtureData.TEST_RPC_ACCOUNT_0); expect(ecSignatureHex).to.be.equal(fixtureData.PERSONAL_MESSAGE_SIGNED_RESULT); }); it('signs a transaction', async () => { @@ -71,7 +71,7 @@ describe('PrivateKeyWalletSubprovider', () => { const payload = { jsonrpc: '2.0', method: 'eth_sign', - params: ['0x0000000000000000000000000000000000000000', messageHex], + params: [fixtureData.TEST_RPC_ACCOUNT_0, messageHex], id: 1, }; const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { @@ -86,7 +86,7 @@ describe('PrivateKeyWalletSubprovider', () => { const payload = { jsonrpc: '2.0', method: 'personal_sign', - params: [messageHex, '0x0000000000000000000000000000000000000000'], + params: [messageHex, fixtureData.TEST_RPC_ACCOUNT_0], id: 1, }; const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { @@ -103,7 +103,7 @@ describe('PrivateKeyWalletSubprovider', () => { const payload = { jsonrpc: '2.0', method: 'eth_sign', - params: ['0x0000000000000000000000000000000000000000', nonHexMessage], + params: [fixtureData.TEST_RPC_ACCOUNT_0, nonHexMessage], id: 1, }; const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { @@ -118,7 +118,7 @@ describe('PrivateKeyWalletSubprovider', () => { const payload = { jsonrpc: '2.0', method: 'personal_sign', - params: [nonHexMessage, '0x0000000000000000000000000000000000000000'], + params: [nonHexMessage, fixtureData.TEST_RPC_ACCOUNT_0], id: 1, }; const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { @@ -128,6 +128,25 @@ describe('PrivateKeyWalletSubprovider', () => { }); provider.sendAsync(payload, callback); }); + it('should throw if `address` param is not the address from private key when calling personal_sign', (done: DoneCallback) => { + const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer(fixtureData.PERSONAL_MESSAGE_STRING)); + const payload = { + jsonrpc: '2.0', + method: 'personal_sign', + params: [messageHex, fixtureData.TEST_RPC_ACCOUNT_1], + id: 1, + }; + const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { + expect(err).to.not.be.a('null'); + expect(err.message).to.be.equal( + `Requested to sign message with address: ${ + fixtureData.TEST_RPC_ACCOUNT_1 + }, instantiated with address: ${fixtureData.TEST_RPC_ACCOUNT_0}`, + ); + done(); + }); + provider.sendAsync(payload, callback); + }); it('should throw if `from` param missing when calling eth_sendTransaction', (done: DoneCallback) => { const tx = { to: '0xafa3f8684e54059998bc3a7b0d2b0da075154d66', @@ -165,6 +184,21 @@ describe('PrivateKeyWalletSubprovider', () => { }); provider.sendAsync(payload, callback); }); + it('should throw if `address` param not found when calling personal_sign', (done: DoneCallback) => { + const messageHex = ethUtils.bufferToHex(ethUtils.toBuffer(fixtureData.PERSONAL_MESSAGE_STRING)); + const payload = { + jsonrpc: '2.0', + method: 'personal_sign', + params: [messageHex, '0x0'], + id: 1, + }; + const callback = reportCallbackErrors(done)((err: Error, response: JSONRPCResponsePayload) => { + expect(err).to.not.be.a('null'); + expect(err.message).to.be.equal(`Expected address to be of type ETHAddressHex, encountered: 0x0`); + 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 index a347ab765..e25cb7eb7 100644 --- a/packages/subproviders/test/unit/redundant_rpc_subprovider_test.ts +++ b/packages/subproviders/test/unit/redundant_rpc_subprovider_test.ts @@ -3,21 +3,24 @@ import * as chai from 'chai'; import * as _ from 'lodash'; import Web3 = require('web3'); import Web3ProviderEngine = require('web3-provider-engine'); +import RpcSubprovider = require('web3-provider-engine/subproviders/rpc'); -import { RedundantRPCSubprovider } from '../../src'; +import { RedundantSubprovider } from '../../src'; +import { Subprovider } from '../../src/subproviders/subprovider'; import { DoneCallback } from '../../src/types'; import { chaiSetup } from '../chai_setup'; +import { ganacheSubprovider } from '../utils/ganache_subprovider'; import { reportCallbackErrors } from '../utils/report_callback_errors'; const expect = chai.expect; chaiSetup.configure(); -describe('RedundantRpcSubprovider', () => { +describe('RedundantSubprovider', () => { 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); + const subproviders = [ganacheSubprovider]; + const redundantSubprovider = new RedundantSubprovider(subproviders); provider.addProvider(redundantSubprovider); provider.start(); @@ -36,8 +39,11 @@ 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 redundantSubprovider = new RedundantRPCSubprovider(endpoints); + const nonExistentSubprovider = new RpcSubprovider({ + rpcUrl: 'http://does-not-exist:3000', + }); + const subproviders = [nonExistentSubprovider as Subprovider, ganacheSubprovider]; + const redundantSubprovider = new RedundantSubprovider(subproviders); provider.addProvider(redundantSubprovider); provider.start(); diff --git a/packages/subproviders/test/utils/configs.ts b/packages/subproviders/test/utils/configs.ts new file mode 100644 index 000000000..341037e4f --- /dev/null +++ b/packages/subproviders/test/utils/configs.ts @@ -0,0 +1,5 @@ +export const configs = { + port: 8545, + networkId: 50, + mnemonic: 'concert load couple harbor equip island argue ramp clarify fence smart topic', +}; diff --git a/packages/subproviders/test/utils/fixture_data.ts b/packages/subproviders/test/utils/fixture_data.ts index 890573d0d..3137e08b0 100644 --- a/packages/subproviders/test/utils/fixture_data.ts +++ b/packages/subproviders/test/utils/fixture_data.ts @@ -1,18 +1,26 @@ const TEST_RPC_ACCOUNT_0 = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; +const TEST_RPC_ACCOUNT_1 = '0x6ecbe1db9ef729cbe972c83fb886247691fb6beb'; +const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; const networkId = 42; export const fixtureData = { + NULL_ADDRESS, TEST_RPC_ACCOUNT_0, TEST_RPC_ACCOUNT_0_ACCOUNT_PRIVATE_KEY: 'F2F48EE19680706196E2E339E5DA3491186E0C4C5030670656B0E0164837257D', + TEST_RPC_ACCOUNT_1, + TEST_RPC_MNEMONIC: 'concert load couple harbor equip island argue ramp clarify fence smart topic', + TEST_RPC_MNEMONIC_BASE_DERIVATION_PATH: `44'/60'/0'/0`, PERSONAL_MESSAGE_STRING: 'hello world', PERSONAL_MESSAGE_SIGNED_RESULT: '0x1b0ec5e2908e993d0c8ab6b46da46be2688fdf03c7ea6686075de37392e50a7d7fcc531446699132fbda915bd989882e0064d417018773a315fb8d43ed063c9b00', - TESTRPC_DERIVATION_PATH: `m/44'/60'/0'/0`, + PERSONAL_MESSAGE_ACCOUNT_1_SIGNED_RESULT: + '0xe7ae0c21d02eb38f2c2a20d9d7876a98cc7ef035b7a4559d49375e2ec735e06f0d0ab0ff92ee56c5ffc28d516e6ed0692d0270feae8796408dbef060c6c7100f01', + TESTRPC_BASE_DERIVATION_PATH: `m/44'/60'/0'/0`, NETWORK_ID: networkId, TX_DATA: { nonce: '0x00', gasPrice: '0x0', gas: '0x2710', - to: '0x0000000000000000000000000000000000000000', + to: NULL_ADDRESS, value: '0x00', chainId: networkId, from: TEST_RPC_ACCOUNT_0, @@ -20,4 +28,6 @@ export const fixtureData = { // This is the signed result of the abouve Transaction Data TX_DATA_SIGNED_RESULT: '0xf85f8080822710940000000000000000000000000000000000000000808078a0712854c73c69445cc1b22a7c3d7312ff9a97fe4ffba35fd636e8236b211b6e7ca0647cee031615e52d916c7c707025bc64ad525d8f1b9876c3435a863b42743178', + TX_DATA_ACCOUNT_1_SIGNED_RESULT: + '0xf85f8080822710940000000000000000000000000000000000000000808078a04b02af7ff3f18ce114b601542cc8ebdc50921354f75dd510d31793453a0710e6a0540082a01e475465801b8186a2edc79ec1a2dcf169b9781c25a58a417023c9ca', }; diff --git a/packages/subproviders/test/utils/ganache_subprovider.ts b/packages/subproviders/test/utils/ganache_subprovider.ts new file mode 100644 index 000000000..ac4a9325c --- /dev/null +++ b/packages/subproviders/test/utils/ganache_subprovider.ts @@ -0,0 +1,18 @@ +import * as fs from 'fs'; + +import { GanacheSubprovider } from '../../src/subproviders/ganache'; +import { configs } from '../utils/configs'; + +const logger = { + log: (arg: any) => { + fs.appendFileSync('ganache.log', `${arg}\n`); + }, +}; + +export const ganacheSubprovider = new GanacheSubprovider({ + logger, + verbose: false, + port: configs.port, + networkId: configs.networkId, + mnemonic: configs.mnemonic, +}); diff --git a/packages/subproviders/tsconfig.json b/packages/subproviders/tsconfig.json index e35816553..72dfee80b 100644 --- a/packages/subproviders/tsconfig.json +++ b/packages/subproviders/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../tsconfig", "compilerOptions": { - "outDir": "lib" + "outDir": "lib", + "downlevelIteration": true }, "include": ["./src/**/*", "./test/**/*"] } |