import * as EthereumTx from 'ethereumjs-tx'; import ethUtil = require('ethereumjs-util'); import * as ledger from 'ledgerco'; import * as _ from 'lodash'; import {LedgerEthConnection, SignPersonalMessageParams, TxParams} from 'ts/types'; import {constants} from 'ts/utils/constants'; import Web3 = require('web3'); import HookedWalletSubprovider = require('web3-provider-engine/subproviders/hooked-wallet'); const NUM_ADDRESSES_TO_FETCH = 10; const ASK_FOR_ON_DEVICE_CONFIRMATION = false; const SHOULD_GET_CHAIN_CODE = false; export class LedgerWallet { public isU2FSupported: boolean; public getAccounts: (callback: (err: Error, accounts: string[]) => void) => void; public signMessage: (msgParams: SignPersonalMessageParams, callback: (err: Error, result?: string) => void) => void; public signTransaction: (txParams: TxParams, callback: (err: Error, result?: string) => void) => void; private getNetworkId: () => number; private path: string; private pathIndex: number; private ledgerEthConnection: LedgerEthConnection; private accounts: string[]; constructor(getNetworkIdFn: () => number) { this.path = constants.DEFAULT_DERIVATION_PATH; this.pathIndex = 0; this.isU2FSupported = false; this.getNetworkId = getNetworkIdFn; this.getAccounts = this.getAccountsAsync.bind(this); this.signMessage = this.signPersonalMessageAsync.bind(this); this.signTransaction = this.signTransactionAsync.bind(this); } public getPath(): string { return this.path; } public setPath(derivationPath: string) { this.path = derivationPath; // HACK: Must re-assign getAccounts, signMessage and signTransaction since they were // previously bound to old values of this.path this.getAccounts = this.getAccountsAsync.bind(this); this.signMessage = this.signPersonalMessageAsync.bind(this); this.signTransaction = this.signTransactionAsync.bind(this); } public setPathIndex(pathIndex: number) { this.pathIndex = pathIndex; // HACK: Must re-assign signMessage & signTransaction since they it was previously bound to // old values of this.path this.signMessage = this.signPersonalMessageAsync.bind(this); this.signTransaction = this.signTransactionAsync.bind(this); } public async getAccountsAsync(callback: (err: Error, accounts: string[]) => void) { if (!_.isUndefined(this.ledgerEthConnection)) { callback(null, []); return; } this.ledgerEthConnection = await this.createLedgerConnectionAsync(); const accounts = []; for (let i = 0; i < NUM_ADDRESSES_TO_FETCH; i++) { try { const derivationPath = `${this.path}/${i}`; const result = await this.ledgerEthConnection.getAddress_async( derivationPath, ASK_FOR_ON_DEVICE_CONFIRMATION, SHOULD_GET_CHAIN_CODE, ); accounts.push(result.address.toLowerCase()); } catch (err) { await this.closeLedgerConnectionAsync(); callback(err, null); return; } } await this.closeLedgerConnectionAsync(); callback(null, accounts); } public async signTransactionAsync(txParams: TxParams, callback: (err: Error, result?: string) => void) { const tx = new EthereumTx(txParams); const networkId = this.getNetworkId(); const chainId = networkId; // Same thing // Set the EIP155 bits tx.raw[6] = Buffer.from([chainId]); // v tx.raw[7] = Buffer.from([]); // r tx.raw[8] = Buffer.from([]); // s const txHex = tx.serialize().toString('hex'); this.ledgerEthConnection = await this.createLedgerConnectionAsync(); try { const derivationPath = this.getDerivationPath(); const result = await this.ledgerEthConnection.signTransaction_async(derivationPath, txHex); // Store signature in transaction tx.v = new Buffer(result.v, 'hex'); tx.r = new Buffer(result.r, 'hex'); tx.s = new Buffer(result.s, 'hex'); // EIP155: v should be chain_id * 2 + {35, 36} const signedChainId = Math.floor((tx.v[0] - 35) / 2); if (signedChainId !== chainId) { const err = new Error('TOO_OLD_LEDGER_FIRMWARE'); callback(err, null); return; } const signedTxHex = `0x${tx.serialize().toString('hex')}`; await this.closeLedgerConnectionAsync(); callback(null, signedTxHex); } catch (err) { await this.closeLedgerConnectionAsync(); callback(err, null); } } public async signPersonalMessageAsync(msgParams: SignPersonalMessageParams, callback: (err: Error, result?: string) => void) { if (!_.isUndefined(this.ledgerEthConnection)) { callback(new Error('Another request is in progress.')); return; } this.ledgerEthConnection = await this.createLedgerConnectionAsync(); try { const derivationPath = this.getDerivationPath(); const result = await this.ledgerEthConnection.signPersonalMessage_async( derivationPath, ethUtil.stripHexPrefix(msgParams.data), ); const v = _.parseInt(result.v) - 27; let vHex = v.toString(16); if (vHex.length < 2) { vHex = `0${v}`; } const signature = `0x${result.r}${result.s}${vHex}`; await this.closeLedgerConnectionAsync(); callback(null, signature); } catch (err) { await this.closeLedgerConnectionAsync(); callback(err, null); } } private async createLedgerConnectionAsync() { if (!_.isUndefined(this.ledgerEthConnection)) { throw new Error('Multiple open connections to the Ledger disallowed.'); } const ledgerConnection = await ledger.comm_u2f.create_async(); const ledgerEthConnection = new ledger.eth(ledgerConnection); return ledgerEthConnection; } private async closeLedgerConnectionAsync() { if (_.isUndefined(this.ledgerEthConnection)) { return; } await this.ledgerEthConnection.comm.close_async(); this.ledgerEthConnection = undefined; } private getDerivationPath() { const derivationPath = `${this.path}/${this.pathIndex}`; return derivationPath; } } export const ledgerWalletSubproviderFactory = (getNetworkIdFn: () => number): LedgerWallet => { const ledgerWallet = new LedgerWallet(getNetworkIdFn); const ledgerWalletSubprovider = new HookedWalletSubprovider(ledgerWallet) as LedgerWallet; ledgerWalletSubprovider.getPath = ledgerWallet.getPath.bind(ledgerWallet); ledgerWalletSubprovider.setPath = ledgerWallet.setPath.bind(ledgerWallet); ledgerWalletSubprovider.setPathIndex = ledgerWallet.setPathIndex.bind(ledgerWallet); return ledgerWalletSubprovider; };