@metamask/transaction-pay-controller 24.0.2 → 24.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/LICENSE +1 -0
  3. package/dist/strategy/relay/relay-quotes.cjs +6 -6
  4. package/dist/strategy/relay/relay-quotes.cjs.map +1 -1
  5. package/dist/strategy/relay/relay-quotes.d.cts.map +1 -1
  6. package/dist/strategy/relay/relay-quotes.d.mts.map +1 -1
  7. package/dist/strategy/relay/relay-quotes.mjs +8 -8
  8. package/dist/strategy/relay/relay-quotes.mjs.map +1 -1
  9. package/dist/strategy/server/perps.cjs +18 -1
  10. package/dist/strategy/server/perps.cjs.map +1 -1
  11. package/dist/strategy/server/perps.d.cts +5 -1
  12. package/dist/strategy/server/perps.d.cts.map +1 -1
  13. package/dist/strategy/server/perps.d.mts +5 -1
  14. package/dist/strategy/server/perps.d.mts.map +1 -1
  15. package/dist/strategy/server/perps.mjs +18 -1
  16. package/dist/strategy/server/perps.mjs.map +1 -1
  17. package/dist/strategy/server/server-quotes.cjs +68 -19
  18. package/dist/strategy/server/server-quotes.cjs.map +1 -1
  19. package/dist/strategy/server/server-quotes.d.cts.map +1 -1
  20. package/dist/strategy/server/server-quotes.d.mts.map +1 -1
  21. package/dist/strategy/server/server-quotes.mjs +69 -20
  22. package/dist/strategy/server/server-quotes.mjs.map +1 -1
  23. package/dist/strategy/server/server-submit.cjs +389 -61
  24. package/dist/strategy/server/server-submit.cjs.map +1 -1
  25. package/dist/strategy/server/server-submit.d.cts.map +1 -1
  26. package/dist/strategy/server/server-submit.d.mts.map +1 -1
  27. package/dist/strategy/server/server-submit.mjs +390 -62
  28. package/dist/strategy/server/server-submit.mjs.map +1 -1
  29. package/dist/strategy/server/types.cjs.map +1 -1
  30. package/dist/strategy/server/types.d.cts +20 -4
  31. package/dist/strategy/server/types.d.cts.map +1 -1
  32. package/dist/strategy/server/types.d.mts +20 -4
  33. package/dist/strategy/server/types.d.mts.map +1 -1
  34. package/dist/strategy/server/types.mjs.map +1 -1
  35. package/dist/utils/feature-flags.cjs +24 -1
  36. package/dist/utils/feature-flags.cjs.map +1 -1
  37. package/dist/utils/feature-flags.d.cts +9 -0
  38. package/dist/utils/feature-flags.d.cts.map +1 -1
  39. package/dist/utils/feature-flags.d.mts +9 -0
  40. package/dist/utils/feature-flags.d.mts.map +1 -1
  41. package/dist/utils/feature-flags.mjs +23 -1
  42. package/dist/utils/feature-flags.mjs.map +1 -1
  43. package/dist/utils/token.cjs +1 -1
  44. package/dist/utils/token.cjs.map +1 -1
  45. package/dist/utils/token.d.cts.map +1 -1
  46. package/dist/utils/token.d.mts.map +1 -1
  47. package/dist/utils/token.mjs +3 -3
  48. package/dist/utils/token.mjs.map +1 -1
  49. package/dist/utils/transaction.cjs +4 -1
  50. package/dist/utils/transaction.cjs.map +1 -1
  51. package/dist/utils/transaction.d.cts.map +1 -1
  52. package/dist/utils/transaction.d.mts.map +1 -1
  53. package/dist/utils/transaction.mjs +4 -1
  54. package/dist/utils/transaction.mjs.map +1 -1
  55. package/package.json +3 -3
@@ -1,12 +1,29 @@
1
- import { ORIGIN_METAMASK, toHex } from "@metamask/controller-utils";
1
+ import { ORIGIN_METAMASK, successfulFetch, toHex } from "@metamask/controller-utils";
2
+ import { SignTypedDataVersion } from "@metamask/keyring-controller";
2
3
  import { createModuleLogger } from "@metamask/utils";
3
4
  import { BigNumber } from "bignumber.js";
4
5
  import { projectLogger } from "../../logger.mjs";
5
6
  import { getServerPollingInterval, getServerPollingTimeout } from "../../utils/feature-flags.mjs";
7
+ import { getNetworkClientId } from "../../utils/provider.mjs";
8
+ import { getLiveTokenBalance, normalizeTokenAddress, TokenAddressTarget } from "../../utils/token.mjs";
6
9
  import { collectTransactionIds, getTransaction, updateTransaction, waitForTransactionConfirmed } from "../../utils/transaction.mjs";
10
+ import { RELAY_DEPOSIT_TYPES } from "../relay/constants.mjs";
7
11
  import { getServerStatus, submitServerIntent } from "./server-api.mjs";
8
12
  import { ServerStatus } from "./types.mjs";
9
13
  const log = createModuleLogger(projectLogger, 'server-strategy');
14
+ const DOMAIN_FIELD_MAP = {
15
+ name: { name: 'name', type: 'string' },
16
+ version: { name: 'version', type: 'string' },
17
+ chainId: { name: 'chainId', type: 'uint256' },
18
+ verifyingContract: { name: 'verifyingContract', type: 'address' },
19
+ salt: { name: 'salt', type: 'bytes32' },
20
+ };
21
+ function isSignatureStep(step) {
22
+ return step.type === 'signature';
23
+ }
24
+ function isTransactionStep(step) {
25
+ return step.type === 'transaction';
26
+ }
10
27
  /**
11
28
  * Submits server intent quotes.
12
29
  *
@@ -31,12 +48,15 @@ async function executeSingleServerQuote(quote, messenger, transaction) {
31
48
  }, (tx) => {
32
49
  tx.txParams.nonce = undefined;
33
50
  });
34
- if (quote.original.gasless) {
35
- await submitViaServerExecute(quote, messenger, transaction);
36
- }
37
- else {
38
- await submitViaTransactionController(quote, messenger, transaction);
51
+ // Phase 1: off-chain signature steps (e.g. Relay authorize, HyperLiquid deposit).
52
+ // Use quote.request.from (resolved accountOverride) not transaction.txParams.from.
53
+ const signatureSteps = quote.original.steps.filter(isSignatureStep);
54
+ for (const step of signatureSteps) {
55
+ await submitSignatureStep(step, quote.request.from, messenger);
39
56
  }
57
+ // Phase 2: on-chain transaction steps (if any).
58
+ await submitTransactionSteps(quote, messenger, transaction);
59
+ // Phase 3: poll until the intent is confirmed on the target chain.
40
60
  const targetHash = await waitForServerCompletion(quote.original, messenger, transaction.id);
41
61
  log('Server request completed', targetHash);
42
62
  updateTransaction({
@@ -48,13 +68,299 @@ async function executeSingleServerQuote(quote, messenger, transaction) {
48
68
  });
49
69
  return { transactionHash: targetHash };
50
70
  }
51
- async function submitViaServerExecute(quote, messenger, transaction) {
52
- const { from, sourceChainId } = quote.request;
53
- const networkClientId = messenger.call('NetworkController:findNetworkClientIdByChainId', sourceChainId);
54
- const nestedTransactions = quote.original.steps.map((step) => ({
71
+ /**
72
+ * Submit the on-chain transaction steps for a server quote.
73
+ *
74
+ * Validates the source balance, builds the complete set of params (including
75
+ * any post-quote or payment-override prepends), then dispatches to the
76
+ * gasless execute path or TransactionController depending on the quote.
77
+ *
78
+ * No-ops when the quote has no transaction steps (signature-only flows).
79
+ *
80
+ * @param quote - Server quote.
81
+ * @param messenger - Controller messenger.
82
+ * @param transaction - Original transaction meta.
83
+ */
84
+ async function submitTransactionSteps(quote, messenger, transaction) {
85
+ const transactionSteps = quote.original.steps.filter(isTransactionStep);
86
+ if (transactionSteps.length === 0) {
87
+ // Signature-only quotes (all steps are signature steps, no transaction
88
+ // steps) have nothing to submit on-chain even when gasless is false.
89
+ const hasSignatureSteps = quote.original.steps.some(isSignatureStep);
90
+ if (!quote.original.gasless && !hasSignatureSteps) {
91
+ throw new Error('Server quote has no steps to submit');
92
+ }
93
+ return;
94
+ }
95
+ const { isHyperliquidSource, isPostQuote, paymentOverride } = quote.request;
96
+ // Skip balance check for HyperLiquid source flows (no on-chain debit),
97
+ // post-quote flows (funds come from the Safe after the original tx executes),
98
+ // and payment-override flows (funds are supplied by the override account).
99
+ if (!isHyperliquidSource && !isPostQuote && !paymentOverride) {
100
+ await validateSourceBalance(quote, messenger);
101
+ }
102
+ const allParams = await buildTransactionParams(quote, transactionSteps, transaction, messenger);
103
+ if (quote.original.gasless) {
104
+ await submitViaServerExecute(quote, allParams, messenger, transaction);
105
+ }
106
+ else {
107
+ await submitViaTransactionController(quote, allParams, messenger, transaction);
108
+ }
109
+ }
110
+ /**
111
+ * Build the complete flat array of TransactionParams for on-chain submission.
112
+ *
113
+ * Converts server transaction steps to params, then prepends any additional
114
+ * calls required by the payment override or post-quote flow. The returned
115
+ * array is ready to pass directly to submitViaServerExecute or
116
+ * submitViaTransactionController.
117
+ *
118
+ * @param quote - Server quote.
119
+ * @param transactionSteps - Transaction steps from the quote (pre-filtered).
120
+ * @param transaction - Original transaction meta.
121
+ * @param messenger - Controller messenger.
122
+ * @returns Complete ordered array of TransactionParams for submission.
123
+ */
124
+ async function buildTransactionParams(quote, transactionSteps, transaction, messenger) {
125
+ const { from, isPostQuote, paymentOverride } = quote.request;
126
+ const { gasLimits, maxFeePerGas, maxPriorityFeePerGas } = quote.original.client;
127
+ const originalType = getEffectiveTransactionType(transaction);
128
+ const relayParams = transactionSteps.map((step, i) => transactionStepToParams(step, i, transactionSteps.length, from, gasLimits, maxFeePerGas, maxPriorityFeePerGas, originalType));
129
+ if (paymentOverride) {
130
+ return prependPaymentOverrideParams(relayParams, quote, transaction, messenger);
131
+ }
132
+ if (isPostQuote && transaction.txParams.to) {
133
+ return prependPostQuoteParams(relayParams, quote, transaction, messenger);
134
+ }
135
+ return relayParams;
136
+ }
137
+ /**
138
+ * Converts a single server transaction step to TransactionParams.
139
+ *
140
+ * @param step - The transaction step.
141
+ * @param index - Zero-based position within the transaction steps array.
142
+ * @param totalSteps - Total number of transaction steps (for type mapping).
143
+ * @param from - Sender address.
144
+ * @param gasLimits - Per-step gas limits from client-side estimation.
145
+ * @param clientMaxFeePerGas - Client-side max fee per gas fallback.
146
+ * @param clientMaxPriorityFeePerGas - Client-side max priority fee per gas fallback.
147
+ * @param originalType - Effective type of the parent transaction.
148
+ * @returns Normalized TransactionParams for this step.
149
+ */
150
+ function transactionStepToParams(step, index, totalSteps, from, gasLimits, clientMaxFeePerGas, clientMaxPriorityFeePerGas, originalType) {
151
+ let gas;
152
+ const gasLimit = gasLimits[index];
153
+ if (gasLimit) {
154
+ gas = toHex(gasLimit);
155
+ }
156
+ else if (step.gasLimit) {
157
+ gas = toHex(step.gasLimit);
158
+ }
159
+ const resolvedMaxFeePerGas = step.maxFeePerGas ?? clientMaxFeePerGas;
160
+ const resolvedMaxPriorityFeePerGas = step.maxPriorityFeePerGas ?? clientMaxPriorityFeePerGas;
161
+ const params = {
55
162
  data: step.data,
163
+ from,
164
+ gas,
165
+ maxFeePerGas: resolvedMaxFeePerGas
166
+ ? toHex(resolvedMaxFeePerGas)
167
+ : undefined,
168
+ maxPriorityFeePerGas: resolvedMaxPriorityFeePerGas
169
+ ? toHex(resolvedMaxPriorityFeePerGas)
170
+ : undefined,
56
171
  to: step.to,
57
- value: step.value,
172
+ type: getTransactionType(index, totalSteps, originalType),
173
+ value: toHex(step.value),
174
+ };
175
+ log('Built transaction params for step', {
176
+ index,
177
+ step: {
178
+ gasLimit: step.gasLimit,
179
+ maxFeePerGas: step.maxFeePerGas,
180
+ maxPriorityFeePerGas: step.maxPriorityFeePerGas,
181
+ value: step.value,
182
+ },
183
+ params: {
184
+ gas: params.gas,
185
+ maxFeePerGas: params.maxFeePerGas,
186
+ maxPriorityFeePerGas: params.maxPriorityFeePerGas,
187
+ type: params.type,
188
+ value: params.value,
189
+ },
190
+ });
191
+ return params;
192
+ }
193
+ /**
194
+ * Determine the relay deposit transaction type for a step at the given index.
195
+ *
196
+ * Single-step quotes always use the deposit type. In multi-step quotes the
197
+ * first step is an approval and subsequent steps are deposits — matching the
198
+ * Relay strategy's convention.
199
+ *
200
+ * @param index - Zero-based index of the step within the transaction step array.
201
+ * @param totalSteps - Total number of transaction steps.
202
+ * @param originalType - Effective type of the parent transaction.
203
+ * @returns The mapped TransactionType for this step.
204
+ */
205
+ function getTransactionType(index, totalSteps, originalType) {
206
+ const depositType = getRelayDepositType(originalType);
207
+ if (totalSteps === 1) {
208
+ return depositType;
209
+ }
210
+ return index === 0 ? 'tokenMethodApprove' : depositType;
211
+ }
212
+ /**
213
+ * Get the relay deposit transaction type based on the parent transaction type.
214
+ *
215
+ * @param originalType - Type of the parent transaction.
216
+ * @returns The mapped relay deposit type, or `relayDeposit` as a fallback.
217
+ */
218
+ function getRelayDepositType(originalType) {
219
+ return ((originalType && RELAY_DEPOSIT_TYPES[originalType]) ??
220
+ 'relayDeposit');
221
+ }
222
+ /**
223
+ * Get the effective transaction type, resolving through batch-type parent
224
+ * transactions to find the nested perps/predict type.
225
+ *
226
+ * @param transaction - The transaction metadata.
227
+ * @returns The resolved type from nested transactions, or the top-level type.
228
+ */
229
+ function getEffectiveTransactionType(transaction) {
230
+ if (transaction.type !== 'batch') {
231
+ return transaction.type;
232
+ }
233
+ const nestedType = transaction.nestedTransactions?.find((tx) => tx.type && RELAY_DEPOSIT_TYPES[tx.type] !== undefined)?.type;
234
+ return nestedType ?? transaction.type;
235
+ }
236
+ /**
237
+ * Prepend payment override calls before the relay transaction steps.
238
+ *
239
+ * For money-account payment override flows the override account supplies
240
+ * the source funds. The override transactions must be batched ahead of the
241
+ * relay deposit steps.
242
+ *
243
+ * @param relayParams - Already-built relay step params.
244
+ * @param quote - Server quote.
245
+ * @param transaction - Original transaction meta.
246
+ * @param messenger - Controller messenger.
247
+ * @returns Combined params with override calls prepended.
248
+ */
249
+ async function prependPaymentOverrideParams(relayParams, quote, transaction, messenger) {
250
+ const { transactionData } = messenger.call('TransactionPayController:getState');
251
+ const { calls: overrideCalls } = await messenger.call('TransactionPayController:getPaymentOverrideData', {
252
+ amount: quote.sourceAmount.human,
253
+ transaction,
254
+ transactionData: transactionData[transaction.id],
255
+ });
256
+ if (!overrideCalls.length) {
257
+ log('No payment override calls to prepend');
258
+ return relayParams;
259
+ }
260
+ log('Prepending payment override calls', { count: overrideCalls.length });
261
+ return [...overrideCalls, ...relayParams];
262
+ }
263
+ /**
264
+ * Prepend the original transaction (or a delegation-wrapped version) before
265
+ * the relay deposit steps for post-quote flows.
266
+ *
267
+ * In post-quote flows the source tokens are held in the Safe and only become
268
+ * available after the original transaction executes as part of the batch.
269
+ * When an accountOverride is active the override account cannot directly
270
+ * execute the original call, so it is wrapped in a delegation transaction.
271
+ *
272
+ * @param relayParams - Already-built relay step params.
273
+ * @param quote - Server quote.
274
+ * @param transaction - Original transaction meta.
275
+ * @param messenger - Controller messenger.
276
+ * @returns Combined params with the original tx prepended.
277
+ */
278
+ async function prependPostQuoteParams(relayParams, quote, transaction, messenger) {
279
+ const hasAccountOverride = quote.request.from.toLowerCase() !==
280
+ transaction.txParams.from.toLowerCase();
281
+ const { maxFeePerGas, maxPriorityFeePerGas } = quote.original.client;
282
+ let prependedParams;
283
+ if (hasAccountOverride) {
284
+ prependedParams = await buildDelegatedOriginalParams(transaction, messenger);
285
+ }
286
+ else {
287
+ prependedParams = {
288
+ data: transaction.txParams.data,
289
+ from: transaction.txParams.from,
290
+ to: transaction.txParams.to,
291
+ value: transaction.txParams.value,
292
+ };
293
+ }
294
+ // Ensure the prepended tx carries the same fee caps as the relay steps so
295
+ // it isn't submitted with undefined maxFeePerGas in a non-7702 batch.
296
+ prependedParams.maxFeePerGas = maxFeePerGas ? toHex(maxFeePerGas) : undefined;
297
+ prependedParams.maxPriorityFeePerGas = maxPriorityFeePerGas
298
+ ? toHex(maxPriorityFeePerGas)
299
+ : undefined;
300
+ log('Prepending post-quote original tx', { hasAccountOverride });
301
+ return [prependedParams, ...relayParams];
302
+ }
303
+ /**
304
+ * Build TransactionParams for a delegation that redeems the original
305
+ * post-quote transaction on behalf of the override account.
306
+ *
307
+ * @param transaction - Original transaction meta to be redeemed.
308
+ * @param messenger - Controller messenger.
309
+ * @returns Transaction params for the delegation tx.
310
+ */
311
+ async function buildDelegatedOriginalParams(transaction, messenger) {
312
+ const delegation = await messenger.call('TransactionPayController:getDelegationTransaction', { transaction });
313
+ log('Delegation result for post-quote original tx', delegation);
314
+ return {
315
+ data: delegation.data,
316
+ from: transaction.txParams.from,
317
+ to: delegation.to,
318
+ value: delegation.value,
319
+ };
320
+ }
321
+ /**
322
+ * Validate that the user's source token balance covers the quote's required
323
+ * source amount before submitting on-chain.
324
+ *
325
+ * Reads the live balance via RPC rather than the cached state so it reflects
326
+ * any concurrent spends. Throws fast with a clear error instead of letting
327
+ * the on-chain transaction revert.
328
+ *
329
+ * @param quote - Server quote containing the required source amount.
330
+ * @param messenger - Controller messenger.
331
+ */
332
+ async function validateSourceBalance(quote, messenger) {
333
+ const { from, sourceChainId, sourceTokenAddress } = quote.request;
334
+ const normalizedSourceTokenAddress = normalizeTokenAddress(sourceTokenAddress, sourceChainId, TokenAddressTarget.MetaMask);
335
+ let currentBalance;
336
+ try {
337
+ currentBalance = await getLiveTokenBalance(messenger, from, sourceChainId, normalizedSourceTokenAddress);
338
+ }
339
+ catch (error) {
340
+ throw new Error(`Cannot validate payment token balance - ${error.message}`);
341
+ }
342
+ const requiredAmount = new BigNumber(quote.sourceAmount.raw);
343
+ const balance = new BigNumber(currentBalance);
344
+ log('Validating source balance', {
345
+ from,
346
+ sourceChainId,
347
+ sourceTokenAddress,
348
+ currentBalance,
349
+ requiredAmount: requiredAmount.toString(10),
350
+ });
351
+ if (balance.isLessThan(requiredAmount)) {
352
+ throw new Error(`Insufficient source token balance for server deposit. ` +
353
+ `Required: ${requiredAmount.toString(10)}, ` +
354
+ `Available: ${balance.toString(10)}`);
355
+ }
356
+ }
357
+ async function submitViaServerExecute(quote, allParams, messenger, transaction) {
358
+ const { from, sourceChainId } = quote.request;
359
+ const networkClientId = getNetworkClientId(messenger, sourceChainId);
360
+ const nestedTransactions = allParams.map((params) => ({
361
+ data: (params.data ?? '0x'),
362
+ to: params.to,
363
+ value: (params.value ?? '0x0'),
58
364
  }));
59
365
  const sourceCallTransaction = {
60
366
  ...transaction,
@@ -93,15 +399,10 @@ async function submitViaServerExecute(quote, messenger, transaction) {
93
399
  throw new Error(`Server submit failed: ${submitResponse.error ?? 'unknown'}`);
94
400
  }
95
401
  }
96
- async function submitViaTransactionController(quote, messenger, transaction) {
402
+ async function submitViaTransactionController(quote, allParams, messenger, transaction) {
97
403
  const { from, sourceChainId, sourceTokenAddress } = quote.request;
98
- const { steps } = quote.original;
99
- if (steps.length === 0) {
100
- throw new Error('Server quote has no steps to submit');
101
- }
102
- const networkClientId = messenger.call('NetworkController:findNetworkClientIdByChainId', sourceChainId);
103
- const { gasLimits, is7702, maxFeePerGas, maxPriorityFeePerGas } = quote.original.client;
104
- const transactionParams = steps.map((step, i) => stepToParams(step, from, gasLimits[i], maxFeePerGas, maxPriorityFeePerGas));
404
+ const { gasLimits, is7702 } = quote.original.client;
405
+ const networkClientId = getNetworkClientId(messenger, sourceChainId);
105
406
  const gasFeeToken = quote.fees.isSourceGasFeeToken
106
407
  ? sourceTokenAddress
107
408
  : undefined;
@@ -110,8 +411,8 @@ async function submitViaTransactionController(quote, messenger, transaction) {
110
411
  gasFeeToken,
111
412
  is7702,
112
413
  networkClientId,
414
+ paramCount: allParams.length,
113
415
  sourceChainId,
114
- stepCount: steps.length,
115
416
  });
116
417
  const transactionIds = [];
117
418
  const { end } = collectTransactionIds(sourceChainId, from, messenger, (transactionId) => {
@@ -126,7 +427,7 @@ async function submitViaTransactionController(quote, messenger, transaction) {
126
427
  });
127
428
  });
128
429
  try {
129
- if (transactionParams.length === 1) {
430
+ if (allParams.length === 1) {
130
431
  const addTransactionOptions = {
131
432
  gasFeeToken,
132
433
  isInternal: true,
@@ -135,16 +436,19 @@ async function submitViaTransactionController(quote, messenger, transaction) {
135
436
  requireApproval: false,
136
437
  };
137
438
  log('Calling addTransaction', {
138
- params: transactionParams[0],
439
+ params: allParams[0],
139
440
  options: addTransactionOptions,
140
441
  });
141
- await messenger.call('TransactionController:addTransaction', transactionParams[0], addTransactionOptions);
442
+ await messenger.call('TransactionController:addTransaction', allParams[0], addTransactionOptions);
142
443
  }
143
444
  else {
144
445
  const gasLimit7702 = is7702 ? toHex(gasLimits[0]) : undefined;
145
- const batchTransactions = transactionParams.map((params, i) => {
146
- const gas = (gasLimit7702 ??
147
- (gasLimits[i] === undefined ? undefined : params.gas));
446
+ const batchTransactions = allParams.map((params) => {
447
+ // params.gas was already resolved correctly by transactionStepToParams
448
+ // (indexed by relay-step position), so use it directly. Indexing
449
+ // allParams position into gasLimits would be wrong when payment-
450
+ // override or post-quote params are prepended.
451
+ const gas = (gasLimit7702 ?? params.gas);
148
452
  return {
149
453
  params: {
150
454
  data: params.data,
@@ -154,6 +458,7 @@ async function submitViaTransactionController(quote, messenger, transaction) {
154
458
  to: params.to,
155
459
  value: params.value,
156
460
  },
461
+ type: params.type,
157
462
  };
158
463
  });
159
464
  const addTransactionBatchOptions = {
@@ -199,44 +504,67 @@ async function submitViaTransactionController(quote, messenger, transaction) {
199
504
  });
200
505
  }
201
506
  }
202
- function stepToParams(step, from, gasLimit, clientMaxFeePerGas, clientMaxPriorityFeePerGas) {
203
- let gas;
204
- if (gasLimit) {
205
- gas = toHex(gasLimit);
206
- }
207
- else if (step.gasLimit) {
208
- gas = toHex(step.gasLimit);
209
- }
210
- const resolvedMaxFeePerGas = step.maxFeePerGas ?? clientMaxFeePerGas;
211
- const resolvedMaxPriorityFeePerGas = step.maxPriorityFeePerGas ?? clientMaxPriorityFeePerGas;
212
- const params = {
213
- data: step.data,
214
- from,
215
- gas,
216
- maxFeePerGas: resolvedMaxFeePerGas
217
- ? toHex(resolvedMaxFeePerGas)
218
- : undefined,
219
- maxPriorityFeePerGas: resolvedMaxPriorityFeePerGas
220
- ? toHex(resolvedMaxPriorityFeePerGas)
221
- : undefined,
222
- to: step.to,
223
- value: toHex(step.value),
224
- };
225
- log('Step to params', {
226
- step: {
227
- gasLimit: step.gasLimit,
228
- maxFeePerGas: step.maxFeePerGas,
229
- maxPriorityFeePerGas: step.maxPriorityFeePerGas,
230
- value: step.value,
231
- },
232
- params: {
233
- gas: params.gas,
234
- maxFeePerGas: params.maxFeePerGas,
235
- maxPriorityFeePerGas: params.maxPriorityFeePerGas,
236
- value: params.value,
507
+ async function submitSignatureStep(step, from, messenger) {
508
+ const { sign, post } = step;
509
+ const typedData = {
510
+ domain: sign.domain,
511
+ types: {
512
+ ...sign.types,
513
+ EIP712Domain: deriveEIP712DomainType(sign.domain),
237
514
  },
515
+ primaryType: sign.primaryType,
516
+ message: sign.value,
517
+ };
518
+ log('Signing typed data for signature step', {
519
+ primaryType: sign.primaryType,
238
520
  });
239
- return params;
521
+ const signature = await messenger.call('KeyringController:signTypedMessage', { from, data: JSON.stringify(typedData) }, SignTypedDataVersion.V4);
522
+ await postSignatureStepResult(post.endpoint, post.method, post.body, signature, post.signatureFormat);
523
+ }
524
+ async function postSignatureStepResult(endpoint, method, body, signature, signatureFormat) {
525
+ let url = endpoint;
526
+ let postBody;
527
+ if (signatureFormat === 'queryParam') {
528
+ url = `${endpoint}?signature=${signature}`;
529
+ postBody = body;
530
+ }
531
+ else {
532
+ // eslint-disable-next-line id-length
533
+ const r = signature.slice(0, 66);
534
+ // eslint-disable-next-line id-length
535
+ const s = `0x${signature.slice(66, 130)}`;
536
+ // eslint-disable-next-line id-length
537
+ const v = parseInt(signature.slice(130, 132), 16);
538
+ postBody = { ...body, signature: { r, s, v } };
539
+ }
540
+ log('Posting signature step result', { url, signatureFormat });
541
+ let result;
542
+ try {
543
+ const response = await successfulFetch(url, {
544
+ method,
545
+ headers: { 'Content-Type': 'application/json' },
546
+ body: JSON.stringify(postBody),
547
+ });
548
+ result = await response.json();
549
+ }
550
+ catch (error) {
551
+ throw new Error(`Signature step POST failed: ${error.message}`);
552
+ }
553
+ log('Signature step POST response', result);
554
+ // For rsv-format steps (HyperLiquid exchange endpoint) the response body
555
+ // carries an explicit status field. Validate it here so a failed deposit
556
+ // throws immediately rather than silently proceeding to the polling phase.
557
+ if (signatureFormat === 'rsv') {
558
+ const status = result?.status;
559
+ if (status !== 'ok') {
560
+ throw new Error(`Signature step rejected by server: ${JSON.stringify(result)}`);
561
+ }
562
+ }
563
+ }
564
+ function deriveEIP712DomainType(domain) {
565
+ return Object.keys(DOMAIN_FIELD_MAP)
566
+ .filter((key) => Object.prototype.hasOwnProperty.call(domain, key))
567
+ .map((key) => DOMAIN_FIELD_MAP[key]);
240
568
  }
241
569
  async function waitForServerCompletion(quote, messenger, transactionId) {
242
570
  const pollingInterval = getServerPollingInterval(messenger);