@ledgerhq/hw-app-eth 7.0.0-nightly.2 → 7.0.0-nightly.20251120135143

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 (134) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/.unimportedrc.json +3 -28
  3. package/CHANGELOG.md +994 -17
  4. package/README.md +100 -0
  5. package/jest.config.ts +18 -0
  6. package/lib/Eth.d.ts +3 -18
  7. package/lib/Eth.d.ts.map +1 -1
  8. package/lib/Eth.js +160 -198
  9. package/lib/Eth.js.map +1 -1
  10. package/lib/errors.d.ts +3 -0
  11. package/lib/errors.d.ts.map +1 -1
  12. package/lib/errors.js +2 -1
  13. package/lib/errors.js.map +1 -1
  14. package/lib/modules/Domains/index.js +6 -15
  15. package/lib/modules/Domains/index.js.map +1 -1
  16. package/lib/modules/EIP712/index.d.ts.map +1 -1
  17. package/lib/modules/EIP712/index.js +112 -125
  18. package/lib/modules/EIP712/index.js.map +1 -1
  19. package/lib/modules/EIP712/types.d.ts +1 -0
  20. package/lib/modules/EIP712/types.d.ts.map +1 -1
  21. package/lib/modules/EIP712/utils.d.ts +1 -0
  22. package/lib/modules/EIP712/utils.d.ts.map +1 -1
  23. package/lib/modules/EIP712/utils.js +14 -24
  24. package/lib/modules/EIP712/utils.js.map +1 -1
  25. package/lib/modules/Uniswap/constants.d.ts.map +1 -1
  26. package/lib/modules/Uniswap/constants.js +1 -0
  27. package/lib/modules/Uniswap/constants.js.map +1 -1
  28. package/lib/modules/Uniswap/decoders.d.ts.map +1 -1
  29. package/lib/modules/Uniswap/decoders.js +8 -3
  30. package/lib/modules/Uniswap/decoders.js.map +1 -1
  31. package/lib/modules/Uniswap/index.d.ts +2 -1
  32. package/lib/modules/Uniswap/index.d.ts.map +1 -1
  33. package/lib/modules/Uniswap/index.js +11 -20
  34. package/lib/modules/Uniswap/index.js.map +1 -1
  35. package/lib/modules/Uniswap/types.d.ts +1 -1
  36. package/lib/modules/Uniswap/types.d.ts.map +1 -1
  37. package/lib/services/ledger/contracts.js +4 -13
  38. package/lib/services/ledger/contracts.js.map +1 -1
  39. package/lib/services/ledger/erc20.d.ts +2 -1
  40. package/lib/services/ledger/erc20.d.ts.map +1 -1
  41. package/lib/services/ledger/erc20.js +16 -32
  42. package/lib/services/ledger/erc20.js.map +1 -1
  43. package/lib/services/ledger/index.d.ts.map +1 -1
  44. package/lib/services/ledger/index.js +34 -32
  45. package/lib/services/ledger/index.js.map +1 -1
  46. package/lib/services/ledger/loadConfig.d.ts.map +1 -1
  47. package/lib/services/ledger/loadConfig.js +7 -1
  48. package/lib/services/ledger/loadConfig.js.map +1 -1
  49. package/lib/services/ledger/nfts.js +9 -19
  50. package/lib/services/ledger/nfts.js.map +1 -1
  51. package/lib/services/types.d.ts +3 -0
  52. package/lib/services/types.d.ts.map +1 -1
  53. package/lib/utils.d.ts +56 -9
  54. package/lib/utils.d.ts.map +1 -1
  55. package/lib/utils.js +175 -81
  56. package/lib/utils.js.map +1 -1
  57. package/lib-es/Eth.d.ts +3 -18
  58. package/lib-es/Eth.d.ts.map +1 -1
  59. package/lib-es/Eth.js +160 -198
  60. package/lib-es/Eth.js.map +1 -1
  61. package/lib-es/errors.d.ts +3 -0
  62. package/lib-es/errors.d.ts.map +1 -1
  63. package/lib-es/errors.js +1 -0
  64. package/lib-es/errors.js.map +1 -1
  65. package/lib-es/modules/Domains/index.js +6 -15
  66. package/lib-es/modules/Domains/index.js.map +1 -1
  67. package/lib-es/modules/EIP712/index.d.ts.map +1 -1
  68. package/lib-es/modules/EIP712/index.js +112 -125
  69. package/lib-es/modules/EIP712/index.js.map +1 -1
  70. package/lib-es/modules/EIP712/types.d.ts +1 -0
  71. package/lib-es/modules/EIP712/types.d.ts.map +1 -1
  72. package/lib-es/modules/EIP712/utils.d.ts +1 -0
  73. package/lib-es/modules/EIP712/utils.d.ts.map +1 -1
  74. package/lib-es/modules/EIP712/utils.js +14 -24
  75. package/lib-es/modules/EIP712/utils.js.map +1 -1
  76. package/lib-es/modules/Uniswap/constants.d.ts.map +1 -1
  77. package/lib-es/modules/Uniswap/constants.js +1 -0
  78. package/lib-es/modules/Uniswap/constants.js.map +1 -1
  79. package/lib-es/modules/Uniswap/decoders.d.ts.map +1 -1
  80. package/lib-es/modules/Uniswap/decoders.js +8 -3
  81. package/lib-es/modules/Uniswap/decoders.js.map +1 -1
  82. package/lib-es/modules/Uniswap/index.d.ts +2 -1
  83. package/lib-es/modules/Uniswap/index.d.ts.map +1 -1
  84. package/lib-es/modules/Uniswap/index.js +11 -20
  85. package/lib-es/modules/Uniswap/index.js.map +1 -1
  86. package/lib-es/modules/Uniswap/types.d.ts +1 -1
  87. package/lib-es/modules/Uniswap/types.d.ts.map +1 -1
  88. package/lib-es/services/ledger/contracts.js +4 -13
  89. package/lib-es/services/ledger/contracts.js.map +1 -1
  90. package/lib-es/services/ledger/erc20.d.ts +2 -1
  91. package/lib-es/services/ledger/erc20.d.ts.map +1 -1
  92. package/lib-es/services/ledger/erc20.js +16 -32
  93. package/lib-es/services/ledger/erc20.js.map +1 -1
  94. package/lib-es/services/ledger/index.d.ts.map +1 -1
  95. package/lib-es/services/ledger/index.js +34 -32
  96. package/lib-es/services/ledger/index.js.map +1 -1
  97. package/lib-es/services/ledger/loadConfig.d.ts.map +1 -1
  98. package/lib-es/services/ledger/loadConfig.js +7 -1
  99. package/lib-es/services/ledger/loadConfig.js.map +1 -1
  100. package/lib-es/services/ledger/nfts.js +9 -19
  101. package/lib-es/services/ledger/nfts.js.map +1 -1
  102. package/lib-es/services/types.d.ts +3 -0
  103. package/lib-es/services/types.d.ts.map +1 -1
  104. package/lib-es/utils.d.ts +56 -9
  105. package/lib-es/utils.d.ts.map +1 -1
  106. package/lib-es/utils.js +148 -81
  107. package/lib-es/utils.js.map +1 -1
  108. package/package.json +19 -15
  109. package/src/Eth.ts +51 -82
  110. package/src/errors.ts +3 -0
  111. package/src/modules/EIP712/index.ts +17 -4
  112. package/src/modules/Uniswap/constants.ts +1 -0
  113. package/src/modules/Uniswap/decoders.ts +10 -3
  114. package/src/modules/Uniswap/index.ts +9 -8
  115. package/src/modules/Uniswap/types.ts +2 -1
  116. package/src/services/ledger/erc20.ts +16 -19
  117. package/src/services/ledger/index.ts +50 -22
  118. package/src/services/ledger/loadConfig.ts +4 -1
  119. package/src/services/ledger/nfts.ts +1 -1
  120. package/src/services/types.ts +12 -0
  121. package/src/utils.ts +170 -85
  122. package/tests/EIP712/filtered-signMessage.unit.test.ts +28 -116
  123. package/tests/EIP712/noFilter-signMessage.unit.test.ts +0 -2
  124. package/tests/ERC20/ERC20-CAL-KO.unit.test.ts +14 -25
  125. package/tests/ERC20/ERC20-CAL-OK.unit.test.ts +15 -10
  126. package/tests/Eth.unit.test.ts +242 -314
  127. package/tests/Uniswap/decoders.unit.test.ts +10 -0
  128. package/tests/Uniswap/index.unit.test.ts +17 -26
  129. package/tests/fixtures/messages/15-permit.json +3 -3
  130. package/tests/fixtures/messages/16-permit2.json +3 -3
  131. package/tests/fixtures/messages/17-uniswapx.json +5 -5
  132. package/tests/fixtures/utils.ts +17 -18
  133. package/tests/ledgerService.unit.test.ts +5 -7
  134. package/tests/utils.unit.test.ts +341 -0
@@ -73,6 +73,15 @@ describe("Uniswap", () => {
73
73
  }
74
74
  });
75
75
 
76
+ it("should decode a SWEEP command input", () => {
77
+ // see tx 0xcc3a651b4f962576ed8f2142326271d090dcb701b754429f99e9b77d38cedc30
78
+ const input =
79
+ "0x000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000001125ffbea29018485cc228e39aade004a359fe990000000000000000000000000000000000000000000000000000000000000000";
80
+ expect(UniswapDecoders["SWEEP"](input, 1)).toEqual([
81
+ "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // USDC
82
+ ]);
83
+ });
84
+
76
85
  it("should return an empty array for other supported commands", () => {
77
86
  const input =
78
87
  "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000204fce5e3e2502611000000000000000000000000000000000000000000000000000000001f942ac9a54ae2700000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000030000000000000000000000003ffeea07a27fab7ad1df5297fa75e77a43cb5790000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000ccae1bc46fb018dd396ed4c45565d4cb9d41098";
@@ -86,6 +95,7 @@ describe("Uniswap", () => {
86
95
  "V3_SWAP_EXACT_OUT",
87
96
  "WRAP_ETH",
88
97
  "UNWRAP_ETH",
98
+ "SWEEP",
89
99
  ].includes(command)
90
100
  ) {
91
101
  return decoder;
@@ -1,30 +1,25 @@
1
1
  import nock from "nock";
2
- import { ethers } from "ethers";
2
+ import { BigNumber } from "@ethersproject/bignumber";
3
+ import { AddressZero } from "@ethersproject/constants";
4
+ import { type Transaction } from "@ethersproject/transactions";
5
+ import SignatureCALEth from "../fixtures/SignatureCALEth";
6
+ import {
7
+ UNISWAP_EXECUTE_SELECTOR,
8
+ UNISWAP_UNIVERSAL_ROUTER_ADDRESS,
9
+ } from "../../src/modules/Uniswap/constants";
3
10
  import {
4
11
  getCommandsAndTokensFromUniswapCalldata,
5
12
  isSupported,
6
13
  loadInfosForUniswap,
7
14
  } from "../../src/modules/Uniswap";
8
- import {
9
- UNISWAP_EXECUTE_SELECTOR,
10
- UNISWAP_UNIVERSAL_ROUTER_ADDRESS,
11
- } from "../../src/modules/Uniswap/constants";
12
- import SignatureCALEth from "../fixtures/SignatureCALEth";
13
15
 
14
16
  nock.disableNetConnect();
15
- jest.mock("@ledgerhq/cryptoassets-evm-signatures/data/evm/index", () => ({
16
- get signatures() {
17
- return {
18
- 1: SignatureCALEth,
19
- };
20
- },
21
- }));
22
17
 
23
18
  describe("Uniswap", () => {
24
19
  describe("index", () => {
25
20
  describe("isSupported", () => {
26
21
  it("should return false for a non-Uniswap transaction", () => {
27
- expect(isSupported("0x", ethers.constants.AddressZero, 1, [])).toBe(false);
22
+ expect(isSupported("0x", AddressZero, 1, [])).toBe(false);
28
23
  });
29
24
 
30
25
  it("should return false for a Uniswap transaction with an invalid selector", () => {
@@ -142,16 +137,16 @@ describe("Uniswap", () => {
142
137
  const calldata =
143
138
  "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000669bad2800000000000000000000000000000000000000000000000000000000000000040a08060c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000003a000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000bdaa645097ef80f9d475f341d0d107a45b3a000000000000000000000000ffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000687ce06c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b00000000000000000000000000000000000000000000000000000000687ce06c00000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000004142794d71565541a435d1bc910db84510c4d31ad35ff8c046222b2c232fcd99a6256a8f1abe7d42a44f3d92d65f9232db76df2fbef3dfa76eca64b034ed4df5321c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000001bffcca36d953d12266cb000000000000000000000000000000000000000000000000027c3dea3f90dafd00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000bdaa645097ef80f9d475f341d0d107a45b3a000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006eb940753b4b52fbec8d33c418133fdb0d4405e6000000000000000000000000000000000000000000000000000000000000003800000000000000000000000000000000000000000000000000000000000000400000000000000000000000001dc813a4ee4410f144ae2122051c1cfc436f24a00000000000000000000000000000000000000000000000000275e122c92b911e";
144
139
 
145
- const transaction: ethers.Transaction = {
140
+ const transaction: Transaction = {
146
141
  to: UNISWAP_UNIVERSAL_ROUTER_ADDRESS,
147
142
  data: calldata,
148
- gasLimit: ethers.BigNumber.from(21_000),
143
+ gasLimit: BigNumber.from(21_000),
149
144
  nonce: 0,
150
- value: ethers.BigNumber.from(0),
145
+ value: BigNumber.from(0),
151
146
  chainId: 1,
152
147
  };
153
148
 
154
- const res = await loadInfosForUniswap(transaction, 1);
149
+ const res = await loadInfosForUniswap(transaction, 1, { staticERC20Signatures: { 1: SignatureCALEth } });
155
150
  expect(res).toEqual({
156
151
  pluginData: Buffer.from(
157
152
  "07556e69737761703fc91a3afd70395cd496c647d5a6cc9d4b2b7fad3593564c3044022014391e8f355867a57fe88f6a5a4dbcb8bf8f888a9db3ff3449caf72d120396bd02200c13d9c3f79400fe0aa0434ac54d59b79503c9964a4abc3e8cd22763e0242935",
@@ -162,21 +157,17 @@ describe("Uniswap", () => {
162
157
  "0457455448c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000012000000013045022100b47ee8551c15a2cf681c649651e987d7e527c481d27c38da1f971a8242792bd3022069c3f688ac5493a23dab5798e3c9b07484765069e1d4be14321aae4d92cb8cbe",
163
158
  "hex",
164
159
  ),
165
- Buffer.from(
166
- "0457455448c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000012000000013045022100b47ee8551c15a2cf681c649651e987d7e527c481d27c38da1f971a8242792bd3022069c3f688ac5493a23dab5798e3c9b07484765069e1d4be14321aae4d92cb8cbe",
167
- "hex",
168
- ),
169
160
  ],
170
161
  });
171
162
  });
172
163
 
173
164
  it("should return an empty object for an unsupported Uniswap transaction", async () => {
174
- const transaction: ethers.Transaction = {
175
- to: ethers.constants.AddressZero,
165
+ const transaction: Transaction = {
166
+ to: AddressZero,
176
167
  data: "0x",
177
- gasLimit: ethers.BigNumber.from(21_000),
168
+ gasLimit: BigNumber.from(21_000),
178
169
  nonce: 0,
179
- value: ethers.BigNumber.from(0),
170
+ value: BigNumber.from(0),
180
171
  chainId: 1,
181
172
  };
182
173
 
@@ -8,10 +8,10 @@
8
8
  "primaryType": "Permit",
9
9
  "message": {
10
10
  "deadline": 1718992051,
11
- "nonce": 0,
12
11
  "spender": "0x111111125421ca6dc452d289314280a0f8842a65",
13
- "owner": "0x6cbcd73cd8e8a42844662f0a0e76d7f79afd933d",
14
- "value": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
12
+ "nonce": 0,
13
+ "value": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
14
+ "owner": "0x6cbcd73cd8e8a42844662f0a0e76d7f79afd933d"
15
15
  },
16
16
  "types": {
17
17
  "EIP712Domain": [
@@ -7,10 +7,10 @@
7
7
  "primaryType": "PermitSingle",
8
8
  "message": {
9
9
  "details": {
10
- "token": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619",
11
- "amount": "69420000000000000000",
12
10
  "expiration": "1718184249",
13
- "nonce": "0"
11
+ "amount": "69420000000000000000",
12
+ "nonce": "0",
13
+ "token": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619"
14
14
  },
15
15
  "spender": "0xec7be89e9d109e7e3fec59c222cf297125fefda2",
16
16
  "sigDeadline": "1715594049"
@@ -15,11 +15,11 @@
15
15
  "deadline": "1718467096",
16
16
  "witness": {
17
17
  "info": {
18
- "reactor": "0x6000da47483062a0d734ba3dc7576ce6a0b645c4",
19
- "swapper": "0x224cbc440944c72e951507e97d8bf5ffa2e3d2b9",
20
18
  "nonce": "1993354326232431306240697018571020274189859638820531235067341608173853199105",
21
- "deadline": "1718467096",
19
+ "swapper": "0x224cbc440944c72e951507e97d8bf5ffa2e3d2b9",
22
20
  "additionalValidationContract": "0x0000000000000000000000000000000000000000",
21
+ "reactor": "0x6000da47483062a0d734ba3dc7576ce6a0b645c4",
22
+ "deadline": "1718467096",
23
23
  "additionalValidationData": "0x"
24
24
  },
25
25
  "decayStartTime": "1718467024",
@@ -31,15 +31,15 @@
31
31
  "inputEndAmount": "100000000000000000",
32
32
  "outputs": [
33
33
  {
34
- "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
35
34
  "startAmount": "348572327",
35
+ "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
36
36
  "endAmount": "332452012",
37
37
  "recipient": "0x224cbc440944c72e951507e97d8bf5ffa2e3d2b9"
38
38
  },
39
39
  {
40
+ "endAmount": "833213",
40
41
  "token": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
41
42
  "startAmount": "873614",
42
- "endAmount": "833213",
43
43
  "recipient": "0x27213e28d7fda5c57fe9e5dd923818dbccf71c47"
44
44
  }
45
45
  ]
@@ -1,13 +1,14 @@
1
- import { ethers } from "ethers";
1
+ import { Interface } from "@ethersproject/abi";
2
+ import { serialize as serializeTransaction } from "@ethersproject/transactions";
2
3
  import ERC20Abi from "./ABI/ERC20.json";
3
4
  import ERC721Abi from "./ABI/ERC721.json";
4
5
  import ERC1155Abi from "./ABI/ERC1155.json";
5
6
  import PARASWAPAbi from "./ABI/PARASWAP.json";
6
7
 
7
- const ERC20Interface = new ethers.utils.Interface(ERC20Abi);
8
- const ERC721Interface = new ethers.utils.Interface(ERC721Abi);
9
- const ERC1155Interface = new ethers.utils.Interface(ERC1155Abi);
10
- const PARASWAPInterface = new ethers.utils.Interface(PARASWAPAbi);
8
+ const ERC20Interface = new Interface(ERC20Abi);
9
+ const ERC721Interface = new Interface(ERC721Abi);
10
+ const ERC1155Interface = new Interface(ERC1155Abi);
11
+ const PARASWAPInterface = new Interface(PARASWAPAbi);
11
12
 
12
13
  export const transactionContracts = {
13
14
  erc20: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
@@ -102,16 +103,14 @@ export const transactionData = {
102
103
  };
103
104
 
104
105
  export const getSerializedTransaction = (to: string, data: string): string =>
105
- ethers.utils
106
- .serializeTransaction({
107
- to,
108
- nonce: 0,
109
- gasLimit: 21000,
110
- data,
111
- value: 1,
112
- chainId: 1,
113
- maxPriorityFeePerGas: 10000,
114
- maxFeePerGas: 1000000,
115
- type: 2,
116
- })
117
- .substring(2);
106
+ serializeTransaction({
107
+ to,
108
+ nonce: 0,
109
+ gasLimit: 21000,
110
+ data,
111
+ value: 1,
112
+ chainId: 1,
113
+ maxPriorityFeePerGas: 10000,
114
+ maxFeePerGas: 1000000,
115
+ type: 2,
116
+ }).substring(2);
@@ -11,7 +11,7 @@ import * as uniswapModule from "../src/modules/Uniswap";
11
11
  import { ResolutionConfig } from "../src/services/types";
12
12
  import { ledgerService } from "../src/Eth";
13
13
 
14
- const loadConfig = getLoadConfig();
14
+ const loadConfig = getLoadConfig({ staticERC20Signatures: { 1: signatureCALEth } });
15
15
  const resolutionConfig: ResolutionConfig = {
16
16
  nft: true,
17
17
  erc20: true,
@@ -676,7 +676,6 @@ describe("Ledger Service", () => {
676
676
  domains: [],
677
677
  erc20Tokens: [
678
678
  "0457455448c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000012000000013045022100b47ee8551c15a2cf681c649651e987d7e527c481d27c38da1f971a8242792bd3022069c3f688ac5493a23dab5798e3c9b07484765069e1d4be14321aae4d92cb8cbe",
679
- "0457455448c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000012000000013045022100b47ee8551c15a2cf681c649651e987d7e527c481d27c38da1f971a8242792bd3022069c3f688ac5493a23dab5798e3c9b07484765069e1d4be14321aae4d92cb8cbe",
680
679
  ],
681
680
  nfts: [],
682
681
  externalPlugin: [
@@ -689,8 +688,8 @@ describe("Ledger Service", () => {
689
688
  plugin: [],
690
689
  });
691
690
  expect(contractServices.loadInfosForContractMethod).toHaveBeenCalledTimes(0);
692
- expect(erc20Services.findERC20SignaturesInfo).toHaveBeenCalledTimes(3);
693
- expect(erc20Services.byContractAddressAndChainId).toHaveBeenCalledTimes(3);
691
+ expect(erc20Services.findERC20SignaturesInfo).toHaveBeenCalledTimes(2);
692
+ expect(erc20Services.byContractAddressAndChainId).toHaveBeenCalledTimes(2);
694
693
  expect(uniswapModule.loadInfosForUniswap).toHaveBeenCalledTimes(1);
695
694
  expect(uniswapModule.getCommandsAndTokensFromUniswapCalldata).toHaveBeenCalledTimes(1);
696
695
  expect(nftServices.getNFTInfo).not.toHaveBeenCalled();
@@ -719,7 +718,6 @@ describe("Ledger Service", () => {
719
718
  domains: [],
720
719
  erc20Tokens: [
721
720
  "0457455448c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000012000000013045022100b47ee8551c15a2cf681c649651e987d7e527c481d27c38da1f971a8242792bd3022069c3f688ac5493a23dab5798e3c9b07484765069e1d4be14321aae4d92cb8cbe",
722
- "0457455448c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000012000000013045022100b47ee8551c15a2cf681c649651e987d7e527c481d27c38da1f971a8242792bd3022069c3f688ac5493a23dab5798e3c9b07484765069e1d4be14321aae4d92cb8cbe",
723
721
  ],
724
722
  nfts: [],
725
723
  externalPlugin: [
@@ -732,8 +730,8 @@ describe("Ledger Service", () => {
732
730
  plugin: [],
733
731
  });
734
732
  expect(contractServices.loadInfosForContractMethod).toHaveBeenCalledTimes(0);
735
- expect(erc20Services.findERC20SignaturesInfo).toHaveBeenCalledTimes(3);
736
- expect(erc20Services.byContractAddressAndChainId).toHaveBeenCalledTimes(3);
733
+ expect(erc20Services.findERC20SignaturesInfo).toHaveBeenCalledTimes(2);
734
+ expect(erc20Services.byContractAddressAndChainId).toHaveBeenCalledTimes(2);
737
735
  expect(uniswapModule.loadInfosForUniswap).toHaveBeenCalledTimes(1);
738
736
  expect(uniswapModule.getCommandsAndTokensFromUniswapCalldata).toHaveBeenCalledTimes(1);
739
737
  expect(nftServices.getNFTInfo).not.toHaveBeenCalled();
@@ -0,0 +1,341 @@
1
+ import BigNumber from "bignumber.js";
2
+ import * as rlp from "@ethersproject/rlp";
3
+ import { AddressZero } from "@ethersproject/constants";
4
+ import { BigNumber as EthersBigNumber } from "@ethersproject/bignumber";
5
+ import { type Transaction, serialize as serializeTransaction } from "@ethersproject/transactions";
6
+ import {
7
+ getChainIdAsUint32,
8
+ getParity,
9
+ getV,
10
+ hexBuffer,
11
+ intAsHexBytes,
12
+ maybeHexBuffer,
13
+ mergeResolutions,
14
+ padHexString,
15
+ safeChunkTransaction,
16
+ splitPath,
17
+ } from "../src/utils";
18
+
19
+ const chainIdsToTest = [
20
+ "1", // Always test Ethereum mainnet
21
+ "56", // Binance Smart Chain
22
+ "134", // Polygon
23
+ "109", // just under the limit for the EIP-155 operation to potentially be more than 1 byte
24
+ "110", // floor limit for the EIP-155 operation to be more than 1 byte if v is 1
25
+ "111", // floor limit for the EIP-155 operation to be more than 1 byte even if v is 0
26
+ "10000", // Random high value
27
+ "2716446429837000", // highest chainId known
28
+ ];
29
+
30
+ describe("Eth app biding", () => {
31
+ describe("Utils", () => {
32
+ describe("padHexString", () => {
33
+ it("should prevent hex string from being odd length", () => {
34
+ expect(padHexString("123")).toEqual("0123");
35
+ });
36
+ });
37
+
38
+ describe("splitPath", () => {
39
+ it("should split derivation path correctly and respect hardened paths", () => {
40
+ expect(splitPath("44'/60'/123/456/789")).toEqual([
41
+ 44 + 0x80000000,
42
+ 60 + 0x80000000,
43
+ 123,
44
+ 456,
45
+ 789,
46
+ ]);
47
+ });
48
+ });
49
+
50
+ describe("hexBuffer", () => {
51
+ it("should convert hex string to buffer", () => {
52
+ const buff = Buffer.from("0123", "hex");
53
+ expect(hexBuffer("0x123")).toEqual(buff);
54
+ expect(hexBuffer("123")).toEqual(buff);
55
+ expect(hexBuffer("0123")).toEqual(buff);
56
+ });
57
+ });
58
+
59
+ describe("maybeHexBuffer", () => {
60
+ it("should bufferize hex string and return null for empty input", () => {
61
+ expect(maybeHexBuffer("0x123")).toEqual(Buffer.from("0123", "hex"));
62
+ expect(maybeHexBuffer("")).toEqual(null);
63
+ });
64
+ });
65
+
66
+ describe("intAsHexBytes", () => {
67
+ it("should convert integer to hex string with correct number of bytes", () => {
68
+ expect(intAsHexBytes(123, 1)).toEqual("7b");
69
+ expect(intAsHexBytes(123, 2)).toEqual("007b");
70
+ expect(intAsHexBytes(123, 4)).toEqual("0000007b");
71
+ });
72
+ });
73
+
74
+ describe("mergeResolutions", () => {
75
+ it("should merge resolutions", () => {
76
+ const resolutions1 = {
77
+ nfts: ["nft1", "nft2"],
78
+ erc20Tokens: ["erc20Token1", "erc20Token2"],
79
+ externalPlugin: [{ payload: "payload1", signature: "signature1" }],
80
+ plugin: ["plugin1", "plugin2"],
81
+ domains: [
82
+ {
83
+ registry: "ens" as const,
84
+ domain: "dev.0xkvn.eth",
85
+ address: "0x6cBCD73CD8e8a42844662f0A0e76D7F79Afd933d",
86
+ type: "forward" as const,
87
+ },
88
+ ],
89
+ };
90
+ const resolutions2 = {
91
+ nfts: ["nft3", "nft4"],
92
+ erc20Tokens: ["erc20Token3", "erc20Token4"],
93
+ externalPlugin: [{ payload: "payload2", signature: "signature2" }],
94
+ plugin: ["plugin3", "plugin4"],
95
+ domains: [
96
+ {
97
+ registry: "ens" as const,
98
+ domain: "0xkvn.eth",
99
+ address: "0xB0xB0b5B0106D69fE64545A60A68C014f7570D3F861",
100
+ type: "reverse" as const,
101
+ },
102
+ ],
103
+ };
104
+ expect(mergeResolutions([resolutions1, resolutions2])).toEqual({
105
+ nfts: ["nft1", "nft2", "nft3", "nft4"],
106
+ erc20Tokens: ["erc20Token1", "erc20Token2", "erc20Token3", "erc20Token4"],
107
+ externalPlugin: [
108
+ { payload: "payload1", signature: "signature1" },
109
+ { payload: "payload2", signature: "signature2" },
110
+ ],
111
+ plugin: ["plugin1", "plugin2", "plugin3", "plugin4"],
112
+ domains: [
113
+ {
114
+ registry: "ens",
115
+ domain: "dev.0xkvn.eth",
116
+ address: "0x6cBCD73CD8e8a42844662f0A0e76D7F79Afd933d",
117
+ type: "forward",
118
+ },
119
+ {
120
+ registry: "ens",
121
+ domain: "0xkvn.eth",
122
+ address: "0xB0xB0b5B0106D69fE64545A60A68C014f7570D3F861",
123
+ type: "reverse",
124
+ },
125
+ ],
126
+ });
127
+ });
128
+ });
129
+
130
+ describe("getParity", () => {
131
+ it("should return the v from the device for typed transactions (EIP-2718)", () => {
132
+ expect(getParity(0, new BigNumber(1), 1)).toEqual(0);
133
+ expect(getParity(1, new BigNumber(1), 1)).toEqual(1);
134
+ expect(getParity(0, new BigNumber(1), 2)).toEqual(0);
135
+ expect(getParity(1, new BigNumber(1), 2)).toEqual(1);
136
+ });
137
+
138
+ it.each(chainIdsToTest)(
139
+ "should return the v from the device for legacy transactions (EIP-155) with chainId: %s",
140
+ (chainId: string) => {
141
+ const chainIdBN = new BigNumber(chainId);
142
+ const chainIdUint4 = parseInt(
143
+ Buffer.from(padHexString(chainIdBN.toString(16)), "hex")
144
+ .subarray(0, 4)
145
+ .toString("hex"),
146
+ 16,
147
+ );
148
+ const chainIdWithEIP155 = chainIdUint4 * 2 + 35;
149
+
150
+ expect([
151
+ getParity(chainIdWithEIP155 % 256, chainIdBN, null),
152
+ getParity((chainIdWithEIP155 + 1) % 256, chainIdBN, null),
153
+ ]).toEqual([0, 1]);
154
+ },
155
+ );
156
+ });
157
+
158
+ describe("getChainIdAsUint32", () => {
159
+ it("should return the chainId as a 4 bytes integer", () => {
160
+ expect(getChainIdAsUint32(1)).toEqual(1);
161
+ expect(getChainIdAsUint32(134)).toEqual(134);
162
+ expect(getChainIdAsUint32(new BigNumber(134))).toEqual(134);
163
+ expect(getChainIdAsUint32(new BigNumber(10_000_000_000))).toEqual(39062500);
164
+ expect(getChainIdAsUint32(10_000_000_000)).toEqual(39062500);
165
+ });
166
+ });
167
+
168
+ describe("getV", () => {
169
+ it("should return the v value from the device for legacy transactions non providing a chainId", () => {
170
+ expect(getV(27, new BigNumber(0), null)).toEqual((27).toString(16));
171
+ expect(getV(28, new BigNumber(0), null)).toEqual((28).toString(16));
172
+ });
173
+
174
+ it.each(chainIdsToTest)(
175
+ "should return the v with EIP-155 applied for legacy transactions providing chainId: %s",
176
+ (chainId: string) => {
177
+ const chainIdBN = new BigNumber(chainId);
178
+ const chainIdUint4 = parseInt(
179
+ Buffer.from(padHexString(chainIdBN.toString(16)), "hex")
180
+ .subarray(0, 4)
181
+ .toString("hex"),
182
+ 16,
183
+ );
184
+ const chainIdWithEIP155 = chainIdUint4 * 2 + 35;
185
+ const vEven = chainIdWithEIP155 % 256;
186
+ const vOdd = (chainIdWithEIP155 + 1) % 256;
187
+
188
+ expect(getV(vEven, chainIdBN, null)).toEqual(
189
+ padHexString(chainIdBN.multipliedBy(2).plus(35).toString(16)),
190
+ );
191
+ expect(getV(vOdd, chainIdBN, null)).toEqual(
192
+ padHexString(chainIdBN.multipliedBy(2).plus(35).plus(1).toString(16)),
193
+ );
194
+ },
195
+ );
196
+
197
+ it.each(chainIdsToTest)(
198
+ "should return the parity for transactions using EIP-2718 and no matter the chainId: %s",
199
+ (chainId: string) => {
200
+ const chainIdBN = new BigNumber(chainId);
201
+ expect(getV(0, chainIdBN, 1)).toEqual("00");
202
+ expect(getV(1, chainIdBN, 1)).toEqual("01");
203
+ expect(getV(0, chainIdBN, 2)).toEqual("00");
204
+ expect(getV(1, chainIdBN, 2)).toEqual("01");
205
+ },
206
+ );
207
+
208
+ it("should throw an error if the v value is invalid", () => {
209
+ expect(() => getV(26, new BigNumber(10_000), null)).toThrow("Invalid v value");
210
+ });
211
+ });
212
+
213
+ describe("safeChunkTransaction", () => {
214
+ // Derivation of 44'/60'/0'/0'/0
215
+ // 21B
216
+ const derivationPathBuff = Buffer.from("058000002c8000003c800000008000000000000000", "hex");
217
+
218
+ it("should return a single chunk if the transaction is small enough", () => {
219
+ const rawTx: Transaction = {
220
+ to: AddressZero,
221
+ nonce: 0,
222
+ value: EthersBigNumber.from(0),
223
+ gasPrice: EthersBigNumber.from(1),
224
+ gasLimit: EthersBigNumber.from(2),
225
+ data: "0x",
226
+ chainId: 1,
227
+ };
228
+ const serialized = serializeTransaction(rawTx);
229
+ const rlpBuff = Buffer.from(serialized.slice(2), "hex");
230
+
231
+ if (rlpBuff.length + derivationPathBuff.length > 255)
232
+ throw new Error("Transaction too big");
233
+
234
+ const payload = Buffer.concat([derivationPathBuff, rlpBuff]);
235
+ expect(safeChunkTransaction(rlpBuff, derivationPathBuff, rawTx.type)).toEqual([payload]);
236
+ });
237
+
238
+ it("should return multiple 255B chunks for typed transactions (EIP-2718)", () => {
239
+ const rawTx: Transaction = {
240
+ to: AddressZero,
241
+ nonce: 0,
242
+ value: EthersBigNumber.from(0),
243
+ gasPrice: EthersBigNumber.from(1),
244
+ gasLimit: EthersBigNumber.from(2),
245
+ data: "0x" + new Array(256).fill("00").join(""),
246
+ chainId: 1,
247
+ type: 1,
248
+ };
249
+ const serialized = serializeTransaction(rawTx);
250
+ const rlpBuff = Buffer.from(serialized.slice(2), "hex");
251
+
252
+ if (rlpBuff.length + derivationPathBuff.length < 255)
253
+ throw new Error("Transaction too small");
254
+
255
+ // Chunk size should be 255B
256
+ const payload = Buffer.concat([derivationPathBuff, rlpBuff]);
257
+ const chunks = safeChunkTransaction(rlpBuff, derivationPathBuff, rawTx.type);
258
+ expect(chunks.length).toEqual(2);
259
+ expect(chunks).toEqual([payload.subarray(0, 255), payload.subarray(255)]);
260
+ });
261
+
262
+ it("should return multiple variable chunks for legacy transactions to prevent chunking just before the [r,s,v] for a 1 byte chainId", () => {
263
+ const rawTx: Transaction = {
264
+ to: AddressZero,
265
+ nonce: 0,
266
+ value: EthersBigNumber.from(0),
267
+ gasPrice: EthersBigNumber.from(1),
268
+ gasLimit: EthersBigNumber.from(2),
269
+ data: "0x" + new Array(458).fill("00").join(""),
270
+ chainId: 127, // RLP of VRS should be only 3B as this value is <= 0x7f
271
+ type: 0,
272
+ };
273
+ const serialized = serializeTransaction(rawTx);
274
+ const rlpBuff = Buffer.from(serialized.slice(2), "hex");
275
+
276
+ if (rlpBuff.length + derivationPathBuff.length !== 513)
277
+ throw new Error("Transaction not chunking before the [r,s,v]");
278
+
279
+ // Chunk size should be 254B to avoid chunking just before the [r,s,v]
280
+ const payload = Buffer.concat([derivationPathBuff, rlpBuff]);
281
+ const chunks = safeChunkTransaction(rlpBuff, derivationPathBuff, rawTx.type);
282
+ expect(chunks.length).toEqual(3);
283
+ expect(chunks).toEqual([
284
+ payload.subarray(0, 254),
285
+ payload.subarray(254, 508),
286
+ payload.subarray(508),
287
+ ]);
288
+ });
289
+
290
+ // Above 6 bytes values, ethers will simply fail parsing/serializing the transaction
291
+ it.each([1, 2, 3, 4, 5, 6])(
292
+ "should return multiple variable chunks for legacy transactions to prevent chunking just before the [r,s,v] for a %s byte(s) chainId",
293
+ (chainIdSizeInBytes: number) => {
294
+ const chainId = new BigNumber("0x" + new Array(chainIdSizeInBytes).fill("81").join(""));
295
+ const encodedVrs = hexBuffer(
296
+ rlp.encode(["0x" + chainId.toString(16), "0x", "0x"]),
297
+ ).subarray(1);
298
+
299
+ for (let i = 1; i <= encodedVrs.length; i++) {
300
+ const rawTx: Transaction = {
301
+ to: AddressZero,
302
+ nonce: 0,
303
+ value: EthersBigNumber.from(0),
304
+ gasPrice: EthersBigNumber.from(1),
305
+ gasLimit: EthersBigNumber.from(2),
306
+ data: "0x" + new Array(458 - chainIdSizeInBytes).fill("00").join(""), // This should make a 492B long rlp
307
+ chainId: chainId.toNumber(),
308
+ type: 0,
309
+ };
310
+ const serialized = serializeTransaction(rawTx);
311
+ const rlpBuff = Buffer.from(serialized.slice(2), "hex");
312
+
313
+ if (rlpBuff.length + derivationPathBuff.length !== 513)
314
+ throw new Error("Transaction not chunking before the [r,s,v]");
315
+
316
+ // Just made from observation, don't treat this as a general rule
317
+ const chunkSize =
318
+ chainIdSizeInBytes < 3
319
+ ? 255 - chainIdSizeInBytes
320
+ : chainIdSizeInBytes < 5
321
+ ? 256 - chainIdSizeInBytes
322
+ : 257 - chainIdSizeInBytes;
323
+ const chunks = safeChunkTransaction(rlpBuff, derivationPathBuff, rawTx.type);
324
+ expect(chunks.length).toEqual(3);
325
+ expect(
326
+ [`0000` + encodedVrs.toString("hex"), `00` + encodedVrs.toString("hex")].includes(
327
+ chunks[2].toString("hex"),
328
+ ),
329
+ ).toBe(true);
330
+ const payload = Buffer.concat([derivationPathBuff, rlpBuff]);
331
+ expect(safeChunkTransaction(rlpBuff, derivationPathBuff, rawTx.type)).toEqual([
332
+ payload.subarray(0, chunkSize),
333
+ payload.subarray(chunkSize, chunkSize * 2),
334
+ payload.subarray(chunkSize * 2),
335
+ ]);
336
+ }
337
+ },
338
+ );
339
+ });
340
+ });
341
+ });