Skip to content

feat(transaction-pay-controller): support second-leg submission of target calls - #10501

Open
matthewwalsh0 wants to merge 16 commits into
mainfrom
feat/transaction-pay-non-atomic-second-leg
Open

matthewwalsh0 wants to merge 16 commits into
mainfrom
feat/transaction-pay-non-atomic-second-leg

Conversation

@matthewwalsh0

@matthewwalsh0 matthewwalsh0 commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

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

  • 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

High Risk
Changes payment settlement, amount resolution, and multi-leg completion semantics across Relay, Server, and Fiat—failures now surface when on-chain amounts are missing instead of silent fallbacks.

Overview
Adds non-atomic / deferred-call support so quotes can deliver funds first and run target calls in a separate second leg on the account that executes them.

Shared second leg: Replaces ma-vault-deposit with submitSecondLeg, used by Relay, Server, and Fiat (including direct mUSD). Quotes expose requiresSecondLeg; Server can request supportsDeferredCalls and honor provider callsDeferred. Settlement amounts come only from on-chain transfer logs (no quote-minimum / order.cryptoAmount fallback). CHOMP race recovery is centralized via withChompRecovery for Money Account deposits.

Behavior fixes: Parent isIntentComplete is set only after a successful second leg (Relay/Server). Relay zeroes origin network fees for Polymarket deposit-wallet Predict withdraws. Server strategy no longer rejects atomic: false; provider names are plain strings.

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

@matthewwalsh0
matthewwalsh0 force-pushed the refactor/transaction-pay-gas-payment branch 2 times, most recently from 93984a0 to 71ec7a0 Compare September 29, 2026 12:19
@matthewwalsh0
matthewwalsh0 force-pushed the feat/transaction-pay-non-atomic-second-leg branch from fa035bc to a0834c6 Compare September 29, 2026 13:41
@matthewwalsh0
matthewwalsh0 force-pushed the refactor/transaction-pay-gas-payment branch from fc6005d to b868774 Compare September 30, 2026 15:45
@matthewwalsh0
matthewwalsh0 force-pushed the feat/transaction-pay-non-atomic-second-leg branch 2 times, most recently from 0fe4cf7 to 4791df2 Compare October 1, 2026 11:47
@matthewwalsh0
matthewwalsh0 force-pushed the refactor/transaction-pay-gas-payment branch from d062d63 to f58e3a8 Compare October 1, 2026 12:31
@matthewwalsh0
matthewwalsh0 force-pushed the feat/transaction-pay-non-atomic-second-leg branch from 4791df2 to 2552899 Compare October 1, 2026 12:32
@matthewwalsh0 matthewwalsh0 changed the title feat(transaction-pay-controller): submit target calls as a second leg when the provider cannot execute them feat(transaction-pay-controller): support second-leg submission of target calls Oct 3, 2026
Base automatically changed from refactor/transaction-pay-gas-payment to main October 5, 2026 08:15
@matthewwalsh0
matthewwalsh0 force-pushed the feat/transaction-pay-non-atomic-second-leg branch from f46884a to acf8b02 Compare October 7, 2026 13:43
… when the provider cannot execute them

Server quote providers either execute the target calls as part of the
quote or decline the request outright, so flows whose calls no provider
can run were simply unsupported.

Request such calls as optional, so providers that cannot execute them
still return a funds-only quote flagged with `callsSupported: false`.
When that happens the quote is marked `requiresSecondLeg` and the calls
are submitted separately on the target chain once the quote settles.

Extract the second-leg submission that the Money Account vault deposit
already performed into a shared `second-leg` util, reading the settled
amount from the settlement transfer logs so the second leg spends what
actually landed rather than the quoted amount. `ma-vault-deposit` is now
a thin Monad-pinned wrapper that layers on CHOMP race handling, and the
relay strategy's recipient resolution moves into the same util so both
strategies settle non-atomic funds on the executing account.
…second leg tests

Raise the `server-quotes.test.ts` counts and add a `second-leg.test.ts`
entry to cover the literals added by the new tests, matching the call
sites already suppressed in those files.

Drop the `ma-vault-deposit.test.ts` entries, whose violations no longer
occur now that the second leg resolves its own execution account.
@matthewwalsh0
matthewwalsh0 force-pushed the feat/transaction-pay-non-atomic-second-leg branch from acf8b02 to e912f9f Compare October 8, 2026 08:47
@matthewwalsh0
matthewwalsh0 marked this pull request as ready for review October 8, 2026 11:03
@matthewwalsh0
matthewwalsh0 requested review from a team as code owners October 8, 2026 11:03
@matthewwalsh0
matthewwalsh0 deployed to default-branch October 8, 2026 11:03 — 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.

Stale Bugbot comment from a previous run.

@matthewwalsh0
matthewwalsh0 requested a review from jpuri October 8, 2026 11:30

// Phase 4: submit the calls the quote could not execute itself, now that the
// funds have settled on the target chain.
if (quote.requiresSecondLeg) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I know this is an old code but should we mark isIntentComplete only after the second leg confirms, so a failed second leg does not leave funds settled with the parent looking done?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Definitely, good spot, will fix in this PR since we're already refactoring.

…t HyperCore from the built quote request

- Send the embedded server transfer to the account the delegated calls run
  from, rather than the paying account, when an account override is active.
- Check the built request body for HyperCore when deciding whether a second
  leg is required, since perps deposits are only rewritten there.
- Read the execution account from txParams.from directly and drop
  resolveExecutionAccount, as from is always set.
…second leg

Mark the parent transaction complete once the second leg has been
submitted, for both the server and relay strategies, so a failed second leg
no longer leaves a failed transaction flagged as complete.
jpuri
jpuri previously approved these changes Oct 9, 2026
…non-atomic-second-leg

# Conflicts:
#	packages/transaction-pay-controller/CHANGELOG.md

@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 2 potential issues.

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 e2cc524. Configure here.

// satisfied by the quote delivering the target token to the recipient.
if (!transaction.nestedTransactions?.length) {
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deferred calls skip without nested txs

High Severity

supportsDeferredCalls is set whenever the account can sign a sponsored EIP-7702 batch, but isSecondLegRequired then returns false whenever nestedTransactions is empty. Calls that live only on txParams.data still get embedded and may come back as callsDeferred: true, after which the second leg is skipped and the parent is marked complete. Funds settle on the execution account and the original target calls never run.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e2cc524. Configure here.

? (nestedTransaction.type ?? TransactionType.tokenMethodApprove)
: TransactionType.contractInteraction,
})),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Second leg skips EIP-7702 upgrade

High Severity

Quote-time deferred-call support includes the EIP-7702 authorizationList so a provider can upgrade the account while executing the calls. submitSecondLeg never receives that list and always submits with disableUpgrade: true. If the provider returns a funds-only quote, an EOA that is not already upgraded on the target chain cannot run the second-leg batch, so the calls fail after the funds have already settled.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e2cc524. Configure here.

This branch was successfully deployed

1 active (outdated) deployment
default-branch — e912f9fa Deployed Oct 8, 2026 by matthewwalsh0 via Determine whether this PR is a release PR #5257
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.

3 participants