@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.
Files changed (95) hide show
  1. package/README.md +66 -100
  2. package/erc20.js +1 -1
  3. package/lib/Eth.d.ts +30 -33
  4. package/lib/Eth.d.ts.map +1 -1
  5. package/lib/Eth.js +282 -324
  6. package/lib/Eth.js.map +1 -1
  7. package/lib/errors.d.ts +7 -0
  8. package/lib/errors.d.ts.map +1 -0
  9. package/lib/errors.js +7 -0
  10. package/lib/errors.js.map +1 -0
  11. package/lib/services/ledger/contracts.d.ts +14 -0
  12. package/lib/services/ledger/contracts.d.ts.map +1 -0
  13. package/lib/{contracts.js → services/ledger/contracts.js} +0 -0
  14. package/lib/services/ledger/contracts.js.map +1 -0
  15. package/lib/services/ledger/erc20.d.ts +22 -0
  16. package/lib/services/ledger/erc20.d.ts.map +1 -0
  17. package/lib/{erc20.js → services/ledger/erc20.js} +0 -0
  18. package/lib/services/ledger/erc20.js.map +1 -0
  19. package/lib/services/ledger/index.d.ts +4 -0
  20. package/lib/services/ledger/index.d.ts.map +1 -0
  21. package/lib/services/ledger/index.js +191 -0
  22. package/lib/services/ledger/index.js.map +1 -0
  23. package/lib/services/ledger/loadConfig.d.ts +3 -0
  24. package/lib/services/ledger/loadConfig.d.ts.map +1 -0
  25. package/lib/{loadConfig.js → services/ledger/loadConfig.js} +0 -0
  26. package/lib/services/ledger/loadConfig.js.map +1 -0
  27. package/lib/services/ledger/nfts.d.ts +10 -0
  28. package/lib/services/ledger/nfts.d.ts.map +1 -0
  29. package/lib/{nfts.js → services/ledger/nfts.js} +10 -10
  30. package/lib/services/ledger/nfts.js.map +1 -0
  31. package/lib/services/types.d.ts +27 -0
  32. package/lib/services/types.d.ts.map +1 -0
  33. package/lib/services/types.js +3 -0
  34. package/lib/services/types.js.map +1 -0
  35. package/lib/utils.d.ts +8 -27
  36. package/lib/utils.d.ts.map +1 -1
  37. package/lib/utils.js +68 -68
  38. package/lib/utils.js.map +1 -1
  39. package/lib-es/Eth.d.ts +30 -33
  40. package/lib-es/Eth.d.ts.map +1 -1
  41. package/lib-es/Eth.js +271 -316
  42. package/lib-es/Eth.js.map +1 -1
  43. package/lib-es/errors.d.ts +7 -0
  44. package/lib-es/errors.d.ts.map +1 -0
  45. package/lib-es/errors.js +4 -0
  46. package/lib-es/errors.js.map +1 -0
  47. package/{lib → lib-es/services/ledger}/contracts.d.ts +1 -1
  48. package/lib-es/services/ledger/contracts.d.ts.map +1 -0
  49. package/lib-es/services/ledger/contracts.js +96 -0
  50. package/lib-es/services/ledger/contracts.js.map +1 -0
  51. package/{lib → lib-es/services/ledger}/erc20.d.ts +0 -0
  52. package/lib-es/services/ledger/erc20.d.ts.map +1 -0
  53. package/lib-es/services/ledger/erc20.js +64 -0
  54. package/lib-es/services/ledger/erc20.js.map +1 -0
  55. package/lib-es/services/ledger/index.d.ts +4 -0
  56. package/lib-es/services/ledger/index.d.ts.map +1 -0
  57. package/lib-es/services/ledger/index.js +189 -0
  58. package/lib-es/services/ledger/index.js.map +1 -0
  59. package/lib-es/services/ledger/loadConfig.d.ts +3 -0
  60. package/lib-es/services/ledger/loadConfig.d.ts.map +1 -0
  61. package/lib-es/services/ledger/loadConfig.js +20 -0
  62. package/lib-es/services/ledger/loadConfig.js.map +1 -0
  63. package/{lib → lib-es/services/ledger}/nfts.d.ts +2 -3
  64. package/lib-es/services/ledger/nfts.d.ts.map +1 -0
  65. package/lib-es/services/ledger/nfts.js +94 -0
  66. package/lib-es/services/ledger/nfts.js.map +1 -0
  67. package/lib-es/services/types.d.ts +27 -0
  68. package/lib-es/services/types.d.ts.map +1 -0
  69. package/lib-es/services/types.js +2 -0
  70. package/lib-es/services/types.js.map +1 -0
  71. package/lib-es/utils.d.ts +8 -27
  72. package/lib-es/utils.d.ts.map +1 -1
  73. package/lib-es/utils.js +66 -61
  74. package/lib-es/utils.js.map +1 -1
  75. package/package.json +3 -3
  76. package/src/Eth.ts +139 -261
  77. package/src/errors.ts +8 -0
  78. package/src/{contracts.ts → services/ledger/contracts.ts} +1 -1
  79. package/src/{erc20.ts → services/ledger/erc20.ts} +0 -0
  80. package/src/services/ledger/index.ts +125 -0
  81. package/src/services/ledger/loadConfig.ts +14 -0
  82. package/src/{nfts.ts → services/ledger/nfts.ts} +11 -12
  83. package/src/services/types.ts +42 -0
  84. package/src/utils.ts +72 -94
  85. package/tests/Eth.test.ts +2 -2
  86. package/lib/contracts.d.ts.map +0 -1
  87. package/lib/contracts.js.map +0 -1
  88. package/lib/erc20.d.ts.map +0 -1
  89. package/lib/erc20.js.map +0 -1
  90. package/lib/loadConfig.d.ts +0 -7
  91. package/lib/loadConfig.d.ts.map +0 -1
  92. package/lib/loadConfig.js.map +0 -1
  93. package/lib/nfts.d.ts.map +0 -1
  94. package/lib/nfts.js.map +0 -1
  95. 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 { ethers } from "ethers";
24
- import { byContractAddressAndChainId } from "./erc20";
25
- import { loadInfosForContractMethod } from "./contracts";
26
- import type { LoadConfig } from "./loadConfig";
27
- import { getNFTInfo, loadNftPlugin } from "./nfts";
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
- * This commands provides a trusted description of an ERC 20 token
172
- * to associate a contract address with a ticker and number of decimals.
173
- *
174
- * It shall be run immediately before performing a transaction involving a contract
175
- * calling this contract address to display the proper token information to the user if necessary.
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
- eth.signTransaction("44'/60'/0'/0/0", "e8018504e3b292008252089428ee52a8f3d6e5d15f8b131996950d7f296c7952872bd72a2487400080").then(result => ...)
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
- const paths = splitPath(path);
203
- let offset = 0;
204
-
205
- const rawTx = Buffer.from(rawTxHex, "hex");
206
- const VALID_TYPES = [1, 2];
207
- const txType = VALID_TYPES.includes(rawTx[0]) ? rawTx[0] : null;
208
- const rlpData = txType === null ? rawTx : rawTx.slice(1, rawTxHex.length);
209
-
210
- const toSend: Buffer[] = [];
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
- vrsOffset = rawTx.length - (rlpVrs.length - 1);
255
-
256
- // First byte > 0xf7 means the length of the list length doesn't fit in a single byte.
257
- if (rlpVrs[0] > 0xf7) {
258
- // Increment vrsOffset to account for that extra byte.
259
- vrsOffset++;
260
-
261
- // Compute size of the list length.
262
- const sizeOfListLen = rlpVrs[0] - 0xf7;
263
-
264
- // Increase rlpOffset by the size of the list length.
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
- const chainIdSrc = decodedTx.chainId;
270
- if (chainIdSrc) {
271
- // Using BigNumber because chainID could be any uint256.
272
- chainId = new BigNumber(chainIdSrc.toString("hex"), 16);
273
- const chainIdTruncatedBuf = Buffer.alloc(4);
274
- if (chainIdSrc.length > 4) {
275
- chainIdSrc.copy(chainIdTruncatedBuf);
276
- } else {
277
- chainIdSrc.copy(chainIdTruncatedBuf, 4 - chainIdSrc.length);
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 maxChunkSize = offset === 0 ? 150 - 1 - paths.length * 4 : 150;
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
- offset === 0 ? 1 + paths.length * 4 + chunkSize : chunkSize
273
+ first ? 1 + paths.length * 4 + chunkSize : chunkSize
296
274
  );
297
275
 
298
- if (offset === 0) {
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
- toSend.push(buffer);
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 provideForContract = async (address) => {
313
- const nftInfo = await getNFTInfo(
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
- if (infos) {
367
- const { plugin, payload, signature, erc20OfInterest, abi } = infos;
298
+ if (chainId.times(2).plus(35).plus(1).isGreaterThan(255)) {
299
+ const oneByteChainId = (chainIdTruncated * 2 + 35) % 256;
368
300
 
369
- if (plugin) {
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
- if (erc20OfInterest && erc20OfInterest.length && abi) {
375
- const contract = new ethers.utils.Interface(abi);
376
- const args = contract.parseTransaction(decodedTx).args;
377
-
378
- for (path of erc20OfInterest) {
379
- const address = path.split(".").reduce((value, seg) => {
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
- await provideForContract(decodedTx.to);
310
+ } else {
311
+ v = response_byte.toString(16);
393
312
  }
394
313
 
395
- return foreach(toSend, (data, i) =>
396
- this.transport
397
- .send(0xe0, 0x04, i === 0 ? 0x00 : 0x80, 0x00, data)
398
- .then((apduResponse) => {
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
- const r = response.slice(1, 1 + 32).toString("hex");
428
- const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
429
- return {
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
- toSend.push(buffer);
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
- return foreach(toSend, (data, i) =>
518
- this.transport
519
- .send(0xe0, 0x08, i === 0 ? 0x00 : 0x80, 0x00, data)
520
- .then((apduResponse) => {
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
- * Set the name of the external plugin that should be used to parse the next transaction
1261
- *
1262
- * @param pluginName string containing the name of the plugin, must have length between 1 and 30 bytes
1263
- * @return True if the method was executed successfully
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
- // ignore older version of ETH app
1316
- return false;
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
+ );
@@ -1,6 +1,6 @@
1
1
  import axios from "axios";
2
2
  import { getLoadConfig } from "./loadConfig";
3
- import type { LoadConfig } from "./loadConfig";
3
+ import type { LoadConfig } from "../types";
4
4
  import { log } from "@ledgerhq/logs";
5
5
 
6
6
  type ContractMethod = {
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
+ }