@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
@@ -1,5 +1,6 @@
1
1
  import { log } from "@ledgerhq/logs";
2
- import { utils, Transaction } from "ethers";
2
+ import { Interface } from "@ethersproject/abi";
3
+ import type { Transaction } from "@ethersproject/transactions";
3
4
  import { byContractAddressAndChainId, findERC20SignaturesInfo } from "../../services/ledger/erc20";
4
5
  import { LoadConfig } from "../../services/types";
5
6
  import { UniswapDecoders } from "./decoders";
@@ -81,7 +82,7 @@ export const getCommandsAndTokensFromUniswapCalldata = (
81
82
  chainId: number,
82
83
  ): CommandsAndTokens => {
83
84
  try {
84
- const [commands, inputs] = new utils.Interface([
85
+ const [commands, inputs] = new Interface([
85
86
  "function execute(bytes calldata commands, bytes[] calldata inputs, uint256 deadline) external payable",
86
87
  ]).decodeFunctionData("execute", calldata) as [`0x${string}`, `0x${string}`[]];
87
88
 
@@ -124,14 +125,14 @@ export const loadInfosForUniswap = async (
124
125
  return {};
125
126
  }
126
127
 
128
+ const uniqueTokens = Array.from(new Set(commandsAndTokens.flatMap(([, tokens]) => tokens)));
127
129
  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
- ),
130
+ uniqueTokens.map(async token => {
131
+ const erc20SignaturesBlob = await findERC20SignaturesInfo(userConfig || {}, chainId);
132
+ return byContractAddressAndChainId(token, chainId, erc20SignaturesBlob, userConfig)?.data;
133
+ }),
134
134
  );
135
+
135
136
  const tokenDescriptors = await tokenDescriptorsPromises.then(descriptors =>
136
137
  descriptors.filter((descriptor): descriptor is Buffer => !!descriptor),
137
138
  );
@@ -9,6 +9,7 @@ export type UniswapSupportedCommand =
9
9
  | "PERMIT2_TRANSFER_FROM"
10
10
  | "PERMIT2_PERMIT_BATCH"
11
11
  | "PERMIT2_TRANSFER_FROM_BATCH"
12
- | "PAY_PORTION";
12
+ | "PAY_PORTION"
13
+ | "SWEEP";
13
14
 
14
15
  export type CommandsAndTokens = [UniswapSupportedCommand | undefined, `0x${string}`[]][];
@@ -1,6 +1,5 @@
1
1
  import axios from "axios";
2
2
  import { log } from "@ledgerhq/logs";
3
- import { signatures as signaturesByChainId } from "@ledgerhq/cryptoassets-evm-signatures/data/evm/index";
4
3
  import { getLoadConfig } from "./loadConfig";
5
4
  import { LoadConfig } from "../types";
6
5
 
@@ -40,18 +39,31 @@ export const byContractAddressAndChainId = (
40
39
  contract: string,
41
40
  chainId: number,
42
41
  erc20SignaturesBlob?: string | null,
42
+ userLoadConfig?: LoadConfig,
43
43
  ): ReturnType<API["byContractAndChainId"]> => {
44
44
  // If we are able to fetch data from s3 bucket that contains dynamic CAL
45
45
  if (erc20SignaturesBlob) {
46
46
  try {
47
47
  return parse(erc20SignaturesBlob).byContractAndChainId(asContractAddress(contract), chainId);
48
48
  } catch (e) {
49
- return get(chainId)?.byContractAndChainId(asContractAddress(contract), chainId);
49
+ // Fall through to static fallback if dynamic CAL parsing fails
50
50
  }
51
51
  }
52
52
 
53
- // the static fallback when dynamic cal is not provided
54
- return get(chainId)?.byContractAndChainId(asContractAddress(contract), chainId);
53
+ // Static fallback from injected signatures (for external library users)
54
+ const loadConfig = userLoadConfig ? getLoadConfig(userLoadConfig) : null;
55
+ if (loadConfig?.staticERC20Signatures?.[chainId]) {
56
+ try {
57
+ return parse(loadConfig.staticERC20Signatures[chainId]).byContractAndChainId(
58
+ asContractAddress(contract),
59
+ chainId,
60
+ );
61
+ } catch (e) {
62
+ log("error", `Failed to parse static ERC20 signatures for chainId ${chainId}: ${String(e)}`);
63
+ }
64
+ }
65
+
66
+ return null;
55
67
  };
56
68
 
57
69
  export type TokenInfo = {
@@ -108,18 +120,3 @@ const parse = (erc20SignaturesBlob: string): API => {
108
120
  map[String(chainId) + ":" + contractAddress],
109
121
  };
110
122
  };
111
-
112
- // this internal get() will lazy load and cache the data from the erc20 data blob
113
- const get: (chainId: number) => API | null = (() => {
114
- const cache: Record<number, API> = {};
115
- return chainId => {
116
- if (cache[chainId]) return cache[chainId];
117
-
118
- const signatureBlob: string | undefined = signaturesByChainId[chainId];
119
- if (!signatureBlob) return null;
120
-
121
- const api = parse(signatureBlob);
122
- cache[chainId] = api;
123
- return api;
124
- };
125
- })();
@@ -1,16 +1,25 @@
1
- import { utils } from "ethers";
1
+ import { parse as parseTransaction } from "@ethersproject/transactions";
2
+ import { Interface } from "@ethersproject/abi";
2
3
  import { log } from "@ledgerhq/logs";
3
4
  import {
4
5
  signDomainResolution,
5
6
  signAddressResolution,
6
7
  } from "@ledgerhq/domain-service/signers/index";
7
8
  import { LedgerEthTransactionResolution, LedgerEthTransactionService, LoadConfig } from "../types";
8
- import { decodeTxInfo, tokenSelectors, nftSelectors, mergeResolutions } from "../../utils";
9
9
  import { UNISWAP_UNIVERSAL_ROUTER_ADDRESS } from "../../modules/Uniswap/constants";
10
10
  import { byContractAddressAndChainId, findERC20SignaturesInfo } from "./erc20";
11
11
  import { loadInfosForUniswap } from "../../modules/Uniswap";
12
12
  import { loadInfosForContractMethod } from "./contracts";
13
13
  import { getNFTInfo, loadNftPlugin } from "./nfts";
14
+ import {
15
+ tokenSelectors,
16
+ nftSelectors,
17
+ mergeResolutions,
18
+ ERC20_CLEAR_SIGNED_SELECTORS,
19
+ ERC1155_CLEAR_SIGNED_SELECTORS,
20
+ ERC721_CLEAR_SIGNED_SELECTORS,
21
+ getChainIdAsUint32,
22
+ } from "../../utils";
14
23
 
15
24
  type PotentialResolutions = {
16
25
  token: boolean | undefined;
@@ -28,7 +37,7 @@ type PotentialResolutions = {
28
37
  */
29
38
  const getAdditionalDataForContract = async (
30
39
  contractAddress: string,
31
- chainIdTruncated: number,
40
+ chainIdUint32: number,
32
41
  loadConfig: LoadConfig,
33
42
  shouldResolve: PotentialResolutions,
34
43
  ): Promise<Pick<LedgerEthTransactionResolution, "nfts" | "erc20Tokens">> => {
@@ -38,7 +47,7 @@ const getAdditionalDataForContract = async (
38
47
  };
39
48
 
40
49
  if (shouldResolve.nft) {
41
- const nftInfo = await getNFTInfo(contractAddress, chainIdTruncated, loadConfig);
50
+ const nftInfo = await getNFTInfo(contractAddress, chainIdUint32, loadConfig);
42
51
 
43
52
  if (nftInfo) {
44
53
  log(
@@ -52,11 +61,12 @@ const getAdditionalDataForContract = async (
52
61
  }
53
62
 
54
63
  if (shouldResolve.token) {
55
- const erc20SignaturesBlob = await findERC20SignaturesInfo(loadConfig, chainIdTruncated);
64
+ const erc20SignaturesBlob = await findERC20SignaturesInfo(loadConfig, chainIdUint32);
56
65
  const erc20Info = byContractAddressAndChainId(
57
66
  contractAddress,
58
- chainIdTruncated,
67
+ chainIdUint32,
59
68
  erc20SignaturesBlob,
69
+ loadConfig,
60
70
  );
61
71
 
62
72
  if (erc20Info) {
@@ -83,8 +93,8 @@ const getAdditionalDataForContract = async (
83
93
  const loadNanoAppPlugins = async (
84
94
  contractAddress: string,
85
95
  selector: string,
86
- decodedTx,
87
- chainIdTruncated: number,
96
+ parsedTransaction,
97
+ chainIdUint32: number,
88
98
  loadConfig: LoadConfig,
89
99
  shouldResolve: PotentialResolutions,
90
100
  ): Promise<LedgerEthTransactionResolution> => {
@@ -100,7 +110,7 @@ const loadNanoAppPlugins = async (
100
110
  const nftPluginPayload = await loadNftPlugin(
101
111
  contractAddress,
102
112
  selector,
103
- chainIdTruncated,
113
+ chainIdUint32,
104
114
  loadConfig,
105
115
  );
106
116
 
@@ -120,7 +130,7 @@ const loadNanoAppPlugins = async (
120
130
  const contractMethodInfos = await loadInfosForContractMethod(
121
131
  contractAddress,
122
132
  selector,
123
- chainIdTruncated,
133
+ chainIdUint32,
124
134
  loadConfig,
125
135
  );
126
136
 
@@ -133,8 +143,8 @@ const loadNanoAppPlugins = async (
133
143
  }
134
144
 
135
145
  if (erc20OfInterest && erc20OfInterest.length && abi) {
136
- const contract = new utils.Interface(abi);
137
- const args = contract.parseTransaction(decodedTx).args;
146
+ const contract = new Interface(abi);
147
+ const args = contract.parseTransaction(parsedTransaction).args;
138
148
 
139
149
  for (const path of erc20OfInterest) {
140
150
  const erc20ContractAddress = path.split(".").reduce((value, seg) => {
@@ -146,7 +156,7 @@ const loadNanoAppPlugins = async (
146
156
 
147
157
  const externalPluginResolution = await getAdditionalDataForContract(
148
158
  erc20ContractAddress,
149
- chainIdTruncated,
159
+ chainIdUint32,
150
160
  loadConfig,
151
161
  {
152
162
  nft: false,
@@ -164,7 +174,10 @@ const loadNanoAppPlugins = async (
164
174
  }
165
175
 
166
176
  if (shouldResolve.uniswapV3) {
167
- const { pluginData, tokenDescriptors } = await loadInfosForUniswap(decodedTx, chainIdTruncated);
177
+ const { pluginData, tokenDescriptors } = await loadInfosForUniswap(
178
+ parsedTransaction,
179
+ chainIdUint32,
180
+ );
168
181
  if (pluginData && tokenDescriptors) {
169
182
  resolution.externalPlugin.push({
170
183
  payload: pluginData.toString("hex"),
@@ -189,17 +202,32 @@ const resolveTransaction: LedgerEthTransactionService["resolveTransaction"] = as
189
202
  resolutionConfig,
190
203
  ) => {
191
204
  const rawTx = Buffer.from(rawTxHex, "hex");
192
- const { decodedTx, chainIdTruncated } = decodeTxInfo(rawTx);
205
+ const parsedTransaction = parseTransaction(`0x${rawTx.toString("hex")}`);
206
+ const chainIdUint32 = getChainIdAsUint32(parsedTransaction.chainId);
193
207
  const { domains } = resolutionConfig;
194
208
 
195
- const contractAddress = decodedTx.to;
196
- const selector = decodedTx.data.length >= 10 && decodedTx.data.substring(0, 10);
209
+ const contractAddress = parsedTransaction.to?.toLowerCase();
210
+ if (!contractAddress)
211
+ return {
212
+ nfts: [],
213
+ erc20Tokens: [],
214
+ externalPlugin: [],
215
+ plugin: [],
216
+ domains: [],
217
+ };
218
+
219
+ const selector = parsedTransaction.data.length >= 10 && parsedTransaction.data.substring(0, 10);
197
220
 
198
221
  const resolutions: Partial<LedgerEthTransactionResolution>[] = [];
199
222
  if (selector) {
200
223
  const shouldResolve: PotentialResolutions = {
201
- token: resolutionConfig.erc20 && tokenSelectors.includes(selector),
202
- nft: resolutionConfig.nft && nftSelectors.includes(selector),
224
+ token:
225
+ resolutionConfig.erc20 && tokenSelectors.includes(selector as ERC20_CLEAR_SIGNED_SELECTORS),
226
+ nft:
227
+ resolutionConfig.nft &&
228
+ nftSelectors.includes(
229
+ selector as ERC721_CLEAR_SIGNED_SELECTORS | ERC1155_CLEAR_SIGNED_SELECTORS,
230
+ ),
203
231
  externalPlugins: resolutionConfig.externalPlugins,
204
232
  uniswapV3: resolutionConfig.uniswapV3,
205
233
  };
@@ -207,8 +235,8 @@ const resolveTransaction: LedgerEthTransactionService["resolveTransaction"] = as
207
235
  const pluginsResolution = await loadNanoAppPlugins(
208
236
  contractAddress,
209
237
  selector,
210
- decodedTx,
211
- chainIdTruncated,
238
+ parsedTransaction,
239
+ chainIdUint32,
212
240
  loadConfig,
213
241
  shouldResolve,
214
242
  );
@@ -218,7 +246,7 @@ const resolveTransaction: LedgerEthTransactionService["resolveTransaction"] = as
218
246
 
219
247
  const contractResolution = await getAdditionalDataForContract(
220
248
  contractAddress,
221
- chainIdTruncated,
249
+ chainIdUint32,
222
250
  loadConfig,
223
251
  shouldResolve,
224
252
  );
@@ -1,11 +1,14 @@
1
1
  import type { LoadConfig } from "../types";
2
2
 
3
- const defaultLoadConfig: Required<LoadConfig> = {
3
+ const defaultLoadConfig: LoadConfig = {
4
4
  nftExplorerBaseURL: "https://nft.api.live.ledger.com/v1/ethereum",
5
5
  pluginBaseURL: "https://cdn.live.ledger.com",
6
6
  extraPlugins: null,
7
7
  cryptoassetsBaseURL: "https://cdn.live.ledger.com/cryptoassets",
8
8
  calServiceURL: "https://crypto-assets-service.api.ledger.com",
9
+ staticERC20Signatures: null,
10
+ staticEIP712SignaturesV1: null,
11
+ staticEIP712SignaturesV2: null,
9
12
  };
10
13
 
11
14
  export function getLoadConfig(userLoadConfig?: LoadConfig): LoadConfig {
@@ -40,7 +40,7 @@ export const getNFTInfo = async (
40
40
  ?.reduce((acc, curr) => (acc += String.fromCharCode(parseInt(curr, 16))), ""); // convert hex to string
41
41
 
42
42
  return {
43
- contractAddress: contractAddress,
43
+ contractAddress,
44
44
  collectionName: collectionName || "",
45
45
  data: payload,
46
46
  };
@@ -29,6 +29,18 @@ export type LoadConfig = {
29
29
  extraPlugins?: any | null;
30
30
  cryptoassetsBaseURL?: string | null;
31
31
  calServiceURL?: string | null;
32
+ // Static ERC20 signatures fallback (base64 encoded blobs by chainId)
33
+ // Can be provided from @ledgerhq/cryptoassets-evm-signatures/data/evm/index
34
+ // Example: { 1: "base64blob...", 137: "base64blob..." }
35
+ staticERC20Signatures?: Record<number, string> | null;
36
+ // Static EIP712 signatures fallback (v1 format)
37
+ // Can be provided from @ledgerhq/cryptoassets-evm-signatures/data/eip712
38
+ // Example: { "1:0xcontract:hash": { fields: [...] }, ... }
39
+ staticEIP712SignaturesV1?: Record<string, any> | null;
40
+ // Static EIP712 signatures fallback (v2 format)
41
+ // Can be provided from @ledgerhq/cryptoassets-evm-signatures/data/eip712_v2
42
+ // Example: { "1:0xcontract:hash": { fields: [...] }, ... }
43
+ staticEIP712SignaturesV2?: Record<string, any> | null;
32
44
  };
33
45
 
34
46
  /**
package/src/utils.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  import { BigNumber } from "bignumber.js";
2
+ import * as rlp from "@ethersproject/rlp";
2
3
  import {
3
4
  ERC20_CLEAR_SIGNED_SELECTORS,
4
5
  ERC721_CLEAR_SIGNED_SELECTORS,
5
6
  ERC1155_CLEAR_SIGNED_SELECTORS,
7
+ DAPP_SELECTORS,
6
8
  } from "@ledgerhq/evm-tools/selectors/index";
7
- import { encode, decode } from "@ethersproject/rlp";
9
+ import type { Transaction } from "@ethersproject/transactions";
8
10
  import { LedgerEthTransactionResolution } from "./services/types";
9
11
 
10
12
  export {
11
13
  ERC20_CLEAR_SIGNED_SELECTORS,
12
14
  ERC721_CLEAR_SIGNED_SELECTORS,
13
15
  ERC1155_CLEAR_SIGNED_SELECTORS,
16
+ DAPP_SELECTORS,
14
17
  };
15
18
 
16
19
  export const padHexString = (str: string) => {
@@ -18,22 +21,27 @@ export const padHexString = (str: string) => {
18
21
  };
19
22
 
20
23
  export function splitPath(path: string): number[] {
21
- const result: number[] = [];
22
- const components = path.split("/");
23
- components.forEach(element => {
24
- let number = parseInt(element, 10);
25
- if (isNaN(number)) {
24
+ const splittedPath: number[] = [];
25
+
26
+ const paths = path.split("/");
27
+ paths.forEach(path => {
28
+ let value = parseInt(path, 10);
29
+ if (isNaN(value)) {
26
30
  return; // FIXME shouldn't it throws instead?
27
31
  }
28
- if (element.length > 1 && element[element.length - 1] === "'") {
29
- number += 0x80000000;
32
+ // Detect hardened paths
33
+ if (path.length > 1 && path[path.length - 1] === "'") {
34
+ value += 0x80000000;
30
35
  }
31
- result.push(number);
36
+ splittedPath.push(value);
32
37
  });
33
- return result;
38
+
39
+ return splittedPath;
34
40
  }
35
41
 
36
42
  export function hexBuffer(str: string): Buffer {
43
+ if (!str) return Buffer.alloc(0);
44
+
37
45
  const strWithoutPrefix = str.startsWith("0x") ? str.slice(2) : str;
38
46
  return Buffer.from(padHexString(strWithoutPrefix), "hex");
39
47
  }
@@ -43,81 +51,6 @@ export function maybeHexBuffer(str: string | null | undefined): Buffer | null |
43
51
  return hexBuffer(str);
44
52
  }
45
53
 
46
- export const decodeTxInfo = (rawTx: Buffer) => {
47
- const VALID_TYPES = [1, 2];
48
- const txType = VALID_TYPES.includes(rawTx[0]) ? rawTx[0] : null;
49
- const rlpData = txType === null ? rawTx : rawTx.slice(1);
50
- const rlpTx = decode(rlpData).map(hex => Buffer.from(hex.slice(2), "hex"));
51
- let chainIdTruncated = 0;
52
- const rlpDecoded = decode(rlpData);
53
-
54
- let decodedTx;
55
- if (txType === 2) {
56
- // EIP1559
57
- decodedTx = {
58
- data: rlpDecoded[7],
59
- to: rlpDecoded[5],
60
- chainId: rlpTx[0],
61
- };
62
- } else if (txType === 1) {
63
- // EIP2930
64
- decodedTx = {
65
- data: rlpDecoded[6],
66
- to: rlpDecoded[4],
67
- chainId: rlpTx[0],
68
- };
69
- } else {
70
- // Legacy tx
71
- decodedTx = {
72
- data: rlpDecoded[5],
73
- to: rlpDecoded[3],
74
- // Default to 1 for non EIP 155 txs
75
- chainId: rlpTx.length > 6 ? rlpTx[6] : Buffer.from("01", "hex"),
76
- };
77
- }
78
-
79
- const chainIdSrc = decodedTx.chainId;
80
- let chainId = new BigNumber(0);
81
- if (chainIdSrc) {
82
- // Using BigNumber because chainID could be any uint256.
83
- chainId = new BigNumber(chainIdSrc.toString("hex"), 16);
84
- const chainIdTruncatedBuf = Buffer.alloc(4);
85
- if (chainIdSrc.length > 4) {
86
- chainIdSrc.copy(chainIdTruncatedBuf);
87
- } else {
88
- chainIdSrc.copy(chainIdTruncatedBuf, 4 - chainIdSrc.length);
89
- }
90
- chainIdTruncated = chainIdTruncatedBuf.readUInt32BE(0);
91
- }
92
-
93
- let vrsOffset = 0;
94
- if (txType === null && rlpTx.length > 6) {
95
- const rlpVrs = Buffer.from(encode(rlpTx.slice(-3)).slice(2), "hex");
96
-
97
- vrsOffset = rawTx.length - (rlpVrs.length - 1);
98
-
99
- // First byte > 0xf7 means the length of the list length doesn't fit in a single byte.
100
- if (rlpVrs[0] > 0xf7) {
101
- // Increment vrsOffset to account for that extra byte.
102
- vrsOffset++;
103
-
104
- // Compute size of the list length.
105
- const sizeOfListLen = rlpVrs[0] - 0xf7;
106
-
107
- // Increase rlpOffset by the size of the list length.
108
- vrsOffset += sizeOfListLen - 1;
109
- }
110
- }
111
-
112
- return {
113
- decodedTx,
114
- txType,
115
- chainId,
116
- chainIdTruncated,
117
- vrsOffset,
118
- };
119
- };
120
-
121
54
  /**
122
55
  * @ignore for the README
123
56
  *
@@ -156,3 +89,155 @@ export const mergeResolutions = (
156
89
 
157
90
  return mergedResolutions;
158
91
  };
92
+
93
+ /**
94
+ * @ignore for the README
95
+ *
96
+ * Ledger devices are returning v with potentially EIP-155 already applied when using legacy transactions.
97
+ * Because that v value is only represented as a single byte, we need to replicate what would be the
98
+ * overflow happening on the device while applying EIP-155 and recover the original parity.
99
+ *
100
+ * @param vFromDevice
101
+ * @param chainIdUint32
102
+ * @returns
103
+ */
104
+ export const getParity = (
105
+ vFromDevice: number,
106
+ chainId: BigNumber,
107
+ transactionType: Transaction["type"],
108
+ ): 0 | 1 => {
109
+ if (transactionType) return vFromDevice as 0 | 1;
110
+
111
+ // The device use a 4 bytes integer to represent the chainId and keeps the highest bytes
112
+ const chainIdUint32 = getChainIdAsUint32(chainId);
113
+
114
+ // Then applies EIP-155 to this chainId
115
+ const chainIdWithEIP155 = chainIdUint32 * 2 + 35;
116
+ // Since it's a single byte, we need to apply the overflow after reaching the max 0xff value and starting again to 0x00
117
+ // for both possible values, the chainId with EIP155 and a 0 or 1 parity included
118
+ const chainIdWithOverflowZero = chainIdWithEIP155 % 256;
119
+ const chainIdWithOverflowOne = (chainIdWithEIP155 + 1) % 256;
120
+
121
+ if (chainIdWithOverflowZero === vFromDevice) {
122
+ return 0;
123
+ } else if (chainIdWithOverflowOne === vFromDevice) {
124
+ return 1;
125
+ }
126
+ throw new Error("Invalid v value");
127
+ };
128
+
129
+ /**
130
+ * @ignore for the README
131
+ *
132
+ * Helper to convert a chainId from a BigNumber to a 4 bytes integer.
133
+ * ChainIds are uint256, but the device limits them to 4 bytes
134
+ *
135
+ * @param {Number|BigNumber} chainId
136
+ * @returns {Number}
137
+ */
138
+ export const getChainIdAsUint32 = (chainId: BigNumber | number): number => {
139
+ const chainIdBuff = Buffer.from(padHexString(new BigNumber(chainId).toString(16)), "hex");
140
+ const chainIdUint32 = chainIdBuff.subarray(0, 4);
141
+
142
+ return parseInt(chainIdUint32.toString("hex"), 16);
143
+ };
144
+
145
+ /**
146
+ * @ignore for the README
147
+ *
148
+ * Depending on the transaction type you're trying to sign with the device, the v value will be different.
149
+ * For legacy transactions, the v value is used to store the chainId, and that chainId can be a uint256,
150
+ * and some math operation should be applied to it in order to comply with EIP-155 for replay attacks.
151
+ *
152
+ * In order to prevent breaking changes at the time, the `v` value has been kept as a single byte
153
+ * which forces us to replicate an overflow happening on the device to get the correct `v` value
154
+ *
155
+ * @param {number} vFromDevice
156
+ * @param {BigNumber} chainId
157
+ * @param {Transaction["type"]} transactionType
158
+ * @returns {string} hexa string of the v value
159
+ */
160
+ export const getV = (
161
+ vFromDevice: number,
162
+ chainId: BigNumber,
163
+ transactionType: Transaction["type"],
164
+ ): string => {
165
+ if (chainId.isZero()) return vFromDevice.toString(16);
166
+
167
+ const parity = getParity(vFromDevice, chainId, transactionType);
168
+ return !transactionType
169
+ ? // Legacy transactions (type 0) should apply EIP-155
170
+ // EIP-155: rlp[(nonce, gasprice, startgas, to, value, data, chainid, 0, 0)]
171
+ padHexString(chainId.times(2).plus(35).plus(parity).toString(16))
172
+ : // Transactions after type 1 should only use partity (00/01) as their v value
173
+ // EIP-2930: 0x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, *signatureYParity*, signatureR, signatureS])
174
+ // EIP-1559: 0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list, *signature_y_parity*, signature_r, signature_s])
175
+ // EIP-4844: 0x03 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, data, access_list, max_fee_per_blob_gas, blob_versioned_hashes, *y_parity*, r, s])
176
+ // EIP-7702: 0x05 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, value, data, access_list, authorization_list, *signature_y_parity*, signature_r, signature_s])
177
+ padHexString(parity.toString(16));
178
+ };
179
+
180
+ /**
181
+ * @ignore for the README
182
+ *
183
+ * In order to prevent the device from considering a transaction RLP as complete before it actually is
184
+ * we need to split the RLP into chunks which could not be mistaken for a complete transaction.
185
+ * This is true for legacy transaction, where the `v` value is used to store the chainId
186
+ *
187
+ * @param {Buffer} transactionRlp
188
+ * @param {Buffer }derivationPath
189
+ * @param {Transaction["type"]} transactionType
190
+ *
191
+ * @returns {Buffer[]}
192
+ */
193
+ export const safeChunkTransaction = (
194
+ transactionRlp: Buffer,
195
+ derivationPath: Buffer,
196
+ transactionType: Transaction["type"],
197
+ ): Buffer[] => {
198
+ const maxChunkSize = 255;
199
+ // The full payload is the derivation path + the complete RLP of the transaction
200
+ const payload = Buffer.concat([derivationPath, transactionRlp]);
201
+ if (payload.length <= maxChunkSize) return [payload];
202
+
203
+ if (transactionType) {
204
+ const chunks = Math.ceil(payload.length / maxChunkSize);
205
+ return new Array(chunks)
206
+ .fill(null)
207
+ .map((_, i) => payload.subarray(i * maxChunkSize, (i + 1) * maxChunkSize));
208
+ }
209
+
210
+ // Decode the RLP of the full transaction and keep only the last 3 elements (v, r, s)
211
+ const decodedVrs: number[] = rlp.decode(transactionRlp).slice(-3);
212
+ // Encode those values back to RLP in order to get the length of this serialized list
213
+ // Result should be something like [0xc0 + list payload length, list.map(rlp)]
214
+ // since only v can be used to store the chainId in legacy transactions
215
+ const encodedVrs = rlp.encode(decodedVrs);
216
+ // Since chainIds are uint256, the list payload length can be 1B (v rlp description) + 32B (v) + 1B (r) + 1B (s) = 35B max (< 55B)
217
+ // Therefore, the RLP of this vrs list should be prefixed by a value between [0xc1, 0xe3] (0xc0 + 35B = 0xe3 max)
218
+ // @see https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/
219
+ // `encodedVrs` is then everything but the first byte of this serialization
220
+ const encodedVrsBuff = hexBuffer(encodedVrs).subarray(1);
221
+
222
+ // Since we want to avoid chunking just before the v,r,s values,
223
+ // we just check the size of that payload and detect
224
+ // if it would fit perfectly in 255B chunks
225
+ // if it does, we chunk smaller parts
226
+ let chunkSize = 0;
227
+ const lastChunkSize = payload.length % maxChunkSize;
228
+ if (lastChunkSize === 0 || lastChunkSize > encodedVrsBuff.length) {
229
+ chunkSize = maxChunkSize;
230
+ } else {
231
+ for (let i = 1; i <= maxChunkSize; i++) {
232
+ const lastChunkSize = payload.length % (maxChunkSize - i);
233
+ if (lastChunkSize === 0 || lastChunkSize > encodedVrsBuff.length) {
234
+ chunkSize = maxChunkSize - i;
235
+ break;
236
+ }
237
+ }
238
+ }
239
+ const chunks = Math.ceil(payload.length / chunkSize);
240
+ return new Array(chunks)
241
+ .fill(null)
242
+ .map((_, i) => payload.subarray(i * chunkSize, (i + 1) * chunkSize));
243
+ };