fix(transaction-pay-controller): use exact input for Relay deposits - #9954
fix(transaction-pay-controller): use exact input for Relay deposits#9954pedronfigueiredo wants to merge 9 commits into
Conversation
eea585b to
63f9f32
Compare
| // start as EXPECTED_OUTPUT until transaction processing determines whether | ||
| // the request embeds transactions. | ||
| const useExactInput = | ||
| (isMaxAmount ?? false) || (request.isPostQuote ?? false); |
There was a problem hiding this comment.
Minor, could also just Boolean(isMaxAmount || request.isPostQuote) if more readable?
There was a problem hiding this comment.
Agreed, Boolean(...) is clearer here. I’ll simplify it.
Addressed in 5f0d8b805e (commit).
| recipient: effectiveRequest.recipient ?? from, | ||
| slippageTolerance, | ||
| tradeType: getTradeType(useExactInput, useExactOutput), | ||
| tradeType: useExactInput ? 'EXACT_INPUT' : 'EXPECTED_OUTPUT', |
There was a problem hiding this comment.
Minor, was the getTradeType function useful if we pass it the request and isMaxAmount it owns the full decision.
| } | ||
|
|
||
| if (!body.txs?.length) { | ||
| body.tradeType = 'EXACT_INPUT'; |
There was a problem hiding this comment.
Minor, is it simpler to assign tradeType once after we've set the transactions?
There was a problem hiding this comment.
Good point. I’ll derive tradeType once from whether transactions were added after processing, instead of setting and overriding it in multiple places.
Addressed in b32aea78f3 (commit).
| recipient: effectiveRequest.recipient ?? from, | ||
| slippageTolerance, | ||
| tradeType: getTradeType(useExactInput, useExactOutput), | ||
| tradeType: useExactInput ? 'EXACT_INPUT' : 'EXPECTED_OUTPUT', |
There was a problem hiding this comment.
This EXPECTED_OUTPUT branch is never hit as we override it regardless if transactions or not?
|
|
||
| if (!body.txs?.length) { | ||
| body.tradeType = 'EXACT_INPUT'; | ||
| body.amount = sourceTokenAmount; |
There was a problem hiding this comment.
Also here, is it more readable to just assign amount once after transactions are processed?
There was a problem hiding this comment.
Agreed. I’ll defer amount too and set it once after transaction processing, preserving the Money Account transaction amount when present.
Addressed in d0eb8912b4 (commit).
| /** Total fees for the target transaction and all quotes. */ | ||
| fees: TransactionPayFees; | ||
|
|
||
| /** Whether all selected quotes are driven by the source input amount. */ |
There was a problem hiding this comment.
Is this a bit ambiguous?
Maybe Whether the quote(s) subtract fees from the destination amount meaning the input amount is static
There was a problem hiding this comment.
Agreed—the fee behavior is more concrete. I’ll update the documentation to clarify that fees are subtracted from the destination amount while the input amount stays static.
Addressed in c848146ce2 (commit).
| isMaxAmount: boolean, | ||
| ): boolean { | ||
| if (quote.strategy === TransactionPayStrategy.Relay) { | ||
| const relayQuote = quote.original as RelayQuote; |
There was a problem hiding this comment.
This breaks the abstraction provided by the TransactionPayStrategy and couples this code to the internals of each one.
The purpose of the TransactionPayQuote type is to abstract what a quote can return, so it would also need a isInputBased property?
There was a problem hiding this comment.
Agreed. I’ll expose isInputBased on TransactionPayQuote, have each strategy populate it, and make totals consume only the normalized property.
Addressed in ccb7854521 (commit).
Explanation
Relay quote behavior is now determined after transaction processing rather than by transaction-type allowlists.
This change:
EXACT_INPUTwithsourceTokenAmountwhen a Relay quote has no embeddedtxs, so the entered source amount remains the total amount paid;EXACT_OUTPUTwith the target amount for quotes that embed transactions, including Money Account calls;tradeTypeandamountonce after transaction processing;TransactionPayQuote.isInputBasedandTransactionPayTotals.isInputBasedflags;The Relay trade-type decision no longer depends on Perps or Predict transaction types. HyperCore requests without embedded transactions follow the same exact-input rule.
Consumer integration
The coordinated client PRs remain drafts until the Core package containing this change is published:
TransactionPayTotals.isInputBased, keeps input-based source amounts stable, and displays the authoritative target amount. It now wires both the v26resolveSourceAmountcallback and v27getBalancecallback through a shared resolver; final integration still requires aligning@metamask/sentinel-api-serviceand regenerating the lockfile.Both clients treat only an explicit
isInputBased: trueas input-based. Mixed aggregates and embedded-transaction/output-based quotes retain the existing Total presentation. Input-based withdrawals use the controller's authoritative target amount; legacy or output-based withdrawals retain the existing fee-subtraction fallback.Local absolute-path
file:links in the client worktrees are integration-only and are not part of either client PR.Validation
yarn workspace @metamask/transaction-pay-controller run testyarn workspace @metamask/transaction-pay-controller run jest --no-coverage src/utils/totals.test.ts src/strategy/relay/relay-quotes.test.ts src/strategy/across/across-quotes.test.ts src/strategy/server/server-quotes.test.ts src/strategy/fiat/fiat-direct-musd.test.ts src/utils/no-op-quote.test.tsyarn workspace @metamask/transaction-pay-controller run buildyarn workspace @metamask/transaction-pay-controller run changelog:validateoxfmt --checkfor changed TypeScript filesgit diff --check origin/main...HEADReferences
Checklist
Note
Medium Risk
Changes quote trade types, amounts, and confirmation totals for MetaMask Pay flows (including HyperCore deposits without embedded txs), with coordinated mobile/extension consumer PRs.
Overview
Relay quoting now picks
tradeTypeandamountafter transaction embedding instead of from transaction-type rules (including the removed HyperCoreEXACT_OUTPUTpath). Quotes without embeddedtxsuseEXACT_INPUTandsourceTokenAmount; quotes with embedded calls (delegation, Money Account post-quote, etc.) stayEXACT_OUTPUTwith the target or override amount.The PR adds optional
TransactionPayQuote.isInputBasedandTransactionPayTotals.isInputBased, set across Relay, Across, Server, Fiat, and no-op normalization.calculateTotalsno longer keys offisMaxAmount; it treats totals as input-based only when every selected quote is input-based, uses that for the paid amount in totals, and avoids double-counting fees already baked into the source side. Relay dust is clamped to zero when exact-input output falls below the prior minimum.Embedded-transaction handlers no longer mutate
tradeType/amounton the draft body;processMoneyAccountPostQuotereturns the raw amount for the final request instead.Reviewed by Cursor Bugbot for commit ccb7854. Bugbot is set up for automated code reviews on this repo. Configure here.