@ledgerhq/hw-app-eth 6.21.3 → 6.22.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -100
- package/erc20.js +1 -1
- package/lib/Eth.d.ts +30 -33
- package/lib/Eth.d.ts.map +1 -1
- package/lib/Eth.js +282 -324
- package/lib/Eth.js.map +1 -1
- package/lib/errors.d.ts +7 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +7 -0
- package/lib/errors.js.map +1 -0
- package/lib/services/ledger/contracts.d.ts +14 -0
- package/lib/services/ledger/contracts.d.ts.map +1 -0
- package/lib/{contracts.js → services/ledger/contracts.js} +0 -0
- package/lib/services/ledger/contracts.js.map +1 -0
- package/lib/services/ledger/erc20.d.ts +22 -0
- package/lib/services/ledger/erc20.d.ts.map +1 -0
- package/lib/{erc20.js → services/ledger/erc20.js} +0 -0
- package/lib/services/ledger/erc20.js.map +1 -0
- package/lib/services/ledger/index.d.ts +4 -0
- package/lib/services/ledger/index.d.ts.map +1 -0
- package/lib/services/ledger/index.js +191 -0
- package/lib/services/ledger/index.js.map +1 -0
- package/lib/services/ledger/loadConfig.d.ts +3 -0
- package/lib/services/ledger/loadConfig.d.ts.map +1 -0
- package/lib/{loadConfig.js → services/ledger/loadConfig.js} +0 -0
- package/lib/services/ledger/loadConfig.js.map +1 -0
- package/lib/services/ledger/nfts.d.ts +10 -0
- package/lib/services/ledger/nfts.d.ts.map +1 -0
- package/lib/{nfts.js → services/ledger/nfts.js} +10 -10
- package/lib/services/ledger/nfts.js.map +1 -0
- package/lib/services/types.d.ts +27 -0
- package/lib/services/types.d.ts.map +1 -0
- package/lib/services/types.js +3 -0
- package/lib/services/types.js.map +1 -0
- package/lib/utils.d.ts +8 -27
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +68 -68
- package/lib/utils.js.map +1 -1
- package/lib-es/Eth.d.ts +30 -33
- package/lib-es/Eth.d.ts.map +1 -1
- package/lib-es/Eth.js +271 -316
- package/lib-es/Eth.js.map +1 -1
- package/lib-es/errors.d.ts +7 -0
- package/lib-es/errors.d.ts.map +1 -0
- package/lib-es/errors.js +4 -0
- package/lib-es/errors.js.map +1 -0
- package/{lib → lib-es/services/ledger}/contracts.d.ts +1 -1
- package/lib-es/services/ledger/contracts.d.ts.map +1 -0
- package/lib-es/services/ledger/contracts.js +96 -0
- package/lib-es/services/ledger/contracts.js.map +1 -0
- package/{lib → lib-es/services/ledger}/erc20.d.ts +0 -0
- package/lib-es/services/ledger/erc20.d.ts.map +1 -0
- package/lib-es/services/ledger/erc20.js +64 -0
- package/lib-es/services/ledger/erc20.js.map +1 -0
- package/lib-es/services/ledger/index.d.ts +4 -0
- package/lib-es/services/ledger/index.d.ts.map +1 -0
- package/lib-es/services/ledger/index.js +189 -0
- package/lib-es/services/ledger/index.js.map +1 -0
- package/lib-es/services/ledger/loadConfig.d.ts +3 -0
- package/lib-es/services/ledger/loadConfig.d.ts.map +1 -0
- package/lib-es/services/ledger/loadConfig.js +20 -0
- package/lib-es/services/ledger/loadConfig.js.map +1 -0
- package/{lib → lib-es/services/ledger}/nfts.d.ts +2 -3
- package/lib-es/services/ledger/nfts.d.ts.map +1 -0
- package/lib-es/services/ledger/nfts.js +94 -0
- package/lib-es/services/ledger/nfts.js.map +1 -0
- package/lib-es/services/types.d.ts +27 -0
- package/lib-es/services/types.d.ts.map +1 -0
- package/lib-es/services/types.js +2 -0
- package/lib-es/services/types.js.map +1 -0
- package/lib-es/utils.d.ts +8 -27
- package/lib-es/utils.d.ts.map +1 -1
- package/lib-es/utils.js +66 -61
- package/lib-es/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/Eth.ts +139 -261
- package/src/errors.ts +8 -0
- package/src/{contracts.ts → services/ledger/contracts.ts} +1 -1
- package/src/{erc20.ts → services/ledger/erc20.ts} +0 -0
- package/src/services/ledger/index.ts +125 -0
- package/src/services/ledger/loadConfig.ts +14 -0
- package/src/{nfts.ts → services/ledger/nfts.ts} +11 -12
- package/src/services/types.ts +42 -0
- package/src/utils.ts +72 -94
- package/tests/Eth.test.ts +2 -2
- package/lib/contracts.d.ts.map +0 -1
- package/lib/contracts.js.map +0 -1
- package/lib/erc20.d.ts.map +0 -1
- package/lib/erc20.js.map +0 -1
- package/lib/loadConfig.d.ts +0 -7
- package/lib/loadConfig.d.ts.map +0 -1
- package/lib/loadConfig.js.map +0 -1
- package/lib/nfts.d.ts.map +0 -1
- package/lib/nfts.js.map +0 -1
- package/src/loadConfig.ts +0 -23
package/src/Eth.ts
CHANGED
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
********************************************************************************/
|
|
17
17
|
// FIXME drop:
|
|
18
|
-
import { splitPath, foreach } from "./utils";
|
|
19
|
-
import { log } from "@ledgerhq/logs";
|
|
20
|
-
import { EthAppPleaseEnableContractData } from "@ledgerhq/errors";
|
|
21
18
|
import type Transport from "@ledgerhq/hw-transport";
|
|
22
19
|
import { BigNumber } from "bignumber.js";
|
|
23
|
-
import {
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
26
|
-
import
|
|
27
|
-
import {
|
|
20
|
+
import { decodeTxInfo } from "./utils";
|
|
21
|
+
// NB: these are temporary import for the deprecated fallback mechanism
|
|
22
|
+
import { LedgerEthTransactionResolution, LoadConfig } from "./services/types";
|
|
23
|
+
import ledgerService from "./services/ledger";
|
|
24
|
+
import {
|
|
25
|
+
EthAppNftNotSupported,
|
|
26
|
+
EthAppPleaseEnableContractData,
|
|
27
|
+
} from "./errors";
|
|
28
28
|
|
|
29
29
|
export type StarkQuantizationType =
|
|
30
30
|
| "eth"
|
|
@@ -40,6 +40,22 @@ const starkQuantizationTypeMap = {
|
|
|
40
40
|
erc721mintable: 5,
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
function splitPath(path: string): number[] {
|
|
44
|
+
const result: number[] = [];
|
|
45
|
+
const components = path.split("/");
|
|
46
|
+
components.forEach((element) => {
|
|
47
|
+
let number = parseInt(element, 10);
|
|
48
|
+
if (isNaN(number)) {
|
|
49
|
+
return; // FIXME shouldn't it throws instead?
|
|
50
|
+
}
|
|
51
|
+
if (element.length > 1 && element[element.length - 1] === "'") {
|
|
52
|
+
number += 0x80000000;
|
|
53
|
+
}
|
|
54
|
+
result.push(number);
|
|
55
|
+
});
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
function hexBuffer(str: string): Buffer {
|
|
44
60
|
return Buffer.from(str.startsWith("0x") ? str.slice(2) : str, "hex");
|
|
45
61
|
}
|
|
@@ -168,119 +184,81 @@ export default class Eth {
|
|
|
168
184
|
}
|
|
169
185
|
|
|
170
186
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* @param {*} info: a blob from "erc20.js" utilities that contains all token information.
|
|
178
|
-
*
|
|
179
|
-
* @example
|
|
180
|
-
* import { byContractAddressAndChainId } from "@ledgerhq/hw-app-eth/erc20"
|
|
181
|
-
* const zrxInfo = byContractAddressAndChainId("0xe41d2489571d322189246dafa5ebde1f4699f498", chainId)
|
|
182
|
-
* if (zrxInfo) await appEth.provideERC20TokenInformation(zrxInfo)
|
|
183
|
-
* const signed = await appEth.signTransaction(path, rawTxHex)
|
|
184
|
-
*/
|
|
185
|
-
provideERC20TokenInformation({ data }: { data: Buffer }): Promise<boolean> {
|
|
186
|
-
return provideERC20TokenInformation(this.transport, data);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* You can sign a transaction and retrieve v, r, s given the raw transaction and the BIP 32 path of the account to sign
|
|
187
|
+
* You can sign a transaction and retrieve v, r, s given the raw transaction and the BIP 32 path of the account to sign.
|
|
188
|
+
*
|
|
189
|
+
* @param path: the BIP32 path to sign the transaction on
|
|
190
|
+
* @param rawTxHex: the raw ethereum transaction in hexadecimal to sign
|
|
191
|
+
* @param resolution: resolution is an object with all "resolved" metadata necessary to allow the device to clear sign information. This includes: ERC20 token information, plugins, contracts, NFT signatures,... You must explicitly provide something to avoid having a warning. By default, you can use Ledger's service or your own resolution service. See services/types.js for the contract. Setting the value to "null" will fallback everything to blind signing but will still allow the device to sign the transaction.
|
|
191
192
|
* @example
|
|
192
|
-
|
|
193
|
+
import ledgerService from "@ledgerhq/hw-app-eth/lib/services/ledger"
|
|
194
|
+
const tx = "e8018504e3b292008252089428ee52a8f3d6e5d15f8b131996950d7f296c7952872bd72a2487400080"; // raw tx to sign
|
|
195
|
+
const resolution = await ledgerService.resolveTransaction(tx);
|
|
196
|
+
const result = eth.signTransaction("44'/60'/0'/0/0", tx, resolution);
|
|
197
|
+
console.log(result);
|
|
193
198
|
*/
|
|
194
199
|
async signTransaction(
|
|
195
200
|
path: string,
|
|
196
|
-
rawTxHex: string
|
|
201
|
+
rawTxHex: string,
|
|
202
|
+
resolution?: LedgerEthTransactionResolution | null
|
|
197
203
|
): Promise<{
|
|
198
204
|
s: string;
|
|
199
205
|
v: string;
|
|
200
206
|
r: string;
|
|
201
207
|
}> {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
let response;
|
|
212
|
-
// Check if the TX is encoded following EIP 155
|
|
213
|
-
const rlpTx = ethers.utils.RLP.decode(rlpData).map((hex) =>
|
|
214
|
-
Buffer.from(hex.slice(2), "hex")
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
let vrsOffset = 0;
|
|
218
|
-
let chainId = new BigNumber(0);
|
|
219
|
-
let chainIdTruncated = 0;
|
|
220
|
-
|
|
221
|
-
const rlpDecoded = ethers.utils.RLP.decode(rlpData);
|
|
222
|
-
|
|
223
|
-
let decodedTx;
|
|
224
|
-
if (txType === 2) {
|
|
225
|
-
// EIP1559
|
|
226
|
-
decodedTx = {
|
|
227
|
-
data: rlpDecoded[7],
|
|
228
|
-
to: rlpDecoded[5],
|
|
229
|
-
chainId: rlpTx[0],
|
|
230
|
-
};
|
|
231
|
-
} else if (txType === 1) {
|
|
232
|
-
// EIP2930
|
|
233
|
-
decodedTx = {
|
|
234
|
-
data: rlpDecoded[6],
|
|
235
|
-
to: rlpDecoded[4],
|
|
236
|
-
chainId: rlpTx[0],
|
|
237
|
-
};
|
|
238
|
-
} else {
|
|
239
|
-
// Legacy tx
|
|
240
|
-
decodedTx = {
|
|
241
|
-
data: rlpDecoded[5],
|
|
242
|
-
to: rlpDecoded[3],
|
|
243
|
-
// Default to 1 for non EIP 155 txs
|
|
244
|
-
chainId: rlpTx.length > 6 ? rlpTx[6] : Buffer.from("0x01", "hex"),
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (txType === null && rlpTx.length > 6) {
|
|
249
|
-
const rlpVrs = Buffer.from(
|
|
250
|
-
ethers.utils.RLP.encode(rlpTx.slice(-3)).slice(2),
|
|
251
|
-
"hex"
|
|
208
|
+
if (resolution === undefined) {
|
|
209
|
+
console.warn(
|
|
210
|
+
"hw-app-eth: signTransaction(path, rawTxHex, resolution): " +
|
|
211
|
+
"please provide the 'resolution' parameter. " +
|
|
212
|
+
"See https://github.com/LedgerHQ/ledgerjs/blob/master/packages/hw-app-eth/README.md " +
|
|
213
|
+
"– the previous signature is deprecated and providing the 3rd 'resolution' parameter explicitly will become mandatory so you have the control on the resolution and the fallback mecanism (e.g. fallback to blind signing or not)." +
|
|
214
|
+
"// Possible solution:\n" +
|
|
215
|
+
" + import ledgerService from '@ledgerhq/hw-app-eth/lib/services/ledger';\n" +
|
|
216
|
+
" + const resolution = await ledgerService.resolveTransaction(rawTxHex);"
|
|
252
217
|
);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
vrsOffset += sizeOfListLen - 1;
|
|
266
|
-
}
|
|
218
|
+
resolution = await ledgerService
|
|
219
|
+
.resolveTransaction(rawTxHex, this.loadConfig, {
|
|
220
|
+
externalPlugins: true,
|
|
221
|
+
erc20: true,
|
|
222
|
+
})
|
|
223
|
+
.catch((e) => {
|
|
224
|
+
console.warn(
|
|
225
|
+
"an error occurred in resolveTransaction => fallback to blind signing: " +
|
|
226
|
+
String(e)
|
|
227
|
+
);
|
|
228
|
+
return null;
|
|
229
|
+
});
|
|
267
230
|
}
|
|
268
231
|
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
232
|
+
// provide to the device resolved information to make it clear sign the signature
|
|
233
|
+
if (resolution) {
|
|
234
|
+
for (const plugin of resolution.plugin) {
|
|
235
|
+
await setPlugin(this.transport, plugin);
|
|
236
|
+
}
|
|
237
|
+
for (const { payload, signature } of resolution.externalPlugin) {
|
|
238
|
+
await setExternalPlugin(this.transport, payload, signature);
|
|
239
|
+
}
|
|
240
|
+
for (const nft of resolution.nfts) {
|
|
241
|
+
await provideNFTInformation(this.transport, Buffer.from(nft, "hex"));
|
|
242
|
+
}
|
|
243
|
+
for (const data of resolution.erc20Tokens) {
|
|
244
|
+
await provideERC20TokenInformation(
|
|
245
|
+
this.transport,
|
|
246
|
+
Buffer.from(data, "hex")
|
|
247
|
+
);
|
|
278
248
|
}
|
|
279
|
-
chainIdTruncated = chainIdTruncatedBuf.readUInt32BE(0);
|
|
280
249
|
}
|
|
281
250
|
|
|
251
|
+
const rawTx = Buffer.from(rawTxHex, "hex");
|
|
252
|
+
const { vrsOffset, txType, chainId, chainIdTruncated } = decodeTxInfo(
|
|
253
|
+
rawTx
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const paths = splitPath(path);
|
|
257
|
+
let response;
|
|
258
|
+
let offset = 0;
|
|
282
259
|
while (offset !== rawTx.length) {
|
|
283
|
-
const
|
|
260
|
+
const first = offset === 0;
|
|
261
|
+
const maxChunkSize = first ? 150 - 1 - paths.length * 4 : 150;
|
|
284
262
|
let chunkSize =
|
|
285
263
|
offset + maxChunkSize > rawTx.length
|
|
286
264
|
? rawTx.length - offset
|
|
@@ -292,10 +270,10 @@ export default class Eth {
|
|
|
292
270
|
}
|
|
293
271
|
|
|
294
272
|
const buffer = Buffer.alloc(
|
|
295
|
-
|
|
273
|
+
first ? 1 + paths.length * 4 + chunkSize : chunkSize
|
|
296
274
|
);
|
|
297
275
|
|
|
298
|
-
if (
|
|
276
|
+
if (first) {
|
|
299
277
|
buffer[0] = paths.length;
|
|
300
278
|
paths.forEach((element, index) => {
|
|
301
279
|
buffer.writeUInt32BE(element, 1 + 4 * index);
|
|
@@ -305,137 +283,42 @@ export default class Eth {
|
|
|
305
283
|
rawTx.copy(buffer, 0, offset, offset + chunkSize);
|
|
306
284
|
}
|
|
307
285
|
|
|
308
|
-
|
|
286
|
+
response = await this.transport
|
|
287
|
+
.send(0xe0, 0x04, first ? 0x00 : 0x80, 0x00, buffer)
|
|
288
|
+
.catch((e) => {
|
|
289
|
+
throw remapTransactionRelatedErrors(e);
|
|
290
|
+
});
|
|
291
|
+
|
|
309
292
|
offset += chunkSize;
|
|
310
293
|
}
|
|
311
294
|
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
address,
|
|
315
|
-
chainIdTruncated,
|
|
316
|
-
this.loadConfig
|
|
317
|
-
);
|
|
318
|
-
if (nftInfo) {
|
|
319
|
-
log(
|
|
320
|
-
"ethereum",
|
|
321
|
-
"loading nft info for " +
|
|
322
|
-
nftInfo.contractAddress +
|
|
323
|
-
" (" +
|
|
324
|
-
nftInfo.collectionName +
|
|
325
|
-
")"
|
|
326
|
-
);
|
|
327
|
-
await provideNFTInformation(this.transport, nftInfo.data);
|
|
328
|
-
} else {
|
|
329
|
-
const erc20Info = byContractAddressAndChainId(
|
|
330
|
-
address,
|
|
331
|
-
chainIdTruncated
|
|
332
|
-
);
|
|
333
|
-
if (erc20Info) {
|
|
334
|
-
log(
|
|
335
|
-
"ethereum",
|
|
336
|
-
"loading erc20token info for " +
|
|
337
|
-
erc20Info.contractAddress +
|
|
338
|
-
" (" +
|
|
339
|
-
erc20Info.ticker +
|
|
340
|
-
")"
|
|
341
|
-
);
|
|
342
|
-
await provideERC20TokenInformation(this.transport, erc20Info.data);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
if (decodedTx.data.length >= 10) {
|
|
348
|
-
const selector = decodedTx.data.substring(0, 10);
|
|
349
|
-
const nftPluginPayload = await loadNftPlugin(
|
|
350
|
-
decodedTx.to,
|
|
351
|
-
selector,
|
|
352
|
-
chainIdTruncated,
|
|
353
|
-
this.loadConfig
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
if (nftPluginPayload) {
|
|
357
|
-
await setPlugin(this.transport, nftPluginPayload);
|
|
358
|
-
} else {
|
|
359
|
-
const infos = await loadInfosForContractMethod(
|
|
360
|
-
decodedTx.to,
|
|
361
|
-
selector,
|
|
362
|
-
chainIdTruncated,
|
|
363
|
-
this.loadConfig
|
|
364
|
-
);
|
|
295
|
+
const response_byte: number = response[0];
|
|
296
|
+
let v = "";
|
|
365
297
|
|
|
366
|
-
|
|
367
|
-
|
|
298
|
+
if (chainId.times(2).plus(35).plus(1).isGreaterThan(255)) {
|
|
299
|
+
const oneByteChainId = (chainIdTruncated * 2 + 35) % 256;
|
|
368
300
|
|
|
369
|
-
|
|
370
|
-
log("ethereum", "loading plugin for " + selector);
|
|
371
|
-
await setExternalPlugin(this.transport, payload, signature);
|
|
372
|
-
}
|
|
301
|
+
const ecc_parity = Math.abs(response_byte - oneByteChainId);
|
|
373
302
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (seg === "-1" && Array.isArray(value)) {
|
|
381
|
-
return value[value.length - 1];
|
|
382
|
-
}
|
|
383
|
-
return value[seg];
|
|
384
|
-
}, args);
|
|
385
|
-
await provideForContract(address);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
} else {
|
|
389
|
-
log("ethereum", "no infos for selector " + selector);
|
|
390
|
-
}
|
|
303
|
+
if (txType != null) {
|
|
304
|
+
// For EIP2930 and EIP1559 tx, v is simply the parity.
|
|
305
|
+
v = ecc_parity % 2 == 1 ? "00" : "01";
|
|
306
|
+
} else {
|
|
307
|
+
// Legacy type transaction with a big chain ID
|
|
308
|
+
v = chainId.times(2).plus(35).plus(ecc_parity).toString(16);
|
|
391
309
|
}
|
|
392
|
-
|
|
310
|
+
} else {
|
|
311
|
+
v = response_byte.toString(16);
|
|
393
312
|
}
|
|
394
313
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
response = apduResponse;
|
|
400
|
-
})
|
|
401
|
-
).then(
|
|
402
|
-
() => {
|
|
403
|
-
const response_byte: number = response.slice(0, 1)[0];
|
|
404
|
-
let v = "";
|
|
405
|
-
|
|
406
|
-
if (chainId.times(2).plus(35).plus(1).isGreaterThan(255)) {
|
|
407
|
-
const oneByteChainId = (chainIdTruncated * 2 + 35) % 256;
|
|
408
|
-
|
|
409
|
-
const ecc_parity = Math.abs(response_byte - oneByteChainId);
|
|
410
|
-
|
|
411
|
-
if (txType != null) {
|
|
412
|
-
// For EIP2930 and EIP1559 tx, v is simply the parity.
|
|
413
|
-
v = ecc_parity % 2 == 1 ? "00" : "01";
|
|
414
|
-
} else {
|
|
415
|
-
// Legacy type transaction with a big chain ID
|
|
416
|
-
v = chainId.times(2).plus(35).plus(ecc_parity).toString(16);
|
|
417
|
-
}
|
|
418
|
-
} else {
|
|
419
|
-
v = response_byte.toString(16);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Make sure v has is prefixed with a 0 if its length is odd ("1" -> "01").
|
|
423
|
-
if (v.length % 2 == 1) {
|
|
424
|
-
v = "0" + v;
|
|
425
|
-
}
|
|
314
|
+
// Make sure v has is prefixed with a 0 if its length is odd ("1" -> "01").
|
|
315
|
+
if (v.length % 2 == 1) {
|
|
316
|
+
v = "0" + v;
|
|
317
|
+
}
|
|
426
318
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
v,
|
|
431
|
-
r,
|
|
432
|
-
s,
|
|
433
|
-
};
|
|
434
|
-
},
|
|
435
|
-
(e) => {
|
|
436
|
-
throw remapTransactionRelatedErrors(e);
|
|
437
|
-
}
|
|
438
|
-
);
|
|
319
|
+
const r = response.slice(1, 1 + 32).toString("hex");
|
|
320
|
+
const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
|
|
321
|
+
return { v, r, s };
|
|
439
322
|
}
|
|
440
323
|
|
|
441
324
|
/**
|
|
@@ -470,7 +353,7 @@ export default class Eth {
|
|
|
470
353
|
console.log("Signature 0x" + result['r'] + result['s'] + v);
|
|
471
354
|
})
|
|
472
355
|
*/
|
|
473
|
-
signPersonalMessage(
|
|
356
|
+
async signPersonalMessage(
|
|
474
357
|
path: string,
|
|
475
358
|
messageHex: string
|
|
476
359
|
): Promise<{
|
|
@@ -481,7 +364,6 @@ export default class Eth {
|
|
|
481
364
|
const paths = splitPath(path);
|
|
482
365
|
let offset = 0;
|
|
483
366
|
const message = Buffer.from(messageHex, "hex");
|
|
484
|
-
const toSend: Buffer[] = [];
|
|
485
367
|
let response;
|
|
486
368
|
|
|
487
369
|
while (offset !== message.length) {
|
|
@@ -510,26 +392,21 @@ export default class Eth {
|
|
|
510
392
|
message.copy(buffer, 0, offset, offset + chunkSize);
|
|
511
393
|
}
|
|
512
394
|
|
|
513
|
-
|
|
395
|
+
response = await this.transport.send(
|
|
396
|
+
0xe0,
|
|
397
|
+
0x08,
|
|
398
|
+
offset === 0 ? 0x00 : 0x80,
|
|
399
|
+
0x00,
|
|
400
|
+
buffer
|
|
401
|
+
);
|
|
402
|
+
|
|
514
403
|
offset += chunkSize;
|
|
515
404
|
}
|
|
516
405
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
response = apduResponse;
|
|
522
|
-
})
|
|
523
|
-
).then(() => {
|
|
524
|
-
const v = response[0];
|
|
525
|
-
const r = response.slice(1, 1 + 32).toString("hex");
|
|
526
|
-
const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
|
|
527
|
-
return {
|
|
528
|
-
v,
|
|
529
|
-
r,
|
|
530
|
-
s,
|
|
531
|
-
};
|
|
532
|
-
});
|
|
406
|
+
const v = response[0];
|
|
407
|
+
const r = response.slice(1, 1 + 32).toString("hex");
|
|
408
|
+
const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
|
|
409
|
+
return { v, r, s };
|
|
533
410
|
}
|
|
534
411
|
|
|
535
412
|
/**
|
|
@@ -1256,27 +1133,28 @@ export default class Eth {
|
|
|
1256
1133
|
);
|
|
1257
1134
|
}
|
|
1258
1135
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1136
|
+
provideERC20TokenInformation({ data }: { data: Buffer }): Promise<boolean> {
|
|
1137
|
+
console.warn(
|
|
1138
|
+
"hw-app-eth: eth.provideERC20TokenInformation is deprecated. signTransaction solves this for you when providing it in `resolution`."
|
|
1139
|
+
);
|
|
1140
|
+
return provideERC20TokenInformation(this.transport, data);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1265
1143
|
setExternalPlugin(
|
|
1266
1144
|
pluginName: string,
|
|
1267
1145
|
contractAddress: string,
|
|
1268
1146
|
selector: string
|
|
1269
1147
|
): Promise<boolean> {
|
|
1148
|
+
console.warn(
|
|
1149
|
+
"hw-app-eth: eth.setExternalPlugin is deprecated. signTransaction solves this for you when providing it in `resolution`."
|
|
1150
|
+
);
|
|
1270
1151
|
return setExternalPlugin(this.transport, pluginName, selector);
|
|
1271
1152
|
}
|
|
1272
1153
|
|
|
1273
|
-
/**
|
|
1274
|
-
* Set the plugin (internal or external) that should be used to parse the next transaction
|
|
1275
|
-
*
|
|
1276
|
-
* @param data string containing the payload and signature that will be parsed and verified by the device.
|
|
1277
|
-
* @return True if the method was executed successfully
|
|
1278
|
-
*/
|
|
1279
1154
|
setPlugin(data: string): Promise<boolean> {
|
|
1155
|
+
console.warn(
|
|
1156
|
+
"hw-app-eth: eth.setPlugin is deprecated. signTransaction solves this for you when providing it in `resolution`."
|
|
1157
|
+
);
|
|
1280
1158
|
return setPlugin(this.transport, data);
|
|
1281
1159
|
}
|
|
1282
1160
|
}
|
|
@@ -1312,8 +1190,8 @@ function provideNFTInformation(
|
|
|
1312
1190
|
return false;
|
|
1313
1191
|
}
|
|
1314
1192
|
if (e && e.statusCode === 0x6d00) {
|
|
1315
|
-
//
|
|
1316
|
-
|
|
1193
|
+
// older version of ETH app => error because we don't allow blind sign when NFT is explicitly requested to be resolved.
|
|
1194
|
+
throw new EthAppNftNotSupported();
|
|
1317
1195
|
}
|
|
1318
1196
|
throw e;
|
|
1319
1197
|
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createCustomErrorClass } from "@ledgerhq/errors";
|
|
2
|
+
|
|
3
|
+
export const EthAppPleaseEnableContractData = createCustomErrorClass(
|
|
4
|
+
"EthAppPleaseEnableContractData"
|
|
5
|
+
);
|
|
6
|
+
export const EthAppNftNotSupported = createCustomErrorClass(
|
|
7
|
+
"EthAppNftNotSupported"
|
|
8
|
+
);
|
|
File without changes
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// This implements the resolution of a Transaction using Ledger's own API
|
|
2
|
+
import { log } from "@ledgerhq/logs";
|
|
3
|
+
import { ethers } from "ethers";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
LedgerEthTransactionResolution,
|
|
7
|
+
LedgerEthTransactionService,
|
|
8
|
+
} from "../types";
|
|
9
|
+
import { loadInfosForContractMethod } from "./contracts";
|
|
10
|
+
import { byContractAddressAndChainId } from "./erc20";
|
|
11
|
+
import { getNFTInfo, loadNftPlugin } from "./nfts";
|
|
12
|
+
import { decodeTxInfo } from "../../utils";
|
|
13
|
+
|
|
14
|
+
const ledgerService: LedgerEthTransactionService = {
|
|
15
|
+
resolveTransaction: async (rawTxHex, loadConfig, resolutionConfig) => {
|
|
16
|
+
const resolution: LedgerEthTransactionResolution = {
|
|
17
|
+
erc20Tokens: [],
|
|
18
|
+
nfts: [],
|
|
19
|
+
externalPlugin: [],
|
|
20
|
+
plugin: [],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function provideERC20TokenInformation(dataHex: string) {
|
|
24
|
+
resolution.erc20Tokens.push(dataHex);
|
|
25
|
+
}
|
|
26
|
+
function provideNFTInformation(dataHex: string) {
|
|
27
|
+
resolution.nfts.push(dataHex);
|
|
28
|
+
}
|
|
29
|
+
function setExternalPlugin(payload: string, signature: string) {
|
|
30
|
+
resolution.externalPlugin.push({ payload, signature });
|
|
31
|
+
}
|
|
32
|
+
function setPlugin(dataHex: string) {
|
|
33
|
+
resolution.plugin.push(dataHex);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const rawTx = Buffer.from(rawTxHex, "hex");
|
|
37
|
+
const { decodedTx, chainIdTruncated } = decodeTxInfo(rawTx);
|
|
38
|
+
const provideForContract = async (address) => {
|
|
39
|
+
const nftInfo = resolutionConfig.nft
|
|
40
|
+
? await getNFTInfo(address, chainIdTruncated, loadConfig)
|
|
41
|
+
: null;
|
|
42
|
+
if (nftInfo) {
|
|
43
|
+
log(
|
|
44
|
+
"ethereum",
|
|
45
|
+
"loaded nft info for " +
|
|
46
|
+
nftInfo.contractAddress +
|
|
47
|
+
" (" +
|
|
48
|
+
nftInfo.collectionName +
|
|
49
|
+
")"
|
|
50
|
+
);
|
|
51
|
+
provideNFTInformation(nftInfo.data);
|
|
52
|
+
} else {
|
|
53
|
+
const erc20Info = byContractAddressAndChainId(
|
|
54
|
+
address,
|
|
55
|
+
chainIdTruncated
|
|
56
|
+
);
|
|
57
|
+
if (erc20Info) {
|
|
58
|
+
log(
|
|
59
|
+
"ethereum",
|
|
60
|
+
"loaded erc20token info for " +
|
|
61
|
+
erc20Info.contractAddress +
|
|
62
|
+
" (" +
|
|
63
|
+
erc20Info.ticker +
|
|
64
|
+
")"
|
|
65
|
+
);
|
|
66
|
+
provideERC20TokenInformation(erc20Info.data.toString("hex"));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (decodedTx.data.length >= 10) {
|
|
72
|
+
const selector = decodedTx.data.substring(0, 10);
|
|
73
|
+
const nftPluginPayload = resolutionConfig.nft
|
|
74
|
+
? await loadNftPlugin(
|
|
75
|
+
decodedTx.to,
|
|
76
|
+
selector,
|
|
77
|
+
chainIdTruncated,
|
|
78
|
+
loadConfig
|
|
79
|
+
)
|
|
80
|
+
: null;
|
|
81
|
+
|
|
82
|
+
if (nftPluginPayload) {
|
|
83
|
+
setPlugin(nftPluginPayload);
|
|
84
|
+
} else {
|
|
85
|
+
const infos = resolutionConfig.externalPlugins
|
|
86
|
+
? await loadInfosForContractMethod(
|
|
87
|
+
decodedTx.to,
|
|
88
|
+
selector,
|
|
89
|
+
chainIdTruncated,
|
|
90
|
+
loadConfig
|
|
91
|
+
)
|
|
92
|
+
: null;
|
|
93
|
+
|
|
94
|
+
if (infos) {
|
|
95
|
+
const { plugin, payload, signature, erc20OfInterest, abi } = infos;
|
|
96
|
+
|
|
97
|
+
if (plugin) {
|
|
98
|
+
log("ethereum", "found plugin for " + selector);
|
|
99
|
+
setExternalPlugin(payload, signature);
|
|
100
|
+
}
|
|
101
|
+
if (erc20OfInterest && erc20OfInterest.length && abi) {
|
|
102
|
+
const contract = new ethers.utils.Interface(abi);
|
|
103
|
+
const args = contract.parseTransaction(decodedTx).args;
|
|
104
|
+
for (const path of erc20OfInterest) {
|
|
105
|
+
const address = path.split(".").reduce((value, seg) => {
|
|
106
|
+
if (seg === "-1" && Array.isArray(value)) {
|
|
107
|
+
return value[value.length - 1];
|
|
108
|
+
}
|
|
109
|
+
return value[seg];
|
|
110
|
+
}, args);
|
|
111
|
+
provideForContract(address);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
log("ethereum", "no infos for selector " + selector);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
provideForContract(decodedTx.to);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return resolution;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default ledgerService;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LoadConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
const defaultLoadConfig = {
|
|
4
|
+
nftExplorerBaseURL: null, // set a value when an official production endpoint is released
|
|
5
|
+
pluginBaseURL: "https://cdn.live.ledger.com",
|
|
6
|
+
extraPlugins: null,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function getLoadConfig(userLoadConfig?: LoadConfig): LoadConfig {
|
|
10
|
+
return {
|
|
11
|
+
...defaultLoadConfig,
|
|
12
|
+
...userLoadConfig,
|
|
13
|
+
};
|
|
14
|
+
}
|