aboutsummaryrefslogtreecommitdiffstats
path: root/packages/website/ts/subproviders/ledger_wallet_subprovider_factory.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/website/ts/subproviders/ledger_wallet_subprovider_factory.ts')
-rw-r--r--packages/website/ts/subproviders/ledger_wallet_subprovider_factory.ts172
1 files changed, 172 insertions, 0 deletions
diff --git a/packages/website/ts/subproviders/ledger_wallet_subprovider_factory.ts b/packages/website/ts/subproviders/ledger_wallet_subprovider_factory.ts
new file mode 100644
index 000000000..df0c5a4db
--- /dev/null
+++ b/packages/website/ts/subproviders/ledger_wallet_subprovider_factory.ts
@@ -0,0 +1,172 @@
+import * as _ from 'lodash';
+import Web3 = require('web3');
+import * as EthereumTx from 'ethereumjs-tx';
+import ethUtil = require('ethereumjs-util');
+import * as ledger from 'ledgerco';
+import HookedWalletSubprovider = require('web3-provider-engine/subproviders/hooked-wallet');
+import {constants} from 'ts/utils/constants';
+import {LedgerEthConnection, SignPersonalMessageParams, TxParams} from 'ts/types';
+
+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;
+};