From bc17b639e0e3475cc8029a22e4c30f87e58b4ef7 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Tue, 25 Aug 2026 14:51:00 +0100 Subject: [PATCH 1/9] fix(transaction-pay-controller): use exact input for Relay deposits --- .../transaction-pay-controller/CHANGELOG.md | 1 + .../src/constants.ts | 6 ++ .../src/strategy/relay/relay-quotes.test.ts | 88 +++++++++++++++++-- .../src/strategy/relay/relay-quotes.ts | 42 ++++++--- .../src/utils/totals.test.ts | 78 ++++++++++++++++ .../src/utils/totals.ts | 22 +++-- 6 files changed, 210 insertions(+), 27 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 96b301bed6f..e8948213781 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Use `EXACT_INPUT` Relay quotes for plain Perps and Predict deposits so the entered amount is the total source amount paid, while preserving `EXACT_OUTPUT` for HyperCore deposit-and-order flows ([CONF-1782](https://consensyssoftware.atlassian.net/browse/CONF-1782)) - Bump `@metamask/sentinel-api-service` from `^1.0.0` to `^1.0.1` ([#9972](https://github.com/MetaMask/core/pull/9972)) ## [27.0.0] diff --git a/packages/transaction-pay-controller/src/constants.ts b/packages/transaction-pay-controller/src/constants.ts index 42eca7d0102..a990c6b6110 100644 --- a/packages/transaction-pay-controller/src/constants.ts +++ b/packages/transaction-pay-controller/src/constants.ts @@ -12,6 +12,12 @@ export const PERPS_DEPOSIT_TYPES: TransactionType[] = [ TransactionType.perpsDepositAndOrder, ]; +/** Plain deposit types whose Relay quote amount is the total source input. */ +export const RELAY_EXACT_INPUT_DEPOSIT_TYPES: TransactionType[] = [ + TransactionType.perpsDeposit, + TransactionType.predictDeposit, +]; + export const CHAIN_ID_ARBITRUM = '0xa4b1' as Hex; export const CHAIN_ID_MAINNET = '0x1' as Hex; export const CHAIN_ID_POLYGON = '0x89' as Hex; diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 03487050b8e..224addcc93a 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -398,6 +398,75 @@ describe('Relay Quotes Utils', () => { ); }); + it('sends request with EXACT_INPUT trade type for Predict deposits', async () => { + const quoteMock = cloneDeep(QUOTE_MOCK); + quoteMock.details.currencyOut.minimumAmount = '100'; + + successfulFetchMock.mockResolvedValue({ + ok: true, + json: async () => quoteMock, + } as never); + + const result = await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: TOKEN_TRANSFER_DATA_MOCK, + }, + type: TransactionType.predictDeposit, + }, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body).toStrictEqual( + expect.objectContaining({ + amount: QUOTE_REQUEST_MOCK.sourceTokenAmount, + tradeType: 'EXACT_INPUT', + }), + ); + expect(result[0].dust).toStrictEqual({ fiat: '0', usd: '0' }); + }); + + it('sends request with EXACT_INPUT trade type for nested Predict deposits', async () => { + successfulFetchMock.mockResolvedValue({ + ok: true, + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + data: TOKEN_TRANSFER_DATA_MOCK, + type: TransactionType.predictDeposit, + }, + ], + type: TransactionType.batch, + }, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body).toStrictEqual( + expect.objectContaining({ + amount: QUOTE_REQUEST_MOCK.sourceTokenAmount, + tradeType: 'EXACT_INPUT', + }), + ); + }); + it('throws if isMaxAmount is true and transaction includes data', async () => { await expect( getRelayQuotes({ @@ -3878,7 +3947,7 @@ describe('Relay Quotes Utils', () => { ).rejects.toThrow(`Source token fiat rate not found`); }); - it('updates request if Arbitrum deposit to Hyperliquid', async () => { + it('requests exact input for Hyperliquid deposits', async () => { const arbitrumToHyperliquidRequest: QuoteRequest = { ...QUOTE_REQUEST_MOCK, targetChainId: CHAIN_ID_ARBITRUM, @@ -3906,17 +3975,17 @@ describe('Relay Quotes Utils', () => { expect(body).toStrictEqual( expect.objectContaining({ - amount: '12300', + amount: QUOTE_REQUEST_MOCK.sourceTokenAmount, destinationChainId: 1337, destinationCurrency: '0x00000000000000000000000000000000', + tradeType: 'EXACT_INPUT', }), ); }); - // A HyperCore deposit funds an order that needs the whole target as margin. - // EXPECTED_OUTPUT only guarantees `target * (1 - slippage)`, which leaves the - // follow-on order short and it fails on insufficient margin. - it('requests an exact output for Hyperliquid deposits so the full margin is guaranteed', async () => { + // A HyperCore deposit followed by an order needs the whole target as margin. + // EXACT_INPUT cannot guarantee that amount after fees and slippage. + it('requests exact output for Hyperliquid deposit-and-order flows', async () => { const arbitrumToHyperliquidRequest: QuoteRequest = { ...QUOTE_REQUEST_MOCK, targetChainId: CHAIN_ID_ARBITRUM, @@ -3942,7 +4011,12 @@ describe('Relay Quotes Utils', () => { successfulFetchMock.mock.calls[0][1]?.body as string, ); - expect(body.tradeType).toBe('EXACT_OUTPUT'); + expect(body).toStrictEqual( + expect.objectContaining({ + amount: '12300', + tradeType: 'EXACT_OUTPUT', + }), + ); }); it('still requests an expected output for non-Hyperliquid targets', async () => { diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 01e684698ac..c33cb7335ae 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -2,6 +2,7 @@ import { Interface } from '@ethersproject/abi'; import { toHex } from '@metamask/controller-utils'; +import { hasTransactionType } from '@metamask/transaction-controller'; import type { AuthorizationList, TransactionMeta, @@ -19,6 +20,7 @@ import { HYPERCORE_USDC_DECIMALS, NATIVE_TOKEN_ADDRESS, PERPS_DEPOSIT_TYPES, + RELAY_EXACT_INPUT_DEPOSIT_TYPES, USDC_DECIMALS, PaymentOverride, } from '../../constants.js'; @@ -303,17 +305,16 @@ async function getSingleQuote( ); try { - // For post-quote or max amount flows, use EXACT_INPUT - user specifies how much to send, - // and we show them how much they'll receive after fees. - // For regular flows with a target amount, use EXPECTED_OUTPUT, except - // HyperCore deposits, which need a guaranteed amount (see below). - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const useExactInput = isMaxAmount || request.isPostQuote; - - // HyperCore perps deposits fund an order that requires the full target as - // margin, so the delivered amount must be guaranteed rather than expected. - // EXPECTED_OUTPUT only guarantees `target * (1 - slippage)`, which leaves - // the follow-on order short and it fails on insufficient margin. + // Source-driven flows use EXACT_INPUT so the amount the user enters is the + // total sent and the destination receives that amount minus fees. + const useExactInput = + (isMaxAmount ?? false) || + (request.isPostQuote ?? false) || + isExactInputDeposit(transaction); + + // A HyperCore deposit followed by an order requires the full target as + // margin, so the delivered amount must be guaranteed. Plain Perps deposits + // use EXACT_INPUT above, while perpsDepositAndOrder reaches this exception. const useExactOutput = !useExactInput && isHypercoreDeposit(request); const useExecute = @@ -612,6 +613,20 @@ async function processMoneyAccountPostQuote( }); } +/** + * Whether the transaction is a plain Perps or Predict deposit whose entered + * amount represents the total source amount sent. + * + * Deposit-and-order flows are excluded because the follow-on order requires a + * guaranteed target amount. + * + * @param transaction - Parent transaction metadata. + * @returns True when the deposit should use exact-input semantics. + */ +function isExactInputDeposit(transaction: TransactionMeta): boolean { + return hasTransactionType(transaction, RELAY_EXACT_INPUT_DEPOSIT_TYPES); +} + /** * Whether the quote deposits into HyperCore USDC. * @@ -831,8 +846,9 @@ function calculateDustUsd(quote: RelayQuote, request: QuoteRequest): BigNumber { const targetUsdRate = new BigNumber(amountUsd).dividedBy(amountFormatted); - const dustRaw = new BigNumber(minimumAmount).minus( - request.targetAmountMinimum, + const dustRaw = BigNumber.maximum( + new BigNumber(minimumAmount).minus(request.targetAmountMinimum), + 0, ); return dustRaw.shiftedBy(-targetDecimals).multipliedBy(targetUsdRate); diff --git a/packages/transaction-pay-controller/src/utils/totals.test.ts b/packages/transaction-pay-controller/src/utils/totals.test.ts index 963acd8fb30..1f4cdb85928 100644 --- a/packages/transaction-pay-controller/src/utils/totals.test.ts +++ b/packages/transaction-pay-controller/src/utils/totals.test.ts @@ -1,3 +1,4 @@ +import { TransactionType } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; import { TransactionPayStrategy } from '../index.js'; @@ -179,6 +180,83 @@ describe('Totals Utils', () => { expect(result.total.usd).toBe('71.68'); }); + it.each([ + { + label: TransactionType.perpsDeposit, + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.perpsDeposit, + }, + }, + { + label: TransactionType.predictDeposit, + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.predictDeposit, + }, + }, + { + label: `nested ${TransactionType.predictDeposit}`, + transaction: { + ...TRANSACTION_META_MOCK, + nestedTransactions: [{ type: TransactionType.predictDeposit }], + type: TransactionType.batch, + }, + }, + ])( + 'does not add Relay fees twice for exact-input $label transactions', + ({ transaction }) => { + const quote = { + ...QUOTE_1_MOCK, + fees: { + ...QUOTE_1_MOCK.fees, + metaMask: { fiat: '0', usd: '0' }, + provider: { fiat: '1', usd: '1' }, + sourceNetwork: { + estimate: { + fiat: '0.5', + human: '0.5', + raw: '500000000000000000', + usd: '0.5', + }, + max: { + fiat: '0.6', + human: '0.6', + raw: '600000000000000000', + usd: '0.6', + }, + }, + targetNetwork: { fiat: '0', usd: '0' }, + }, + sourceAmount: { + fiat: '100', + human: '100', + raw: '100000000', + usd: '100', + }, + strategy: TransactionPayStrategy.Relay, + targetAmount: { fiat: '99', usd: '99' }, + }; + const token = { + ...TOKEN_1_MOCK, + amountFiat: '100', + amountUsd: '100', + }; + + const result = calculateTotals({ + quotes: [quote], + tokens: [token], + messenger: MESSENGER_MOCK, + transaction, + }); + + expect(result.total).toStrictEqual({ + fiat: '100.5', + usd: '100.5', + }); + }, + ); + it('returns total using fiatPaymentAmount when fiat strategy is present', () => { const fiatQuote: TransactionPayQuote = { ...QUOTE_1_MOCK, diff --git a/packages/transaction-pay-controller/src/utils/totals.ts b/packages/transaction-pay-controller/src/utils/totals.ts index 26d587de3e8..823ccd00a7e 100644 --- a/packages/transaction-pay-controller/src/utils/totals.ts +++ b/packages/transaction-pay-controller/src/utils/totals.ts @@ -1,7 +1,11 @@ +import { hasTransactionType } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; import { BigNumber } from 'bignumber.js'; -import { TransactionPayStrategy } from '../constants.js'; +import { + RELAY_EXACT_INPUT_DEPOSIT_TYPES, + TransactionPayStrategy, +} from '../constants.js'; import type { FiatValue, TransactionPayControllerMessenger, @@ -78,23 +82,27 @@ export function calculateTotals({ const amountFiat = sumProperty(quoteTokens, (token) => token.amountFiat); const amountUsd = sumProperty(quoteTokens, (token) => token.amountUsd); const hasQuotes = quotes.length > 0; + const isRelayExactInputDeposit = + quotes.some((quote) => quote.strategy === TransactionPayStrategy.Relay) && + hasTransactionType(transaction, RELAY_EXACT_INPUT_DEPOSIT_TYPES); + const useTargetAmount = Boolean(isMaxAmount) || isRelayExactInputDeposit; const sourceAmountFiat = getSourceAmount({ hasFiatStrategy, fiatPaymentAmount, - isMaxAmount, hasQuotes, targetAmount: targetAmount.fiat, tokenAmount: amountFiat, + useTargetAmount, }); const sourceAmountUsd = getSourceAmount({ hasFiatStrategy, fiatPaymentAmount, - isMaxAmount, hasQuotes, targetAmount: targetAmount.usd, tokenAmount: amountUsd, + useTargetAmount, }); const totalFiat = new BigNumber(providerFee.fiat) @@ -152,32 +160,32 @@ export function calculateTotals({ * @param request - Request parameters. * @param request.hasFiatStrategy - Whether a fiat strategy quote is present. * @param request.fiatPaymentAmount - The fiat payment amount, if applicable. - * @param request.isMaxAmount - Whether the transaction is a maximum amount transaction. * @param request.hasQuotes - Whether any quotes are present. * @param request.targetAmount - The target amount from quotes. * @param request.tokenAmount - The summed token amount. + * @param request.useTargetAmount - Whether fees are already included in the source amount. * @returns The payment amount to include in totals. */ function getSourceAmount({ hasFiatStrategy, fiatPaymentAmount, - isMaxAmount, hasQuotes, targetAmount, tokenAmount, + useTargetAmount, }: { hasFiatStrategy: boolean; fiatPaymentAmount?: string; - isMaxAmount?: boolean; hasQuotes: boolean; targetAmount: string; tokenAmount: string; + useTargetAmount: boolean; }): string { if (hasFiatStrategy) { return fiatPaymentAmount ?? '0'; } - if (isMaxAmount && hasQuotes) { + if (useTargetAmount && hasQuotes) { return targetAmount; } From 9ce287244ff39ae88dd363124ec42a24554a3c3a Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Tue, 25 Aug 2026 14:52:18 +0100 Subject: [PATCH 2/9] docs(transaction-pay-controller): link Relay deposit changelog --- packages/transaction-pay-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index e8948213781..987d9687ae6 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Use `EXACT_INPUT` Relay quotes for plain Perps and Predict deposits so the entered amount is the total source amount paid, while preserving `EXACT_OUTPUT` for HyperCore deposit-and-order flows ([CONF-1782](https://consensyssoftware.atlassian.net/browse/CONF-1782)) +- Use `EXACT_INPUT` Relay quotes for plain Perps and Predict deposits so the entered amount is the total source amount paid, while preserving `EXACT_OUTPUT` for HyperCore deposit-and-order flows ([#9954](https://github.com/MetaMask/core/pull/9954)) - Bump `@metamask/sentinel-api-service` from `^1.0.0` to `^1.0.1` ([#9972](https://github.com/MetaMask/core/pull/9972)) ## [27.0.0] From 63f9f32b4c24adafa896cc5fb32fa6d5f62fe90a Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Wed, 26 Aug 2026 14:15:23 +0100 Subject: [PATCH 3/9] fix(transaction-pay-controller): expose input-based quote semantics --- .../transaction-pay-controller/CHANGELOG.md | 2 +- .../src/constants.ts | 6 - .../src/strategy/relay/relay-quotes.test.ts | 36 +-- .../src/strategy/relay/relay-quotes.ts | 73 +----- .../transaction-pay-controller/src/types.ts | 3 + .../src/utils/quotes.test.ts | 1 + .../src/utils/totals.test.ts | 236 ++++++++++++------ .../src/utils/totals.ts | 49 +++- 8 files changed, 230 insertions(+), 176 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 987d9687ae6..f783b9f4fc9 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Use `EXACT_INPUT` Relay quotes for plain Perps and Predict deposits so the entered amount is the total source amount paid, while preserving `EXACT_OUTPUT` for HyperCore deposit-and-order flows ([#9954](https://github.com/MetaMask/core/pull/9954)) +- Use `EXACT_INPUT` Relay quotes when no transaction is embedded, expose the optional `TransactionPayTotals.isInputBased` flag from selected quote semantics, and preserve `EXACT_OUTPUT` for embedded transactions such as Money Account calls ([#9954](https://github.com/MetaMask/core/pull/9954)) - Bump `@metamask/sentinel-api-service` from `^1.0.0` to `^1.0.1` ([#9972](https://github.com/MetaMask/core/pull/9972)) ## [27.0.0] diff --git a/packages/transaction-pay-controller/src/constants.ts b/packages/transaction-pay-controller/src/constants.ts index a990c6b6110..42eca7d0102 100644 --- a/packages/transaction-pay-controller/src/constants.ts +++ b/packages/transaction-pay-controller/src/constants.ts @@ -12,12 +12,6 @@ export const PERPS_DEPOSIT_TYPES: TransactionType[] = [ TransactionType.perpsDepositAndOrder, ]; -/** Plain deposit types whose Relay quote amount is the total source input. */ -export const RELAY_EXACT_INPUT_DEPOSIT_TYPES: TransactionType[] = [ - TransactionType.perpsDeposit, - TransactionType.predictDeposit, -]; - export const CHAIN_ID_ARBITRUM = '0xa4b1' as Hex; export const CHAIN_ID_MAINNET = '0x1' as Hex; export const CHAIN_ID_POLYGON = '0x89' as Hex; diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 224addcc93a..fe6b9c9b1d1 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -290,13 +290,13 @@ describe('Relay Quotes Utils', () => { expect(body).toStrictEqual( expect.objectContaining({ - amount: QUOTE_REQUEST_MOCK.targetAmountMinimum, + amount: QUOTE_REQUEST_MOCK.sourceTokenAmount, destinationChainId: 2, destinationCurrency: QUOTE_REQUEST_MOCK.targetTokenAddress, originChainId: 1, originCurrency: QUOTE_REQUEST_MOCK.sourceTokenAddress, recipient: QUOTE_REQUEST_MOCK.from, - tradeType: 'EXPECTED_OUTPUT', + tradeType: 'EXACT_INPUT', user: QUOTE_REQUEST_MOCK.from, }), ); @@ -398,7 +398,7 @@ describe('Relay Quotes Utils', () => { ); }); - it('sends request with EXACT_INPUT trade type for Predict deposits', async () => { + it('sends request with EXACT_INPUT trade type for token transfers', async () => { const quoteMock = cloneDeep(QUOTE_MOCK); quoteMock.details.currencyOut.minimumAmount = '100'; @@ -416,7 +416,7 @@ describe('Relay Quotes Utils', () => { txParams: { data: TOKEN_TRANSFER_DATA_MOCK, }, - type: TransactionType.predictDeposit, + type: TransactionType.simpleSend, }, }); @@ -433,7 +433,7 @@ describe('Relay Quotes Utils', () => { expect(result[0].dust).toStrictEqual({ fiat: '0', usd: '0' }); }); - it('sends request with EXACT_INPUT trade type for nested Predict deposits', async () => { + it('sends request with EXACT_INPUT trade type for nested token transfers', async () => { successfulFetchMock.mockResolvedValue({ ok: true, json: async () => QUOTE_MOCK, @@ -448,7 +448,7 @@ describe('Relay Quotes Utils', () => { nestedTransactions: [ { data: TOKEN_TRANSFER_DATA_MOCK, - type: TransactionType.predictDeposit, + type: TransactionType.simpleSend, }, ], type: TransactionType.batch, @@ -509,6 +509,7 @@ describe('Relay Quotes Utils', () => { expect(body).toStrictEqual( expect.objectContaining({ + amount: QUOTE_REQUEST_MOCK.targetAmountMinimum, authorizationList: [ { chainId: 1, @@ -609,14 +610,14 @@ describe('Relay Quotes Utils', () => { }); expect(result[0].original.request).toStrictEqual({ - amount: QUOTE_REQUEST_MOCK.targetAmountMinimum, + amount: QUOTE_REQUEST_MOCK.sourceTokenAmount, destinationChainId: 2, destinationCurrency: QUOTE_REQUEST_MOCK.targetTokenAddress, originChainId: 1, originCurrency: QUOTE_REQUEST_MOCK.sourceTokenAddress, recipient: QUOTE_REQUEST_MOCK.from, slippageTolerance: '50', - tradeType: 'EXPECTED_OUTPUT', + tradeType: 'EXACT_INPUT', user: QUOTE_REQUEST_MOCK.from, }); }); @@ -3947,7 +3948,7 @@ describe('Relay Quotes Utils', () => { ).rejects.toThrow(`Source token fiat rate not found`); }); - it('requests exact input for Hyperliquid deposits', async () => { + it('requests exact input for Hyperliquid deposits without embedded transactions', async () => { const arbitrumToHyperliquidRequest: QuoteRequest = { ...QUOTE_REQUEST_MOCK, targetChainId: CHAIN_ID_ARBITRUM, @@ -3983,9 +3984,7 @@ describe('Relay Quotes Utils', () => { ); }); - // A HyperCore deposit followed by an order needs the whole target as margin. - // EXACT_INPUT cannot guarantee that amount after fees and slippage. - it('requests exact output for Hyperliquid deposit-and-order flows', async () => { + it('requests exact input for Hyperliquid deposit-and-order flows without embedded transactions', async () => { const arbitrumToHyperliquidRequest: QuoteRequest = { ...QUOTE_REQUEST_MOCK, targetChainId: CHAIN_ID_ARBITRUM, @@ -4013,13 +4012,13 @@ describe('Relay Quotes Utils', () => { expect(body).toStrictEqual( expect.objectContaining({ - amount: '12300', - tradeType: 'EXACT_OUTPUT', + amount: QUOTE_REQUEST_MOCK.sourceTokenAmount, + tradeType: 'EXACT_INPUT', }), ); }); - it('still requests an expected output for non-Hyperliquid targets', async () => { + it('requests exact input for non-Hyperliquid targets without embedded transactions', async () => { successfulFetchMock.mockResolvedValue({ ok: true, json: async () => QUOTE_MOCK, @@ -4036,7 +4035,12 @@ describe('Relay Quotes Utils', () => { successfulFetchMock.mock.calls[0][1]?.body as string, ); - expect(body.tradeType).toBe('EXPECTED_OUTPUT'); + expect(body).toStrictEqual( + expect.objectContaining({ + amount: QUOTE_REQUEST_MOCK.sourceTokenAmount, + tradeType: 'EXACT_INPUT', + }), + ); }); it('does not convert to Hyperliquid deposit when parent transaction is not a Perps deposit', async () => { diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index c33cb7335ae..b15b8306ae1 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -2,7 +2,6 @@ import { Interface } from '@ethersproject/abi'; import { toHex } from '@metamask/controller-utils'; -import { hasTransactionType } from '@metamask/transaction-controller'; import type { AuthorizationList, TransactionMeta, @@ -20,7 +19,6 @@ import { HYPERCORE_USDC_DECIMALS, NATIVE_TOKEN_ADDRESS, PERPS_DEPOSIT_TYPES, - RELAY_EXACT_INPUT_DEPOSIT_TYPES, USDC_DECIMALS, PaymentOverride, } from '../../constants.js'; @@ -305,17 +303,11 @@ async function getSingleQuote( ); try { - // Source-driven flows use EXACT_INPUT so the amount the user enters is the - // total sent and the destination receives that amount minus fees. + // Explicit source-driven flows use EXACT_INPUT immediately. Other flows + // start as EXPECTED_OUTPUT until transaction processing determines whether + // the request embeds transactions. const useExactInput = - (isMaxAmount ?? false) || - (request.isPostQuote ?? false) || - isExactInputDeposit(transaction); - - // A HyperCore deposit followed by an order requires the full target as - // margin, so the delivered amount must be guaranteed. Plain Perps deposits - // use EXACT_INPUT above, while perpsDepositAndOrder reaches this exception. - const useExactOutput = !useExactInput && isHypercoreDeposit(request); + (isMaxAmount ?? false) || (request.isPostQuote ?? false); const useExecute = supports7702 && @@ -346,7 +338,7 @@ async function getSingleQuote( : {}), recipient: effectiveRequest.recipient ?? from, slippageTolerance, - tradeType: getTradeType(useExactInput, useExactOutput), + tradeType: useExactInput ? 'EXACT_INPUT' : 'EXPECTED_OUTPUT', user: from, }; @@ -386,6 +378,11 @@ async function getSingleQuote( body.refundTo = effectiveRequest.refundTo; } + if (!body.txs?.length) { + body.tradeType = 'EXACT_INPUT'; + body.amount = sourceTokenAmount; + } + log('Request body', body); const quote = await fetchRelayQuote(messenger, body, signal); @@ -613,56 +610,6 @@ async function processMoneyAccountPostQuote( }); } -/** - * Whether the transaction is a plain Perps or Predict deposit whose entered - * amount represents the total source amount sent. - * - * Deposit-and-order flows are excluded because the follow-on order requires a - * guaranteed target amount. - * - * @param transaction - Parent transaction metadata. - * @returns True when the deposit should use exact-input semantics. - */ -function isExactInputDeposit(transaction: TransactionMeta): boolean { - return hasTransactionType(transaction, RELAY_EXACT_INPUT_DEPOSIT_TYPES); -} - -/** - * Whether the quote deposits into HyperCore USDC. - * - * `normalizeRequest` remaps Arbitrum-USDC perps deposits to HyperCore before - * the quote is built, so the check is against the normalized target. - * - * @param request - Normalized quote request. - * @returns True when the target is HyperCore USDC. - */ -function isHypercoreDeposit(request: QuoteRequest): boolean { - return ( - !request.isHyperliquidSource && - request.targetChainId === CHAIN_ID_HYPERCORE && - request.targetTokenAddress.toLowerCase() === - HYPERCORE_USDC_ADDRESS.toLowerCase() - ); -} - -/** - * Resolve the Relay trade type for a quote. - * - * @param useExactInput - Whether the user specified the amount to send. - * @param useExactOutput - Whether the delivered amount must be guaranteed. - * @returns The Relay trade type. - */ -function getTradeType( - useExactInput: boolean | undefined, - useExactOutput: boolean, -): RelayQuoteRequest['tradeType'] { - if (useExactInput) { - return 'EXACT_INPUT'; - } - - return useExactOutput ? 'EXACT_OUTPUT' : 'EXPECTED_OUTPUT'; -} - /** * Normalizes requests for Relay. * diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index d940c0dbcdc..85ef756fcf6 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -838,6 +838,9 @@ export type TransactionPayTotals = { /** Total fees for the target transaction and all quotes. */ fees: TransactionPayFees; + /** Whether all selected quotes are driven by the source input amount. */ + isInputBased?: boolean; + /** Total amount of source token required. */ sourceAmount: Amount; diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index c0135333d94..ac3d246c11b 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -76,6 +76,7 @@ const QUOTE_MOCK = { } as TransactionPayQuote; const TOTALS_MOCK = { + isInputBased: false, fees: { provider: { fiat: '7.89', diff --git a/packages/transaction-pay-controller/src/utils/totals.test.ts b/packages/transaction-pay-controller/src/utils/totals.test.ts index 1f4cdb85928..dad45697e79 100644 --- a/packages/transaction-pay-controller/src/utils/totals.test.ts +++ b/packages/transaction-pay-controller/src/utils/totals.test.ts @@ -1,4 +1,3 @@ -import { TransactionType } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; import { TransactionPayStrategy } from '../index.js'; @@ -8,6 +7,8 @@ import type { TransactionPayQuote, TransactionPayRequiredToken, } from '../types.js'; +import type { AcrossQuote } from '../strategy/across/types.js'; +import type { RelayQuote } from '../strategy/relay/types.js'; import { calculateTransactionGasCost } from './gas.js'; import { calculateTotals } from './totals.js'; @@ -49,7 +50,9 @@ const QUOTE_1_MOCK: TransactionPayQuote = { usd: '6.66', }, }, - original: undefined, + original: { + request: { tradeType: 'exactOutput' }, + } as AcrossQuote, request: {} as QuoteRequest, sourceAmount: { human: '7.77', @@ -109,7 +112,9 @@ const QUOTE_2_MOCK: TransactionPayQuote = { usd: '12.12', }, }, - original: undefined, + original: { + request: { tradeType: 'exactOutput' }, + } as AcrossQuote, request: {} as QuoteRequest, sourceAmount: { human: '13.13', @@ -126,6 +131,26 @@ const QUOTE_2_MOCK: TransactionPayQuote = { const TRANSACTION_META_MOCK = {} as TransactionMeta; +function getRelayQuote( + tradeType: RelayQuote['request']['tradeType'], +): TransactionPayQuote { + return { + ...QUOTE_1_MOCK, + original: { request: { tradeType } } as RelayQuote, + strategy: TransactionPayStrategy.Relay, + }; +} + +function getAcrossQuote( + tradeType: AcrossQuote['request']['tradeType'], +): TransactionPayQuote { + return { + ...QUOTE_2_MOCK, + original: { request: { tradeType } } as AcrossQuote, + strategy: TransactionPayStrategy.Across, + }; +} + describe('Totals Utils', () => { const calculateTransactionGasCostMock = jest.mocked( calculateTransactionGasCost, @@ -167,10 +192,13 @@ describe('Totals Utils', () => { expect(result.total.usd).toBe('52.08'); }); - it('returns adjusted total when isMaxAmount is true', () => { + it('returns adjusted total for strategies without a provider trade type when isMaxAmount is true', () => { const result = calculateTotals({ isMaxAmount: true, - quotes: [QUOTE_1_MOCK, QUOTE_2_MOCK], + quotes: [ + { ...QUOTE_1_MOCK, strategy: TransactionPayStrategy.Server }, + { ...QUOTE_2_MOCK, strategy: TransactionPayStrategy.Server }, + ], tokens: [TOKEN_1_MOCK, TOKEN_2_MOCK], messenger: MESSENGER_MOCK, transaction: TRANSACTION_META_MOCK, @@ -180,82 +208,132 @@ describe('Totals Utils', () => { expect(result.total.usd).toBe('71.68'); }); - it.each([ - { - label: TransactionType.perpsDeposit, - transaction: { - ...TRANSACTION_META_MOCK, - type: TransactionType.perpsDeposit, - }, - }, - { - label: TransactionType.predictDeposit, - transaction: { - ...TRANSACTION_META_MOCK, - type: TransactionType.predictDeposit, - }, - }, - { - label: `nested ${TransactionType.predictDeposit}`, - transaction: { - ...TRANSACTION_META_MOCK, - nestedTransactions: [{ type: TransactionType.predictDeposit }], - type: TransactionType.batch, - }, - }, - ])( - 'does not add Relay fees twice for exact-input $label transactions', - ({ transaction }) => { - const quote = { - ...QUOTE_1_MOCK, - fees: { - ...QUOTE_1_MOCK.fees, - metaMask: { fiat: '0', usd: '0' }, - provider: { fiat: '1', usd: '1' }, - sourceNetwork: { - estimate: { - fiat: '0.5', - human: '0.5', - raw: '500000000000000000', - usd: '0.5', - }, - max: { - fiat: '0.6', - human: '0.6', - raw: '600000000000000000', - usd: '0.6', - }, + it('returns isInputBased true for an exact-input Relay quote', () => { + const result = calculateTotals({ + quotes: [getRelayQuote('EXACT_INPUT')], + tokens: [], + messenger: MESSENGER_MOCK, + transaction: TRANSACTION_META_MOCK, + }); + + expect(result.isInputBased).toBe(true); + }); + + it('returns isInputBased false for an exact-output Relay quote', () => { + const result = calculateTotals({ + quotes: [getRelayQuote('EXACT_OUTPUT')], + tokens: [], + messenger: MESSENGER_MOCK, + transaction: TRANSACTION_META_MOCK, + }); + + expect(result.isInputBased).toBe(false); + }); + + it('returns isInputBased true for an exact-input Across quote', () => { + const result = calculateTotals({ + quotes: [getAcrossQuote('exactInput')], + tokens: [], + messenger: MESSENGER_MOCK, + transaction: TRANSACTION_META_MOCK, + }); + + expect(result.isInputBased).toBe(true); + }); + + it('returns isInputBased false for an exact-output Across quote', () => { + const result = calculateTotals({ + quotes: [getAcrossQuote('exactOutput')], + tokens: [], + messenger: MESSENGER_MOCK, + transaction: TRANSACTION_META_MOCK, + }); + + expect(result.isInputBased).toBe(false); + }); + + it('returns isInputBased false when there are no quotes', () => { + const result = calculateTotals({ + quotes: [], + tokens: [], + messenger: MESSENGER_MOCK, + transaction: TRANSACTION_META_MOCK, + }); + + expect(result.isInputBased).toBe(false); + }); + + it('returns isInputBased false for mixed input- and output-based quotes', () => { + const result = calculateTotals({ + quotes: [getRelayQuote('EXACT_INPUT'), QUOTE_2_MOCK], + tokens: [], + messenger: MESSENGER_MOCK, + transaction: TRANSACTION_META_MOCK, + }); + + expect(result.isInputBased).toBe(false); + }); + + it('returns isInputBased true when all aggregate quotes are input-based', () => { + const result = calculateTotals({ + quotes: [getRelayQuote('EXACT_INPUT'), getAcrossQuote('exactInput')], + tokens: [], + messenger: MESSENGER_MOCK, + transaction: TRANSACTION_META_MOCK, + }); + + expect(result.isInputBased).toBe(true); + }); + + it('does not add Relay fees twice for exact-input quotes', () => { + const quote = { + ...getRelayQuote('EXACT_INPUT'), + fees: { + ...QUOTE_1_MOCK.fees, + metaMask: { fiat: '0', usd: '0' }, + provider: { fiat: '1', usd: '1' }, + sourceNetwork: { + estimate: { + fiat: '0.5', + human: '0.5', + raw: '500000000000000000', + usd: '0.5', + }, + max: { + fiat: '0.6', + human: '0.6', + raw: '600000000000000000', + usd: '0.6', }, - targetNetwork: { fiat: '0', usd: '0' }, - }, - sourceAmount: { - fiat: '100', - human: '100', - raw: '100000000', - usd: '100', }, - strategy: TransactionPayStrategy.Relay, - targetAmount: { fiat: '99', usd: '99' }, - }; - const token = { - ...TOKEN_1_MOCK, - amountFiat: '100', - amountUsd: '100', - }; - - const result = calculateTotals({ - quotes: [quote], - tokens: [token], - messenger: MESSENGER_MOCK, - transaction, - }); - - expect(result.total).toStrictEqual({ - fiat: '100.5', - usd: '100.5', - }); - }, - ); + targetNetwork: { fiat: '0', usd: '0' }, + }, + sourceAmount: { + fiat: '100', + human: '100', + raw: '100000000', + usd: '100', + }, + targetAmount: { fiat: '99', usd: '99' }, + }; + const token = { + ...TOKEN_1_MOCK, + amountFiat: '100', + amountUsd: '100', + }; + + const result = calculateTotals({ + quotes: [quote], + tokens: [token], + messenger: MESSENGER_MOCK, + transaction: TRANSACTION_META_MOCK, + }); + + expect(result.total).toStrictEqual({ + fiat: '100.5', + usd: '100.5', + }); + }); it('returns total using fiatPaymentAmount when fiat strategy is present', () => { const fiatQuote: TransactionPayQuote = { diff --git a/packages/transaction-pay-controller/src/utils/totals.ts b/packages/transaction-pay-controller/src/utils/totals.ts index 823ccd00a7e..a03409ac82c 100644 --- a/packages/transaction-pay-controller/src/utils/totals.ts +++ b/packages/transaction-pay-controller/src/utils/totals.ts @@ -1,11 +1,9 @@ -import { hasTransactionType } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; import { BigNumber } from 'bignumber.js'; -import { - RELAY_EXACT_INPUT_DEPOSIT_TYPES, - TransactionPayStrategy, -} from '../constants.js'; +import { TransactionPayStrategy } from '../constants.js'; +import type { AcrossQuote } from '../strategy/across/types.js'; +import type { RelayQuote } from '../strategy/relay/types.js'; import type { FiatValue, TransactionPayControllerMessenger, @@ -82,10 +80,9 @@ export function calculateTotals({ const amountFiat = sumProperty(quoteTokens, (token) => token.amountFiat); const amountUsd = sumProperty(quoteTokens, (token) => token.amountUsd); const hasQuotes = quotes.length > 0; - const isRelayExactInputDeposit = - quotes.some((quote) => quote.strategy === TransactionPayStrategy.Relay) && - hasTransactionType(transaction, RELAY_EXACT_INPUT_DEPOSIT_TYPES); - const useTargetAmount = Boolean(isMaxAmount) || isRelayExactInputDeposit; + const isInputBased = + hasQuotes && + quotes.every((quote) => isQuoteInputBased(quote, Boolean(isMaxAmount))); const sourceAmountFiat = getSourceAmount({ hasFiatStrategy, @@ -93,7 +90,7 @@ export function calculateTotals({ hasQuotes, targetAmount: targetAmount.fiat, tokenAmount: amountFiat, - useTargetAmount, + useTargetAmount: isInputBased, }); const sourceAmountUsd = getSourceAmount({ @@ -102,7 +99,7 @@ export function calculateTotals({ hasQuotes, targetAmount: targetAmount.usd, tokenAmount: amountUsd, - useTargetAmount, + useTargetAmount: isInputBased, }); const totalFiat = new BigNumber(providerFee.fiat) @@ -145,6 +142,7 @@ export function calculateTotals({ }, targetNetwork: targetNetworkFee, }, + isInputBased, sourceAmount, targetAmount, total: { @@ -154,6 +152,35 @@ export function calculateTotals({ }; } +/** + * Whether a selected quote is driven by its source input amount. + * + * @param quote - Selected quote. + * @param isMaxAmount - Whether the target transaction uses the maximum input. + * @returns Whether the quote uses input-based semantics. + */ +function isQuoteInputBased( + quote: TransactionPayQuote, + isMaxAmount: boolean, +): boolean { + if (quote.strategy === TransactionPayStrategy.Relay) { + const relayQuote = quote.original as RelayQuote; + return relayQuote.request.tradeType === 'EXACT_INPUT'; + } + + if (quote.strategy === TransactionPayStrategy.Across) { + const acrossQuote = quote.original as AcrossQuote; + return acrossQuote.request.tradeType === 'exactInput'; + } + + return ( + isMaxAmount || + quote.request.isMaxAmount === true || + quote.request.isPostQuote === true || + quote.request.isHyperliquidSource === true + ); +} + /** * Get the source amount to include in totals. * From f610730841ab369d0e808ccbb1796e58a9a2dd10 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Wed, 26 Aug 2026 15:04:36 +0100 Subject: [PATCH 4/9] chore(transaction-pay-controller): format totals tests --- packages/transaction-pay-controller/src/utils/totals.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/transaction-pay-controller/src/utils/totals.test.ts b/packages/transaction-pay-controller/src/utils/totals.test.ts index dad45697e79..aea53cba26e 100644 --- a/packages/transaction-pay-controller/src/utils/totals.test.ts +++ b/packages/transaction-pay-controller/src/utils/totals.test.ts @@ -2,13 +2,13 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import { TransactionPayStrategy } from '../index.js'; import type { TransactionPayControllerMessenger } from '../index.js'; +import type { AcrossQuote } from '../strategy/across/types.js'; +import type { RelayQuote } from '../strategy/relay/types.js'; import type { QuoteRequest, TransactionPayQuote, TransactionPayRequiredToken, } from '../types.js'; -import type { AcrossQuote } from '../strategy/across/types.js'; -import type { RelayQuote } from '../strategy/relay/types.js'; import { calculateTransactionGasCost } from './gas.js'; import { calculateTotals } from './totals.js'; From 5f0d8b805e8da30cdf330d69a6ebc35f162c84ac Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 27 Aug 2026 10:14:34 +0100 Subject: [PATCH 5/9] fix: address PR comment about exact input boolean --- .../src/strategy/relay/relay-quotes.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index b15b8306ae1..ff594442a4c 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -306,8 +306,7 @@ async function getSingleQuote( // Explicit source-driven flows use EXACT_INPUT immediately. Other flows // start as EXPECTED_OUTPUT until transaction processing determines whether // the request embeds transactions. - const useExactInput = - (isMaxAmount ?? false) || (request.isPostQuote ?? false); + const useExactInput = Boolean(isMaxAmount || request.isPostQuote); const useExecute = supports7702 && From b32aea78f3fe8fa30350747c9126374858cce1b4 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 27 Aug 2026 10:25:13 +0100 Subject: [PATCH 6/9] fix: assign Relay trade type after transactions --- .../src/strategy/relay/polymarket/withdraw.ts | 2 +- .../src/strategy/relay/relay-quotes.ts | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/polymarket/withdraw.ts b/packages/transaction-pay-controller/src/strategy/relay/polymarket/withdraw.ts index 7591200dbb2..acd9bccafc2 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/polymarket/withdraw.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/polymarket/withdraw.ts @@ -132,7 +132,7 @@ export async function buildPolymarketDepositWalletSimulation( } export async function applyPolymarketDepositWalletOverrides( - body: RelayQuoteRequest, + body: Omit, request: QuoteRequest, messenger: TransactionPayControllerMessenger, ): Promise { diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index ff594442a4c..9b83dbc9886 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -82,6 +82,7 @@ const POST_QUOTE_GAS_BUFFER = 1.1; const PAYMENT_OVERRIDE_GAS = 75_000; const ZERO_AMOUNT = { fiat: '0', human: '0', raw: '0', usd: '0' }; +type RelayQuoteRequestDraft = Omit; type RelayStepData = RelayTransactionStep['items'][0]['data']; type RelayGasResult = { @@ -323,7 +324,7 @@ async function getSingleQuote( ? { ...request, recipient: nonAtomicRecipient } : request; - const body: RelayQuoteRequest = { + const body: RelayQuoteRequestDraft = { amount: useExactInput ? sourceTokenAmount : targetAmountMinimum, destinationChainId: Number(targetChainId), destinationCurrency: targetTokenAddress, @@ -337,7 +338,6 @@ async function getSingleQuote( : {}), recipient: effectiveRequest.recipient ?? from, slippageTolerance, - tradeType: useExactInput ? 'EXACT_INPUT' : 'EXPECTED_OUTPUT', user: from, }; @@ -378,13 +378,17 @@ async function getSingleQuote( } if (!body.txs?.length) { - body.tradeType = 'EXACT_INPUT'; body.amount = sourceTokenAmount; } - log('Request body', body); + const finalBody: RelayQuoteRequest = { + ...body, + tradeType: body.txs?.length ? 'EXACT_OUTPUT' : 'EXACT_INPUT', + }; + + log('Request body', finalBody); - const quote = await fetchRelayQuote(messenger, body, signal); + const quote = await fetchRelayQuote(messenger, finalBody, signal); log('Fetched relay quote', quote); @@ -469,7 +473,7 @@ async function resolveNonAtomicRecipient( async function processTransactions( transaction: TransactionMeta, request: QuoteRequest, - requestBody: RelayQuoteRequest, + requestBody: RelayQuoteRequestDraft, messenger: TransactionPayControllerMessenger, ): Promise { // Skip when skipProcessTransactions (defaulting to isPostQuote) is set — the @@ -523,7 +527,6 @@ async function processTransactions( requestBody.authorizationList = normalizeAuthorizationList( delegation.authorizationList, ); - requestBody.tradeType = 'EXACT_OUTPUT'; const tokenTransferData = nestedTransactions?.find((nestedTx) => nestedTx.data?.startsWith(TOKEN_TRANSFER_FOUR_BYTE), @@ -560,7 +563,7 @@ async function processTransactions( async function processMoneyAccountPostQuote( transaction: TransactionMeta, request: QuoteRequest, - requestBody: RelayQuoteRequest, + requestBody: RelayQuoteRequestDraft, messenger: TransactionPayControllerMessenger, ): Promise { const { transactionData: transactionDataList } = messenger.call( @@ -589,7 +592,6 @@ async function processMoneyAccountPostQuote( const rawAmount = transactionData?.tokens?.[0]?.amountRaw ?? '0'; requestBody.authorizationList = normalizeAuthorizationList(authorizationList); - requestBody.tradeType = 'EXACT_OUTPUT'; requestBody.amount = rawAmount; requestBody.txs = [ { From d0eb8912b4633a06a7066460720a7ff1d701c943 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 27 Aug 2026 10:31:58 +0100 Subject: [PATCH 7/9] fix: assign Relay amount after transactions --- .../src/strategy/relay/polymarket/withdraw.ts | 2 +- .../src/strategy/relay/relay-quotes.ts | 33 +++++++++---------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/polymarket/withdraw.ts b/packages/transaction-pay-controller/src/strategy/relay/polymarket/withdraw.ts index acd9bccafc2..3fa526df88b 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/polymarket/withdraw.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/polymarket/withdraw.ts @@ -132,7 +132,7 @@ export async function buildPolymarketDepositWalletSimulation( } export async function applyPolymarketDepositWalletOverrides( - body: Omit, + body: Omit, request: QuoteRequest, messenger: TransactionPayControllerMessenger, ): Promise { diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 9b83dbc9886..41b646c7185 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -82,7 +82,10 @@ const POST_QUOTE_GAS_BUFFER = 1.1; const PAYMENT_OVERRIDE_GAS = 75_000; const ZERO_AMOUNT = { fiat: '0', human: '0', raw: '0', usd: '0' }; -type RelayQuoteRequestDraft = Omit; +type RelayQuoteRequestDraft = Omit< + RelayQuoteRequest, + 'amount' | 'tradeType' +>; type RelayStepData = RelayTransactionStep['items'][0]['data']; type RelayGasResult = { @@ -284,7 +287,6 @@ async function getSingleQuote( const { from, - isMaxAmount, sourceChainId, sourceTokenAddress, sourceTokenAmount, @@ -304,11 +306,6 @@ async function getSingleQuote( ); try { - // Explicit source-driven flows use EXACT_INPUT immediately. Other flows - // start as EXPECTED_OUTPUT until transaction processing determines whether - // the request embeds transactions. - const useExactInput = Boolean(isMaxAmount || request.isPostQuote); - const useExecute = supports7702 && isRelayExecuteEnabled(messenger) && @@ -325,7 +322,6 @@ async function getSingleQuote( : request; const body: RelayQuoteRequestDraft = { - amount: useExactInput ? sourceTokenAmount : targetAmountMinimum, destinationChainId: Number(targetChainId), destinationCurrency: targetTokenAddress, originChainId: Number(sourceChainId), @@ -358,13 +354,15 @@ async function getSingleQuote( messenger, ); + let transactionAmount: string | undefined; + if ( !processedTransactions && isAtomic && effectiveRequest.isPostQuote && effectiveRequest.paymentOverride === PaymentOverride.MoneyAccount ) { - await processMoneyAccountPostQuote( + transactionAmount = await processMoneyAccountPostQuote( transaction, effectiveRequest, body, @@ -377,13 +375,13 @@ async function getSingleQuote( body.refundTo = effectiveRequest.refundTo; } - if (!body.txs?.length) { - body.amount = sourceTokenAmount; - } - + const hasTransactions = Boolean(body.txs?.length); const finalBody: RelayQuoteRequest = { ...body, - tradeType: body.txs?.length ? 'EXACT_OUTPUT' : 'EXACT_INPUT', + amount: + transactionAmount ?? + (hasTransactions ? targetAmountMinimum : sourceTokenAmount), + tradeType: hasTransactions ? 'EXACT_OUTPUT' : 'EXACT_INPUT', }; log('Request body', finalBody); @@ -565,7 +563,7 @@ async function processMoneyAccountPostQuote( request: QuoteRequest, requestBody: RelayQuoteRequestDraft, messenger: TransactionPayControllerMessenger, -): Promise { +): Promise { const { transactionData: transactionDataList } = messenger.call( 'TransactionPayController:getState', ); @@ -585,14 +583,13 @@ async function processMoneyAccountPostQuote( if (!overrideCalls.length) { log('No payment override calls for money account post-quote'); - return; + return undefined; } const fundingRecipient = recipient ?? request.from; const rawAmount = transactionData?.tokens?.[0]?.amountRaw ?? '0'; requestBody.authorizationList = normalizeAuthorizationList(authorizationList); - requestBody.amount = rawAmount; requestBody.txs = [ { to: request.targetTokenAddress, @@ -609,6 +606,8 @@ async function processMoneyAccountPostQuote( log('Added money account deposit calls to quote body', { callCount: overrideCalls.length, }); + + return rawAmount; } /** From c848146ce29555d331d4a5578800552a258033fa Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 27 Aug 2026 10:36:25 +0100 Subject: [PATCH 8/9] docs: clarify input-based quote totals --- packages/transaction-pay-controller/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 85ef756fcf6..a5fa478b76b 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -838,7 +838,7 @@ export type TransactionPayTotals = { /** Total fees for the target transaction and all quotes. */ fees: TransactionPayFees; - /** Whether all selected quotes are driven by the source input amount. */ + /** Whether the selected quotes subtract fees from the destination amount, meaning the input amount is static. */ isInputBased?: boolean; /** Total amount of source token required. */ From ccb7854521d661df29f2357cd29a20b330532587 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 27 Aug 2026 10:45:29 +0100 Subject: [PATCH 9/9] fix: expose input-based quote semantics --- .../transaction-pay-controller/CHANGELOG.md | 2 +- .../src/strategy/across/across-quotes.test.ts | 4 +- .../src/strategy/across/across-quotes.ts | 1 + .../strategy/fiat/fiat-direct-musd.test.ts | 1 + .../src/strategy/fiat/fiat-direct-musd.ts | 1 + .../src/strategy/relay/relay-quotes.test.ts | 4 +- .../src/strategy/relay/relay-quotes.ts | 6 +- .../src/strategy/server/server-quotes.test.ts | 6 +- .../src/strategy/server/server-quotes.ts | 9 ++- .../transaction-pay-controller/src/types.ts | 3 + .../src/utils/no-op-quote.test.ts | 1 + .../src/utils/no-op-quote.ts | 1 + .../src/utils/quotes.ts | 1 - .../src/utils/totals.test.ts | 73 ++++++++----------- .../src/utils/totals.ts | 37 +--------- 15 files changed, 60 insertions(+), 90 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index f783b9f4fc9..c716ff78a85 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Use `EXACT_INPUT` Relay quotes when no transaction is embedded, expose the optional `TransactionPayTotals.isInputBased` flag from selected quote semantics, and preserve `EXACT_OUTPUT` for embedded transactions such as Money Account calls ([#9954](https://github.com/MetaMask/core/pull/9954)) +- Use `EXACT_INPUT` Relay quotes when no transaction is embedded, expose optional `TransactionPayQuote.isInputBased` and `TransactionPayTotals.isInputBased` flags, and preserve `EXACT_OUTPUT` for embedded transactions such as Money Account calls ([#9954](https://github.com/MetaMask/core/pull/9954)) - Bump `@metamask/sentinel-api-service` from `^1.0.0` to `^1.0.1` ([#9972](https://github.com/MetaMask/core/pull/9972)) ## [27.0.0] diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts index 4af8a0553c8..33df6b264f6 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.test.ts @@ -321,13 +321,15 @@ describe('Across Quotes', () => { json: async () => QUOTE_MOCK, } as Response); - await getAcrossQuotes({ + const result = await getAcrossQuotes({ accountSupports7702: true, messenger, requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], transaction: TRANSACTION_META_MOCK, }); + expect(result[0].isInputBased).toBe(true); + const [url] = successfulFetchMock.mock.calls[0]; const params = new URL(url as string).searchParams; diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts index edb775d9913..a6ea9dc6cf1 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -428,6 +428,7 @@ async function normalizeQuote( sourceNetwork, targetNetwork, }, + isInputBased: original.request.tradeType === 'exactInput', original: { ...original, metamask, diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.test.ts index 785b82cf095..2181285c0ad 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.test.ts @@ -183,6 +183,7 @@ describe('fiat-direct-musd', () => { }, targetNetwork: { fiat: '0', usd: '0' }, }), + isInputBased: false, original: { rampsQuote: RAMPS_QUOTE_MOCK, relayQuote: undefined }, request: expect.objectContaining({ from: MONEY_ACCOUNT_ADDRESS_MOCK, diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.ts index e1efce2806e..9140f0470f9 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-direct-musd.ts @@ -205,6 +205,7 @@ function combineDirectMusdFiatQuote({ }, targetNetwork: { fiat: '0', usd: '0' }, }, + isInputBased: false, original: { rampsQuote: fiatQuote, relayQuote: undefined, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index fe6b9c9b1d1..55036405c19 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -379,13 +379,15 @@ describe('Relay Quotes Utils', () => { json: async () => QUOTE_MOCK, } as never); - await getRelayQuotes({ + const result = await getRelayQuotes({ accountSupports7702: true, messenger, requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], transaction: TRANSACTION_META_MOCK, }); + expect(result[0].isInputBased).toBe(true); + const body = JSON.parse( successfulFetchMock.mock.calls[0][1]?.body as string, ); diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 41b646c7185..10748d73df3 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -82,10 +82,7 @@ const POST_QUOTE_GAS_BUFFER = 1.1; const PAYMENT_OVERRIDE_GAS = 75_000; const ZERO_AMOUNT = { fiat: '0', human: '0', raw: '0', usd: '0' }; -type RelayQuoteRequestDraft = Omit< - RelayQuoteRequest, - 'amount' | 'tradeType' ->; +type RelayQuoteRequestDraft = Omit; type RelayStepData = RelayTransactionStep['items'][0]['data']; type RelayGasResult = { @@ -768,6 +765,7 @@ async function normalizeQuote( sourceNetwork, targetNetwork, }, + isInputBased: quote.request.tradeType === 'EXACT_INPUT', original: { ...quote, metamask, diff --git a/packages/transaction-pay-controller/src/strategy/server/server-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/server/server-quotes.test.ts index bd28a89abd5..d2fb37a82ac 100644 --- a/packages/transaction-pay-controller/src/strategy/server/server-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/server/server-quotes.test.ts @@ -201,14 +201,15 @@ describe('server-quotes', () => { ); }); - it('maps max-amount transactions to EXACT_INPUT quote requests', async () => { - await getServerQuotes({ + it('maps max-amount transactions to input-based EXACT_INPUT quotes', async () => { + const result = await getServerQuotes({ accountSupports7702: true, messenger, requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], transaction: TRANSACTION_META_MOCK, }); + expect(result[0].isInputBased).toBe(true); expect(fetchServerQuoteMock).toHaveBeenCalledWith( messenger, expect.objectContaining({ @@ -412,6 +413,7 @@ describe('server-quotes', () => { }, targetNetwork: { fiat: '0', usd: '0' }, }, + isInputBased: false, original: { client: { gasLimits: [], diff --git a/packages/transaction-pay-controller/src/strategy/server/server-quotes.ts b/packages/transaction-pay-controller/src/strategy/server/server-quotes.ts index 12a4fc649ab..9c14ad7a3c9 100644 --- a/packages/transaction-pay-controller/src/strategy/server/server-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/server/server-quotes.ts @@ -124,7 +124,12 @@ async function getQuotesForRequest( const normalized = await Promise.all( fulfilledResults.map((result) => - normalizeQuote(result, quoteRequest, messenger), + normalizeQuote( + result, + quoteRequest, + messenger, + body.tradeType === ServerTradeType.ExactInput, + ), ), ); @@ -322,6 +327,7 @@ async function normalizeQuote( result: FulfilledServerQuoteResult, quoteRequest: QuoteRequest, messenger: TransactionPayControllerMessenger, + isInputBased: boolean, ): Promise> { const { quote } = result; const { gasless } = quote; @@ -369,6 +375,7 @@ async function normalizeQuote( }, targetNetwork: ZERO_FIAT_VALUE, }, + isInputBased, original: { client: { gasLimits: sourceNetwork.gasLimits, diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index a5fa478b76b..d029e1e85b7 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -673,6 +673,9 @@ export type TransactionPayQuote = { /** Fees associated with the transaction pay quote. */ fees: TransactionPayFees; + /** Whether fees are subtracted from the destination amount, meaning the input amount is static. */ + isInputBased?: boolean; + /** Raw quote data returned by the provider. */ original: OriginalQuote; diff --git a/packages/transaction-pay-controller/src/utils/no-op-quote.test.ts b/packages/transaction-pay-controller/src/utils/no-op-quote.test.ts index b955bcb3148..8363b8ede8d 100644 --- a/packages/transaction-pay-controller/src/utils/no-op-quote.test.ts +++ b/packages/transaction-pay-controller/src/utils/no-op-quote.test.ts @@ -20,6 +20,7 @@ describe('No-Op Quote Utils', () => { const quote = buildNoOpQuote(FROM_MOCK, PAYMENT_TOKEN_MOCK); expect(quote.strategy).toBe(TransactionPayStrategy.None); + expect(quote.isInputBased).toBe(false); }); it('builds quote with zero fees and amounts', () => { diff --git a/packages/transaction-pay-controller/src/utils/no-op-quote.ts b/packages/transaction-pay-controller/src/utils/no-op-quote.ts index a4bcec8d4fd..d42666e3fee 100644 --- a/packages/transaction-pay-controller/src/utils/no-op-quote.ts +++ b/packages/transaction-pay-controller/src/utils/no-op-quote.ts @@ -46,6 +46,7 @@ export function buildNoOpQuote( }, targetNetwork: zeroFiat(), }, + isInputBased: false, original: null, request: { from, diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index 48cf9ff93f8..b12e45816ca 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -168,7 +168,6 @@ export async function updateQuotes( const totals = calculateTotals({ fiatPaymentAmount: fiatPayment?.amountFiat, - isMaxAmount, messenger, quotes: executableQuotes as TransactionPayQuote[], tokens, diff --git a/packages/transaction-pay-controller/src/utils/totals.test.ts b/packages/transaction-pay-controller/src/utils/totals.test.ts index aea53cba26e..a3429114142 100644 --- a/packages/transaction-pay-controller/src/utils/totals.test.ts +++ b/packages/transaction-pay-controller/src/utils/totals.test.ts @@ -2,8 +2,6 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import { TransactionPayStrategy } from '../index.js'; import type { TransactionPayControllerMessenger } from '../index.js'; -import type { AcrossQuote } from '../strategy/across/types.js'; -import type { RelayQuote } from '../strategy/relay/types.js'; import type { QuoteRequest, TransactionPayQuote, @@ -50,9 +48,8 @@ const QUOTE_1_MOCK: TransactionPayQuote = { usd: '6.66', }, }, - original: { - request: { tradeType: 'exactOutput' }, - } as AcrossQuote, + isInputBased: false, + original: {}, request: {} as QuoteRequest, sourceAmount: { human: '7.77', @@ -112,9 +109,8 @@ const QUOTE_2_MOCK: TransactionPayQuote = { usd: '12.12', }, }, - original: { - request: { tradeType: 'exactOutput' }, - } as AcrossQuote, + isInputBased: false, + original: {}, request: {} as QuoteRequest, sourceAmount: { human: '13.13', @@ -131,24 +127,11 @@ const QUOTE_2_MOCK: TransactionPayQuote = { const TRANSACTION_META_MOCK = {} as TransactionMeta; -function getRelayQuote( - tradeType: RelayQuote['request']['tradeType'], +function getQuote( + isInputBased: boolean, + quote: TransactionPayQuote = QUOTE_1_MOCK, ): TransactionPayQuote { - return { - ...QUOTE_1_MOCK, - original: { request: { tradeType } } as RelayQuote, - strategy: TransactionPayStrategy.Relay, - }; -} - -function getAcrossQuote( - tradeType: AcrossQuote['request']['tradeType'], -): TransactionPayQuote { - return { - ...QUOTE_2_MOCK, - original: { request: { tradeType } } as AcrossQuote, - strategy: TransactionPayStrategy.Across, - }; + return { ...quote, isInputBased }; } describe('Totals Utils', () => { @@ -192,13 +175,9 @@ describe('Totals Utils', () => { expect(result.total.usd).toBe('52.08'); }); - it('returns adjusted total for strategies without a provider trade type when isMaxAmount is true', () => { + it('returns adjusted total when all quotes are input-based', () => { const result = calculateTotals({ - isMaxAmount: true, - quotes: [ - { ...QUOTE_1_MOCK, strategy: TransactionPayStrategy.Server }, - { ...QUOTE_2_MOCK, strategy: TransactionPayStrategy.Server }, - ], + quotes: [getQuote(true), getQuote(true, QUOTE_2_MOCK)], tokens: [TOKEN_1_MOCK, TOKEN_2_MOCK], messenger: MESSENGER_MOCK, transaction: TRANSACTION_META_MOCK, @@ -208,9 +187,9 @@ describe('Totals Utils', () => { expect(result.total.usd).toBe('71.68'); }); - it('returns isInputBased true for an exact-input Relay quote', () => { + it('returns isInputBased true for an input-based quote', () => { const result = calculateTotals({ - quotes: [getRelayQuote('EXACT_INPUT')], + quotes: [getQuote(true)], tokens: [], messenger: MESSENGER_MOCK, transaction: TRANSACTION_META_MOCK, @@ -219,9 +198,9 @@ describe('Totals Utils', () => { expect(result.isInputBased).toBe(true); }); - it('returns isInputBased false for an exact-output Relay quote', () => { + it('returns isInputBased false for an output-based quote', () => { const result = calculateTotals({ - quotes: [getRelayQuote('EXACT_OUTPUT')], + quotes: [getQuote(false)], tokens: [], messenger: MESSENGER_MOCK, transaction: TRANSACTION_META_MOCK, @@ -230,9 +209,14 @@ describe('Totals Utils', () => { expect(result.isInputBased).toBe(false); }); - it('returns isInputBased true for an exact-input Across quote', () => { + it('returns isInputBased true regardless of the strategy', () => { const result = calculateTotals({ - quotes: [getAcrossQuote('exactInput')], + quotes: [ + { + ...getQuote(true), + strategy: TransactionPayStrategy.Server, + }, + ], tokens: [], messenger: MESSENGER_MOCK, transaction: TRANSACTION_META_MOCK, @@ -241,9 +225,12 @@ describe('Totals Utils', () => { expect(result.isInputBased).toBe(true); }); - it('returns isInputBased false for an exact-output Across quote', () => { + it('returns isInputBased false when the property is omitted', () => { + const quoteWithoutInputBased = { ...QUOTE_1_MOCK }; + delete quoteWithoutInputBased.isInputBased; + const result = calculateTotals({ - quotes: [getAcrossQuote('exactOutput')], + quotes: [quoteWithoutInputBased], tokens: [], messenger: MESSENGER_MOCK, transaction: TRANSACTION_META_MOCK, @@ -265,7 +252,7 @@ describe('Totals Utils', () => { it('returns isInputBased false for mixed input- and output-based quotes', () => { const result = calculateTotals({ - quotes: [getRelayQuote('EXACT_INPUT'), QUOTE_2_MOCK], + quotes: [getQuote(true), QUOTE_2_MOCK], tokens: [], messenger: MESSENGER_MOCK, transaction: TRANSACTION_META_MOCK, @@ -276,7 +263,7 @@ describe('Totals Utils', () => { it('returns isInputBased true when all aggregate quotes are input-based', () => { const result = calculateTotals({ - quotes: [getRelayQuote('EXACT_INPUT'), getAcrossQuote('exactInput')], + quotes: [getQuote(true), getQuote(true, QUOTE_2_MOCK)], tokens: [], messenger: MESSENGER_MOCK, transaction: TRANSACTION_META_MOCK, @@ -285,9 +272,9 @@ describe('Totals Utils', () => { expect(result.isInputBased).toBe(true); }); - it('does not add Relay fees twice for exact-input quotes', () => { + it('does not add fees twice for input-based quotes', () => { const quote = { - ...getRelayQuote('EXACT_INPUT'), + ...getQuote(true), fees: { ...QUOTE_1_MOCK.fees, metaMask: { fiat: '0', usd: '0' }, diff --git a/packages/transaction-pay-controller/src/utils/totals.ts b/packages/transaction-pay-controller/src/utils/totals.ts index a03409ac82c..75b2ede52f4 100644 --- a/packages/transaction-pay-controller/src/utils/totals.ts +++ b/packages/transaction-pay-controller/src/utils/totals.ts @@ -2,8 +2,6 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import { BigNumber } from 'bignumber.js'; import { TransactionPayStrategy } from '../constants.js'; -import type { AcrossQuote } from '../strategy/across/types.js'; -import type { RelayQuote } from '../strategy/relay/types.js'; import type { FiatValue, TransactionPayControllerMessenger, @@ -19,7 +17,6 @@ import { calculateTransactionGasCost } from './gas.js'; * * @param request - Request parameters. * @param request.fiatPaymentAmount - The amount of the transaction in fiat. - * @param request.isMaxAmount - Whether the transaction is a maximum amount transaction. * @param request.quotes - List of bridge quotes. * @param request.messenger - Controller messenger. * @param request.tokens - List of required tokens. @@ -28,14 +25,12 @@ import { calculateTransactionGasCost } from './gas.js'; */ export function calculateTotals({ fiatPaymentAmount, - isMaxAmount, quotes, messenger, tokens, transaction, }: { fiatPaymentAmount?: string; - isMaxAmount?: boolean; quotes: TransactionPayQuote[]; messenger: TransactionPayControllerMessenger; tokens: TransactionPayRequiredToken[]; @@ -81,8 +76,7 @@ export function calculateTotals({ const amountUsd = sumProperty(quoteTokens, (token) => token.amountUsd); const hasQuotes = quotes.length > 0; const isInputBased = - hasQuotes && - quotes.every((quote) => isQuoteInputBased(quote, Boolean(isMaxAmount))); + hasQuotes && quotes.every((quote) => quote.isInputBased === true); const sourceAmountFiat = getSourceAmount({ hasFiatStrategy, @@ -152,35 +146,6 @@ export function calculateTotals({ }; } -/** - * Whether a selected quote is driven by its source input amount. - * - * @param quote - Selected quote. - * @param isMaxAmount - Whether the target transaction uses the maximum input. - * @returns Whether the quote uses input-based semantics. - */ -function isQuoteInputBased( - quote: TransactionPayQuote, - isMaxAmount: boolean, -): boolean { - if (quote.strategy === TransactionPayStrategy.Relay) { - const relayQuote = quote.original as RelayQuote; - return relayQuote.request.tradeType === 'EXACT_INPUT'; - } - - if (quote.strategy === TransactionPayStrategy.Across) { - const acrossQuote = quote.original as AcrossQuote; - return acrossQuote.request.tradeType === 'exactInput'; - } - - return ( - isMaxAmount || - quote.request.isMaxAmount === true || - quote.request.isPostQuote === true || - quote.request.isHyperliquidSource === true - ); -} - /** * Get the source amount to include in totals. *