Merge pull request #1431 from MarcZenn/feature/subproviders/trezor-subprovider

Feature/subproviders/trezor subprovider
This commit is contained in:
Fabio B
2019-05-23 13:58:34 +01:00
committed by GitHub
4 changed files with 276 additions and 4 deletions

View File

@@ -29,6 +29,7 @@ export { PrivateKeyWalletSubprovider } from './subproviders/private_key_wallet';
export { MnemonicWalletSubprovider } from './subproviders/mnemonic_wallet';
export { MetamaskSubprovider } from './subproviders/metamask_subprovider';
export { EthLightwalletSubprovider } from './subproviders/eth_lightwallet_subprovider';
export { TrezorSubprovider } from './subproviders/trezor';
export {
Callback,
@@ -46,6 +47,7 @@ export {
OnNextCompleted,
MnemonicWalletSubproviderConfigs,
LedgerGetAddressResult,
TrezorSubproviderConfig,
} from './types';
export { ECSignature, EIP712Object, EIP712ObjectValue, EIP712TypedData, EIP712Types, EIP712Parameter } from '@0x/types';

View File

@@ -0,0 +1,197 @@
import { assert } from '@0x/assert';
import { addressUtils } from '@0x/utils';
import EthereumTx = require('ethereumjs-tx');
import * as _ from 'lodash';
import HDNode = require('hdkey');
import {
DerivedHDKeyInfo,
PartialTxParams,
TrezorConnectResponse,
TrezorGetPublicKeyResponsePayload,
TrezorResponseErrorPayload,
TrezorSignMsgResponsePayload,
TrezorSignTxResponsePayload,
TrezorSubproviderConfig,
WalletSubproviderErrors,
} from '../types';
import { walletUtils } from '../utils/wallet_utils';
import { BaseWalletSubprovider } from './base_wallet_subprovider';
const PRIVATE_KEY_PATH = `44'/60'/0'/0`;
const DEFAULT_NUM_ADDRESSES_TO_FETCH = 10;
const DEFAULT_ADDRESS_SEARCH_LIMIT = 1000;
export class TrezorSubprovider extends BaseWalletSubprovider {
private readonly _privateKeyPath: string;
private readonly _trezorConnectClientApi: any;
private readonly _networkId: number;
private readonly _addressSearchLimit: number;
/**
* Instantiates a TrezorSubprovider. Defaults to private key path set to `44'/60'/0'/0/`.
* Must be initialized with trezor-connect API module https://github.com/trezor/connect.
* @param TrezorSubprovider config object containing trezor-connect API
* @return TrezorSubprovider instance
*/
constructor(config: TrezorSubproviderConfig) {
super();
this._privateKeyPath = PRIVATE_KEY_PATH;
this._trezorConnectClientApi = config.trezorConnectClientApi;
this._networkId = config.networkId;
this._addressSearchLimit =
!_.isUndefined(config.accountFetchingConfigs) &&
!_.isUndefined(config.accountFetchingConfigs.addressSearchLimit)
? config.accountFetchingConfigs.addressSearchLimit
: DEFAULT_ADDRESS_SEARCH_LIMIT;
}
/**
* Retrieve a users Trezor account. This method is automatically called
* when issuing a `eth_accounts` JSON RPC request via your providerEngine
* instance.
* @return An array of accounts
*/
public async getAccountsAsync(numberOfAccounts: number = DEFAULT_NUM_ADDRESSES_TO_FETCH): Promise<string[]> {
const initialDerivedKeyInfo = await this._initialDerivedKeyInfoAsync();
const derivedKeyInfos = walletUtils.calculateDerivedHDKeyInfos(initialDerivedKeyInfo, numberOfAccounts);
const accounts = _.map(derivedKeyInfos, k => k.address);
return accounts;
}
/**
* Signs a transaction on the Trezor with the account specificed by the `from` field in txParams.
* If you've added the TrezorSubprovider 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(txData: PartialTxParams): Promise<string> {
if (_.isUndefined(txData.from) || !addressUtils.isAddress(txData.from)) {
throw new Error(WalletSubproviderErrors.FromAddressMissingOrInvalid);
}
txData.value = txData.value ? txData.value : '0x0';
txData.data = txData.data ? txData.data : '0x';
txData.gas = txData.gas ? txData.gas : '0x0';
txData.gasPrice = txData.gasPrice ? txData.gasPrice : '0x0';
const initialDerivedKeyInfo = await this._initialDerivedKeyInfoAsync();
const derivedKeyInfo = this._findDerivedKeyInfoForAddress(initialDerivedKeyInfo, txData.from);
const fullDerivationPath = derivedKeyInfo.derivationPath;
const response: TrezorConnectResponse = await this._trezorConnectClientApi.ethereumSignTransaction({
path: fullDerivationPath,
transaction: {
to: txData.to,
value: txData.value,
data: txData.data,
chainId: this._networkId,
nonce: txData.nonce,
gasLimit: txData.gas,
gasPrice: txData.gasPrice,
},
});
if (response.success) {
const payload: TrezorSignTxResponsePayload = response.payload;
const tx = new EthereumTx(txData);
// Set the EIP155 bits
const vIndex = 6;
tx.raw[vIndex] = Buffer.from([1]); // v
const rIndex = 7;
tx.raw[rIndex] = Buffer.from([]); // r
const sIndex = 8;
tx.raw[sIndex] = Buffer.from([]); // s
// slice off leading 0x
tx.v = Buffer.from(payload.v.slice(2), 'hex');
tx.r = Buffer.from(payload.r.slice(2), 'hex');
tx.s = Buffer.from(payload.s.slice(2), 'hex');
return `0x${tx.serialize().toString('hex')}`;
} else {
const payload: TrezorResponseErrorPayload = response.payload;
throw new Error(payload.error);
}
}
/**
* Sign a personal Ethereum signed message. The signing account will be the account
* associated with the provided address. If you've added the TrezorSubprovider 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 initialDerivedKeyInfo = await this._initialDerivedKeyInfoAsync();
const derivedKeyInfo = this._findDerivedKeyInfoForAddress(initialDerivedKeyInfo, address);
const fullDerivationPath = derivedKeyInfo.derivationPath;
const response: TrezorConnectResponse = await this._trezorConnectClientApi.ethereumSignMessage({
path: fullDerivationPath,
message: data,
hex: false,
});
if (response.success) {
const payload: TrezorSignMsgResponsePayload = response.payload;
return `0x${payload.signature}`;
} else {
const payload: TrezorResponseErrorPayload = response.payload;
throw new Error(payload.error);
}
}
/**
* TODO:: eth_signTypedData is currently not supported on Trezor devices.
* @param address Address of the account to sign with
* @param data the typed data object
* @return Signature hex string (order: rsv)
*/
// tslint:disable-next-line:prefer-function-over-method
public async signTypedDataAsync(address: string, typedData: any): Promise<string> {
throw new Error(WalletSubproviderErrors.MethodNotSupported);
}
private async _initialDerivedKeyInfoAsync(): Promise<DerivedHDKeyInfo> {
const parentKeyDerivationPath = `m/${this._privateKeyPath}`;
const response: TrezorConnectResponse = await this._trezorConnectClientApi.getPublicKey({ path: parentKeyDerivationPath });
if (response.success) {
const payload: TrezorGetPublicKeyResponsePayload = response.payload;
const hdKey = new HDNode();
hdKey.publicKey = new Buffer(payload.publicKey, 'hex');
hdKey.chainCode = new Buffer(payload.chainCode, 'hex');
const address = walletUtils.addressOfHDKey(hdKey);
const initialDerivedKeyInfo = {
hdKey,
address,
derivationPath: parentKeyDerivationPath,
baseDerivationPath: this._privateKeyPath,
};
return initialDerivedKeyInfo;
} else {
const payload: TrezorResponseErrorPayload = response.payload;
throw new Error(payload.error);
}
}
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;
}
}

View File

@@ -1,7 +1,6 @@
import { ECSignature } from '@0x/types';
import { JSONRPCRequestPayload } from 'ethereum-types';
import HDNode = require('hdkey');
export interface LedgerCommunicationClient {
close: () => Promise<void>;
}
@@ -136,3 +135,43 @@ export type NextCallback = (callback?: OnNextCompleted) => void;
export interface JSONRPCRequestPayloadWithMethod extends JSONRPCRequestPayload {
method: string;
}
export interface TrezorSubproviderConfig {
accountFetchingConfigs: AccountFetchingConfigs;
trezorConnectClientApi: any;
networkId: number;
}
export interface TrezorGetPublicKeyResponsePayload {
path: {
[index: number]: number;
};
serializedPath: string;
childNumb: number;
xpub: string;
chainCode: string;
publicKey: string;
fingerprint: number;
depth: number;
}
export interface TrezorSignTxResponsePayload {
v: string;
r: string;
s: string;
}
export interface TrezorSignMsgResponsePayload {
address: string;
signature: string;
}
export interface TrezorResponseErrorPayload {
error: string;
}
export interface TrezorConnectResponse {
payload: any;
id: number;
success: boolean;
}

View File

@@ -2305,6 +2305,10 @@ aes-js@^3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a"
aes-js@^3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a"
agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
@@ -3968,6 +3972,14 @@ bs58check@^2.1.2:
create-hash "^1.1.0"
safe-buffer "^5.1.2"
bs58check@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc"
dependencies:
bs58 "^4.0.0"
create-hash "^1.1.0"
safe-buffer "^5.1.2"
bser@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
@@ -6832,6 +6844,20 @@ ethereumjs-wallet@0.6.2:
utf8 "^3.0.0"
uuid "^3.3.2"
ethereumjs-wallet@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/ethereumjs-wallet/-/ethereumjs-wallet-0.6.3.tgz#b0eae6f327637c2aeb9ccb9047b982ac542e6ab1"
dependencies:
aes-js "^3.1.1"
bs58check "^2.1.2"
ethereumjs-util "^6.0.0"
hdkey "^1.1.0"
randombytes "^2.0.6"
safe-buffer "^5.1.2"
scrypt.js "^0.3.0"
utf8 "^3.0.0"
uuid "^3.3.2"
ethers@~4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.4.tgz#d3f85e8b27f4b59537e06526439b0fb15b44dc65"
@@ -8505,7 +8531,7 @@ hdkey@^0.7.1:
coinstring "^2.0.0"
secp256k1 "^3.0.1"
hdkey@^1.0.0:
hdkey@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/hdkey/-/hdkey-1.1.0.tgz#e74e7b01d2c47f797fa65d1d839adb7a44639f29"
dependencies:
@@ -13751,7 +13777,7 @@ randomatic@^1.1.3:
is-number "^3.0.0"
kind-of "^4.0.0"
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"
dependencies:
@@ -15133,6 +15159,14 @@ scrypt.js@0.2.0, scrypt.js@^0.2.0:
scrypt "^6.0.2"
scryptsy "^1.2.1"
scrypt.js@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/scrypt.js/-/scrypt.js-0.3.0.tgz#6c62d61728ad533c8c376a2e5e3e86d41a95c4c0"
dependencies:
scryptsy "^1.2.1"
optionalDependencies:
scrypt "^6.0.2"
scrypt@^6.0.2:
version "6.0.3"
resolved "https://registry.yarnpkg.com/scrypt/-/scrypt-6.0.3.tgz#04e014a5682b53fa50c2d5cce167d719c06d870d"
@@ -17332,7 +17366,7 @@ utf8@^2.1.1:
utf8@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1"
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"