/
ledger.ts
171 lines (146 loc) · 4.85 KB
/
ledger.ts
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
import { Transaction as EthTx, TxData } from 'ethereumjs-tx';
import { addHexPrefix, toBuffer } from 'ethereumjs-util';
import TransportU2F from '@ledgerhq/hw-transport-u2f';
import LedgerEth from '@ledgerhq/hw-app-eth';
import { translateRaw } from 'v2/translations';
import { getTransactionFields } from 'v2/services/EthService';
import { HardwareWallet, ChainCodeResponse } from './hardware';
import { byContractAddress } from '@ledgerhq/hw-app-eth/erc20';
// Ledger throws a few types of errors
interface U2FError {
metaData: {
type: string;
code: number;
};
}
interface ErrorWithId {
id: string;
message: string;
name: string;
stack: string;
}
type LedgerError = U2FError | ErrorWithId | Error | string;
export class LedgerWallet extends HardwareWallet {
public static async getChainCode(dpath: string): Promise<ChainCodeResponse> {
return makeApp()
.then(app => app.getAddress(dpath, false, true))
.then(res => {
return {
publicKey: res.publicKey,
chainCode: res.chainCode
};
})
.catch((err: LedgerError) => {
throw new Error(ledgerErrToMessage(err));
});
}
constructor(address: string, dPath: string, index: number) {
super(address, dPath, index);
}
public async signRawTransaction(t: EthTx): Promise<Buffer> {
const txFields = getTransactionFields(t);
t.v = toBuffer(t.getChainId());
t.r = toBuffer(0);
t.s = toBuffer(0);
try {
const ethApp = await makeApp();
if (t.getChainId() === 1) {
const tokenInfo = byContractAddress(t.to.toString('hex'));
if (tokenInfo) {
await ethApp.provideERC20TokenInformation(tokenInfo);
}
}
const result = await ethApp.signTransaction(this.getPath(), t.serialize().toString('hex'));
let v = result.v;
if (t.getChainId() > 0) {
// EIP155 support. check/recalc signature v value.
// Please see https://github.com/LedgerHQ/blue-app-eth/commit/8260268b0214810872dabd154b476f5bb859aac0
// currently, ledger returns only 1-byte truncated signatur_v
const rv = parseInt(v, 16);
let cv = t.getChainId() * 2 + 35; // calculated signature v, without signature bit.
/* tslint:disable no-bitwise */
if (rv !== cv && (rv & cv) !== rv) {
// (rv !== cv) : for v is truncated byte case
// (rv & cv): make cv to truncated byte
// (rv & cv) !== rv: signature v bit needed
cv += 1; // add signature v bit.
}
v = cv.toString(16);
}
const txToSerialize: TxData = {
...txFields,
v: addHexPrefix(v),
r: addHexPrefix(result.r),
s: addHexPrefix(result.s)
};
return new EthTx(txToSerialize).serialize();
} catch (err) {
throw Error(err + '. Check to make sure contract data is on');
}
}
public async signMessage(msg: string): Promise<string> {
if (!msg) {
throw Error('No message to sign');
}
try {
const msgHex = Buffer.from(msg).toString('hex');
const ethApp = await makeApp();
const signed = await ethApp.signPersonalMessage(this.getPath(), msgHex);
const combined = addHexPrefix(signed.r + signed.s + signed.v.toString(16));
return combined;
} catch (err) {
throw new Error(ledgerErrToMessage(err));
}
}
public async displayAddress() {
const path = `${this.dPath}/${this.index}`;
try {
const ethApp = await makeApp();
await ethApp.getAddress(path, true, false);
return true;
} catch (err) {
console.error('Failed to display Ledger address:', err);
return false;
}
}
public getWalletType(): string {
return translateRaw('X_LEDGER');
}
}
async function makeApp() {
const transport = await TransportU2F.create();
return new LedgerEth(transport);
}
const isU2FError = (err: LedgerError): err is U2FError => !!err && !!(err as U2FError).metaData;
const isStringError = (err: LedgerError): err is string => typeof err === 'string';
const isErrorWithId = (err: LedgerError): err is ErrorWithId =>
err.hasOwnProperty('id') && err.hasOwnProperty('message');
function ledgerErrToMessage(err: LedgerError) {
// https://developers.yubico.com/U2F/Libraries/Client_error_codes.html
if (isU2FError(err)) {
// Timeout
if (err.metaData.code === 5) {
return translateRaw('LEDGER_TIMEOUT');
}
return err.metaData.type;
}
if (isStringError(err)) {
// Wrong app logged into
if (err.includes('6804')) {
return translateRaw('LEDGER_WRONG_APP');
}
// Ledger locked
if (err.includes('6801')) {
return translateRaw('LEDGER_LOCKED');
}
return err;
}
if (isErrorWithId(err)) {
// Browser doesn't support U2F
if (err.message.includes('U2F not supported')) {
return translateRaw('U2F_NOT_SUPPORTED');
}
}
// Other
return err.toString();
}