@metamask-previews/transaction-pay-controller 30.1.0-preview-c9f31ce → 30.1.0-preview-5e24e4d22

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - Support Predict withdraw in `ServerStrategy` ([#10702](https://github.com/MetaMask/core/pull/10702))
13
+ - Polymarket deposit wallet withdrawals unwrap directly to a provider deposit address, then sweep any leftover or refunded USDC.e back into the deposit wallet.
14
+ - Polymarket deposit wallet withdrawals have no source network fee, as the Polymarket relayer pays origin gas.
15
+ - Non-atomic post-quote payment overrides, such as withdrawing to a Money Account, settle on the override recipient and run the override calls as a second leg.
16
+ - Post-quote quotes honour `refundTo`, include the original transaction in gas estimation, and reserve the gas fee token cost from the source amount.
17
+
10
18
  ### Changed
11
19
 
12
20
  - Bump `@metamask/transaction-controller` from `^72.2.0` to `^72.2.1` ([#10817](https://github.com/MetaMask/core/pull/10817))
@@ -0,0 +1,18 @@
1
+ import type { Hex } from '@metamask/utils';
2
+ import type { ServerQuote } from './types.js';
3
+ /**
4
+ * Whether a server quote routes through a deposit step, i.e. the user funds the
5
+ * request by transferring to the provider rather than calling a swap contract.
6
+ *
7
+ * @param steps - Server quote steps.
8
+ * @returns True when the quote has a deposit step.
9
+ */
10
+ export declare function hasServerDepositStep(steps: ServerQuote['steps']): boolean;
11
+ /**
12
+ * Get the calldata of the server deposit transaction step.
13
+ *
14
+ * @param quote - Server quote.
15
+ * @returns The deposit calldata, or `undefined` when there is none.
16
+ */
17
+ export declare function getServerDepositData(quote: ServerQuote): Hex | undefined;
18
+ //# sourceMappingURL=deposit-step.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deposit-step.d.ts","sourceRoot":"","sources":["../../../src/strategy/server/deposit-step.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAG3C,OAAO,KAAK,EAAE,WAAW,EAAyB,MAAM,YAAY,CAAC;AAOrE;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,OAAO,CAEzE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,GAAG,SAAS,CAExE"}
@@ -0,0 +1,37 @@
1
+ import { BigNumber } from 'bignumber.js';
2
+ const TOKEN_TRANSFER_FOUR_BYTE = '0xa9059cbb';
3
+ // `0x` + 4-byte selector + two 32-byte arguments (recipient, amount).
4
+ const TOKEN_TRANSFER_DATA_LENGTH = 2 + 8 + 64 * 2;
5
+ /**
6
+ * Whether a server quote routes through a deposit step, i.e. the user funds the
7
+ * request by transferring to the provider rather than calling a swap contract.
8
+ *
9
+ * @param steps - Server quote steps.
10
+ * @returns True when the quote has a deposit step.
11
+ */
12
+ export function hasServerDepositStep(steps) {
13
+ return steps.some(isDepositStep);
14
+ }
15
+ /**
16
+ * Get the calldata of the server deposit transaction step.
17
+ *
18
+ * @param quote - Server quote.
19
+ * @returns The deposit calldata, or `undefined` when there is none.
20
+ */
21
+ export function getServerDepositData(quote) {
22
+ return quote.steps.find(isDepositStep)?.data;
23
+ }
24
+ /**
25
+ * Whether a step is a plain ERC-20 `transfer` with no extra calldata and no
26
+ * native value, which is how a provider deposit address is funded.
27
+ *
28
+ * @param step - Server quote step.
29
+ * @returns True when the step is a plain token transfer.
30
+ */
31
+ function isDepositStep(step) {
32
+ return (step.type === 'transaction' &&
33
+ step.data.length === TOKEN_TRANSFER_DATA_LENGTH &&
34
+ step.data.toLowerCase().startsWith(TOKEN_TRANSFER_FOUR_BYTE) &&
35
+ new BigNumber(step.value || '0').isZero());
36
+ }
37
+ //# sourceMappingURL=deposit-step.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deposit-step.js","sourceRoot":"","sources":["../../../src/strategy/server/deposit-step.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAIzC,MAAM,wBAAwB,GAAG,YAAY,CAAC;AAE9C,sEAAsE;AACtE,MAAM,0BAA0B,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AAElD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAA2B;IAC9D,OAAO,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;AACnC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAkB;IACrD,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC;AAC/C,CAAC;AAED;;;;;;GAMG;AACH,SAAS,aAAa,CACpB,IAAkC;IAElC,OAAO,CACL,IAAI,CAAC,IAAI,KAAK,aAAa;QAC3B,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,0BAA0B;QAC/C,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,wBAAwB,CAAC;QAC5D,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,CAC1C,CAAC;AACJ,CAAC","sourcesContent":["import type { Hex } from '@metamask/utils';\nimport { BigNumber } from 'bignumber.js';\n\nimport type { ServerQuote, ServerTransactionStep } from './types.js';\n\nconst TOKEN_TRANSFER_FOUR_BYTE = '0xa9059cbb';\n\n// `0x` + 4-byte selector + two 32-byte arguments (recipient, amount).\nconst TOKEN_TRANSFER_DATA_LENGTH = 2 + 8 + 64 * 2;\n\n/**\n * Whether a server quote routes through a deposit step, i.e. the user funds the\n * request by transferring to the provider rather than calling a swap contract.\n *\n * @param steps - Server quote steps.\n * @returns True when the quote has a deposit step.\n */\nexport function hasServerDepositStep(steps: ServerQuote['steps']): boolean {\n return steps.some(isDepositStep);\n}\n\n/**\n * Get the calldata of the server deposit transaction step.\n *\n * @param quote - Server quote.\n * @returns The deposit calldata, or `undefined` when there is none.\n */\nexport function getServerDepositData(quote: ServerQuote): Hex | undefined {\n return quote.steps.find(isDepositStep)?.data;\n}\n\n/**\n * Whether a step is a plain ERC-20 `transfer` with no extra calldata and no\n * native value, which is how a provider deposit address is funded.\n *\n * @param step - Server quote step.\n * @returns True when the step is a plain token transfer.\n */\nfunction isDepositStep(\n step: ServerQuote['steps'][number],\n): step is ServerTransactionStep {\n return (\n step.type === 'transaction' &&\n step.data.length === TOKEN_TRANSFER_DATA_LENGTH &&\n step.data.toLowerCase().startsWith(TOKEN_TRANSFER_FOUR_BYTE) &&\n new BigNumber(step.value || '0').isZero()\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"server-quotes.d.ts","sourceRoot":"","sources":["../../../src/strategy/server/server-quotes.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACV,2BAA2B,EAI3B,mBAAmB,EACpB,MAAM,gBAAgB,CAAC;AAmBxB,OAAO,KAAK,EACV,WAAW,EAKZ,MAAM,YAAY,CAAC;AA6CpB;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,mBAAmB,CAAC,WAAW,CAAC,EAAE,CAAC,CAY7C"}
1
+ {"version":3,"file":"server-quotes.d.ts","sourceRoot":"","sources":["../../../src/strategy/server/server-quotes.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACV,2BAA2B,EAI3B,mBAAmB,EACpB,MAAM,gBAAgB,CAAC;AAuBxB,OAAO,KAAK,EACV,WAAW,EAKZ,MAAM,YAAY,CAAC;AA6CpB;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,mBAAmB,CAAC,WAAW,CAAC,EAAE,CAAC,CAY7C"}
@@ -5,12 +5,16 @@ import { BigNumber } from 'bignumber.js';
5
5
  import { CHAIN_ID_HYPERCORE, PaymentOverride, TransactionPayStrategy, } from '../../constants.js';
6
6
  import { projectLogger } from '../../logger.js';
7
7
  import { getFiatValueFromUsd } from '../../utils/amounts.js';
8
- import { getFeatureFlags, getSlippage, isEIP7702Chain, } from '../../utils/feature-flags.js';
8
+ import { getSlippage, isEIP7702Chain } from '../../utils/feature-flags.js';
9
9
  import { GasPaymentMode, resolveGasPayment, resolveGasStationCost, } from '../../utils/gas-payment.js';
10
10
  import { calculateGasCost, getGasFee } from '../../utils/gas.js';
11
- import { estimateQuoteGasLimits } from '../../utils/quote-gas.js';
11
+ import { getPolymarketDepositWalletOverrides, getPredictWithdrawFeeTokenAccount, getPredictWithdrawSafeAddress, } from '../../utils/polymarket/withdraw.js';
12
+ import { reservePostQuoteGas } from '../../utils/post-quote.js';
13
+ import { estimateSourceGas } from '../../utils/quote-gas.js';
14
+ import { resolveNonAtomicRecipient } from '../../utils/second-leg.js';
12
15
  import { getTokenFiatRate } from '../../utils/token.js';
13
16
  import { getQuotePricing, TradeType } from '../../utils/trade-type.js';
17
+ import { hasServerDepositStep } from './deposit-step.js';
14
18
  import { normalizeServerPerpsRequest } from './perps.js';
15
19
  import { fetchServerQuote } from './server-api.js';
16
20
  const log = createModuleLogger(projectLogger, 'server-quotes');
@@ -37,31 +41,68 @@ export async function getServerQuotes(request) {
37
41
  return quotes.flat();
38
42
  }
39
43
  async function getQuotesForRequest(quoteRequest, fullRequest) {
40
- const { accountSupports7702, messenger, signal, transaction } = fullRequest;
41
44
  try {
42
- const body = await buildServerQuoteRequest(quoteRequest, transaction, messenger, accountSupports7702);
43
- log('Request body', body);
44
- const response = await fetchServerQuote(messenger, body, signal);
45
- log('Raw quote response', response);
46
- const fulfilledResults = response.results.filter(isFulfilledResult);
47
- // The second leg runs from `request.recipient`, so it has to match the
48
- // executing account the quote settles on.
49
- const settledRequest = {
50
- ...quoteRequest,
51
- recipient: transaction.txParams.from,
52
- };
53
- const normalized = await Promise.all(fulfilledResults.map((result) => normalizeQuote(result, settledRequest, messenger, body.tradeType === TradeType.ExactInput, transaction, accountSupports7702, isSecondLegRequired(body, quoteRequest, transaction, result.quote))));
54
- log('Normalized quotes', normalized);
55
- return normalized;
45
+ const quotes = await fetchQuotes(quoteRequest, fullRequest);
46
+ if (!quoteRequest.isPostQuote) {
47
+ return quotes;
48
+ }
49
+ // Post-quote flows commit the whole source balance, so any gas paid in the
50
+ // source token has to be reserved by re-quoting the same provider for less.
51
+ return await Promise.all(quotes.map((quote) => reservePostQuoteGas({
52
+ quote,
53
+ requote: (adjustedRequest) => requoteProvider(adjustedRequest, fullRequest, quote.original),
54
+ })));
56
55
  }
57
56
  catch (error) {
58
57
  log('Error fetching quotes', { error });
59
58
  return [];
60
59
  }
61
60
  }
61
+ async function requoteProvider(quoteRequest, fullRequest, { provider }) {
62
+ const [quote] = await fetchQuotes(quoteRequest, fullRequest, provider);
63
+ if (!quote) {
64
+ throw new Error(`No re-quote from provider: ${provider}`);
65
+ }
66
+ return quote;
67
+ }
68
+ async function fetchQuotes(quoteRequest, fullRequest, provider) {
69
+ const { accountSupports7702, messenger, signal, transaction } = fullRequest;
70
+ const settledRequest = await resolveSettledRequest(quoteRequest, transaction, messenger);
71
+ const body = await buildServerQuoteRequest(settledRequest, transaction, messenger, accountSupports7702);
72
+ if (provider) {
73
+ body.providers = [provider];
74
+ }
75
+ log('Request body', body);
76
+ const response = await fetchServerQuote(messenger, body, signal);
77
+ log('Raw quote response', response);
78
+ const fulfilledResults = response.results.filter((result) => isFulfilledResult(result) &&
79
+ (!provider || result.provider === provider) &&
80
+ isUsableDepositAddressQuote(body, result));
81
+ const normalized = await Promise.all(fulfilledResults.map((result) => normalizeQuote(result, settledRequest, messenger, body, transaction, accountSupports7702, isSecondLegRequired(body, settledRequest, transaction, result.quote))));
82
+ log('Normalized quotes', normalized);
83
+ return normalized;
84
+ }
85
+ /**
86
+ * Carry the account the funds settle on as the request recipient.
87
+ *
88
+ * The second leg runs from `request.recipient`, which has to match where the
89
+ * funds actually landed. Non-atomic flows settle on the account submitting the
90
+ * second leg, such as the Money Account of a Predict withdraw. Otherwise the
91
+ * quote settles on the executing account rather than the payer.
92
+ *
93
+ * @param quoteRequest - Quote request.
94
+ * @param transaction - Original transaction meta.
95
+ * @param messenger - Controller messenger.
96
+ * @returns The request with the settlement recipient applied.
97
+ */
98
+ async function resolveSettledRequest(quoteRequest, transaction, messenger) {
99
+ const recipient = (await resolveNonAtomicRecipient(transaction, quoteRequest, messenger)) ??
100
+ transaction.txParams.from;
101
+ return { ...quoteRequest, recipient };
102
+ }
62
103
  async function buildServerQuoteRequest(quoteRequest, transaction, messenger, accountSupports7702) {
63
104
  const normalizedRequest = normalizeServerPerpsRequest(quoteRequest, transaction);
64
- const { atomic, from, isMaxAmount, isPostQuote, paymentOverride, sourceChainId, sourceTokenAddress, sourceTokenAmount, targetAmountMinimum, targetChainId, targetTokenAddress, } = normalizedRequest;
105
+ const { atomic, from, isMaxAmount, isPolymarketDepositWallet, isPostQuote, paymentOverride, refundTo, sourceChainId, sourceTokenAddress, sourceTokenAmount, targetAmountMinimum, targetChainId, targetTokenAddress, } = normalizedRequest;
65
106
  const singleData = getSingleTransactionData(transaction);
66
107
  const isHypercore = targetChainId === CHAIN_ID_HYPERCORE;
67
108
  const isTokenTransfer = !isHypercore && Boolean(singleData?.startsWith(TOKEN_TRANSFER_FOUR_BYTE));
@@ -70,9 +111,12 @@ async function buildServerQuoteRequest(quoteRequest, transaction, messenger, acc
70
111
  const executionAccount = transaction.txParams.from;
71
112
  const recipient = isTokenTransfer && singleData
72
113
  ? decodeTransferRecipient(singleData)
73
- : executionAccount;
114
+ : (normalizedRequest.recipient ?? executionAccount);
74
115
  const isHypercoreSource = sourceChainId === CHAIN_ID_HYPERCORE;
116
+ // Deposit wallet withdraws are broadcast by the Polymarket relayer rather
117
+ // than as a gasless EIP-7702 batch, so the provider must not quote gasless.
75
118
  const supportsGasless = !isHypercoreSource &&
119
+ !isPolymarketDepositWallet &&
76
120
  accountSupports7702 &&
77
121
  isEIP7702Chain(messenger, sourceChainId);
78
122
  const body = {
@@ -83,6 +127,9 @@ async function buildServerQuoteRequest(quoteRequest, transaction, messenger, acc
83
127
  slippage: Math.round(getSlippage(messenger, sourceChainId, sourceTokenAddress) * 10000),
84
128
  supportsGasless,
85
129
  };
130
+ if (isPolymarketDepositWallet) {
131
+ await applyDepositWalletOverrides(body, from, messenger);
132
+ }
86
133
  const hasNoData = singleData === undefined || singleData === '0x';
87
134
  const skipDelegation = hasNoData ||
88
135
  isTokenTransfer ||
@@ -92,8 +139,13 @@ async function buildServerQuoteRequest(quoteRequest, transaction, messenger, acc
92
139
  // they must not be embedded in the quote.
93
140
  atomic === false ||
94
141
  (isPostQuote ?? false) ||
95
- (isMaxAmount ?? false);
96
- if (isPostQuote && paymentOverride === PaymentOverride.MoneyAccount) {
142
+ (isMaxAmount ?? false) ||
143
+ // The deposit wallet funds the quote with a plain token transfer, so there
144
+ // are no calls to embed.
145
+ (isPolymarketDepositWallet ?? false);
146
+ if (isPostQuote &&
147
+ atomic !== false &&
148
+ paymentOverride === PaymentOverride.MoneyAccount) {
97
149
  await processMoneyAccountPostQuote(transaction, normalizedRequest, body, messenger);
98
150
  }
99
151
  else if (!skipDelegation) {
@@ -127,6 +179,12 @@ async function buildServerQuoteRequest(quoteRequest, transaction, messenger, acc
127
179
  body.authorizationList = normalizeAuthorizationList(delegation.authorizationList);
128
180
  }
129
181
  }
182
+ else if (isPostQuote || atomic === false) {
183
+ // Honour the caller's refund address so a failed quote refunds the account
184
+ // the funds came from, such as the Predict Safe, rather than the payer. A
185
+ // deposit wallet refund address is kept, as the sweep reads refunds there.
186
+ body.refundTo ??= refundTo;
187
+ }
130
188
  const pricing = getQuotePricing({
131
189
  hasCalls: Boolean(body.calls?.length),
132
190
  sourceTokenAmount,
@@ -170,6 +228,24 @@ function canDeferCalls({ accountSupports7702, messenger, targetChainId, transact
170
228
  transaction.chainId === targetChainId &&
171
229
  isEIP7702Chain(messenger, targetChainId));
172
230
  }
231
+ /**
232
+ * Quote a deposit wallet Predict withdraw from the deposit wallet itself.
233
+ *
234
+ * The deposit wallet funds the provider's deposit address with the USDC.e
235
+ * unwrapped from its pUSD, and is also the refund address so the sweep can wrap
236
+ * any refund back into pUSD.
237
+ *
238
+ * @param body - Quote request body to update.
239
+ * @param from - The user EOA that owns the deposit wallet.
240
+ * @param messenger - Controller messenger.
241
+ */
242
+ async function applyDepositWalletOverrides(body, from, messenger) {
243
+ const { depositWallet, sourceTokenAddress } = await getPolymarketDepositWalletOverrides(from, messenger);
244
+ body.source = { ...body.source, token: sourceTokenAddress };
245
+ body.sender = depositWallet;
246
+ body.refundTo = depositWallet;
247
+ body.useDepositAddress = true;
248
+ }
173
249
  function normalizeAuthorizationList(authorizationList) {
174
250
  return authorizationList.map((entry) => ({
175
251
  address: entry.address,
@@ -230,6 +306,8 @@ function shouldRequestQuote(quoteRequest) {
230
306
  *
231
307
  * A second leg is needed either because we declined to embed the calls, or
232
308
  * because the provider that won the quote told us it will not execute them.
309
+ * Non-atomic post-quote flows always need one: their calls come from the
310
+ * payment override rather than the transaction, and run once funds settle.
233
311
  *
234
312
  * @param body - The built server quote request.
235
313
  * @param quoteRequest - The originating quote request.
@@ -243,6 +321,19 @@ function isSecondLegRequired(body, quoteRequest, transaction, quote) {
243
321
  if (body.calls?.length && !quote.callsDeferred) {
244
322
  return false;
245
323
  }
324
+ // HyperCore settles off-chain, so there is no target-chain transfer to read
325
+ // a settled amount from. Checks the built body rather than the original
326
+ // request, since perps deposits are only rewritten to HyperCore there.
327
+ if (body.target.chainId === HYPERCORE_CHAIN_ID_NUMBER ||
328
+ body.source.chainId === HYPERCORE_CHAIN_ID_NUMBER) {
329
+ return false;
330
+ }
331
+ // Post-quote flows submit the original transaction alongside the quote, so
332
+ // the second leg never re-runs it. Only non-atomic flows have override calls
333
+ // left to run once the funds settle.
334
+ if (quoteRequest.isPostQuote) {
335
+ return quoteRequest.atomic === false;
336
+ }
246
337
  // No calls to run. Plain funding transfers and empty calldata are already
247
338
  // satisfied by the quote delivering the target token to the recipient.
248
339
  if (!transaction.nestedTransactions?.length) {
@@ -255,23 +346,12 @@ function isSecondLegRequired(body, quoteRequest, transaction, quote) {
255
346
  if (singleData.startsWith(TOKEN_TRANSFER_FOUR_BYTE)) {
256
347
  return false;
257
348
  }
258
- // HyperCore settles off-chain, so there is no target-chain transfer to read
259
- // a settled amount from. Checks the built body rather than the original
260
- // request, since perps deposits are only rewritten to HyperCore there.
261
- if (body.target.chainId === HYPERCORE_CHAIN_ID_NUMBER ||
262
- body.source.chainId === HYPERCORE_CHAIN_ID_NUMBER) {
263
- return false;
264
- }
265
- // Post-quote flows submit the original transaction separately, so running it
266
- // again as a second leg would double-execute it.
267
- if (quoteRequest.isPostQuote) {
268
- return false;
269
- }
270
349
  return true;
271
350
  }
272
- async function normalizeQuote(result, quoteRequest, messenger, isInputBased, transaction, accountSupports7702, requiresSecondLeg) {
351
+ async function normalizeQuote(result, quoteRequest, messenger, body, transaction, accountSupports7702, requiresSecondLeg) {
273
352
  const { quote } = result;
274
353
  const { gasless } = quote;
354
+ const isInputBased = body.tradeType === TradeType.ExactInput;
275
355
  const transactionSteps = quote.steps.filter(isTransactionStep);
276
356
  const isSignatureOnly = transactionSteps.length === 0;
277
357
  const sourceNetwork = await calculateSourceNetworkCost({
@@ -279,6 +359,7 @@ async function normalizeQuote(result, quoteRequest, messenger, isInputBased, tra
279
359
  gasless: gasless || isSignatureOnly,
280
360
  messenger,
281
361
  quoteRequest,
362
+ sender: body.sender,
282
363
  steps: transactionSteps,
283
364
  transaction,
284
365
  });
@@ -352,7 +433,7 @@ async function normalizeQuote(result, quoteRequest, messenger, isInputBased, tra
352
433
  },
353
434
  };
354
435
  }
355
- async function calculateSourceNetworkCost({ accountSupports7702, gasless, messenger, quoteRequest, steps, transaction, }) {
436
+ async function calculateSourceNetworkCost({ accountSupports7702, gasless, messenger, quoteRequest, sender, steps, transaction, }) {
356
437
  const noFees = {
357
438
  estimate: ZERO_AMOUNT,
358
439
  gasLimits: [],
@@ -361,9 +442,11 @@ async function calculateSourceNetworkCost({ accountSupports7702, gasless, messen
361
442
  maxFeePerGas: undefined,
362
443
  maxPriorityFeePerGas: undefined,
363
444
  };
364
- const { from, sourceChainId, sourceTokenAddress } = quoteRequest;
445
+ const { from, isPolymarketDepositWallet, sourceChainId, sourceTokenAddress } = quoteRequest;
446
+ // A deposit wallet withdraw is broadcast by the Polymarket relayer, which
447
+ // pays the gas, so the user only signs.
365
448
  const gasPayment = resolveGasPayment({
366
- isDelegated: gasless,
449
+ isDelegated: gasless || Boolean(isPolymarketDepositWallet),
367
450
  sourceTokenAddress,
368
451
  sponsorship: {
369
452
  accountSupports7702,
@@ -372,7 +455,10 @@ async function calculateSourceNetworkCost({ accountSupports7702, gasless, messen
372
455
  },
373
456
  });
374
457
  if (gasPayment.mode === GasPaymentMode.Delegation) {
375
- log('Zeroing source network fees for gasless quote');
458
+ log('Zeroing source network fees as the user does not pay origin gas', {
459
+ gasless,
460
+ isPolymarketDepositWallet,
461
+ });
376
462
  return noFees;
377
463
  }
378
464
  if (gasPayment.mode === GasPaymentMode.Sponsored) {
@@ -389,34 +475,38 @@ async function calculateSourceNetworkCost({ accountSupports7702, gasless, messen
389
475
  : { maxFeePerGas: undefined, maxPriorityFeePerGas: undefined };
390
476
  const maxFeePerGas = firstStep.maxFeePerGas ?? gasFeeEstimate.maxFeePerGas;
391
477
  const maxPriorityFeePerGas = firstStep.maxPriorityFeePerGas ?? gasFeeEstimate.maxPriorityFeePerGas;
392
- const gasTransactions = steps.map((step) => stepToGasTransaction(step, from));
393
- const gasResult = await estimateQuoteGasLimits({
394
- fallbackGas: getFeatureFlags(messenger).relayFallbackGas,
395
- fallbackOnSimulationFailure: true,
478
+ // Legacy Safe Predict withdraws hold the source token in the Safe, so deposit
479
+ // routes are estimated from the Safe. Swap routes keep the sender, as DEX
480
+ // aggregators reject contract callers.
481
+ const fromOverride = getPredictWithdrawSafeAddress(quoteRequest, transaction, hasServerDepositStep(steps));
482
+ const { gasLimits, is7702, totalGasEstimate, totalGasLimit } = await estimateSourceGas({
483
+ fromOverride,
396
484
  messenger,
397
- transactions: gasTransactions,
485
+ request: quoteRequest,
486
+ transaction,
487
+ transactions: steps.map((step) => stepToGasTransaction(step, sender)),
398
488
  });
399
- const { is7702 } = gasResult;
400
- const gasLimits = is7702
401
- ? [gasResult.gasLimits[0].max]
402
- : gasResult.gasLimits.map((gasLimit) => gasLimit.max);
403
489
  const estimate = calculateGasCost({
404
490
  chainId: chainIdHex,
405
- gas: gasResult.totalGasEstimate,
491
+ gas: totalGasEstimate,
406
492
  maxFeePerGas: maxFeePerGas ?? '0',
407
493
  maxPriorityFeePerGas: maxPriorityFeePerGas ?? '0',
408
494
  messenger,
409
495
  });
410
496
  const max = calculateGasCost({
411
497
  chainId: chainIdHex,
412
- gas: gasResult.totalGasLimit,
498
+ gas: totalGasLimit,
413
499
  isMax: true,
414
500
  maxFeePerGas: maxFeePerGas ?? '0',
415
501
  maxPriorityFeePerGas: maxPriorityFeePerGas ?? '0',
416
502
  messenger,
417
503
  });
418
504
  const fees = { maxFeePerGas, maxPriorityFeePerGas };
505
+ // Every Predict withdraw holds the source token in the Safe, so the fee token
506
+ // lookup must use it even on swap routes.
507
+ const feeTokenAccount = getPredictWithdrawFeeTokenAccount(quoteRequest, transaction);
419
508
  const gasStationCost = await resolveGasStationCost({
509
+ feeTokenAccount,
420
510
  firstStepData: {
421
511
  data: firstStep.data,
422
512
  to: firstStep.to,
@@ -429,13 +519,16 @@ async function calculateSourceNetworkCost({ accountSupports7702, gasless, messen
429
519
  sourceChainId,
430
520
  sourceTokenAddress,
431
521
  },
432
- totalGasEstimate: gasResult.totalGasEstimate,
433
- totalItemCount: steps.length,
522
+ totalGasEstimate,
523
+ totalItemCount: feeTokenAccount
524
+ ? steps.length + 1
525
+ : Math.max(steps.length, gasLimits.length),
434
526
  });
435
527
  if (!gasStationCost.amount) {
436
528
  return { estimate, gasLimits, is7702, max, ...fees };
437
529
  }
438
530
  log('Using gas fee token for source network', {
531
+ feeTokenAccount,
439
532
  gasFeeTokenCost: gasStationCost.amount,
440
533
  });
441
534
  return {
@@ -469,6 +562,25 @@ function isFulfilledResult(result) {
469
562
  result.quote.input !== undefined &&
470
563
  result.quote.output !== undefined);
471
564
  }
565
+ /**
566
+ * Whether a quote can be funded as requested. A deposit-address request is only
567
+ * usable when the quote funds the provider with a plain token transfer, since
568
+ * that transfer is what gets replaced by an unwrap straight to the deposit
569
+ * address.
570
+ *
571
+ * @param body - The built server quote request.
572
+ * @param result - Fulfilled provider result.
573
+ * @returns True when the quote is usable.
574
+ */
575
+ function isUsableDepositAddressQuote(body, result) {
576
+ if (!body.useDepositAddress || hasServerDepositStep(result.quote.steps)) {
577
+ return true;
578
+ }
579
+ log('Ignoring deposit address quote without a plain token transfer', {
580
+ provider: result.provider,
581
+ });
582
+ return false;
583
+ }
472
584
  function decodeTransferRecipient(data) {
473
585
  return TRANSFER_INTERFACE.decodeFunctionData('transfer', data).to.toLowerCase();
474
586
  }