Merge pull request #1431 from MarcZenn/feature/subproviders/trezor-subprovider
Feature/subproviders/trezor subprovider
This commit is contained in:
@@ -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';
|
||||
|
||||
197
packages/subproviders/src/subproviders/trezor.ts
Normal file
197
packages/subproviders/src/subproviders/trezor.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
40
yarn.lock
40
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user