aboutsummaryrefslogblamecommitdiffstats
path: root/packages/website/ts/subproviders/ledger_wallet_subprovider_factory.ts
blob: bfabc90ae91efcfa38cce0dd3a4c115d9413b5b3 (plain) (tree)
1
2
3
4
5
6
7
8


                                            
                            
                                                                                  


                                                                                            



































































































































































                                                                                                            
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;
};