aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJacob Evans <dekz@dekz.net>2018-04-12 15:35:32 +0800
committerGitHub <noreply@github.com>2018-04-12 15:35:32 +0800
commited0c64fdcf5c180107f54ad916c11c4901a4c01c (patch)
tree89d71313275a970121bc8241ad20045160197505
parent364d8824af412790e3cd4d82ff7f98c2f2a75fa1 (diff)
parent9a91e39b3f9419287ba0c508b86d519db4e72dd7 (diff)
downloaddexon-0x-contracts-ed0c64fdcf5c180107f54ad916c11c4901a4c01c.tar.gz
dexon-0x-contracts-ed0c64fdcf5c180107f54ad916c11c4901a4c01c.tar.zst
dexon-0x-contracts-ed0c64fdcf5c180107f54ad916c11c4901a4c01c.zip
Merge pull request #507 from 0xProject/feature/subproviders/mnemonic-wallet-subprovider
Mnemonic wallet subprovider and multiple accounts in ledger
-rw-r--r--packages/subproviders/CHANGELOG.json19
-rw-r--r--packages/subproviders/package.json9
-rw-r--r--packages/subproviders/src/globals.d.ts11
-rw-r--r--packages/subproviders/src/index.ts4
-rw-r--r--packages/subproviders/src/subproviders/base_wallet_subprovider.ts5
-rw-r--r--packages/subproviders/src/subproviders/ledger.ts144
-rw-r--r--packages/subproviders/src/subproviders/mnemonic_wallet.ts145
-rw-r--r--packages/subproviders/src/subproviders/private_key_wallet.ts (renamed from packages/subproviders/src/subproviders/private_key_wallet_subprovider.ts)39
-rw-r--r--packages/subproviders/src/types.ts33
-rw-r--r--packages/subproviders/src/utils/wallet_utils.ts79
-rw-r--r--packages/subproviders/test/integration/ledger_subprovider_test.ts24
-rw-r--r--packages/subproviders/test/unit/ledger_subprovider_test.ts13
-rw-r--r--packages/subproviders/test/unit/mnemonic_wallet_subprovider_test.ts215
-rw-r--r--packages/subproviders/test/unit/private_key_wallet_subprovider_test.ts44
-rw-r--r--packages/subproviders/test/utils/fixture_data.ts14
-rw-r--r--packages/subproviders/tsconfig.json3
-rw-r--r--packages/typescript-typings/CHANGELOG.json4
-rw-r--r--packages/typescript-typings/types/hdkey/index.d.ts11
-rw-r--r--packages/website/ts/blockchain.ts8
-rw-r--r--packages/website/ts/components/dialogs/ledger_config_dialog.tsx1
-rw-r--r--packages/website/ts/containers/subproviders_documentation.ts9
21 files changed, 704 insertions, 130 deletions
diff --git a/packages/subproviders/CHANGELOG.json b/packages/subproviders/CHANGELOG.json
index 6ab3e7093..d3ba7a928 100644
--- a/packages/subproviders/CHANGELOG.json
+++ b/packages/subproviders/CHANGELOG.json
@@ -8,8 +8,25 @@
"pr": 500
},
{
- "note": "Add private key subprovider and refactor shared functionality into a base wallet subprovider",
+ "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
diff --git a/packages/subproviders/package.json b/packages/subproviders/package.json
index 36133e417..c27b1d2fc 100644
--- a/packages/subproviders/package.json
+++ b/packages/subproviders/package.json
@@ -21,15 +21,14 @@
"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/"
}
@@ -46,6 +45,7 @@
"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",
@@ -56,6 +56,7 @@
"@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 b84473e45..ff28b8a8d 100644
--- a/packages/subproviders/src/index.ts
+++ b/packages/subproviders/src/index.ts
@@ -12,12 +12,12 @@ 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/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/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 9e81dfac5..0d6e67bd1 100644
--- a/packages/subproviders/test/integration/ledger_subprovider_test.ts
+++ b/packages/subproviders/test/integration/ledger_subprovider_test.ts
@@ -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 88d38f59b..892c2acd0 100644
--- a/packages/subproviders/test/unit/ledger_subprovider_test.ts
+++ b/packages/subproviders/test/unit/ledger_subprovider_test.ts
@@ -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);
});
@@ -129,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) => {
@@ -146,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) => {
@@ -165,6 +165,7 @@ describe('LedgerSubprovider', () => {
gasPrice: '0x00',
nonce: '0x00',
gas: '0x00',
+ from: FAKE_ADDRESS,
};
const payload = {
jsonrpc: '2.0',
@@ -187,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) => {
@@ -202,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/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/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/**/*"]
}
diff --git a/packages/typescript-typings/CHANGELOG.json b/packages/typescript-typings/CHANGELOG.json
index 39af8ace0..2ea5ff0ec 100644
--- a/packages/typescript-typings/CHANGELOG.json
+++ b/packages/typescript-typings/CHANGELOG.json
@@ -5,6 +5,10 @@
{
"note": "Add types for more packages",
"pr": 501
+ },
+ {
+ "note": "Add types for HDKey",
+ "pr": 507
}
],
"timestamp": 1523462196
diff --git a/packages/typescript-typings/types/hdkey/index.d.ts b/packages/typescript-typings/types/hdkey/index.d.ts
new file mode 100644
index 000000000..84b751bd7
--- /dev/null
+++ b/packages/typescript-typings/types/hdkey/index.d.ts
@@ -0,0 +1,11 @@
+declare module 'hdkey' {
+ class HDNode {
+ public static fromMasterSeed(seed: Buffer): HDNode;
+ public publicKey: Buffer;
+ public privateKey: Buffer;
+ public chainCode: Buffer;
+ public constructor();
+ public derive(path: string): HDNode;
+ }
+ export = HDNode;
+}
diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts
index 7d79542a3..99e4856ce 100644
--- a/packages/website/ts/blockchain.ts
+++ b/packages/website/ts/blockchain.ts
@@ -78,7 +78,7 @@ export class Blockchain {
private _userAddressIfExists: string;
private _cachedProvider: Provider;
private _cachedProviderNetworkId: number;
- private _ledgerSubprovider: LedgerWalletSubprovider;
+ private _ledgerSubprovider: LedgerSubprovider;
private _defaultGasPrice: BigNumber;
private static _getNameGivenProvider(provider: Provider): string {
const providerType = utils.getProviderType(provider);
@@ -180,12 +180,6 @@ export class Blockchain {
}
this._ledgerSubprovider.setPath(path);
}
- public updateLedgerDerivationIndex(pathIndex: number) {
- if (_.isUndefined(this._ledgerSubprovider)) {
- return; // noop
- }
- this._ledgerSubprovider.setPathIndex(pathIndex);
- }
public async updateProviderToLedgerAsync(networkId: number) {
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
diff --git a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx
index d7190c0bb..a72d33183 100644
--- a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx
+++ b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx
@@ -199,7 +199,6 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
}
private _onAddressSelected(selectedRowIndexes: number[]) {
const selectedRowIndex = selectedRowIndexes[0];
- this.props.blockchain.updateLedgerDerivationIndex(selectedRowIndex);
const selectedAddress = this.state.userAddresses[selectedRowIndex];
const selectAddressBalance = this.state.addressBalances[selectedRowIndex];
this.props.dispatcher.updateUserAddress(selectedAddress);
diff --git a/packages/website/ts/containers/subproviders_documentation.ts b/packages/website/ts/containers/subproviders_documentation.ts
index 4a956acea..017d708a4 100644
--- a/packages/website/ts/containers/subproviders_documentation.ts
+++ b/packages/website/ts/containers/subproviders_documentation.ts
@@ -30,6 +30,8 @@ const docSections = {
redundantRPCSubprovider: 'redundantRPCSubprovider',
ganacheSubprovider: 'ganacheSubprovider',
nonceTrackerSubprovider: 'nonceTrackerSubprovider',
+ privateKeyWalletSubprovider: 'privateKeyWalletSubprovider',
+ mnemonicWalletSubprovider: 'mnemonicWalletSubprovider',
types: docConstants.TYPES_SECTION_NAME,
};
@@ -44,6 +46,8 @@ const docsInfoConfig: DocsInfoConfig = {
subprovider: [docSections.subprovider],
['ledger-subprovider']: [docSections.ledgerSubprovider],
['ledger-node-hid-issue']: [docSections.ledgerNodeHid],
+ ['private-key-wallet-subprovider']: [docSections.privateKeyWalletSubprovider],
+ ['mnemonic-wallet-subprovider']: [docSections.mnemonicWalletSubprovider],
['factory-methods']: [docSections.factoryMethods],
['emptyWallet-subprovider']: [docSections.emptyWalletSubprovider],
['fakeGasEstimate-subprovider']: [docSections.fakeGasEstimateSubprovider],
@@ -61,6 +65,8 @@ const docsInfoConfig: DocsInfoConfig = {
sectionNameToModulePath: {
[docSections.subprovider]: ['"subproviders/src/subproviders/subprovider"'],
[docSections.ledgerSubprovider]: ['"subproviders/src/subproviders/ledger"'],
+ [docSections.privateKeyWalletSubprovider]: ['"subproviders/src/subproviders/private_key_wallet"'],
+ [docSections.mnemonicWalletSubprovider]: ['"subproviders/src/subproviders/mnemonic_wallet"'],
[docSections.factoryMethods]: ['"subproviders/src/index"'],
[docSections.emptyWalletSubprovider]: ['"subproviders/src/subproviders/empty_wallet_subprovider"'],
[docSections.fakeGasEstimateSubprovider]: ['"subproviders/src/subproviders/fake_gas_estimate_subprovider"'],
@@ -75,6 +81,8 @@ const docsInfoConfig: DocsInfoConfig = {
visibleConstructors: [
docSections.subprovider,
docSections.ledgerSubprovider,
+ docSections.privateKeyWalletSubprovider,
+ docSections.mnemonicWalletSubprovider,
docSections.emptyWalletSubprovider,
docSections.fakeGasEstimateSubprovider,
docSections.injectedWeb3Subprovider,
@@ -98,6 +106,7 @@ const docsInfoConfig: DocsInfoConfig = {
'PartialTxParams',
'LedgerEthereumClient',
'LedgerSubproviderConfigs',
+ 'MnemonicWalletSubproviderConfigs',
'OnNextCompleted',
'Provider',
],