@ledgerhq/hw-app-eth 6.21.3 → 6.22.0

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 (86) hide show
  1. package/README.md +51 -100
  2. package/erc20.js +1 -1
  3. package/lib/Eth.d.ts +14 -33
  4. package/lib/Eth.d.ts.map +1 -1
  5. package/lib/Eth.js +273 -305
  6. package/lib/Eth.js.map +1 -1
  7. package/lib/services/ledger/contracts.d.ts +14 -0
  8. package/lib/services/ledger/contracts.d.ts.map +1 -0
  9. package/lib/{contracts.js → services/ledger/contracts.js} +0 -0
  10. package/lib/services/ledger/contracts.js.map +1 -0
  11. package/lib/services/ledger/erc20.d.ts +22 -0
  12. package/lib/services/ledger/erc20.d.ts.map +1 -0
  13. package/lib/{erc20.js → services/ledger/erc20.js} +0 -0
  14. package/lib/services/ledger/erc20.js.map +1 -0
  15. package/lib/services/ledger/index.d.ts +4 -0
  16. package/lib/services/ledger/index.d.ts.map +1 -0
  17. package/lib/services/ledger/index.js +168 -0
  18. package/lib/services/ledger/index.js.map +1 -0
  19. package/lib/services/ledger/loadConfig.d.ts +3 -0
  20. package/lib/services/ledger/loadConfig.d.ts.map +1 -0
  21. package/lib/{loadConfig.js → services/ledger/loadConfig.js} +0 -0
  22. package/lib/services/ledger/loadConfig.js.map +1 -0
  23. package/lib/services/ledger/nfts.d.ts +10 -0
  24. package/lib/services/ledger/nfts.d.ts.map +1 -0
  25. package/lib/{nfts.js → services/ledger/nfts.js} +3 -14
  26. package/lib/services/ledger/nfts.js.map +1 -0
  27. package/lib/services/types.d.ts +18 -0
  28. package/lib/services/types.d.ts.map +1 -0
  29. package/lib/services/types.js +3 -0
  30. package/lib/services/types.js.map +1 -0
  31. package/lib/utils.d.ts +8 -27
  32. package/lib/utils.d.ts.map +1 -1
  33. package/lib/utils.js +68 -68
  34. package/lib/utils.js.map +1 -1
  35. package/lib-es/Eth.d.ts +14 -33
  36. package/lib-es/Eth.d.ts.map +1 -1
  37. package/lib-es/Eth.js +261 -296
  38. package/lib-es/Eth.js.map +1 -1
  39. package/{lib → lib-es/services/ledger}/contracts.d.ts +1 -1
  40. package/lib-es/services/ledger/contracts.d.ts.map +1 -0
  41. package/lib-es/services/ledger/contracts.js +96 -0
  42. package/lib-es/services/ledger/contracts.js.map +1 -0
  43. package/{lib → lib-es/services/ledger}/erc20.d.ts +0 -0
  44. package/lib-es/services/ledger/erc20.d.ts.map +1 -0
  45. package/lib-es/services/ledger/erc20.js +64 -0
  46. package/lib-es/services/ledger/erc20.js.map +1 -0
  47. package/lib-es/services/ledger/index.d.ts +4 -0
  48. package/lib-es/services/ledger/index.d.ts.map +1 -0
  49. package/lib-es/services/ledger/index.js +166 -0
  50. package/lib-es/services/ledger/index.js.map +1 -0
  51. package/lib-es/services/ledger/loadConfig.d.ts +3 -0
  52. package/lib-es/services/ledger/loadConfig.d.ts.map +1 -0
  53. package/lib-es/services/ledger/loadConfig.js +20 -0
  54. package/lib-es/services/ledger/loadConfig.js.map +1 -0
  55. package/{lib → lib-es/services/ledger}/nfts.d.ts +2 -3
  56. package/lib-es/services/ledger/nfts.d.ts.map +1 -0
  57. package/lib-es/services/ledger/nfts.js +83 -0
  58. package/lib-es/services/ledger/nfts.js.map +1 -0
  59. package/lib-es/services/types.d.ts +18 -0
  60. package/lib-es/services/types.d.ts.map +1 -0
  61. package/lib-es/services/types.js +2 -0
  62. package/lib-es/services/types.js.map +1 -0
  63. package/lib-es/utils.d.ts +8 -27
  64. package/lib-es/utils.d.ts.map +1 -1
  65. package/lib-es/utils.js +66 -61
  66. package/lib-es/utils.js.map +1 -1
  67. package/package.json +3 -3
  68. package/src/Eth.ts +124 -258
  69. package/src/{contracts.ts → services/ledger/contracts.ts} +1 -1
  70. package/src/{erc20.ts → services/ledger/erc20.ts} +0 -0
  71. package/src/services/ledger/index.ts +119 -0
  72. package/src/services/ledger/loadConfig.ts +14 -0
  73. package/src/{nfts.ts → services/ledger/nfts.ts} +5 -18
  74. package/src/services/types.ts +28 -0
  75. package/src/utils.ts +72 -94
  76. package/tests/Eth.test.ts +2 -2
  77. package/lib/contracts.d.ts.map +0 -1
  78. package/lib/contracts.js.map +0 -1
  79. package/lib/erc20.d.ts.map +0 -1
  80. package/lib/erc20.js.map +0 -1
  81. package/lib/loadConfig.d.ts +0 -7
  82. package/lib/loadConfig.d.ts.map +0 -1
  83. package/lib/loadConfig.js.map +0 -1
  84. package/lib/nfts.d.ts.map +0 -1
  85. package/lib/nfts.js.map +0 -1
  86. package/src/loadConfig.ts +0 -23
package/src/Eth.ts CHANGED
@@ -15,16 +15,13 @@
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
18
  import { EthAppPleaseEnableContractData } from "@ledgerhq/errors";
21
19
  import type Transport from "@ledgerhq/hw-transport";
22
20
  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";
21
+ import { decodeTxInfo } from "./utils";
22
+ // NB: these are temporary import for the deprecated fallback mechanism
23
+ import { LedgerEthTransactionResolution, LoadConfig } from "./services/types";
24
+ import ledgerService from "./services/ledger";
28
25
 
29
26
  export type StarkQuantizationType =
30
27
  | "eth"
@@ -40,6 +37,22 @@ const starkQuantizationTypeMap = {
40
37
  erc721mintable: 5,
41
38
  };
42
39
 
40
+ function splitPath(path: string): number[] {
41
+ const result: number[] = [];
42
+ const components = path.split("/");
43
+ components.forEach((element) => {
44
+ let number = parseInt(element, 10);
45
+ if (isNaN(number)) {
46
+ return; // FIXME shouldn't it throws instead?
47
+ }
48
+ if (element.length > 1 && element[element.length - 1] === "'") {
49
+ number += 0x80000000;
50
+ }
51
+ result.push(number);
52
+ });
53
+ return result;
54
+ }
55
+
43
56
  function hexBuffer(str: string): Buffer {
44
57
  return Buffer.from(str.startsWith("0x") ? str.slice(2) : str, "hex");
45
58
  }
@@ -168,119 +181,72 @@ export default class Eth {
168
181
  }
169
182
 
170
183
  /**
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
184
+ * You can sign a transaction and retrieve v, r, s given the raw transaction and the BIP 32 path of the account to sign.
185
+ *
186
+ * @param path: the BIP32 path to sign the transaction on
187
+ * @param rawTxHex: the raw ethereum transaction in hexadecimal to sign
188
+ * @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
189
  * @example
192
- eth.signTransaction("44'/60'/0'/0/0", "e8018504e3b292008252089428ee52a8f3d6e5d15f8b131996950d7f296c7952872bd72a2487400080").then(result => ...)
190
+ import ledgerService from "@ledgerhq/hw-app-eth/lib/services/ledger"
191
+ const tx = "e8018504e3b292008252089428ee52a8f3d6e5d15f8b131996950d7f296c7952872bd72a2487400080"; // raw tx to sign
192
+ const resolution = await ledgerService.resolveTransaction(tx);
193
+ const result = eth.signTransaction("44'/60'/0'/0/0", tx, resolution);
194
+ console.log(result);
193
195
  */
194
196
  async signTransaction(
195
197
  path: string,
196
- rawTxHex: string
198
+ rawTxHex: string,
199
+ resolution?: LedgerEthTransactionResolution | null
197
200
  ): Promise<{
198
201
  s: string;
199
202
  v: string;
200
203
  r: string;
201
204
  }> {
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"
205
+ if (resolution === undefined) {
206
+ console.warn(
207
+ "hw-app-eth: signTransaction(path, rawTxHex, resolution): " +
208
+ "please provide the 'resolution' parameter. " +
209
+ "See https://github.com/LedgerHQ/ledgerjs/blob/master/packages/hw-app-eth/README.md " +
210
+ "– 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)." +
211
+ "// Possible solution:\n" +
212
+ " + import ledgerService from '@ledgerhq/hw-app-eth/lib/services/ledger';\n" +
213
+ " + const resolution = await ledgerService.resolveTransaction(rawTxHex);"
252
214
  );
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
- }
215
+ resolution = await ledgerService
216
+ .resolveTransaction(rawTxHex, this.loadConfig)
217
+ .catch(() => null);
267
218
  }
268
219
 
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);
220
+ // provide to the device resolved information to make it clear sign the signature
221
+ if (resolution) {
222
+ for (const plugin of resolution.plugin) {
223
+ await setPlugin(this.transport, plugin);
224
+ }
225
+ for (const { payload, signature } of resolution.externalPlugin) {
226
+ await setExternalPlugin(this.transport, payload, signature);
227
+ }
228
+ for (const nft of resolution.nfts) {
229
+ await provideNFTInformation(this.transport, Buffer.from(nft, "hex"));
230
+ }
231
+ for (const data of resolution.erc20Tokens) {
232
+ await provideERC20TokenInformation(
233
+ this.transport,
234
+ Buffer.from(data, "hex")
235
+ );
278
236
  }
279
- chainIdTruncated = chainIdTruncatedBuf.readUInt32BE(0);
280
237
  }
281
238
 
239
+ const rawTx = Buffer.from(rawTxHex, "hex");
240
+ const { vrsOffset, txType, chainId, chainIdTruncated } = decodeTxInfo(
241
+ rawTx
242
+ );
243
+
244
+ const paths = splitPath(path);
245
+ let response;
246
+ let offset = 0;
282
247
  while (offset !== rawTx.length) {
283
- const maxChunkSize = offset === 0 ? 150 - 1 - paths.length * 4 : 150;
248
+ const first = offset === 0;
249
+ const maxChunkSize = first ? 150 - 1 - paths.length * 4 : 150;
284
250
  let chunkSize =
285
251
  offset + maxChunkSize > rawTx.length
286
252
  ? rawTx.length - offset
@@ -292,10 +258,10 @@ export default class Eth {
292
258
  }
293
259
 
294
260
  const buffer = Buffer.alloc(
295
- offset === 0 ? 1 + paths.length * 4 + chunkSize : chunkSize
261
+ first ? 1 + paths.length * 4 + chunkSize : chunkSize
296
262
  );
297
263
 
298
- if (offset === 0) {
264
+ if (first) {
299
265
  buffer[0] = paths.length;
300
266
  paths.forEach((element, index) => {
301
267
  buffer.writeUInt32BE(element, 1 + 4 * index);
@@ -305,137 +271,42 @@ export default class Eth {
305
271
  rawTx.copy(buffer, 0, offset, offset + chunkSize);
306
272
  }
307
273
 
308
- toSend.push(buffer);
274
+ response = await this.transport
275
+ .send(0xe0, 0x04, first ? 0x00 : 0x80, 0x00, buffer)
276
+ .catch((e) => {
277
+ throw remapTransactionRelatedErrors(e);
278
+ });
279
+
309
280
  offset += chunkSize;
310
281
  }
311
282
 
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
- );
283
+ const response_byte: number = response[0];
284
+ let v = "";
365
285
 
366
- if (infos) {
367
- const { plugin, payload, signature, erc20OfInterest, abi } = infos;
286
+ if (chainId.times(2).plus(35).plus(1).isGreaterThan(255)) {
287
+ const oneByteChainId = (chainIdTruncated * 2 + 35) % 256;
368
288
 
369
- if (plugin) {
370
- log("ethereum", "loading plugin for " + selector);
371
- await setExternalPlugin(this.transport, payload, signature);
372
- }
289
+ const ecc_parity = Math.abs(response_byte - oneByteChainId);
373
290
 
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
- }
291
+ if (txType != null) {
292
+ // For EIP2930 and EIP1559 tx, v is simply the parity.
293
+ v = ecc_parity % 2 == 1 ? "00" : "01";
294
+ } else {
295
+ // Legacy type transaction with a big chain ID
296
+ v = chainId.times(2).plus(35).plus(ecc_parity).toString(16);
391
297
  }
392
- await provideForContract(decodedTx.to);
298
+ } else {
299
+ v = response_byte.toString(16);
393
300
  }
394
301
 
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
- }
302
+ // Make sure v has is prefixed with a 0 if its length is odd ("1" -> "01").
303
+ if (v.length % 2 == 1) {
304
+ v = "0" + v;
305
+ }
426
306
 
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
- );
307
+ const r = response.slice(1, 1 + 32).toString("hex");
308
+ const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
309
+ return { v, r, s };
439
310
  }
440
311
 
441
312
  /**
@@ -470,7 +341,7 @@ export default class Eth {
470
341
  console.log("Signature 0x" + result['r'] + result['s'] + v);
471
342
  })
472
343
  */
473
- signPersonalMessage(
344
+ async signPersonalMessage(
474
345
  path: string,
475
346
  messageHex: string
476
347
  ): Promise<{
@@ -481,7 +352,6 @@ export default class Eth {
481
352
  const paths = splitPath(path);
482
353
  let offset = 0;
483
354
  const message = Buffer.from(messageHex, "hex");
484
- const toSend: Buffer[] = [];
485
355
  let response;
486
356
 
487
357
  while (offset !== message.length) {
@@ -510,26 +380,21 @@ export default class Eth {
510
380
  message.copy(buffer, 0, offset, offset + chunkSize);
511
381
  }
512
382
 
513
- toSend.push(buffer);
383
+ response = await this.transport.send(
384
+ 0xe0,
385
+ 0x08,
386
+ offset === 0 ? 0x00 : 0x80,
387
+ 0x00,
388
+ buffer
389
+ );
390
+
514
391
  offset += chunkSize;
515
392
  }
516
393
 
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
- });
394
+ const v = response[0];
395
+ const r = response.slice(1, 1 + 32).toString("hex");
396
+ const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
397
+ return { v, r, s };
533
398
  }
534
399
 
535
400
  /**
@@ -1256,27 +1121,28 @@ export default class Eth {
1256
1121
  );
1257
1122
  }
1258
1123
 
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
- */
1124
+ provideERC20TokenInformation({ data }: { data: Buffer }): Promise<boolean> {
1125
+ console.warn(
1126
+ "hw-app-eth: eth.provideERC20TokenInformation is deprecated. signTransaction solves this for you when providing it in `resolution`."
1127
+ );
1128
+ return provideERC20TokenInformation(this.transport, data);
1129
+ }
1130
+
1265
1131
  setExternalPlugin(
1266
1132
  pluginName: string,
1267
1133
  contractAddress: string,
1268
1134
  selector: string
1269
1135
  ): Promise<boolean> {
1136
+ console.warn(
1137
+ "hw-app-eth: eth.setExternalPlugin is deprecated. signTransaction solves this for you when providing it in `resolution`."
1138
+ );
1270
1139
  return setExternalPlugin(this.transport, pluginName, selector);
1271
1140
  }
1272
1141
 
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
1142
  setPlugin(data: string): Promise<boolean> {
1143
+ console.warn(
1144
+ "hw-app-eth: eth.setPlugin is deprecated. signTransaction solves this for you when providing it in `resolution`."
1145
+ );
1280
1146
  return setPlugin(this.transport, data);
1281
1147
  }
1282
1148
  }
@@ -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,119 @@
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) => {
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 = await getNFTInfo(address, chainIdTruncated, loadConfig);
40
+ if (nftInfo) {
41
+ log(
42
+ "ethereum",
43
+ "loaded nft info for " +
44
+ nftInfo.contractAddress +
45
+ " (" +
46
+ nftInfo.collectionName +
47
+ ")"
48
+ );
49
+ provideNFTInformation(nftInfo.data);
50
+ } else {
51
+ const erc20Info = byContractAddressAndChainId(
52
+ address,
53
+ chainIdTruncated
54
+ );
55
+ if (erc20Info) {
56
+ log(
57
+ "ethereum",
58
+ "loaded erc20token info for " +
59
+ erc20Info.contractAddress +
60
+ " (" +
61
+ erc20Info.ticker +
62
+ ")"
63
+ );
64
+ provideERC20TokenInformation(erc20Info.data.toString("hex"));
65
+ }
66
+ }
67
+ };
68
+
69
+ if (decodedTx.data.length >= 10) {
70
+ const selector = decodedTx.data.substring(0, 10);
71
+ const nftPluginPayload = await loadNftPlugin(
72
+ decodedTx.to,
73
+ selector,
74
+ chainIdTruncated,
75
+ loadConfig
76
+ );
77
+
78
+ if (nftPluginPayload) {
79
+ setPlugin(nftPluginPayload);
80
+ } else {
81
+ const infos = await loadInfosForContractMethod(
82
+ decodedTx.to,
83
+ selector,
84
+ chainIdTruncated,
85
+ loadConfig
86
+ );
87
+
88
+ if (infos) {
89
+ const { plugin, payload, signature, erc20OfInterest, abi } = infos;
90
+
91
+ if (plugin) {
92
+ log("ethereum", "found plugin for " + selector);
93
+ setExternalPlugin(payload, signature);
94
+ }
95
+ if (erc20OfInterest && erc20OfInterest.length && abi) {
96
+ const contract = new ethers.utils.Interface(abi);
97
+ const args = contract.parseTransaction(decodedTx).args;
98
+ for (const path of erc20OfInterest) {
99
+ const address = path.split(".").reduce((value, seg) => {
100
+ if (seg === "-1" && Array.isArray(value)) {
101
+ return value[value.length - 1];
102
+ }
103
+ return value[seg];
104
+ }, args);
105
+ provideForContract(address);
106
+ }
107
+ }
108
+ } else {
109
+ log("ethereum", "no infos for selector " + selector);
110
+ }
111
+ }
112
+ provideForContract(decodedTx.to);
113
+ }
114
+
115
+ return resolution;
116
+ },
117
+ };
118
+
119
+ 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
+ }
@@ -1,12 +1,11 @@
1
1
  import axios from "axios";
2
2
  import { getLoadConfig } from "./loadConfig";
3
- import type { LoadConfig } from "./loadConfig";
4
- import { log } from "@ledgerhq/logs";
3
+ import type { LoadConfig } from "../types";
5
4
 
6
5
  type NftInfo = {
7
6
  contractAddress: string;
8
7
  collectionName: string;
9
- data: Buffer;
8
+ data: string;
10
9
  };
11
10
 
12
11
  type BackendResponse = {
@@ -21,13 +20,7 @@ export const getNFTInfo = async (
21
20
  const { nftExplorerBaseURL } = getLoadConfig(userLoadConfig);
22
21
  if (!nftExplorerBaseURL) return;
23
22
  const url = `${nftExplorerBaseURL}/${chainId}/contracts/${contractAddress}`;
24
- const response = await axios
25
- .get<BackendResponse>(url)
26
- .then((r) => r.data)
27
- .catch((e) => {
28
- log("error", "could not fetch from " + url + ": " + String(e));
29
- return null;
30
- });
23
+ const response = await axios.get<BackendResponse>(url).then((r) => r.data);
31
24
  if (!response) return;
32
25
 
33
26
  const payload = response["payload"];
@@ -36,7 +29,7 @@ export const getNFTInfo = async (
36
29
  return {
37
30
  contractAddress: contractAddress,
38
31
  collectionName: collectionName,
39
- data: Buffer.from(payload, "hex"),
32
+ data: payload,
40
33
  };
41
34
  };
42
35
 
@@ -50,13 +43,7 @@ export const loadNftPlugin = async (
50
43
  if (!nftExplorerBaseURL) return;
51
44
  const url = `${nftExplorerBaseURL}/${chainId}/contracts/${contractAddress}/plugin-selector/${selector}`;
52
45
 
53
- const response = await axios
54
- .get<BackendResponse>(url)
55
- .then((r) => r.data)
56
- .catch((e) => {
57
- log("error", "could not fetch from " + url + ": " + String(e));
58
- return null;
59
- });
46
+ const response = await axios.get<BackendResponse>(url).then((r) => r.data);
60
47
  if (!response) return;
61
48
 
62
49
  const payload = response["payload"];