Skip to content

refactor(transaction-pay-controller): consolidate origin gas payment resolution - #10404

Merged
matthewwalsh0 merged 9 commits into
mainfrom
refactor/transaction-pay-gas-payment
Oct 5, 2026
Merged

matthewwalsh0 merged 9 commits into
mainfrom
refactor/transaction-pay-gas-payment

Conversation

@matthewwalsh0

@matthewwalsh0 matthewwalsh0 commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Explanation

Origin gas payment was decided independently inside each strategy. The Across, Relay and Server quote/submit modules each re-derived whether the origin gas fee could be paid from the source token, duplicating native-balance checks, gas-station eligibility probes and sponsorship predicates — and they did not always agree with one another.

  • utils/gas-station.ts is replaced by utils/gas-payment.ts, which owns the whole decision: resolveGasStationCost for quote-time cost estimation, resolveGasPayment for submit-time payment-mode resolution, and shouldUseGasStation for the "is the gas station both needed and available" predicate.
  • Every strategy module now delegates to those helpers, so the payment-mode priority (delegation > sponsored > gas fee token > native) and the sponsorship predicate are each expressed in exactly one place instead of being restated per strategy.
  • The flows where a relayer covers origin gas instead of the user — the Relay execute flow and HyperLiquid withdrawals — are resolved through that same priority order via a single isDelegated input, rather than each carrying its own bespoke branch.
  • The Relay single-transaction and batch submit paths are unified. These had diverged: the batch builder set gasFeeToken directly from the quote without consulting sponsorship, whereas the single-transaction path did consult it.

One reviewer-visible behaviour change falls out of that last point: a Relay batch request now omits gasFeeToken when gas is sponsored, matching what the single-transaction path already did. No existing test exercised the sponsored-plus-batch combination, so this is not covered by a test-diff.

Otherwise this is a pure refactor, covered by the existing suites passing unchanged. Nothing touched here is exported from the package index, so there is no consumer-facing change and no changelog entry.

References

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 how origin gas is resolved and billed across all pay strategies at quote and submit time, including TransactionController options that affect what users are charged.

Overview
Introduces utils/gas-payment.ts in place of gas-station.ts, centralizing quote-time gas-station pricing (resolveGasStationCost), submit-time mode selection (resolveGasPayment), and post-submit verification (logGasPaymentOutcome). Across, Relay, and Server quote/submit paths now share one priority order: delegation → sponsored → source-token gas fee → native.

User-visible fix: when a quote prices origin gas in the source token, submit now always passes excludeNativeTokenForFee: true with gasFeeToken (Relay and Server previously omitted this, so TransactionController could drop the fee token at publish and charge native instead). Sponsored same-chain routes and relayer/HyperLiquid “no user origin gas” cases are handled through the same resolver rather than duplicated branches.

Relay batch submit now aligns with single-tx submit by suppressing gasFeeToken when gas is sponsored. gas-station.ts is removed; tests move to gas-payment.test.ts with expanded server/relay coverage.

Reviewed by Cursor Bugbot for commit f58e3a8. Bugbot is set up for automated code reviews on this repo. Configure here.

@matthewwalsh0
matthewwalsh0 force-pushed the refactor/transaction-pay-gas-payment branch from 0a6e11f to 930a36c Compare September 23, 2026 16:10
@matthewwalsh0
matthewwalsh0 changed the base branch from main to feat/pay-server-exact-output September 23, 2026 16:10
Base automatically changed from feat/pay-server-exact-output to main September 23, 2026 16:33
@matthewwalsh0
matthewwalsh0 force-pushed the refactor/transaction-pay-gas-payment branch 2 times, most recently from 3a95931 to ddbc208 Compare September 25, 2026 12:02
@matthewwalsh0
matthewwalsh0 changed the base branch from main to perf/transaction-pay-direct-assets September 25, 2026 12:02
@matthewwalsh0
matthewwalsh0 force-pushed the perf/transaction-pay-direct-assets branch from ff2ab05 to 3d5bb62 Compare September 28, 2026 09:42
@matthewwalsh0
matthewwalsh0 force-pushed the refactor/transaction-pay-gas-payment branch from 2229830 to 93984a0 Compare September 28, 2026 16:59
Base automatically changed from perf/transaction-pay-direct-assets to main September 29, 2026 09:02
@matthewwalsh0
matthewwalsh0 force-pushed the refactor/transaction-pay-gas-payment branch from 93984a0 to 71ec7a0 Compare September 29, 2026 12:19
matthewwalsh0 added a commit that referenced this pull request Sep 30, 2026
…eased

The rebase onto the 30.0.0 release re-filed the #10404 entry under the
published 29.2.2 heading, describing a change that is not in that release.
matthewwalsh0 added a commit that referenced this pull request Sep 30, 2026
…eased

The rebase onto the 30.0.0 release re-filed the #10404 entry under the
published 29.2.2 heading, describing a change that is not in that release.
@matthewwalsh0
matthewwalsh0 force-pushed the refactor/transaction-pay-gas-payment branch from fc6005d to b868774 Compare September 30, 2026 15:45
…resolution

Replace `utils/gas-station.ts` with `utils/gas-payment.ts`, which owns both
quote-time gas-station cost estimation and submit-time payment-mode
resolution.

Each strategy previously re-derived whether the origin gas fee should be paid
from the source token, duplicating native-balance checks, gas-station
eligibility probes and sponsorship predicates across the Across, Relay and
Server strategies. Those modules now delegate to `resolveGasStationCost`,
`resolveGasPayment` and `shouldUseGasStation`.

This also unifies the Relay single-transaction and batch submit paths, which
had diverged: the batch builder set `gasFeeToken` without checking
sponsorship, whereas the single-transaction path did.

Internal only; none of this surface is exported from the package index.
…route Relay delegation through the resolver

Fold the Relay execute and HyperLiquid withdrawal branches into
`resolveGasPayment` via `isDelegated`, so "a relayer pays origin gas" is
decided in one place rather than being hand-rolled per flow. The module
already documented HyperLiquid withdrawals as a delegation case, so leaving
that branch outside the resolver contradicted its stated contract. Both
branches previously returned identical values and the resolver short-circuits
on `isDelegated` before evaluating sponsorship, so the precedence and results
are unchanged; the two log messages collapse into one carrying the flow as
structured fields.

Also stop exporting `GasPaymentRequest`, `GasStationCost`,
`GasStationCostRequest`, `GasStationStepData` and `SponsorshipRequest`, none
of which are referenced outside the module.
…when origin gas uses the source token

Only the across strategy set `excludeNativeTokenForFee`, so for the relay
and server strategies the `TransactionController` re-checked the native
balance at publish time and silently dropped the gas fee token, charging
the user in native token instead of the quoted source token.

Resolve the flag centrally in `resolveGasPayment` and propagate it from
every strategy that submits via the `TransactionController`, so the fee
shown in the quote is the fee the user is charged.

Also route the server strategy's source network cost through the shared
resolver so sponsored same-chain routes zero their fees via the gas
station hook, and add `logGasPaymentOutcome` to surface cases where the
gas fee token is still dropped before publish.
The entry was added under the already-released 29.2.0 heading.
…t is decided

`resolveGasPayment` runs twice: at quote time to estimate the fee, and at
submit time to build the `TransactionController` options. At quote time
the gas station has not been consulted, so an absent `isSourceGasFeeToken`
collapses the mode to `Native` by default rather than by decision.
Logging it announced that the user pays gas in native token while the
source token was still in play.

Log only when `isSourceGasFeeToken` is present, i.e. at submit time.
…eased

The rebase onto the 30.0.0 release re-filed the #10404 entry under the
published 29.2.2 heading, describing a change that is not in that release.
…e gas payment split

Both suppression files key on file path, so renaming `gas-station.test.ts`
to `gas-payment.test.ts` orphaned its oxlint entry and surfaced the
carried-over mock violations as new. Move the entry to the new path and
drop the stale one.

Also raise the `server-quotes.test.ts` tsc counts to cover the request
literals added by the new tests. These omit the required `from`, matching
the 32 call sites already suppressed in that file; supplying a value would
change what the tests feed the code under test.
@matthewwalsh0
matthewwalsh0 force-pushed the refactor/transaction-pay-gas-payment branch from d062d63 to f58e3a8 Compare October 1, 2026 12:31
@matthewwalsh0
matthewwalsh0 marked this pull request as ready for review October 1, 2026 12:36
@matthewwalsh0
matthewwalsh0 requested review from a team as code owners October 1, 2026 12:36
@matthewwalsh0
matthewwalsh0 deployed to default-branch October 1, 2026 12:36 — with GitHub Actions Active

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f58e3a8. Configure here.

@matthewwalsh0
matthewwalsh0 requested a review from OGPoyraz October 1, 2026 12:45
@matthewwalsh0
matthewwalsh0 added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit d8dd48f Oct 5, 2026
350 checks passed
@matthewwalsh0
matthewwalsh0 deleted the refactor/transaction-pay-gas-payment branch October 5, 2026 08:15
@georgeweiler georgeweiler mentioned this pull request Oct 8, 2026
3 of 4 tasks
Naz-Ovh pushed a commit to 0x-fork/metamask-core that referenced this pull request Oct 10, 2026
## Explanation

Scoped release so clients can pick up MetaMask#10749. Monorepo release
**1320.0.0**, following merged
[Release/1319.0.0](MetaMask#10756).

- **`@metamask/ramps-controller` 26.3.0 → 27.0.0** (major).
`hydrateVbaOnboarding` is a read: it no longer signs a wallet, posts a
self-hosted address, creates an autoramp, or starts the KYC poll.
Callers create an autoramp with
`createAutoramp(buildMusdAutorampRequest(...))`. Messenger now requires
`KycController:readSessionStatus` and
`KycController:fetchSessionStatusOnce`. Also includes the unpublished
MetaMask#10743 additions (`needs_source_currency`, `buildMusdAutorampRequest`)
and the profile-sync range bump to `^34.0.2`.
- **`@metamask/kyc-controller` 0.6.1 → 0.7.0** (minor, matching earlier
0.x breaking bumps). Publishes `readSessionStatus` and
`fetchSessionStatusOnce` from MetaMask#10749. The same version also ships the
unpublished breaking `aal2Token` requirement on `startSession` /
`createUkycSession` (MetaMask#10632) and the 72-hour capability token (MetaMask#10679).
- **`@metamask/transaction-pay-controller` 30.0.1 → 30.0.2** (patch).
Published because ramps-controller is a major and this is its only
dependent. The range moves to `^27.0.0`. This version also includes the
unpublished gas-fee fix from MetaMask#10404.

Other packages with unreleased changes are deferred. `ramps-controller`
keeps its existing dependency ranges, including
`@metamask/profile-sync-controller` at `^34.0.2` from MetaMask#10756.

## References

- Ships MetaMask#10749
- Also publishes unpublished changes already on main: MetaMask#10743, MetaMask#10632,
MetaMask#10679, MetaMask#10404, MetaMask#10752, MetaMask#10756

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've communicated my changes to consumers by [updating changelogs
for packages I've
changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md)
- [ ] I've introduced [breaking
changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md)
in this PR and have prepared draft pull requests for clients and
consumer packages to resolve them

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> The release ships major ramps-controller and breaking KYC/VBA
onboarding API changes; this PR only versions and documents them, but
client upgrades must adopt new messenger delegations and onboarding call
patterns.
> 
> **Overview**
> Cuts **monorepo release 1320.0.0** by moving unreleased changelog
notes into versioned sections and bumping published package versions—no
new runtime code in this diff.
> 
> **`@metamask/ramps-controller` 27.0.0** (major) documents shipping
MetaMask#10749/MetaMask#10743: `hydrateVbaOnboarding` becomes a read-only snapshot
(wallet sign, registration POST, autoramp creation, and KYC polling move
to callers), new persisted wallet-registration state and
`needs_wallet_registration` / `needs_source_currency` statuses, and
messenger now requires `KycController:readSessionStatus` and
`KycController:fetchSessionStatusOnce`.
> 
> **`@metamask/kyc-controller` 0.7.0** publishes those KYC actions plus
breaking **`aal2Token`** requirements on UKYC session
creation/`startSession`.
> 
> **`@metamask/transaction-pay-controller` 30.0.2** updates its
dependency range to **`@metamask/ramps-controller` ^27.0.0** and records
the ramps bump in its changelog (along with fixes already on main such
as MetaMask#10404).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
6cb8531. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
github-merge-queue Bot pushed a commit that referenced this pull request Oct 12, 2026
…rget calls (#10501)

## Explanation

Server quote providers either execute the target calls or decline the
request, and the server strategy declined non-atomic requests outright,
so flows whose calls no provider can run were unsupported.

- Server quote requests now set `supportsDeferredCalls`, so a provider
that can't execute the calls returns a funds-only quote (`callsDeferred:
true`) instead of declining.
- These quotes are flagged `requiresSecondLeg`, and the calls are
submitted separately on the target chain once the funds settle.
- The server strategy now accepts non-atomic requests.
- Every strategy (Relay, Server, Fiat) submits its second leg through a
shared `submitSecondLeg` util, which:
- Reads the settled amount and block from the settlement transaction's
transfer logs.
- Throws when there is no settlement hash or no matching transfer,
rather than falling back to the quote minimum or the fiat order amount.
- Applies CHOMP race handling for Money Account deposits, which
previously only ran for fiat.
- Non-atomic quotes now settle funds on the account that executes the
calls rather than the funding account, shared by the Relay and Server
strategies.
- `ma-vault-deposit` is removed in favour of the shared util.
- Server provider names are typed as plain strings, so new backend
providers need no client change.

## References

Stacked on #10404.

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've communicated my changes to consumers by [updating changelogs
for packages I've
changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md)
- [ ] I've introduced [breaking
changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md)
in this PR and have prepared draft pull requests for clients and
consumer packages to resolve them

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> Changes multi-leg settlement, amount resolution, and completion
semantics across Relay, Server, and Fiat paths; stricter on-chain reads
can fail flows that previously fell back to provider-reported amounts.
> 
> **Overview**
> Adds **non-atomic pay flows** where a quote may only move funds and
the target calls run afterward on the chain where they settled.
> 
> Quotes can now carry **`requiresSecondLeg`** (driven by server
`callsDeferred` / `supportsDeferredCalls`, explicit `atomic: false`,
etc.). **Relay**, **Server**, and **Fiat** (including direct mUSD vault)
all submit that follow-up through shared **`submitSecondLeg`**,
replacing **`ma-vault-deposit`**. Settlement amounts come **only from
on-chain transfer logs** via a settlement hash; missing hash or transfer
**fails** instead of using ramp `cryptoAmount` or quote minimums.
> 
> **Server** quotes settle on the **executing account**
(`txParams.from`) vs the payer, Money Account post-quote drops the
forwarding transfer, and **non-atomic** requests are allowed.
**`isIntentComplete`** is set **after** a successful second leg
(Relay/Server). Relay **zeros origin network fees** for Polymarket
deposit-wallet Predict withdraws. CHOMP race recovery moves into
**`withChompRecovery`** around second-leg vault deposits. Server
provider IDs are plain **strings** instead of an enum.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
6244985. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

This branch was successfully deployed

1 active deployment
default-branch — f58e3a84 Deployed Oct 1, 2026 by matthewwalsh0 via Determine whether this PR is a release PR #5075
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