Skip to content

Comments

feat(transaction-pay): add Across strategy support#7886

Open
pedronfigueiredo wants to merge 2 commits intocor-6992-extract-fallback-mechanismfrom
cor-6997-across-strategy
Open

feat(transaction-pay): add Across strategy support#7886
pedronfigueiredo wants to merge 2 commits intocor-6992-extract-fallback-mechanismfrom
cor-6997-across-strategy

Conversation

@pedronfigueiredo
Copy link
Contributor

@pedronfigueiredo pedronfigueiredo commented Feb 10, 2026

Explanation

This PR extracts the Across strategy work on top of the provider-fallback baseline.

It adds:

  • Across strategy wiring (across strategy type + registration)
  • Across quote retrieval/parsing/normalization
  • Across submit flow for approval + swap/deposit execution paths
  • Across-specific types/constants required for compilation
  • Minimal selection/config hooks to make Across selectable
  • Changelog entries for transaction-controller and transaction-pay-controller

References

Addresses: https://github.com/MetaMask/MetaMask-planning/issues/6997

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

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 Across pay strategy to transaction-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 try Across after Relay by default, while also gating RelayStrategy.supports on relay.enabled. Also adds new TransactionType values perpsAcrossDeposit/predictAcrossDeposit for tagging Across submissions and updates changelogs/tests accordingly.

Written by Cursor Bugbot for commit ca79a44. This will update automatically on new commits. Configure here.

@pedronfigueiredo pedronfigueiredo force-pushed the cor-6997-across-strategy branch 2 times, most recently from 665488f to 8be8f8c Compare February 10, 2026 12:09
@pedronfigueiredo pedronfigueiredo force-pushed the cor-6992-extract-fallback-mechanism branch 4 times, most recently from 9cd0f37 to a6c9af9 Compare February 13, 2026 10:47
return getFeatureFlags(messenger).relayFallbackGas;
}

export async function estimateGasWithBufferOrFallback({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

estimateGasLimit?

};
}

export function getFallbackGas(messenger: TransactionPayControllerMessenger): {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be in feature-flags.ts?

error = caughtError;
}

const fallbackGas = getFallbackGas(messenger);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debatable whether we should have fallback gas or throw as it implies the transaction would revert?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fallback is now option but off by default

across?: PayStrategyConfigRaw;
relay?: {
enabled?: boolean;
relayQuoteUrl?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this redundant as we already have the top level value?

slippage: number;
};

export type PayStrategyConfigRaw = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be AcrossConfig?

messenger,
});

const max = calculateGasCost({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need gas station support here, but can do that in a future PR and re-use the Relay logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a ticket for this

request: QuoteRequest,
): Promise<TransactionPayQuote<AcrossQuote>['fees']['sourceNetwork']> {
const { from } = request;
const { swapTx } = quote;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can they not return gas fee properties?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure I understand the question here, it's simply normalising to hex if needed.

}

await Promise.all(
transactionIds.map((txId) => waitForTransactionConfirmed(txId, messenger)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@pedronfigueiredo pedronfigueiredo force-pushed the cor-6992-extract-fallback-mechanism branch 4 times, most recently from 08bac6a to 26c090a Compare February 16, 2026 10:51
await new Promise((resolve) =>
setTimeout(resolve, ACROSS_STATUS_POLL_INTERVAL),
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web

return providerFeeUsd.isNegative()
? new BigNumber(0)
: providerFeeUsd;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web


export type AcrossActionRequestBody = {
actions: AcrossAction[];
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

usd: '0',
},
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

@pedronfigueiredo pedronfigueiredo force-pushed the cor-6992-extract-fallback-mechanism branch 3 times, most recently from d7d7490 to b6a43e3 Compare February 20, 2026 14:44
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

params.set('destinationChainId', String(parseInt(targetChainId, 16)));
params.set('depositor', from);
params.set('recipient', recipient);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

messenger,
to: swapTx.to,
value: swapTx.value ?? '0x0',
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

tx.requiredTransactionIds ??= [];
tx.requiredTransactionIds.push(transactionId);
},
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants