From 41708ed8d33f43351da5036298943ae4468a4481 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Sun, 4 Jan 2026 21:17:23 +0000 Subject: [PATCH 01/13] Add isMaxAmount property Add messenger action and method. Update trade type and amount. Update totals. --- .../src/TransactionPayController.ts | 15 ++++++++++++++- packages/transaction-pay-controller/src/index.ts | 1 + .../src/strategy/relay/relay-quotes.ts | 11 +++++++++-- .../src/strategy/relay/types.ts | 2 +- packages/transaction-pay-controller/src/types.ts | 10 ++++++++++ .../src/utils/source-amounts.ts | 13 ++++++++++++- .../src/utils/totals.ts | 9 +++++++++ .../src/utils/transaction.ts | 12 ++++++++++++ 8 files changed, 68 insertions(+), 5 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index e1602ba8abf..b4b240a61d9 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -81,6 +81,12 @@ export class TransactionPayController extends BaseController< }); } + setIsMax(transactionId: string, isMaxAmount: boolean): void { + this.#updateTransactionData(transactionId, (transactionData) => { + transactionData.isMaxAmount = isMaxAmount; + }); + } + #removeTransactionData(transactionId: string): void { this.update((state) => { delete state.transactionData[transactionId]; @@ -98,6 +104,7 @@ export class TransactionPayController extends BaseController< let current = transactionData[transactionId]; const originalPaymentToken = current?.paymentToken; const originalTokens = current?.tokens; + const originalIsMaxAmount = current?.isMaxAmount; if (!current) { transactionData[transactionId] = { @@ -114,8 +121,9 @@ export class TransactionPayController extends BaseController< current.paymentToken !== originalPaymentToken; const isTokensUpdated = current.tokens !== originalTokens; + const isIsMaxUpdated = current.isMaxAmount !== originalIsMaxAmount; - if (isPaymentTokenUpdated || isTokensUpdated) { + if (isPaymentTokenUpdated || isTokensUpdated || isIsMaxUpdated) { updateSourceAmounts(transactionId, current as never, this.messenger); shouldUpdateQuotes = true; @@ -148,5 +156,10 @@ export class TransactionPayController extends BaseController< 'TransactionPayController:updatePaymentToken', this.updatePaymentToken.bind(this), ); + + this.messenger.registerActionHandler( + 'TransactionPayController:setIsMax', + this.setIsMax.bind(this), + ); } } diff --git a/packages/transaction-pay-controller/src/index.ts b/packages/transaction-pay-controller/src/index.ts index 04b8fe4d86b..380e35a42d2 100644 --- a/packages/transaction-pay-controller/src/index.ts +++ b/packages/transaction-pay-controller/src/index.ts @@ -6,6 +6,7 @@ export type { TransactionPayControllerGetStrategyAction, TransactionPayControllerMessenger, TransactionPayControllerOptions, + TransactionPayControllerSetIsMaxAction, TransactionPayControllerState, TransactionPayControllerStateChangeEvent, TransactionPayControllerUpdatePaymentTokenAction, 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 3ddb0478788..1ddd9b3b612 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -89,16 +89,23 @@ async function getSingleQuote( 0, ); + const { transactionData } = messenger.call( + 'TransactionPayController:getState', + ); + const isMaxAmount = transactionData[transaction.id]?.isMaxAmount; + try { const body: RelayQuoteRequest = { - amount: request.targetAmountMinimum, + amount: isMaxAmount + ? request.sourceTokenAmount + : request.targetAmountMinimum, destinationChainId: Number(request.targetChainId), destinationCurrency: request.targetTokenAddress, originChainId: Number(request.sourceChainId), originCurrency: request.sourceTokenAddress, recipient: request.from, slippageTolerance, - tradeType: 'EXPECTED_OUTPUT', + tradeType: isMaxAmount ? 'EXACT_INPUT' : 'EXPECTED_OUTPUT', user: request.from, }; diff --git a/packages/transaction-pay-controller/src/strategy/relay/types.ts b/packages/transaction-pay-controller/src/strategy/relay/types.ts index fbab58bc223..2eb031ee260 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/types.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/types.ts @@ -17,7 +17,7 @@ export type RelayQuoteRequest = { recipient: Hex; refundTo?: Hex; slippageTolerance?: string; - tradeType: 'EXPECTED_OUTPUT' | 'EXACT_OUTPUT'; + tradeType: 'EXPECTED_OUTPUT' | 'EXACT_OUTPUT' | 'EXACT_INPUT'; txs?: { to: Hex; data: Hex; diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 1a2595f6d3a..8120ba307b7 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -85,6 +85,12 @@ export type TransactionPayControllerUpdatePaymentTokenAction = { handler: (request: UpdatePaymentTokenRequest) => void; }; +/** Action to set the max amount flag for a transaction. */ +export type TransactionPayControllerSetIsMaxAction = { + type: `${typeof CONTROLLER_NAME}:setIsMax`; + handler: (transactionId: string, isMaxAmount: boolean) => void; +}; + export type TransactionPayControllerStateChangeEvent = ControllerStateChangeEvent< typeof CONTROLLER_NAME, @@ -95,6 +101,7 @@ export type TransactionPayControllerActions = | TransactionPayControllerGetDelegationTransactionAction | TransactionPayControllerGetStateAction | TransactionPayControllerGetStrategyAction + | TransactionPayControllerSetIsMaxAction | TransactionPayControllerUpdatePaymentTokenAction; export type TransactionPayControllerEvents = @@ -132,6 +139,9 @@ export type TransactionData = { /** Whether quotes are currently being retrieved. */ isLoading: boolean; + /** Whether the user has selected the maximum amount. */ + isMaxAmount?: boolean; + /** Source token selected for the transaction. */ paymentToken?: TransactionPaymentToken; diff --git a/packages/transaction-pay-controller/src/utils/source-amounts.ts b/packages/transaction-pay-controller/src/utils/source-amounts.ts index 2677c4861bb..339b476f523 100644 --- a/packages/transaction-pay-controller/src/utils/source-amounts.ts +++ b/packages/transaction-pay-controller/src/utils/source-amounts.ts @@ -2,7 +2,7 @@ import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import { getTokenFiatRate } from './token'; -import { getTransaction } from './transaction'; +import { getTransaction, getTransactionData } from './transaction'; import type { TransactionPayControllerMessenger, TransactionPaymentToken, @@ -119,6 +119,17 @@ function calculateSourceAmount( return undefined; } + const transactionData = getTransactionData(transactionId, messenger); + const isMaxAmount = transactionData?.isMaxAmount ?? false; + + if (isMaxAmount) { + return { + sourceAmountHuman: paymentToken.balanceHuman, + sourceAmountRaw: paymentToken.balanceRaw, + targetTokenAddress: token.address, + }; + } + return { sourceAmountHuman, sourceAmountRaw, diff --git a/packages/transaction-pay-controller/src/utils/totals.ts b/packages/transaction-pay-controller/src/utils/totals.ts index d3ece1efaae..9efcf38ece6 100644 --- a/packages/transaction-pay-controller/src/utils/totals.ts +++ b/packages/transaction-pay-controller/src/utils/totals.ts @@ -2,6 +2,7 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import { BigNumber } from 'bignumber.js'; import { calculateTransactionGasCost } from './gas'; +import { getTransactionData } from './transaction'; import type { Amount, FiatValue, @@ -60,6 +61,8 @@ export function calculateTotals({ (singleToken) => !singleToken.skipIfBalance, ); + const transactionData = getTransactionData(transaction.id, messenger); + const isMaxAmount = transactionData?.isMaxAmount ?? false; const amountFiat = sumProperty(quoteTokens, (token) => token.amountFiat); const amountUsd = sumProperty(quoteTokens, (token) => token.amountUsd); @@ -67,12 +70,18 @@ export function calculateTotals({ .plus(sourceNetworkFeeEstimate.fiat) .plus(targetNetworkFee.fiat) .plus(amountFiat) + .plus(isMaxAmount ? sourceAmount.fiat : 0) + .minus(isMaxAmount ? amountFiat : 0) + .minus(isMaxAmount ? providerFee.fiat : 0) .toString(10); const totalUsd = new BigNumber(providerFee.usd) .plus(sourceNetworkFeeEstimate.usd) .plus(targetNetworkFee.usd) .plus(amountUsd) + .plus(isMaxAmount ? sourceAmount.usd : 0) + .minus(isMaxAmount ? amountUsd : 0) + .minus(isMaxAmount ? providerFee.usd : 0) .toString(10); const estimatedDuration = Number( diff --git a/packages/transaction-pay-controller/src/utils/transaction.ts b/packages/transaction-pay-controller/src/utils/transaction.ts index 7a5d14d2285..4b834be3f1d 100644 --- a/packages/transaction-pay-controller/src/utils/transaction.ts +++ b/packages/transaction-pay-controller/src/utils/transaction.ts @@ -7,6 +7,7 @@ import { cloneDeep } from 'lodash'; import { parseRequiredTokens } from './required-tokens'; import { projectLogger } from '../logger'; import type { + TransactionData, TransactionPayControllerMessenger, UpdateTransactionDataCallback, } from '../types'; @@ -39,6 +40,17 @@ export function getTransaction( ); } +export function getTransactionData( + transactionId: string, + messenger: TransactionPayControllerMessenger, +): TransactionData | undefined { + const { transactionData } = messenger.call( + 'TransactionPayController:getState', + ); + + return transactionData[transactionId]; +} + /** * Poll for transaction changes and update the transaction data accordingly. * From 84b739eabb02d84de37e3ea979404f86beca9222 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Sun, 4 Jan 2026 21:31:44 +0000 Subject: [PATCH 02/13] Add transaction util unit tests --- .../src/utils/transaction.test.ts | 31 ++++++++++++++++++- .../src/utils/transaction.ts | 7 +++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/src/utils/transaction.test.ts b/packages/transaction-pay-controller/src/utils/transaction.test.ts index bd6f7ddb6ee..48716943906 100644 --- a/packages/transaction-pay-controller/src/utils/transaction.test.ts +++ b/packages/transaction-pay-controller/src/utils/transaction.test.ts @@ -12,6 +12,7 @@ import { FINALIZED_STATUSES, collectTransactionIds, getTransaction, + getTransactionData, pollTransactionChanges, updateTransaction, waitForTransactionConfirmed, @@ -41,11 +42,17 @@ const TRANSCTION_TOKEN_REQUIRED_MOCK = { balanceUsd: '5', } as TransactionPayRequiredToken; +const TRANSACTION_DATA_MOCK = { + isLoading: false, + tokens: [TRANSCTION_TOKEN_REQUIRED_MOCK], +} as TransactionData; + describe('Transaction Utils', () => { const parseRequiredTokensMock = jest.mocked(parseRequiredTokens); const { - messenger, + getControllerStateMock, getTransactionControllerStateMock, + messenger, publish, updateTransactionMock, } = getMessengerMock(); @@ -78,6 +85,28 @@ describe('Transaction Utils', () => { }); }); + describe('getTransactionData', () => { + it('returns transaction data', () => { + getControllerStateMock.mockReturnValue({ + transactionData: { + [TRANSACTION_ID_MOCK]: TRANSACTION_DATA_MOCK, + }, + }); + + const result = getTransactionData(TRANSACTION_ID_MOCK, messenger); + expect(result).toBe(TRANSACTION_DATA_MOCK); + }); + + it('returns undefined if transaction data not found', () => { + getControllerStateMock.mockReturnValue({ + transactionData: {}, + }); + + const result = getTransactionData(TRANSACTION_ID_MOCK, messenger); + expect(result).toBeUndefined(); + }); + }); + describe('pollTransactionChanges', () => { it('updates state for new transactions', () => { const updateTransactionDataMock = jest.fn(); diff --git a/packages/transaction-pay-controller/src/utils/transaction.ts b/packages/transaction-pay-controller/src/utils/transaction.ts index 4b834be3f1d..13f38e765fc 100644 --- a/packages/transaction-pay-controller/src/utils/transaction.ts +++ b/packages/transaction-pay-controller/src/utils/transaction.ts @@ -40,6 +40,13 @@ export function getTransaction( ); } +/** + * Get transaction data for a specific transaction ID, if it exists. + * + * @param transactionId - ID of the transaction to get data for. + * @param messenger - Controller messenger. + * @returns Transaction data from the state, if it exists. + */ export function getTransactionData( transactionId: string, messenger: TransactionPayControllerMessenger, From fbfa2f689f0cf7a8be92d64e9ce500ba39886406 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Sun, 4 Jan 2026 22:10:35 +0000 Subject: [PATCH 03/13] Update source amount unit tests Remove getTransactionData util. --- .../src/strategy/relay/relay-quotes.ts | 30 +++++++++++-------- .../transaction-pay-controller/src/types.ts | 3 ++ .../src/utils/quotes.test.ts | 1 + .../src/utils/quotes.ts | 10 +++++-- .../src/utils/source-amounts.test.ts | 19 ++++++++++++ .../src/utils/source-amounts.ts | 10 +++---- .../src/utils/totals.ts | 6 ++-- .../src/utils/transaction.test.ts | 29 ------------------ .../src/utils/transaction.ts | 19 ------------ 9 files changed, 56 insertions(+), 71 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 1ddd9b3b612..e9ae879f550 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -89,24 +89,28 @@ async function getSingleQuote( 0, ); - const { transactionData } = messenger.call( - 'TransactionPayController:getState', - ); - const isMaxAmount = transactionData[transaction.id]?.isMaxAmount; + const { + from, + isMaxAmount, + sourceChainId, + sourceTokenAddress, + sourceTokenAmount, + targetAmountMinimum, + targetChainId, + targetTokenAddress, + } = request; try { const body: RelayQuoteRequest = { - amount: isMaxAmount - ? request.sourceTokenAmount - : request.targetAmountMinimum, - destinationChainId: Number(request.targetChainId), - destinationCurrency: request.targetTokenAddress, - originChainId: Number(request.sourceChainId), - originCurrency: request.sourceTokenAddress, - recipient: request.from, + amount: isMaxAmount ? sourceTokenAmount : targetAmountMinimum, + destinationChainId: Number(targetChainId), + destinationCurrency: targetTokenAddress, + originChainId: Number(sourceChainId), + originCurrency: sourceTokenAddress, + recipient: from, slippageTolerance, tradeType: isMaxAmount ? 'EXACT_INPUT' : 'EXPECTED_OUTPUT', - user: request.from, + user: from, }; await processTransactions(transaction, request, body, messenger); diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 8120ba307b7..09b267f4f11 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -267,6 +267,9 @@ export type QuoteRequest = { /** Address of the user's account. */ from: Hex; + /** Whether the transaction is a maximum amount transaction. */ + isMaxAmount?: boolean; + /** Balance of the source token in atomic format without factoring token decimals. */ sourceBalanceRaw: string; diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 4f5fb338744..4445c62301d 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -222,6 +222,7 @@ describe('Quotes Utils', () => { messenger, requests: [ { + isMaxAmount: false, from: TRANSACTION_META_MOCK.txParams.from, sourceBalanceRaw: TRANSACTION_DATA_MOCK.paymentToken?.balanceRaw, sourceTokenAmount: diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index 1610965248e..9156c56c483 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -55,10 +55,11 @@ export async function updateQuotes( log('Updating quotes', { transactionId }); - const { paymentToken, sourceAmounts, tokens } = transactionData; + const { isMaxAmount, paymentToken, sourceAmounts, tokens } = transactionData; const requests = buildQuoteRequests({ from: transaction.txParams.from as Hex, + isMaxAmount: isMaxAmount ?? false, paymentToken, sourceAmounts, tokens, @@ -77,8 +78,9 @@ export async function updateQuotes( ); const totals = calculateTotals({ - quotes: quotes as TransactionPayQuote[], + isMaxAmount, messenger, + quotes: quotes as TransactionPayQuote[], tokens, transaction, }); @@ -209,6 +211,7 @@ export async function refreshQuotes( * * @param request - Request parameters. * @param request.from - Address from which the transaction is sent. + * @param request.isMaxAmount - Whether the transaction is a maximum amount transaction. * @param request.paymentToken - Payment token used for the transaction. * @param request.sourceAmounts - Source amounts for the transaction. * @param request.tokens - Required tokens for the transaction. @@ -217,12 +220,14 @@ export async function refreshQuotes( */ function buildQuoteRequests({ from, + isMaxAmount, paymentToken, sourceAmounts, tokens, transactionId, }: { from: Hex; + isMaxAmount: boolean; paymentToken: TransactionPaymentToken | undefined; sourceAmounts: TransactionPaySourceAmount[] | undefined; tokens: TransactionPayRequiredToken[]; @@ -239,6 +244,7 @@ function buildQuoteRequests({ return { from, + isMaxAmount, sourceBalanceRaw: paymentToken.balanceRaw, sourceTokenAmount: sourceAmount.sourceAmountRaw, sourceChainId: paymentToken.chainId, diff --git a/packages/transaction-pay-controller/src/utils/source-amounts.test.ts b/packages/transaction-pay-controller/src/utils/source-amounts.test.ts index f8af32058c3..9612a79e032 100644 --- a/packages/transaction-pay-controller/src/utils/source-amounts.test.ts +++ b/packages/transaction-pay-controller/src/utils/source-amounts.test.ts @@ -163,6 +163,25 @@ describe('Source Amounts Utils', () => { expect(transactionData.sourceAmounts).toStrictEqual([]); }); + it('uses payment token balance if isMaxAmount is true', () => { + const transactionData: TransactionData = { + isLoading: false, + isMaxAmount: true, + paymentToken: PAYMENT_TOKEN_MOCK, + tokens: [TRANSACTION_TOKEN_MOCK], + }; + + updateSourceAmounts(TRANSACTION_ID_MOCK, transactionData, messenger); + + expect(transactionData.sourceAmounts).toStrictEqual([ + { + sourceAmountHuman: PAYMENT_TOKEN_MOCK.balanceHuman, + sourceAmountRaw: PAYMENT_TOKEN_MOCK.balanceRaw, + targetTokenAddress: TRANSACTION_TOKEN_MOCK.address, + }, + ]); + }); + it('does nothing if no payment token', () => { const transactionData: TransactionData = { isLoading: false, diff --git a/packages/transaction-pay-controller/src/utils/source-amounts.ts b/packages/transaction-pay-controller/src/utils/source-amounts.ts index 339b476f523..f98cd422c37 100644 --- a/packages/transaction-pay-controller/src/utils/source-amounts.ts +++ b/packages/transaction-pay-controller/src/utils/source-amounts.ts @@ -2,7 +2,7 @@ import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import { getTokenFiatRate } from './token'; -import { getTransaction, getTransactionData } from './transaction'; +import { getTransaction } from './transaction'; import type { TransactionPayControllerMessenger, TransactionPaymentToken, @@ -35,7 +35,7 @@ export function updateSourceAmounts( return; } - const { paymentToken, tokens } = transactionData; + const { isMaxAmount, paymentToken, tokens } = transactionData; if (!tokens.length || !paymentToken) { return; @@ -48,6 +48,7 @@ export function updateSourceAmounts( singleToken, messenger, transactionId, + isMaxAmount ?? false, ), ) .filter(Boolean) as TransactionPaySourceAmount[]; @@ -64,6 +65,7 @@ export function updateSourceAmounts( * @param token - Target token to cover. * @param messenger - Controller messenger. * @param transactionId - ID of the transaction. + * @param isMaxAmount - Whether the transaction is a maximum amount transaction. * @returns The source amount or undefined if calculation failed. */ function calculateSourceAmount( @@ -71,6 +73,7 @@ function calculateSourceAmount( token: TransactionPayRequiredToken, messenger: TransactionPayControllerMessenger, transactionId: string, + isMaxAmount: boolean, ): TransactionPaySourceAmount | undefined { const paymentTokenFiatRate = getTokenFiatRate( messenger, @@ -119,9 +122,6 @@ function calculateSourceAmount( return undefined; } - const transactionData = getTransactionData(transactionId, messenger); - const isMaxAmount = transactionData?.isMaxAmount ?? false; - if (isMaxAmount) { return { sourceAmountHuman: paymentToken.balanceHuman, diff --git a/packages/transaction-pay-controller/src/utils/totals.ts b/packages/transaction-pay-controller/src/utils/totals.ts index 9efcf38ece6..f2c9085ffa7 100644 --- a/packages/transaction-pay-controller/src/utils/totals.ts +++ b/packages/transaction-pay-controller/src/utils/totals.ts @@ -2,7 +2,6 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import { BigNumber } from 'bignumber.js'; import { calculateTransactionGasCost } from './gas'; -import { getTransactionData } from './transaction'; import type { Amount, FiatValue, @@ -16,6 +15,7 @@ import type { * Calculate totals for a list of quotes and tokens. * * @param request - Request parameters. + * @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. @@ -23,11 +23,13 @@ import type { * @returns The calculated totals in USD and fiat currency. */ export function calculateTotals({ + isMaxAmount, quotes, messenger, tokens, transaction, }: { + isMaxAmount?: boolean; quotes: TransactionPayQuote[]; messenger: TransactionPayControllerMessenger; tokens: TransactionPayRequiredToken[]; @@ -61,8 +63,6 @@ export function calculateTotals({ (singleToken) => !singleToken.skipIfBalance, ); - const transactionData = getTransactionData(transaction.id, messenger); - const isMaxAmount = transactionData?.isMaxAmount ?? false; const amountFiat = sumProperty(quoteTokens, (token) => token.amountFiat); const amountUsd = sumProperty(quoteTokens, (token) => token.amountUsd); diff --git a/packages/transaction-pay-controller/src/utils/transaction.test.ts b/packages/transaction-pay-controller/src/utils/transaction.test.ts index 48716943906..ce7145bbd27 100644 --- a/packages/transaction-pay-controller/src/utils/transaction.test.ts +++ b/packages/transaction-pay-controller/src/utils/transaction.test.ts @@ -12,7 +12,6 @@ import { FINALIZED_STATUSES, collectTransactionIds, getTransaction, - getTransactionData, pollTransactionChanges, updateTransaction, waitForTransactionConfirmed, @@ -42,15 +41,9 @@ const TRANSCTION_TOKEN_REQUIRED_MOCK = { balanceUsd: '5', } as TransactionPayRequiredToken; -const TRANSACTION_DATA_MOCK = { - isLoading: false, - tokens: [TRANSCTION_TOKEN_REQUIRED_MOCK], -} as TransactionData; - describe('Transaction Utils', () => { const parseRequiredTokensMock = jest.mocked(parseRequiredTokens); const { - getControllerStateMock, getTransactionControllerStateMock, messenger, publish, @@ -85,28 +78,6 @@ describe('Transaction Utils', () => { }); }); - describe('getTransactionData', () => { - it('returns transaction data', () => { - getControllerStateMock.mockReturnValue({ - transactionData: { - [TRANSACTION_ID_MOCK]: TRANSACTION_DATA_MOCK, - }, - }); - - const result = getTransactionData(TRANSACTION_ID_MOCK, messenger); - expect(result).toBe(TRANSACTION_DATA_MOCK); - }); - - it('returns undefined if transaction data not found', () => { - getControllerStateMock.mockReturnValue({ - transactionData: {}, - }); - - const result = getTransactionData(TRANSACTION_ID_MOCK, messenger); - expect(result).toBeUndefined(); - }); - }); - describe('pollTransactionChanges', () => { it('updates state for new transactions', () => { const updateTransactionDataMock = jest.fn(); diff --git a/packages/transaction-pay-controller/src/utils/transaction.ts b/packages/transaction-pay-controller/src/utils/transaction.ts index 13f38e765fc..7a5d14d2285 100644 --- a/packages/transaction-pay-controller/src/utils/transaction.ts +++ b/packages/transaction-pay-controller/src/utils/transaction.ts @@ -7,7 +7,6 @@ import { cloneDeep } from 'lodash'; import { parseRequiredTokens } from './required-tokens'; import { projectLogger } from '../logger'; import type { - TransactionData, TransactionPayControllerMessenger, UpdateTransactionDataCallback, } from '../types'; @@ -40,24 +39,6 @@ export function getTransaction( ); } -/** - * Get transaction data for a specific transaction ID, if it exists. - * - * @param transactionId - ID of the transaction to get data for. - * @param messenger - Controller messenger. - * @returns Transaction data from the state, if it exists. - */ -export function getTransactionData( - transactionId: string, - messenger: TransactionPayControllerMessenger, -): TransactionData | undefined { - const { transactionData } = messenger.call( - 'TransactionPayController:getState', - ); - - return transactionData[transactionId]; -} - /** * Poll for transaction changes and update the transaction data accordingly. * From fc9a41de626c6a866a57c39c0ba3b4736094aae9 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Sun, 4 Jan 2026 22:19:36 +0000 Subject: [PATCH 04/13] Update totals unit tests --- .../src/utils/totals.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/transaction-pay-controller/src/utils/totals.test.ts b/packages/transaction-pay-controller/src/utils/totals.test.ts index 98c5891acde..d01b04184ba 100644 --- a/packages/transaction-pay-controller/src/utils/totals.test.ts +++ b/packages/transaction-pay-controller/src/utils/totals.test.ts @@ -150,6 +150,19 @@ describe('Totals Utils', () => { expect(result.total.usd).toBe('51.08'); }); + it('returns adjusted total when isMaxAmount is true', () => { + const result = calculateTotals({ + isMaxAmount: true, + quotes: [QUOTE_1_MOCK, QUOTE_2_MOCK], + tokens: [TOKEN_1_MOCK, TOKEN_2_MOCK], + messenger: MESSENGER_MOCK, + transaction: TRANSACTION_META_MOCK, + }); + + expect(result.total.fiat).toBe('50.88'); + expect(result.total.usd).toBe('56.34'); + }); + it('returns total excluding token amount not in quote', () => { const result = calculateTotals({ quotes: [QUOTE_1_MOCK, QUOTE_2_MOCK], From 8f50b578143a2095f227e3b28988d841ead021f6 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Sun, 4 Jan 2026 22:49:04 +0000 Subject: [PATCH 05/13] Add relay quotes unit tests --- .../src/strategy/relay/relay-quotes.test.ts | 23 +++++++++++++++++++ .../src/strategy/relay/relay-quotes.ts | 5 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) 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 9a95aa1e286..0055c331f5e 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 @@ -255,6 +255,29 @@ describe('Relay Quotes Utils', () => { ); }); + it('sends request with EXACT_INPUT trade type when isMaxAmount is true', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], + transaction: TRANSACTION_META_MOCK, + }); + + 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('includes transactions in request', async () => { successfulFetchMock.mockResolvedValue({ json: async () => QUOTE_MOCK, 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 e9ae879f550..4d9d3310199 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -152,12 +152,13 @@ async function processTransactions( messenger: TransactionPayControllerMessenger, ): Promise { const { nestedTransactions, txParams } = transaction; + const { isMaxAmount, targetChainId } = request; const data = txParams?.data as Hex | undefined; const singleData = nestedTransactions?.length === 1 ? nestedTransactions[0].data : data; - const isHypercore = request.targetChainId === CHAIN_ID_HYPERCORE; + const isHypercore = targetChainId === CHAIN_ID_HYPERCORE; const isTokenTransfer = !isHypercore && Boolean(singleData?.startsWith(TOKEN_TRANSFER_FOUR_BYTE)); @@ -168,7 +169,7 @@ async function processTransactions( log('Updating recipient as token transfer', requestBody.recipient); } - const skipDelegation = isTokenTransfer || isHypercore; + const skipDelegation = isTokenTransfer || isHypercore || isMaxAmount; if (skipDelegation) { log('Skipping delegation as token transfer or Hypercore deposit'); From 62f2ed625f25818875145fbc9e9946c7c6b96691 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Tue, 6 Jan 2026 16:29:26 +0000 Subject: [PATCH 06/13] Rename method and action --- .../src/TransactionPayController.ts | 6 +++--- packages/transaction-pay-controller/src/index.ts | 2 +- packages/transaction-pay-controller/src/types.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index b4b240a61d9..a4aa4efa44d 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -81,7 +81,7 @@ export class TransactionPayController extends BaseController< }); } - setIsMax(transactionId: string, isMaxAmount: boolean): void { + setIsMaxAmount(transactionId: string, isMaxAmount: boolean): void { this.#updateTransactionData(transactionId, (transactionData) => { transactionData.isMaxAmount = isMaxAmount; }); @@ -158,8 +158,8 @@ export class TransactionPayController extends BaseController< ); this.messenger.registerActionHandler( - 'TransactionPayController:setIsMax', - this.setIsMax.bind(this), + 'TransactionPayController:setIsMaxAmount', + this.setIsMaxAmount.bind(this), ); } } diff --git a/packages/transaction-pay-controller/src/index.ts b/packages/transaction-pay-controller/src/index.ts index 380e35a42d2..d3cb83c35f3 100644 --- a/packages/transaction-pay-controller/src/index.ts +++ b/packages/transaction-pay-controller/src/index.ts @@ -6,7 +6,7 @@ export type { TransactionPayControllerGetStrategyAction, TransactionPayControllerMessenger, TransactionPayControllerOptions, - TransactionPayControllerSetIsMaxAction, + TransactionPayControllerSetIsMaxAmountAction, TransactionPayControllerState, TransactionPayControllerStateChangeEvent, TransactionPayControllerUpdatePaymentTokenAction, diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 09b267f4f11..ea9d4db62a4 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -86,8 +86,8 @@ export type TransactionPayControllerUpdatePaymentTokenAction = { }; /** Action to set the max amount flag for a transaction. */ -export type TransactionPayControllerSetIsMaxAction = { - type: `${typeof CONTROLLER_NAME}:setIsMax`; +export type TransactionPayControllerSetIsMaxAmountAction = { + type: `${typeof CONTROLLER_NAME}:setIsMaxAmount`; handler: (transactionId: string, isMaxAmount: boolean) => void; }; @@ -101,7 +101,7 @@ export type TransactionPayControllerActions = | TransactionPayControllerGetDelegationTransactionAction | TransactionPayControllerGetStateAction | TransactionPayControllerGetStrategyAction - | TransactionPayControllerSetIsMaxAction + | TransactionPayControllerSetIsMaxAmountAction | TransactionPayControllerUpdatePaymentTokenAction; export type TransactionPayControllerEvents = From 591c431461af1c52f37552e07a5952de535455f2 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Tue, 6 Jan 2026 17:57:16 +0000 Subject: [PATCH 07/13] Fix coverage --- .../src/TransactionPayController.test.ts | 12 ++++++++++ .../src/TransactionPayController.ts | 22 +++++++++---------- .../src/strategy/relay/types.ts | 2 +- .../src/utils/transaction.test.ts | 2 +- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index 80fece81294..82232be1a13 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -74,6 +74,18 @@ describe('TransactionPayController', () => { }); }); + describe('setIsMaxAmount', () => { + it('updates state', () => { + const controller = createController(); + + controller.setIsMaxAmount(TRANSACTION_ID_MOCK, true); + + expect( + controller.state.transactionData[TRANSACTION_ID_MOCK].isMaxAmount, + ).toBe(true); + }); + }); + describe('getStrategy Action', () => { it('returns relay if no callback', async () => { createController(); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index a4aa4efa44d..9bba57d98d8 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -74,6 +74,12 @@ export class TransactionPayController extends BaseController< }); } + setIsMaxAmount(transactionId: string, isMaxAmount: boolean): void { + this.#updateTransactionData(transactionId, (transactionData) => { + transactionData.isMaxAmount = isMaxAmount; + }); + } + updatePaymentToken(request: UpdatePaymentTokenRequest): void { updatePaymentToken(request, { messenger: this.messenger, @@ -81,12 +87,6 @@ export class TransactionPayController extends BaseController< }); } - setIsMaxAmount(transactionId: string, isMaxAmount: boolean): void { - this.#updateTransactionData(transactionId, (transactionData) => { - transactionData.isMaxAmount = isMaxAmount; - }); - } - #removeTransactionData(transactionId: string): void { this.update((state) => { delete state.transactionData[transactionId]; @@ -123,7 +123,7 @@ export class TransactionPayController extends BaseController< const isTokensUpdated = current.tokens !== originalTokens; const isIsMaxUpdated = current.isMaxAmount !== originalIsMaxAmount; - if (isPaymentTokenUpdated || isTokensUpdated || isIsMaxUpdated) { + if (isPaymentTokenUpdated || isIsMaxUpdated || isTokensUpdated) { updateSourceAmounts(transactionId, current as never, this.messenger); shouldUpdateQuotes = true; @@ -153,13 +153,13 @@ export class TransactionPayController extends BaseController< ); this.messenger.registerActionHandler( - 'TransactionPayController:updatePaymentToken', - this.updatePaymentToken.bind(this), + 'TransactionPayController:setIsMaxAmount', + this.setIsMaxAmount.bind(this), ); this.messenger.registerActionHandler( - 'TransactionPayController:setIsMaxAmount', - this.setIsMaxAmount.bind(this), + 'TransactionPayController:updatePaymentToken', + this.updatePaymentToken.bind(this), ); } } diff --git a/packages/transaction-pay-controller/src/strategy/relay/types.ts b/packages/transaction-pay-controller/src/strategy/relay/types.ts index 2eb031ee260..28cb5677eb8 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/types.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/types.ts @@ -17,7 +17,7 @@ export type RelayQuoteRequest = { recipient: Hex; refundTo?: Hex; slippageTolerance?: string; - tradeType: 'EXPECTED_OUTPUT' | 'EXACT_OUTPUT' | 'EXACT_INPUT'; + tradeType: 'EXACT_INPUT' | 'EXACT_OUTPUT' | 'EXPECTED_OUTPUT'; txs?: { to: Hex; data: Hex; diff --git a/packages/transaction-pay-controller/src/utils/transaction.test.ts b/packages/transaction-pay-controller/src/utils/transaction.test.ts index ce7145bbd27..bd6f7ddb6ee 100644 --- a/packages/transaction-pay-controller/src/utils/transaction.test.ts +++ b/packages/transaction-pay-controller/src/utils/transaction.test.ts @@ -44,8 +44,8 @@ const TRANSCTION_TOKEN_REQUIRED_MOCK = { describe('Transaction Utils', () => { const parseRequiredTokensMock = jest.mocked(parseRequiredTokens); const { - getTransactionControllerStateMock, messenger, + getTransactionControllerStateMock, publish, updateTransactionMock, } = getMessengerMock(); From e44ac8b61080783740c8da31e9c690ea96b2a3b2 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Tue, 6 Jan 2026 18:10:04 +0000 Subject: [PATCH 08/13] Update changelog --- packages/transaction-pay-controller/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 6db47aa063a..90a62aaedbc 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `TransactionPayController:setIsMaxAmount` messenger action to set max amount flag for a transaction ([#7562](https://github.com/MetaMask/core/pull/7562)) + - Add `isMaxAmount` property to `TransactionData` type. + - Update Relay quote requests to use `EXACT_INPUT` trade type when max amount is selected. + - Update totals calculation to account for max amount selection. + ### Changed - Bump `@metamask/controller-utils` from `^11.17.0` to `^11.18.0` ([#7583](https://github.com/MetaMask/core/pull/7583)) From f10dd311248fb1ef595e19ae8a872aed8be71b65 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Wed, 7 Jan 2026 23:53:32 +0000 Subject: [PATCH 09/13] Add target amount --- packages/transaction-controller/src/types.ts | 3 +++ .../src/strategy/bridge/bridge-quotes.ts | 1 + .../src/strategy/relay/relay-quotes.ts | 9 ++++++++- .../src/strategy/relay/types.ts | 1 + .../src/strategy/test/TestStrategy.ts | 6 ++++++ packages/transaction-pay-controller/src/types.ts | 6 ++++++ .../transaction-pay-controller/src/utils/quotes.ts | 1 + .../transaction-pay-controller/src/utils/totals.ts | 13 +++++-------- 8 files changed, 31 insertions(+), 9 deletions(-) diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 9d42d444f95..2d6aaf3d565 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -2071,6 +2071,9 @@ export type MetamaskPayMetadata = { /** Total network fee in fiat currency, including the original and bridge transactions. */ networkFeeFiat?: string; + /** Total amount of target token provided in fiat currency. */ + targetFiat?: string; + /** Address of the payment token that the transaction funds were sourced from. */ tokenAddress?: Hex; diff --git a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts index d30aa73e98a..a82e6e2bbe1 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.ts @@ -554,6 +554,7 @@ function normalizeQuote( original: quote, request, sourceAmount, + targetAmount, strategy: TransactionPayStrategy.Bridge, }; } 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 4d9d3310199..6e6feb4cacb 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -275,7 +275,7 @@ async function normalizeQuote( ): Promise> { const { messenger } = fullRequest; const { details } = quote; - const { currencyIn } = details; + const { currencyIn, currencyOut } = details; const { usdToFiatRate } = getFiatRates(messenger, request); @@ -306,6 +306,12 @@ async function normalizeQuote( ...getFiatValueFromUsd(new BigNumber(currencyIn.amountUsd), usdToFiatRate), }; + const targetAmount: Amount = { + human: currencyOut.amountFormatted, + raw: currencyOut.amount, + ...getFiatValueFromUsd(new BigNumber(currencyOut.amountUsd), usdToFiatRate), + }; + const metamask = { gasLimits, }; @@ -325,6 +331,7 @@ async function normalizeQuote( }, request, sourceAmount, + targetAmount, strategy: TransactionPayStrategy.Relay, }; } diff --git a/packages/transaction-pay-controller/src/strategy/relay/types.ts b/packages/transaction-pay-controller/src/strategy/relay/types.ts index 28cb5677eb8..4883e2837cf 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/types.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/types.ts @@ -38,6 +38,7 @@ export type RelayQuote = { }; }; currencyOut: { + amount: string; amountFormatted: string; amountUsd: string; currency: { diff --git a/packages/transaction-pay-controller/src/strategy/test/TestStrategy.ts b/packages/transaction-pay-controller/src/strategy/test/TestStrategy.ts index b0ca2cc7359..06b470bcb8e 100644 --- a/packages/transaction-pay-controller/src/strategy/test/TestStrategy.ts +++ b/packages/transaction-pay-controller/src/strategy/test/TestStrategy.ts @@ -54,6 +54,12 @@ export class TestStrategy implements PayStrategy { raw: '456000', usd: '4.56', }, + targetAmount: { + human: '5.67', + fiat: '5.67', + raw: '567000', + usd: '5.67', + }, strategy: TransactionPayStrategy.Test, }, ]; diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index ea9d4db62a4..8539a6e15ff 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -335,6 +335,9 @@ export type TransactionPayQuote = { /** Name of the strategy used to retrieve the quote. */ strategy: TransactionPayStrategy; + + /** Amount of target token provided. */ + targetAmount: Amount; }; /** Request to get quotes for a transaction. */ @@ -428,6 +431,9 @@ export type TransactionPayTotals = { /** Total amount of source token required. */ sourceAmount: Amount; + /** Total amount of target token provided. */ + targetAmount: Amount; + /** Overall total cost for the target transaction and all quotes. */ total: FiatValue; }; diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index 9156c56c483..a242607fbf7 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -150,6 +150,7 @@ function syncTransaction({ bridgeFeeFiat: totals.fees.provider.usd, chainId: paymentToken.chainId, networkFeeFiat: totals.fees.sourceNetwork.estimate.usd, + targetFiat: totals.targetAmount.usd, tokenAddress: paymentToken.address, totalFiat: totals.total.usd, }; diff --git a/packages/transaction-pay-controller/src/utils/totals.ts b/packages/transaction-pay-controller/src/utils/totals.ts index f2c9085ffa7..7588857a9cb 100644 --- a/packages/transaction-pay-controller/src/utils/totals.ts +++ b/packages/transaction-pay-controller/src/utils/totals.ts @@ -58,6 +58,7 @@ export function calculateTotals({ : transactionNetworkFee; const sourceAmount = sumAmounts(quotes.map((quote) => quote.sourceAmount)); + const targetAmount = sumAmounts(quotes.map((quote) => quote.targetAmount)); const quoteTokens = tokens.filter( (singleToken) => !singleToken.skipIfBalance, @@ -65,23 +66,18 @@ export function calculateTotals({ const amountFiat = sumProperty(quoteTokens, (token) => token.amountFiat); const amountUsd = sumProperty(quoteTokens, (token) => token.amountUsd); + const hasQuotes = quotes.length > 0; const totalFiat = new BigNumber(providerFee.fiat) .plus(sourceNetworkFeeEstimate.fiat) .plus(targetNetworkFee.fiat) - .plus(amountFiat) - .plus(isMaxAmount ? sourceAmount.fiat : 0) - .minus(isMaxAmount ? amountFiat : 0) - .minus(isMaxAmount ? providerFee.fiat : 0) + .plus(isMaxAmount && hasQuotes ? targetAmount.fiat : amountFiat) .toString(10); const totalUsd = new BigNumber(providerFee.usd) .plus(sourceNetworkFeeEstimate.usd) .plus(targetNetworkFee.usd) - .plus(amountUsd) - .plus(isMaxAmount ? sourceAmount.usd : 0) - .minus(isMaxAmount ? amountUsd : 0) - .minus(isMaxAmount ? providerFee.usd : 0) + .plus(isMaxAmount && hasQuotes ? targetAmount.usd : amountUsd) .toString(10); const estimatedDuration = Number( @@ -109,6 +105,7 @@ export function calculateTotals({ targetNetwork: targetNetworkFee, }, sourceAmount, + targetAmount, total: { fiat: totalFiat, usd: totalUsd, From 75f04502525f484e4367a0c76bfbd18e301a2806 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 8 Jan 2026 09:56:15 +0000 Subject: [PATCH 10/13] Fix unit tests --- .../src/strategy/test/TestStrategy.test.ts | 6 ++++++ .../src/utils/quotes.test.ts | 7 +++++++ .../src/utils/totals.test.ts | 16 ++++++++++++++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts b/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts index 3207baf5899..a6ed67f3c1a 100644 --- a/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts +++ b/packages/transaction-pay-controller/src/strategy/test/TestStrategy.test.ts @@ -67,6 +67,12 @@ describe('TestStrategy', () => { usd: expect.any(String), }, strategy: TransactionPayStrategy.Test, + targetAmount: { + fiat: expect.any(String), + human: expect.any(String), + raw: expect.any(String), + usd: expect.any(String), + }, }, ]); }); diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index 4445c62301d..a247cef45d1 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -73,6 +73,12 @@ const TOTALS_MOCK = { }, }, }, + targetAmount: { + fiat: '5.67', + human: '5.67', + raw: '567000', + usd: '6.78', + }, total: { fiat: '1.23', usd: '4.56', @@ -301,6 +307,7 @@ describe('Quotes Utils', () => { bridgeFeeFiat: TOTALS_MOCK.fees.provider.usd, chainId: TRANSACTION_DATA_MOCK.paymentToken?.chainId, networkFeeFiat: TOTALS_MOCK.fees.sourceNetwork.estimate.usd, + targetFiat: TOTALS_MOCK.targetAmount.usd, tokenAddress: TRANSACTION_DATA_MOCK.paymentToken?.address, totalFiat: TOTALS_MOCK.total.usd, }, diff --git a/packages/transaction-pay-controller/src/utils/totals.test.ts b/packages/transaction-pay-controller/src/utils/totals.test.ts index d01b04184ba..59c1beb6a59 100644 --- a/packages/transaction-pay-controller/src/utils/totals.test.ts +++ b/packages/transaction-pay-controller/src/utils/totals.test.ts @@ -53,6 +53,12 @@ const QUOTE_1_MOCK: TransactionPayQuote = { usd: '8.88', }, strategy: TransactionPayStrategy.Test, + targetAmount: { + human: '9.99', + fiat: '9.99', + raw: '999000000000000', + usd: '10.10', + }, }; const TOKEN_1_MOCK = { @@ -105,6 +111,12 @@ const QUOTE_2_MOCK: TransactionPayQuote = { usd: '14.14', }, strategy: TransactionPayStrategy.Test, + targetAmount: { + human: '15.15', + fiat: '15.15', + raw: '1515000000000000', + usd: '16.16', + }, }; const TRANSACTION_META_MOCK = {} as TransactionMeta; @@ -159,8 +171,8 @@ describe('Totals Utils', () => { transaction: TRANSACTION_META_MOCK, }); - expect(result.total.fiat).toBe('50.88'); - expect(result.total.usd).toBe('56.34'); + expect(result.total.fiat).toBe('64'); + expect(result.total.usd).toBe('70.68'); }); it('returns total excluding token amount not in quote', () => { From 349de900603847c5f6284bd55ae9d1efb583494e Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 8 Jan 2026 10:50:48 +0000 Subject: [PATCH 11/13] Add unit tests --- .../transaction-pay-controller/CHANGELOG.md | 4 +++- .../src/strategy/bridge/bridge-quotes.test.ts | 14 ++++++++++++ .../src/strategy/relay/relay-quotes.test.ts | 22 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 90a62aaedbc..fe01d1da3c6 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,8 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `TransactionPayController:setIsMaxAmount` messenger action to set max amount flag for a transaction ([#7562](https://github.com/MetaMask/core/pull/7562)) +- **BREAKING:** Support max amount quotes ([#7562](https://github.com/MetaMask/core/pull/7562)) + - Add `TransactionPayController:setIsMaxAmount` messenger action. - Add `isMaxAmount` property to `TransactionData` type. + - Add `targetAmount` property to `TransactionPayQuote` and `TransactionPayTotals`. - Update Relay quote requests to use `EXACT_INPUT` trade type when max amount is selected. - Update totals calculation to account for max amount selection. diff --git a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts index 920e5a57627..0eb0b433b47 100644 --- a/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/bridge/bridge-quotes.test.ts @@ -716,6 +716,20 @@ describe('Bridge Quotes Utils', () => { }); }); + it('returns target amount in quote', async () => { + const quotes = await getBridgeQuotes({ + ...request, + requests: [QUOTE_REQUEST_1_MOCK], + }); + + expect(quotes[0].targetAmount).toStrictEqual({ + fiat: '24.6', + human: '12.3', + raw: QUOTE_REQUEST_1_MOCK.targetAmountMinimum, + usd: '36.9', + }); + }); + it('returns target network fee in quote', async () => { calculateTransactionGasCostMock.mockReturnValue({ fiat: '1.23', 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 0055c331f5e..dc6dadabbc2 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 @@ -69,9 +69,12 @@ const QUOTE_REQUEST_MOCK: QuoteRequest = { const QUOTE_MOCK = { details: { currencyIn: { + amount: '1240000000000000000', + amountFormatted: '1.24', amountUsd: '1.24', }, currencyOut: { + amount: '100', amountFormatted: '1.0', amountUsd: '1.23', currency: { @@ -997,6 +1000,25 @@ describe('Relay Quotes Utils', () => { }); }); + it('includes target amount in quote', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); + + const result = await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(result[0].targetAmount).toStrictEqual({ + human: QUOTE_MOCK.details.currencyOut.amountFormatted, + raw: QUOTE_MOCK.details.currencyOut.amount, + usd: '1.23', + fiat: '2.46', + }); + }); + it('throws if fetching quote fails', async () => { successfulFetchMock.mockRejectedValue(new Error('Fetch error')); From 46afec06e359ca38019cd43bb6ae2471ef94c24f Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 8 Jan 2026 11:02:45 +0000 Subject: [PATCH 12/13] Add error --- .../src/strategy/relay/relay-quotes.test.ts | 23 +++++++++++++++---- .../src/strategy/relay/relay-quotes.ts | 7 +++++- 2 files changed, 25 insertions(+), 5 deletions(-) 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 dc6dadabbc2..33b9f63b6b9 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 @@ -252,7 +252,7 @@ describe('Relay Quotes Utils', () => { originChainId: 1, originCurrency: QUOTE_REQUEST_MOCK.sourceTokenAddress, recipient: QUOTE_REQUEST_MOCK.from, - tradeType: 'EXACT_OUTPUT', + tradeType: 'EXPECTED_OUTPUT', user: QUOTE_REQUEST_MOCK.from, }), ); @@ -281,6 +281,23 @@ describe('Relay Quotes Utils', () => { ); }); + it('throws if isMaxAmount is true and transaction includes data', async () => { + await expect( + getRelayQuotes({ + messenger, + requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: '0xabc' as Hex, + }, + } as TransactionMeta, + }), + ).rejects.toThrow( + 'Max amount quotes do not support included transactions', + ); + }); + it('includes transactions in request', async () => { successfulFetchMock.mockResolvedValue({ json: async () => QUOTE_MOCK, @@ -340,15 +357,13 @@ describe('Relay Quotes Utils', () => { expect(result[0].original.request).toStrictEqual({ amount: QUOTE_REQUEST_MOCK.targetAmountMinimum, - authorizationList: expect.any(Array), destinationChainId: 2, destinationCurrency: QUOTE_REQUEST_MOCK.targetTokenAddress, originChainId: 1, originCurrency: QUOTE_REQUEST_MOCK.sourceTokenAddress, recipient: QUOTE_REQUEST_MOCK.from, slippageTolerance: '50', - tradeType: 'EXACT_OUTPUT', - txs: expect.any(Array), + tradeType: 'EXPECTED_OUTPUT', user: QUOTE_REQUEST_MOCK.from, }); }); 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 6e6feb4cacb..4a75d1f1fb9 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -169,13 +169,18 @@ async function processTransactions( log('Updating recipient as token transfer', requestBody.recipient); } - const skipDelegation = isTokenTransfer || isHypercore || isMaxAmount; + const hasNoData = singleData === undefined || singleData === '0x'; + const skipDelegation = hasNoData || isTokenTransfer || isHypercore; if (skipDelegation) { log('Skipping delegation as token transfer or Hypercore deposit'); return; } + if (isMaxAmount) { + throw new Error('Max amount quotes do not support included transactions'); + } + const delegation = await messenger.call( 'TransactionPayController:getDelegationTransaction', { transaction }, From 26359d3ad0e7ede79894924f5987acda82e511d4 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 8 Jan 2026 11:06:16 +0000 Subject: [PATCH 13/13] Update changelog --- packages/transaction-controller/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 18ca9c6c0c7..01d77e04570 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add optional `targetFiat` property to `MetamaskPayMetadata` type ([#7562](https://github.com/MetaMask/core/pull/7562)) - Add optional `isStateOnly` property to `TransactionMeta` and `AddTransactionOptions` ([#7591](https://github.com/MetaMask/core/pull/7591)) - Transactions with `isStateOnly` set to `true` have no lifecycle and are not signed or published. - Transactions are also excluded from confirmation polling.