feat(transaction-pay): add Across strategy support#7886
feat(transaction-pay): add Across strategy support#7886pedronfigueiredo wants to merge 2 commits intocor-6992-extract-fallback-mechanismfrom
Conversation
packages/transaction-pay-controller/src/strategy/across/across-submit.ts
Show resolved
Hide resolved
packages/transaction-pay-controller/src/strategy/across/across-submit.ts
Outdated
Show resolved
Hide resolved
665488f to
8be8f8c
Compare
packages/transaction-pay-controller/src/strategy/across/across-quotes.ts
Outdated
Show resolved
Hide resolved
8be8f8c to
3f52702
Compare
packages/transaction-pay-controller/src/strategy/across/across-submit.ts
Show resolved
Hide resolved
packages/transaction-pay-controller/src/strategy/across/across-submit.ts
Show resolved
Hide resolved
3f52702 to
789a704
Compare
packages/transaction-pay-controller/src/strategy/across/across-submit.ts
Outdated
Show resolved
Hide resolved
789a704 to
6953629
Compare
packages/transaction-pay-controller/src/strategy/across/across-quotes.ts
Outdated
Show resolved
Hide resolved
6953629 to
8c1c7e5
Compare
9cd0f37 to
a6c9af9
Compare
| return getFeatureFlags(messenger).relayFallbackGas; | ||
| } | ||
|
|
||
| export async function estimateGasWithBufferOrFallback({ |
| }; | ||
| } | ||
|
|
||
| export function getFallbackGas(messenger: TransactionPayControllerMessenger): { |
There was a problem hiding this comment.
Should this be in feature-flags.ts?
| error = caughtError; | ||
| } | ||
|
|
||
| const fallbackGas = getFallbackGas(messenger); |
There was a problem hiding this comment.
Debatable whether we should have fallback gas or throw as it implies the transaction would revert?
There was a problem hiding this comment.
fallback is now option but off by default
| across?: PayStrategyConfigRaw; | ||
| relay?: { | ||
| enabled?: boolean; | ||
| relayQuoteUrl?: string; |
There was a problem hiding this comment.
Is this redundant as we already have the top level value?
| slippage: number; | ||
| }; | ||
|
|
||
| export type PayStrategyConfigRaw = { |
There was a problem hiding this comment.
Should this be AcrossConfig?
| messenger, | ||
| }); | ||
|
|
||
| const max = calculateGasCost({ |
There was a problem hiding this comment.
We also need gas station support here, but can do that in a future PR and re-use the Relay logic.
There was a problem hiding this comment.
added a ticket for this
| request: QuoteRequest, | ||
| ): Promise<TransactionPayQuote<AcrossQuote>['fees']['sourceNetwork']> { | ||
| const { from } = request; | ||
| const { swapTx } = quote; |
There was a problem hiding this comment.
Do we also need to include gas cost of approvalTxns?
| const chainId = toHex(params.chainId); | ||
| const value = toHex(params.value ?? '0x0'); | ||
|
|
||
| const gas = await estimateGasWithBuffer( |
There was a problem hiding this comment.
We don't want to duplicate the gas logic again, but store the gas limits in the quote to use during submit, so we know the network fees will match.
| data: params.data, | ||
| from, | ||
| gas: toHex(gas), | ||
| maxFeePerGas: normalizeOptionalHex(params.maxFeePerGas), |
There was a problem hiding this comment.
Can they not return gas fee properties?
There was a problem hiding this comment.
not sure I understand the question here, it's simply normalising to hex if needed.
| } | ||
|
|
||
| await Promise.all( | ||
| transactionIds.map((txId) => waitForTransactionConfirmed(txId, messenger)), |
There was a problem hiding this comment.
We're waiting for the deposit transactions to succeed, but we also need to poll the Across API to verify the status of their transactions on the target chain.
Looks like /deposit/status?
08bac6a to
26c090a
Compare
8c1c7e5 to
68679f6
Compare
packages/transaction-pay-controller/src/strategy/across/across-quotes.ts
Show resolved
Hide resolved
| await new Promise((resolve) => | ||
| setTimeout(resolve, ACROSS_STATUS_POLL_INTERVAL), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Status polling error prevents intent completion after confirmed deposit
Medium Severity
The waitForAcrossCompletion polling loop has no try/catch around successfulFetch or response.json(). A transient network error or malformed response on any single polling attempt causes the entire function to throw. Since submitTransactions calls this at the end, the error propagates up to executeSingleQuote, which skips the isIntentComplete = true update on the parent transaction — even though the on-chain deposit was already confirmed via waitForTransactionConfirmed. This leaves the parent transaction stuck in an incomplete state and surfaces an error to the user for a successfully submitted deposit.
Additional Locations (1)
| return providerFeeUsd.isNegative() | ||
| ? new BigNumber(0) | ||
| : providerFeeUsd; | ||
| } |
There was a problem hiding this comment.
Fallback provider fee conflates exchange rate with actual fee
Medium Severity
When fees.total.amountUsd is unavailable, calculateProviderUsd computes the provider fee as inputUSD − expectedOutputUSD. The expectedOutputRaw passed in is a fallback-chained value that may be minOutputAmount or targetAmountMinimum instead of the actual expected output. For cross-asset bridges (e.g., ETH → USDC), this delta includes the exchange-rate difference between the two tokens, significantly overstating the actual provider/bridging fee shown to the user.
Additional Locations (1)
0df61f2 to
43816bc
Compare
|
|
||
| export type AcrossActionRequestBody = { | ||
| actions: AcrossAction[]; | ||
| }; |
There was a problem hiding this comment.
Unused exported types for unimplemented delegation feature
Low Severity
AcrossAction, AcrossActionArg, and AcrossActionRequestBody are exported types that are never imported or referenced anywhere in the codebase. A grep confirms zero imports. The PR discussion confirms delegation is not supported yet ("delegation not supported yet, so we can build the actions directly from the transactions"), so these types serve no current purpose and will confuse future developers trying to understand which types are part of the working Across implementation.
| usd: '0', | ||
| }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Duplicated sumAmounts logic already exists in utilities
Low Severity
The local sumAmounts function in across-quotes.ts duplicates the private sumAmounts in utils/totals.ts. Both reduce an Amount[] by summing fiat, human, raw, and usd fields. Having two independent implementations risks inconsistent bug fixes if the Amount type evolves or a calculation needs correction — one copy could be updated while the other is missed. Extracting the shared logic into a common exported utility would prevent this.
d7d7490 to
b6a43e3
Compare
43816bc to
bee7414
Compare
bee7414 to
ca79a44
Compare
| params.set('destinationChainId', String(parseInt(targetChainId, 16))); | ||
| params.set('depositor', from); | ||
| params.set('recipient', recipient); | ||
|
|
There was a problem hiding this comment.
Across quote request can send undefined amount
High Severity
getAcrossQuotes filters out requests based on targetAmountMinimum === '0' and later sets amount from targetAmountMinimum for non-max quotes without guarding. If targetAmountMinimum is undefined (or '0' for max quotes), this can either drop valid max-amount requests or send amount=undefined to Across.
| messenger, | ||
| to: swapTx.to, | ||
| value: swapTx.value ?? '0x0', | ||
| }); |
There was a problem hiding this comment.
Across quotes fail on gas simulation errors
Medium Severity
calculateSourceNetworkCost calls estimateGasLimitWithBufferOrFallback without fallbackOnSimulationFailure, so any simulationFails result throws and aborts Across quote normalization, even though the submit path enables fallback and could proceed with a reasonable default.
| tx.requiredTransactionIds ??= []; | ||
| tx.requiredTransactionIds.push(transactionId); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Required transaction IDs may be lost
Medium Severity
requiredTransactionIds is appended via repeated updateTransaction calls inside the collectTransactionIds callback. Because updateTransaction performs a full transaction replacement from a cloned snapshot, closely spaced events can race and overwrite previous requiredTransactionIds updates.


Explanation
This PR extracts the Across strategy work on top of the provider-fallback baseline.
It adds:
acrossstrategy type + registration)transaction-controllerandtransaction-pay-controllerReferences
Addresses: https://github.com/MetaMask/MetaMask-planning/issues/6997
Checklist
Note
Medium Risk
Adds a new cross-chain submission flow that constructs and sends approval/swap transactions and introduces polling against an external Across API; failures could impact transaction execution or status reporting.
Overview
Adds a new
Acrosspay strategy totransaction-pay-controller, including quote retrieval/normalization (getAcrossQuotes) and an execution path (submitAcrossQuotes) that can submit ERC-20 approvals + the swap/deposit (single tx or batch), track required transaction IDs, and optionally poll Across for completion using a deposit id.Introduces feature-flag driven pay-strategy config (
getPayStrategiesConfig) and makes strategy selection tryAcrossafterRelayby default, while also gatingRelayStrategy.supportsonrelay.enabled. Also adds newTransactionTypevaluesperpsAcrossDeposit/predictAcrossDepositfor tagging Across submissions and updates changelogs/tests accordingly.Written by Cursor Bugbot for commit ca79a44. This will update automatically on new commits. Configure here.