@ledgerhq/hw-app-eth 7.0.0-next.0 → 7.0.0-nightly.1

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 (70) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.unimportedrc.json +35 -3
  3. package/CHANGELOG.md +21 -5
  4. package/README.md +2 -1
  5. package/lib/Eth.d.ts.map +1 -1
  6. package/lib/Eth.js +1 -0
  7. package/lib/Eth.js.map +1 -1
  8. package/lib/modules/Uniswap/constants.d.ts +24 -0
  9. package/lib/modules/Uniswap/constants.d.ts.map +1 -0
  10. package/lib/modules/Uniswap/constants.js +59 -0
  11. package/lib/modules/Uniswap/constants.js.map +1 -0
  12. package/lib/modules/Uniswap/decoders.d.ts +3 -0
  13. package/lib/modules/Uniswap/decoders.d.ts.map +1 -0
  14. package/lib/modules/Uniswap/decoders.js +46 -0
  15. package/lib/modules/Uniswap/decoders.js.map +1 -0
  16. package/lib/modules/Uniswap/index.d.ts +44 -0
  17. package/lib/modules/Uniswap/index.d.ts.map +1 -0
  18. package/lib/modules/Uniswap/index.js +143 -0
  19. package/lib/modules/Uniswap/index.js.map +1 -0
  20. package/lib/modules/Uniswap/types.d.ts +3 -0
  21. package/lib/modules/Uniswap/types.d.ts.map +1 -0
  22. package/lib/modules/Uniswap/types.js +3 -0
  23. package/lib/modules/Uniswap/types.js.map +1 -0
  24. package/lib/services/ledger/index.d.ts.map +1 -1
  25. package/lib/services/ledger/index.js +20 -5
  26. package/lib/services/ledger/index.js.map +1 -1
  27. package/lib/services/types.d.ts +1 -0
  28. package/lib/services/types.d.ts.map +1 -1
  29. package/lib/utils.js +1 -1
  30. package/lib/utils.js.map +1 -1
  31. package/lib-es/Eth.d.ts.map +1 -1
  32. package/lib-es/Eth.js +1 -0
  33. package/lib-es/Eth.js.map +1 -1
  34. package/lib-es/modules/Uniswap/constants.d.ts +24 -0
  35. package/lib-es/modules/Uniswap/constants.d.ts.map +1 -0
  36. package/lib-es/modules/Uniswap/constants.js +56 -0
  37. package/lib-es/modules/Uniswap/constants.js.map +1 -0
  38. package/lib-es/modules/Uniswap/decoders.d.ts +3 -0
  39. package/lib-es/modules/Uniswap/decoders.d.ts.map +1 -0
  40. package/lib-es/modules/Uniswap/decoders.js +43 -0
  41. package/lib-es/modules/Uniswap/decoders.js.map +1 -0
  42. package/lib-es/modules/Uniswap/index.d.ts +44 -0
  43. package/lib-es/modules/Uniswap/index.d.ts.map +1 -0
  44. package/lib-es/modules/Uniswap/index.js +137 -0
  45. package/lib-es/modules/Uniswap/index.js.map +1 -0
  46. package/lib-es/modules/Uniswap/types.d.ts +3 -0
  47. package/lib-es/modules/Uniswap/types.d.ts.map +1 -0
  48. package/lib-es/modules/Uniswap/types.js +2 -0
  49. package/lib-es/modules/Uniswap/types.js.map +1 -0
  50. package/lib-es/services/ledger/index.d.ts.map +1 -1
  51. package/lib-es/services/ledger/index.js +20 -5
  52. package/lib-es/services/ledger/index.js.map +1 -1
  53. package/lib-es/services/types.d.ts +1 -0
  54. package/lib-es/services/types.d.ts.map +1 -1
  55. package/lib-es/utils.js +1 -1
  56. package/lib-es/utils.js.map +1 -1
  57. package/package.json +10 -5
  58. package/src/Eth.ts +1 -0
  59. package/src/modules/Uniswap/constants.ts +60 -0
  60. package/src/modules/Uniswap/decoders.ts +62 -0
  61. package/src/modules/Uniswap/index.ts +168 -0
  62. package/src/modules/Uniswap/types.ts +14 -0
  63. package/src/services/ledger/index.ts +22 -5
  64. package/src/services/types.ts +2 -0
  65. package/src/utils.ts +1 -1
  66. package/tests/Eth.unit.test.ts +65 -3
  67. package/tests/Uniswap/decoders.unit.test.ts +101 -0
  68. package/tests/Uniswap/index.unit.test.ts +188 -0
  69. package/tests/fixtures/utils.ts +8 -1
  70. package/tests/ledgerService.unit.test.ts +113 -19
@@ -0,0 +1,168 @@
1
+ import { log } from "@ledgerhq/logs";
2
+ import { utils, Transaction } from "ethers";
3
+ import { byContractAddressAndChainId, findERC20SignaturesInfo } from "../../services/ledger/erc20";
4
+ import { LoadConfig } from "../../services/types";
5
+ import { UniswapDecoders } from "./decoders";
6
+ import { CommandsAndTokens } from "./types";
7
+ import {
8
+ SWAP_COMMANDS,
9
+ UNISWAP_EXECUTE_SELECTOR,
10
+ UNISWAP_UNIVERSAL_ROUTER_ADDRESS,
11
+ UNISWAP_COMMANDS,
12
+ } from "./constants";
13
+
14
+ /**
15
+ * @ignore for external documentation
16
+ *
17
+ * Indicates if the given calldata is supported by the Uniswap plugin
18
+ * applying some basic checks and testing some assumptions
19
+ * specific to the Uniswap plugin internals
20
+ *
21
+ * @param {`0x${string}`} calldata
22
+ * @param {string | undefined} to
23
+ * @param {number} chainId
24
+ * @param {CommandsAndTokens} commandsAndTokens
25
+ * @returns {boolean}
26
+ */
27
+ export const isSupported = (
28
+ calldata: `0x${string}`,
29
+ to: string | undefined,
30
+ chainId: number,
31
+ commandsAndTokens: CommandsAndTokens,
32
+ ): boolean => {
33
+ const selector = calldata.slice(0, 10);
34
+ const contractAddress = to?.toLowerCase();
35
+ if (
36
+ selector !== UNISWAP_EXECUTE_SELECTOR ||
37
+ contractAddress !== UNISWAP_UNIVERSAL_ROUTER_ADDRESS ||
38
+ !commandsAndTokens.length
39
+ ) {
40
+ return false;
41
+ }
42
+
43
+ let endingAsset;
44
+ for (let i = 0; i < commandsAndTokens.length; i++) {
45
+ const [command, tokens] = commandsAndTokens[i];
46
+
47
+ if (!command) return false;
48
+ if (!SWAP_COMMANDS.includes(command)) continue;
49
+
50
+ const poolVersion = command.slice(0, 2) as "V2" | "V3";
51
+ if (
52
+ endingAsset &&
53
+ // Chained swaps should work as a pipe regarding the traded assets:
54
+ // The last asset of swap 1 should be the first asset of swap 2
55
+ // and the same pool version should be used for both swaps
56
+ (endingAsset.asset !== tokens[0] || endingAsset.poolVersion !== poolVersion)
57
+ ) {
58
+ return false;
59
+ } else {
60
+ endingAsset = {
61
+ poolVersion,
62
+ asset: tokens[tokens.length - 1],
63
+ };
64
+ }
65
+ }
66
+
67
+ return true;
68
+ };
69
+
70
+ /**
71
+ * @ignore for external documentation
72
+ *
73
+ * Provides a list of commands and associated tokens for a given Uniswap calldata
74
+ *
75
+ * @param {`0x${string}`} calldata
76
+ * @param {number} chainId
77
+ * @returns {CommandsAndTokens}
78
+ */
79
+ export const getCommandsAndTokensFromUniswapCalldata = (
80
+ calldata: `0x${string}`,
81
+ chainId: number,
82
+ ): CommandsAndTokens => {
83
+ try {
84
+ const [commands, inputs] = new utils.Interface([
85
+ "function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable",
86
+ ]).decodeFunctionData("execute", calldata) as [`0x${string}`, `0x${string}`[]];
87
+
88
+ const commandsBuffer = Buffer.from(commands.slice(2), "hex");
89
+ return commandsBuffer.reduce((acc, curr, i) => {
90
+ const commandName = UNISWAP_COMMANDS[`0x${curr.toString(16).padStart(2, "0")}`];
91
+ if (!commandName) return [...acc, [undefined, []]];
92
+
93
+ const commandDecoder = UniswapDecoders[commandName];
94
+ return [...acc, [commandName, commandDecoder(inputs[i], chainId)]];
95
+ }, [] as CommandsAndTokens);
96
+ } catch (e) {
97
+ log("Uniswap", "Error decoding Uniswap calldata", e);
98
+ return [];
99
+ }
100
+ };
101
+
102
+ /**
103
+ * @ignore for external documentation
104
+ *
105
+ * Returns the necessary APDUs to load the Uniswap plugin
106
+ * and the token descriptors for a transaction
107
+ *
108
+ * @param {Transaction} transaction
109
+ * @param {number} chainId
110
+ * @param {LoadConfig} userConfig
111
+ * @returns {Promise<{ pluginData?: Buffer; tokenDescriptors?: Buffer[] }>}
112
+ */
113
+ export const loadInfosForUniswap = async (
114
+ transaction: Transaction,
115
+ chainId: number,
116
+ userConfig?: LoadConfig,
117
+ ): Promise<{ pluginData?: Buffer; tokenDescriptors?: Buffer[] }> => {
118
+ const selector = transaction.data.slice(0, 10) as `0x${string}`;
119
+ const commandsAndTokens = getCommandsAndTokensFromUniswapCalldata(
120
+ transaction.data as `0x${string}`,
121
+ chainId,
122
+ );
123
+ if (!isSupported(selector, transaction.to, chainId, commandsAndTokens)) {
124
+ return {};
125
+ }
126
+
127
+ const tokenDescriptorsPromises = Promise.all(
128
+ commandsAndTokens.flatMap(([, tokens]) =>
129
+ tokens.map(async token => {
130
+ const erc20SignaturesBlob = await findERC20SignaturesInfo(userConfig || {}, chainId);
131
+ return byContractAddressAndChainId(token, chainId, erc20SignaturesBlob)?.data;
132
+ }),
133
+ ),
134
+ );
135
+ const tokenDescriptors = await tokenDescriptorsPromises.then(descriptors =>
136
+ descriptors.filter((descriptor): descriptor is Buffer => !!descriptor),
137
+ );
138
+
139
+ const pluginName = "Uniswap";
140
+ // 1 byte for the length of the plugin name
141
+ const lengthBuff = Buffer.alloc(1);
142
+ lengthBuff.writeUIntBE(pluginName.length, 0, 1);
143
+ // dynamic length bytes for the plugin name
144
+ const pluginNameBuff = Buffer.from(pluginName);
145
+ // 20 bytes for the contract address
146
+ const contractAddressBuff = Buffer.from(UNISWAP_UNIVERSAL_ROUTER_ADDRESS.slice(2), "hex");
147
+ // 4 bytes for the selector
148
+ const selectorBuff = Buffer.from(UNISWAP_EXECUTE_SELECTOR.slice(2), "hex");
149
+ // 70 bytes for the signature
150
+ const signature = Buffer.from(
151
+ // Signature is hardcoded as it would create issues by being in the CAL ethereum.json file
152
+ "3044022014391e8f355867a57fe88f6a5a4dbcb8bf8f888a9db3ff3449caf72d120396bd02200c13d9c3f79400fe0aa0434ac54d59b79503c9964a4abc3e8cd22763e0242935",
153
+ "hex",
154
+ );
155
+
156
+ const pluginData = Buffer.concat([
157
+ lengthBuff,
158
+ pluginNameBuff,
159
+ contractAddressBuff,
160
+ selectorBuff,
161
+ signature,
162
+ ]);
163
+
164
+ return {
165
+ pluginData,
166
+ tokenDescriptors,
167
+ };
168
+ };
@@ -0,0 +1,14 @@
1
+ export type UniswapSupportedCommand =
2
+ | "V2_SWAP_EXACT_IN"
3
+ | "V2_SWAP_EXACT_OUT"
4
+ | "V3_SWAP_EXACT_IN"
5
+ | "V3_SWAP_EXACT_OUT"
6
+ | "WRAP_ETH"
7
+ | "UNWRAP_ETH"
8
+ | "PERMIT2_PERMIT"
9
+ | "PERMIT2_TRANSFER_FROM"
10
+ | "PERMIT2_PERMIT_BATCH"
11
+ | "PERMIT2_TRANSFER_FROM_BATCH"
12
+ | "PAY_PORTION";
13
+
14
+ export type CommandsAndTokens = [UniswapSupportedCommand | undefined, `0x${string}`[]][];
@@ -1,20 +1,22 @@
1
- // This implements the resolution of a Transaction using Ledger's own API
1
+ import { utils } from "ethers";
2
2
  import { log } from "@ledgerhq/logs";
3
- import { Interface } from "@ethersproject/abi";
4
3
  import {
5
4
  signDomainResolution,
6
5
  signAddressResolution,
7
6
  } from "@ledgerhq/domain-service/signers/index";
8
7
  import { LedgerEthTransactionResolution, LedgerEthTransactionService, LoadConfig } from "../types";
8
+ import { decodeTxInfo, tokenSelectors, nftSelectors, mergeResolutions } from "../../utils";
9
+ import { UNISWAP_UNIVERSAL_ROUTER_ADDRESS } from "../../modules/Uniswap/constants";
9
10
  import { byContractAddressAndChainId, findERC20SignaturesInfo } from "./erc20";
11
+ import { loadInfosForUniswap } from "../../modules/Uniswap";
10
12
  import { loadInfosForContractMethod } from "./contracts";
11
13
  import { getNFTInfo, loadNftPlugin } from "./nfts";
12
- import { decodeTxInfo, tokenSelectors, nftSelectors, mergeResolutions } from "../../utils";
13
14
 
14
15
  type PotentialResolutions = {
15
16
  token: boolean | undefined;
16
17
  nft: boolean | undefined;
17
18
  externalPlugins: boolean | undefined;
19
+ uniswapV3: boolean | undefined;
18
20
  };
19
21
 
20
22
  /**
@@ -112,7 +114,9 @@ const loadNanoAppPlugins = async (
112
114
  }
113
115
  }
114
116
 
115
- if (shouldResolve.externalPlugins) {
117
+ // Uniswap has its own way of working, so we need to handle it separately
118
+ // This will prevent an error if we add Uniswap to the CAL service
119
+ if (shouldResolve.externalPlugins && contractAddress !== UNISWAP_UNIVERSAL_ROUTER_ADDRESS) {
116
120
  const contractMethodInfos = await loadInfosForContractMethod(
117
121
  contractAddress,
118
122
  selector,
@@ -129,7 +133,7 @@ const loadNanoAppPlugins = async (
129
133
  }
130
134
 
131
135
  if (erc20OfInterest && erc20OfInterest.length && abi) {
132
- const contract = new Interface(abi);
136
+ const contract = new utils.Interface(abi);
133
137
  const args = contract.parseTransaction(decodedTx).args;
134
138
 
135
139
  for (const path of erc20OfInterest) {
@@ -148,6 +152,7 @@ const loadNanoAppPlugins = async (
148
152
  nft: false,
149
153
  externalPlugins: false,
150
154
  token: true, // enforcing resolution of tokens for external plugins that need info on assets (e.g. for a swap)
155
+ uniswapV3: false,
151
156
  },
152
157
  );
153
158
  resolution = mergeResolutions([resolution, externalPluginResolution]);
@@ -158,6 +163,17 @@ const loadNanoAppPlugins = async (
158
163
  }
159
164
  }
160
165
 
166
+ if (shouldResolve.uniswapV3) {
167
+ const { pluginData, tokenDescriptors } = await loadInfosForUniswap(decodedTx, chainIdTruncated);
168
+ if (pluginData && tokenDescriptors) {
169
+ resolution.externalPlugin.push({
170
+ payload: pluginData.toString("hex"),
171
+ signature: "",
172
+ });
173
+ resolution.erc20Tokens.push(...tokenDescriptors.map(d => d.toString("hex")));
174
+ }
175
+ }
176
+
161
177
  return resolution;
162
178
  };
163
179
 
@@ -185,6 +201,7 @@ const resolveTransaction: LedgerEthTransactionService["resolveTransaction"] = as
185
201
  token: resolutionConfig.erc20 && tokenSelectors.includes(selector),
186
202
  nft: resolutionConfig.nft && nftSelectors.includes(selector),
187
203
  externalPlugins: resolutionConfig.externalPlugins,
204
+ uniswapV3: resolutionConfig.uniswapV3,
188
205
  };
189
206
 
190
207
  const pluginsResolution = await loadNanoAppPlugins(
@@ -44,6 +44,8 @@ export type ResolutionConfig = {
44
44
  erc20?: boolean;
45
45
  // List of trusted names (ENS for now) to clear sign
46
46
  domains?: DomainDescriptor[];
47
+ // activate uniswap v3 plugin resolution
48
+ uniswapV3?: boolean;
47
49
  };
48
50
 
49
51
  export type LedgerEthTransactionService = {
package/src/utils.ts CHANGED
@@ -72,7 +72,7 @@ export const decodeTxInfo = (rawTx: Buffer) => {
72
72
  data: rlpDecoded[5],
73
73
  to: rlpDecoded[3],
74
74
  // Default to 1 for non EIP 155 txs
75
- chainId: rlpTx.length > 6 ? rlpTx[6] : Buffer.from("0x01", "hex"),
75
+ chainId: rlpTx.length > 6 ? rlpTx[6] : Buffer.from("01", "hex"),
76
76
  };
77
77
  }
78
78
 
@@ -1,3 +1,4 @@
1
+ import nock from "nock";
1
2
  import axios from "axios";
2
3
  import { fail } from "assert";
3
4
  import { ethers } from "ethers";
@@ -11,10 +12,11 @@ import ERC1155_ABI from "./fixtures/ABI/ERC1155.json";
11
12
  import PARASWAP_ABI from "./fixtures/ABI/PARASWAP.json";
12
13
  import { ResolutionConfig } from "../src/services/types";
13
14
  import ParaswapJSON from "./fixtures/REST/Paraswap-Plugin.json";
15
+ import { transactionContracts, transactionData } from "./fixtures/utils";
14
16
  import { byContractAddressAndChainId } from "../src/services/ledger/erc20";
15
17
  import { ERC1155_CLEAR_SIGNED_SELECTORS, ERC721_CLEAR_SIGNED_SELECTORS } from "../src/utils";
16
18
 
17
- jest.mock("axios");
19
+ nock.disableNetConnect();
18
20
 
19
21
  describe("Eth app biding", () => {
20
22
  describe("clearSignTransaction", () => {
@@ -293,7 +295,7 @@ describe("Eth app biding", () => {
293
295
  "44'/60'/0'/0/0",
294
296
  ethers.utils
295
297
  .serializeTransaction({
296
- to: "0xdef171fe48cf0115b1d80b88dc8eab59176fee57",
298
+ to: transactionContracts.paraswap,
297
299
  value: ethers.BigNumber.from("0"),
298
300
  gasLimit: ethers.BigNumber.from("298891"),
299
301
  maxPriorityFeePerGas: ethers.BigNumber.from("1150000000"),
@@ -322,7 +324,7 @@ describe("Eth app biding", () => {
322
324
  type: 2,
323
325
  })
324
326
  .substring(2),
325
- { erc20: true, externalPlugins: true, nft: true },
327
+ { erc20: true, externalPlugins: true, nft: true, uniswapV3: true },
326
328
  true,
327
329
  );
328
330
  expect(result).toEqual({
@@ -333,6 +335,66 @@ describe("Eth app biding", () => {
333
335
  expect(spy).toHaveBeenCalledTimes(3); // 1 time plugin json file + 2 times CAL signatures <-- FIXME 1 time should be enough
334
336
  });
335
337
 
338
+ it("should clear sign the Uniswap Universal Router transaction", async () => {
339
+ const spy = jest.spyOn(axios, "get");
340
+ spy.mockImplementation(async url => {
341
+ if (url?.includes("erc20-signatures")) {
342
+ return { data: CAL_ETH } as any;
343
+ }
344
+ return Promise.reject({ response: { status: 404 } }) as any;
345
+ });
346
+ const transport = await openTransportReplayer(
347
+ RecordStore.fromString(`
348
+ => e01200006607556e69737761703fc91a3afd70395cd496c647d5a6cc9d4b2b7fad3593564c3044022014391e8f355867a57fe88f6a5a4dbcb8bf8f888a9db3ff3449caf72d120396bd02200c13d9c3f79400fe0aa0434ac54d59b79503c9964a4abc3e8cd22763e0242935
349
+ <= 9000
350
+ => e00a0000680457455448c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000012000000013045022100b47ee8551c15a2cf681c649651e987d7e527c481d27c38da1f971a8242792bd3022069c3f688ac5493a23dab5798e3c9b07484765069e1d4be14321aae4d92cb8cbe
351
+ <= 009000
352
+ => e00a0000680457455448c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000012000000013045022100b47ee8551c15a2cf681c649651e987d7e527c481d27c38da1f971a8242792bd3022069c3f688ac5493a23dab5798e3c9b07484765069e1d4be14321aae4d92cb8cbe
353
+ <= 019000
354
+ => e004000096058000002c8000003c80000000000000000000000002f9044f018084448b9b8085143fc44a5883048f8b943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad80b904243593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000
355
+ <= 9000
356
+ => e00480009600000000000000000000000000669ba25a00000000000000000000000000000000000000000000000000000000000000030a010c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000
357
+ <= 9000
358
+ => e0048000960000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000016000000000000000000000000055747be9f9f5beb232ad59fe7af013b81d95fd5e000000000000000000000000ffffffffffffffffffffffffffffff
359
+ <= 9000
360
+ => e004800096ffffffffff0000000000000000000000000000000000000000000000000000000066c32ea60000000000000000000000000000000000000000000000000000000000000006000000000000000000000000ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b00000000000000000000000000000000000000000000000000000000669ba25a0000000000000000000000000000000000
361
+ <= 9000
362
+ => e0048000960000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000413cbf00ab90b6d1b17401cbf49e00c40f98bcb3f39461ca65e26009f9e9f77029279a4587efa2d792ea61ede56e0fbd7c1305007bc59d09bc60eaba46efa23edd1c0000000000000000000000000000000000000000000000000000000000000000000000000000
363
+ <= 9000
364
+ => e0048000960000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000233a3559d9da00000000000000000000000000000000000000000000000062e76d8ff4b926e80000000000000000000000000000000000000000000000000000000000000
365
+ <= 9000
366
+ => e0048000960000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc200271055747be9f9f5beb232ad59fe7af013b81d95fd5e00000000000000000000000000000000000000000000000000000000000000000000000000000000
367
+ <= 9000
368
+ => e00480004e0000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000233a3559d9da000c0
369
+ <= 00a42d6ead5539eb1ac552b65b46df4e8fd87c37da54a280d33dc7a78992b56bb02244776870b31cfb50ea71c0ea31a30e6e513bd9766f74d61dc52b523ac256879000
370
+ `),
371
+ );
372
+ const eth = new Eth(transport);
373
+ const result = await eth.clearSignTransaction(
374
+ "44'/60'/0'/0/0",
375
+ ethers.utils
376
+ .serializeTransaction({
377
+ to: transactionContracts.uniswapUniversaRouter,
378
+ value: ethers.BigNumber.from("0"),
379
+ gasLimit: ethers.BigNumber.from("298891"),
380
+ maxPriorityFeePerGas: ethers.BigNumber.from("1150000000"),
381
+ maxFeePerGas: ethers.BigNumber.from("86969174616"),
382
+ data: transactionData.uniswap["permit2>swap-out-v3>unwrap"],
383
+ chainId: 1,
384
+ nonce: 0,
385
+ type: 2,
386
+ })
387
+ .substring(2),
388
+ { erc20: true, externalPlugins: true, nft: true, uniswapV3: true },
389
+ true,
390
+ );
391
+ expect(result).toEqual({
392
+ v: "00",
393
+ r: "a42d6ead5539eb1ac552b65b46df4e8fd87c37da54a280d33dc7a78992b56bb0",
394
+ s: "2244776870b31cfb50ea71c0ea31a30e6e513bd9766f74d61dc52b523ac25687",
395
+ });
396
+ });
397
+
336
398
  it("should throw in case of error with strict mode", async () => {
337
399
  const err = new Error("strictModeCatchThis");
338
400
  jest.spyOn(ledgerService, "resolveTransaction").mockRejectedValue(err);
@@ -0,0 +1,101 @@
1
+ import { WETH_PER_CHAIN_ID } from "../../src/modules/Uniswap/constants";
2
+ import { UniswapDecoders } from "../../src/modules/Uniswap/decoders";
3
+
4
+ describe("Uniswap", () => {
5
+ describe("decoders", () => {
6
+ it("should decode a V2_SWAP_EXACT_IN command input", () => {
7
+ // see tx 0x9d032ce73fb2c10432186087dae1b77f05fdd125982bfd04dfa47a23229dcd7f
8
+ const input =
9
+ "0x000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000008ef059372e0e255c6f18185b00000000000000000000000000000000000000000000000002520ee13087c56000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000069ee720c120ec7c9c52a625c04414459b3185f23000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2";
10
+ expect(UniswapDecoders["V2_SWAP_EXACT_IN"](input, 1)).toEqual([
11
+ "0x69ee720c120ec7c9c52a625c04414459b3185f23", // PEEZY
12
+ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // WETH
13
+ ]);
14
+ });
15
+
16
+ it("should decode a V2_SWAP_EXACT_OUT command input", () => {
17
+ // see tx 0x6fcd1f97b5c7d365b7b3c3d426c7039f45f40bdb4e7f6ac7b7fbe45c7ffe2846
18
+ const input =
19
+ "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000060a24181e400000000000000000000000000000000000000000000000000000d0853f8cc9607b000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000036b25341b2ff1bbc1b019b041ec7423a6cb21969000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2";
20
+ expect(UniswapDecoders["V2_SWAP_EXACT_OUT"](input, 1)).toEqual([
21
+ "0x36b25341b2ff1bbc1b019b041ec7423a6cb21969", // LAMBEAU
22
+ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // WETH
23
+ ]);
24
+ });
25
+
26
+ it("should decode a V3_SWAP_EXACT_IN command input", () => {
27
+ // see tx 0xf70e9fd3397af35a01d99ec33eb2652baf9de4cbe2bc042ddfb693dacb2184f2
28
+ const input =
29
+ "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000274d48fed3e800000000000000000000000000000000000000000000000064e8c5a11639f40000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc200271055747be9f9f5beb232ad59fe7af013b81d95fd5e000000000000000000000000000000000000000000";
30
+ expect(UniswapDecoders["V3_SWAP_EXACT_IN"](input, 1)).toEqual([
31
+ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // WETH
32
+ "0x55747be9f9f5beb232ad59fe7af013b81d95fd5e", // TBO
33
+ ]);
34
+ });
35
+
36
+ it("should decode a V3_SWAP_EXACT_OUT command input", () => {
37
+ // see tx 0x12522daaa016979bf86c8732e2d86b975629c99ddacc86ca935d84a48a00988f
38
+ const input =
39
+ "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000233a3559d9da00000000000000000000000000000000000000000000000062e76d8ff4b926e800000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc200271055747be9f9f5beb232ad59fe7af013b81d95fd5e000000000000000000000000000000000000000000";
40
+ expect(UniswapDecoders["V3_SWAP_EXACT_OUT"](input, 1)).toEqual([
41
+ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", // WETH
42
+ "0x55747be9f9f5beb232ad59fe7af013b81d95fd5e", // TBO
43
+ ]);
44
+ });
45
+
46
+ it("should decode a WRAP_ETH command input", () => {
47
+ // see tx 0xf9b0b54a822bae7df4826016661eb213ec934147b7a226ecf88c49cf6ba3ef0d
48
+ const input =
49
+ "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000032a04cac0ca0000";
50
+
51
+ const supportedChainIds = Object.keys(WETH_PER_CHAIN_ID);
52
+
53
+ for (const chainId of supportedChainIds) {
54
+ const expectedAddress = WETH_PER_CHAIN_ID[chainId];
55
+ expect(UniswapDecoders["WRAP_ETH"](input, chainId)).toEqual([
56
+ expectedAddress instanceof Error ? undefined : expectedAddress.toLowerCase(),
57
+ ]);
58
+ }
59
+ });
60
+
61
+ it("should decode a UNWRAP_ETH command input", () => {
62
+ // see tx 0xd2a99d50c3c2bb70040ec94e10aef6d558f8babc92474cfcc2a1aa29da9afdc5
63
+ const input =
64
+ "0x0000000000000000000000009580d04543fb19860925c649bb3a7741d7ec75d9000000000000000000000000000000000000000000000000011f162a2bc89bc4";
65
+
66
+ const supportedChainIds = Object.keys(WETH_PER_CHAIN_ID);
67
+
68
+ for (const chainId of supportedChainIds) {
69
+ const expectedAddress = WETH_PER_CHAIN_ID[chainId];
70
+ expect(UniswapDecoders["UNWRAP_ETH"](input, chainId)).toEqual([
71
+ expectedAddress instanceof Error ? undefined : expectedAddress.toLowerCase(),
72
+ ]);
73
+ }
74
+ });
75
+
76
+ it("should return an empty array for other supported commands", () => {
77
+ const input =
78
+ "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000204fce5e3e2502611000000000000000000000000000000000000000000000000000000001f942ac9a54ae2700000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000030000000000000000000000003ffeea07a27fab7ad1df5297fa75e77a43cb5790000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000ccae1bc46fb018dd396ed4c45565d4cb9d41098";
79
+ const otherCommandDecoders = Object.entries(UniswapDecoders)
80
+ .map(([command, decoder]) => {
81
+ if (
82
+ ![
83
+ "V2_SWAP_EXACT_IN",
84
+ "V2_SWAP_EXACT_OUT",
85
+ "V3_SWAP_EXACT_IN",
86
+ "V3_SWAP_EXACT_OUT",
87
+ "WRAP_ETH",
88
+ "UNWRAP_ETH",
89
+ ].includes(command)
90
+ ) {
91
+ return decoder;
92
+ }
93
+ })
94
+ .filter(Boolean);
95
+
96
+ for (const decoder of otherCommandDecoders) {
97
+ expect(decoder!(input, 1)).toEqual([]);
98
+ }
99
+ });
100
+ });
101
+ });