Skip to content

Comments

feat: Implement max amount with gas station fallback#7927

Open
OGPoyraz wants to merge 20 commits intomainfrom
ogp/25466
Open

feat: Implement max amount with gas station fallback#7927
OGPoyraz wants to merge 20 commits intomainfrom
ogp/25466

Conversation

@OGPoyraz
Copy link
Member

@OGPoyraz OGPoyraz commented Feb 13, 2026

Explanation

This PR introduces a dedicated max-amount gas-station fallback flow for Relay, extracted into a focused helper and wired into quote orchestration. The goal was fixing max-amount mUSD conversion failures caused by gas-fee-token estimation dead-ends but in fact it fixes for any kind of intent.

The helper (getMaxAmountQuoteWithGasStationFallback) now implements a clear two-phase flow with explicit fallback points:

  1. Phase-1 quote (full max amount)
  • Request quote using original max source amount.
  1. Early-return guards (return phase-1 immediately)
  • If source gas fee token is not used and native balance already covers gas.
  • If maxGaslessEnabled is false.
  • If gas station is disabled/unsupported for source chain.
  1. Gas-cost estimation strategy
  • First try direct estimation from quote/gas-station params.
  • If that fails, request a probe quote with smaller source amount (PROBE_AMOUNT_PERCENTAGE = 0.25) to discover gas fee token + amount.
  • Uses TransactionController:getGasFeeTokens and calculateGasFeeTokenCost to normalize source-token gas fee amount.
  1. Phase-2 quote (adjusted max)
  • Compute adjusted amount as: adjusted = sourceAmount - estimatedGasCost.
  • Request phase-2 quote with adjusted source amount.
  1. Validation before accepting phase-2
  • Re-estimate gas on phase-2 quote.
  • Require affordability: adjusted + validationGasCost <= originalSourceAmount.
  • If valid, mark twoPhaseQuoteForMaxAmount = true and return phase-2.
  • Otherwise fallback to phase-1.

References

gasless.max.mov

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
Changes core Relay quote selection for isMaxAmount requests and introduces new gas-cost estimation/validation paths; while heavily tested, errors could affect max-amount pricing or fallback behavior in production.

Overview
Adds a dedicated two-phase max-amount quoting path for the Relay strategy that can fall back to gas-station-derived gas costs (and optionally a smaller “probe” quote) to produce an adjusted max quote when the initial max quote can’t be priced safely in source-token gas units.

Refactors Relay quote gas-fee-token handling by extracting getGasStationEligibility/getGasStationCostInSourceTokenRaw (including multi-item gas normalization and support for decimal simulation fields), wires the new max-amount flow into getRelayQuotes, and tags successful phase-2 quotes via original.metamask.isMaxGasStation (type extended accordingly). Test coverage is expanded significantly, including post-quote normalization and ensuring max-gas-station submissions keep the existing gasFeeToken-only behavior.

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

@OGPoyraz OGPoyraz marked this pull request as ready for review February 16, 2026 11:04
@OGPoyraz OGPoyraz requested review from a team as code owners February 16, 2026 11:04
if (
!isAdjustedAmountAffordable(
adjustedSourceAmount,
validationGasEstimate.amount,
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 care about the new amount do we, as we calculated the old one to be exactly what is remaining of the balance, so do we want to use that to avoid dust?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we shouldn't blindly trust the new amount, but we also avoid forcing the original estimate when quote internals shift slightly. This keeps max safety and minimizes dust risk without introducing a new submit API in this PR.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense as MVP I think, but avoiding dust wherever possible is a good goal, we can circle back later.


markQuoteAsTwoPhaseForMaxAmount(phase2Quote);

return phase2Quote;
Copy link
Member

Choose a reason for hiding this comment

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

Potentially a separate PR, but are the gas fee tokens consistent in your testing?

Or should we also update relay-submit to specify a new gasFeeTokenAmount property to the TransactionController to avoid any dust?

Copy link
Member

Choose a reason for hiding this comment

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

Discussed in other comments, no new submit API for now for simplicity, at the cost of minor dust 👍

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 1 potential issue.


function parseGasValue(value: string): BigNumber {
if (value.toLowerCase().startsWith('0x')) {
return new BigNumber(value.slice(2), 16);
Copy link
Member

Choose a reason for hiding this comment

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

Minor, BigNumber should handle 0x automatically, even without the base argument.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done, removed

if (
!isAdjustedAmountAffordable(
adjustedSourceAmount,
validationGasEstimate.amount,
Copy link
Member

Choose a reason for hiding this comment

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

Makes sense as MVP I think, but avoiding dust wherever possible is a good goal, we can circle back later.


markQuoteAsTwoPhaseForMaxAmount(phase2Quote);

return phase2Quote;
Copy link
Member

Choose a reason for hiding this comment

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

Discussed in other comments, no new submit API for now for simplicity, at the cost of minor dust 👍

request,
};

const { relayDisabledGasStationChains } = getFeatureFlags(messenger);
Copy link
Member

Choose a reason for hiding this comment

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

Minor, could we encapsulate this within getGasStationEligibility since we pass the messenger also?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good call, relocated in getGasStationEligibility

request: QuoteRequest;
};

export async function getRelayMaxGasStationQuote(
Copy link
Member

Choose a reason for hiding this comment

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

Minor, could we get a high level summary of the algorithm here in JSDoc?

Copy link
Member Author

Choose a reason for hiding this comment

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

Added JSDoc

);

if (
!isAdjustedAmountAffordable(
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 another benefit of the future add transaction API changes, since we can guarantee we can still afford the original value since we subtracted it from our balance?

But this new one may have gone up.

Copy link
Member Author

@OGPoyraz OGPoyraz Feb 25, 2026

Choose a reason for hiding this comment

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

Yes, that’s right, subtraction used an earlier estimate, and the phase2 gas cost can come back higher.

A future submit API change (explicit gasFeeTokenAmount) would help execution time determinism/dust, but it does not fully replace this quote-time affordability guard. We still need to validate that adjusted + latestGasCost <= originalBalance before accepting phase2.

return primaryEstimate;
}

const probeCost = await getProbeGasCostInSourceTokenRaw(context);
Copy link
Member

Choose a reason for hiding this comment

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

How can the above fail?

If no gas fee tokens are returned from the original quote, then should we not just fallback at that point?

Why would it help to try get another quote with a smaller amount?

Copy link
Member Author

Choose a reason for hiding this comment

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

From testing, at full max, fee-token estimation/simulation can return no usable gas-fee-token result (or no match) because the transaction is too tight at the balance boundary.

With a smaller probe amount, the same route often becomes estimable and returns either:
isSourceGasFeeToken directly, or a usable getGasFeeTokens result we can normalize.

So this is a recovery path, not a bypass. It lets us rescue cases that would otherwise unnecessarily fall back.

return undefined;
}

if (probeQuote.fees.isSourceGasFeeToken) {
Copy link
Member

Choose a reason for hiding this comment

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

Is everything below a duplication of getGasCostFromQuoteOrGasStation?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good call, removed below by re-using getGasCostFromQuoteOrGasStation

sourceTokenAddress,
},
totalGasEstimate,
totalItemCount: Math.max(relayParams.length, gasLimits.length),
Copy link
Member

Choose a reason for hiding this comment

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

I can see this was part of the old code, but I can't see how we could have multiple gas limits with a single relay params.

Copy link
Member Author

Choose a reason for hiding this comment

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

Isn't that comes from post-quote handling? If it's a post-quote with an original transaction, in non-7702 path prepends original transaction gas (gasLimits = [originalTxGas, ...gasLimits])

};
metamask: {
gasLimits: number[];
isMaxGasStation?: boolean;
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 purely for future use if we change the submission flow to use the exact gas fee token amount?

Should we omit now if not used?

Copy link
Member Author

@OGPoyraz OGPoyraz Feb 25, 2026

Choose a reason for hiding this comment

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

This is purely for metric purposes, so we can track if 2 phase quoting successfully initiated and quote replaced then we will flag metrics it on client.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enable gasless mUSD conversions for max amount transactions

2 participants