aboutsummaryrefslogtreecommitdiffstats
path: root/packages/website/ts/subproviders/ledger_wallet_subprovider_factory.ts
blob: df0c5a4dbde2bfebab40423c98ac0f853cd05440 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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;
};