From 7213f2055f3215b254ae53759e33a7b98eebd288 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 16 Jun 2026 19:47:32 +0530 Subject: [PATCH 1/6] fix: MM Pay transaction with isQuoteRequired that have same source and destination chain and token --- .../src/strategy/relay/relay-quotes.test.ts | 104 ++++++++++++++++++ .../src/strategy/relay/relay-quotes.ts | 35 +++++- .../src/utils/source-amounts.test.ts | 51 +++++++++ .../src/utils/source-amounts.ts | 9 +- 4 files changed, 196 insertions(+), 3 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 2a21f0cf30b..e6b01c16f90 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 @@ -1373,6 +1373,110 @@ describe('Relay Quotes Utils', () => { expect(estimateGasBatchMock).not.toHaveBeenCalled(); }); + it('sets user to txParams.from when same token and chain with accountOverride', async () => { + const txParamsFrom = '0xOriginalSender000000000000000000000000' as Hex; + const accountOverride = + '0xOverrideAccount0000000000000000000000000' as Hex; + const tokenAddress = '0xTokenAddress00000000000000000000000000' as Hex; + const chainId = '0x89' as Hex; + + successfulFetchMock.mockResolvedValue({ + ok: true, + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + from: accountOverride, + sourceChainId: chainId, + sourceTokenAddress: tokenAddress, + targetChainId: chainId, + targetTokenAddress: tokenAddress, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { from: txParamsFrom }, + } as TransactionMeta, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body.user).toBe(txParamsFrom); + }); + + it('keeps user as from when source and destination differ even with accountOverride', async () => { + const txParamsFrom = '0xOriginalSender000000000000000000000000' as Hex; + const accountOverride = + '0xOverrideAccount0000000000000000000000000' as Hex; + + successfulFetchMock.mockResolvedValue({ + ok: true, + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + from: accountOverride, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { from: txParamsFrom }, + } as TransactionMeta, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body.user).toBe(accountOverride); + }); + + it('keeps user as from when same token and chain without accountOverride', async () => { + const tokenAddress = '0xTokenAddress00000000000000000000000000' as Hex; + const chainId = '0x89' as Hex; + + successfulFetchMock.mockResolvedValue({ + ok: true, + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + sourceChainId: chainId, + sourceTokenAddress: tokenAddress, + targetChainId: chainId, + targetTokenAddress: tokenAddress, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { from: FROM_MOCK }, + } as TransactionMeta, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body.user).toBe(FROM_MOCK); + }); + it('does not prepend original transaction for post-quote when txParams.to is missing', async () => { successfulFetchMock.mockResolvedValue({ ok: true, 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 49b62484274..e4d7c2ab87e 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -280,6 +280,8 @@ async function getSingleQuote( isRelayExecuteEnabled(messenger) && isEIP7702Chain(messenger, sourceChainId); + const quoteUser = getQuoteUser(request, transaction, from); + const body: RelayQuoteRequest = { amount: useExactInput ? sourceTokenAmount : targetAmountMinimum, destinationChainId: Number(targetChainId), @@ -292,7 +294,7 @@ async function getSingleQuote( recipient: request.recipient ?? from, slippageTolerance, tradeType: useExactInput ? 'EXACT_INPUT' : 'EXPECTED_OUTPUT', - user: from, + user: quoteUser, }; if (request.isPolymarketDepositWallet) { @@ -1181,6 +1183,37 @@ function getTransferRecipient(data: Hex): Hex { .decodeFunctionData('transfer', data) .to.toLowerCase(); } +/** + * Determine the `user` address for a Relay quote request. + * + * When source and destination are the same token on the same chain and an + * accountOverride is active, use the original `txParams.from` so that Relay + * sees the transaction sender rather than the override address. + * + * @param request - Quote request. + * @param transaction - Parent transaction metadata. + * @param from - Resolved wallet address (`accountOverride ?? txParams.from`). + * @returns The address to set as `user` on the quote body. + */ +function getQuoteUser( + request: QuoteRequest, + transaction: TransactionMeta, + from: Hex, +): Hex { + const { sourceChainId, sourceTokenAddress, targetChainId, targetTokenAddress } = + request; + + const isSameSourceAndTarget = + sourceChainId === targetChainId && + sourceTokenAddress.toLowerCase() === targetTokenAddress.toLowerCase(); + + const txParamsFrom = transaction.txParams?.from as Hex | undefined; + const hasAccountOverride = + txParamsFrom && from.toLowerCase() !== txParamsFrom.toLowerCase(); + + return isSameSourceAndTarget && hasAccountOverride ? txParamsFrom : from; +} + function getSubsidizedFeeAmountUsd(quote: RelayQuote): BigNumber { const subsidizedFee = quote.fees?.subsidized; const amountUsd = new BigNumber(subsidizedFee?.amountUsd ?? '0'); 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 e8db92d137b..42db1177a39 100644 --- a/packages/transaction-pay-controller/src/utils/source-amounts.test.ts +++ b/packages/transaction-pay-controller/src/utils/source-amounts.test.ts @@ -436,6 +436,57 @@ describe('Source Amounts Utils', () => { ]); }); + it('does not filter out same token when isQuoteRequired is true in post-quote flow', () => { + const transactionData: TransactionData = { + isLoading: false, + isPostQuote: true, + isQuoteRequired: true, + paymentToken: DESTINATION_TOKEN_MOCK, + tokens: [ + { + ...TRANSACTION_TOKEN_MOCK, + address: DESTINATION_TOKEN_MOCK.address, + chainId: DESTINATION_TOKEN_MOCK.chainId, + skipIfBalance: false, + }, + ], + }; + + updateSourceAmounts(TRANSACTION_ID_MOCK, transactionData, messenger); + + expect(transactionData.sourceAmounts).toStrictEqual([ + { + sourceAmountHuman: TRANSACTION_TOKEN_MOCK.amountHuman, + sourceAmountRaw: TRANSACTION_TOKEN_MOCK.amountRaw, + sourceBalanceRaw: TRANSACTION_TOKEN_MOCK.balanceRaw, + sourceChainId: DESTINATION_TOKEN_MOCK.chainId, + sourceTokenAddress: DESTINATION_TOKEN_MOCK.address, + targetTokenAddress: DESTINATION_TOKEN_MOCK.address, + }, + ]); + }); + + it('still filters out same token when isQuoteRequired is false in post-quote flow', () => { + const transactionData: TransactionData = { + isLoading: false, + isPostQuote: true, + isQuoteRequired: false, + paymentToken: DESTINATION_TOKEN_MOCK, + tokens: [ + { + ...TRANSACTION_TOKEN_MOCK, + address: DESTINATION_TOKEN_MOCK.address, + chainId: DESTINATION_TOKEN_MOCK.chainId, + skipIfBalance: false, + }, + ], + }; + + updateSourceAmounts(TRANSACTION_ID_MOCK, transactionData, messenger); + + expect(transactionData.sourceAmounts).toStrictEqual([]); + }); + it('still filters out same token when isHyperliquidSource is false in post-quote flow', () => { 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 f2bcd31a501..a0c313e1b23 100644 --- a/packages/transaction-pay-controller/src/utils/source-amounts.ts +++ b/packages/transaction-pay-controller/src/utils/source-amounts.ts @@ -51,13 +51,15 @@ export function updateSourceAmounts( // For post-quote flows, source amounts are calculated differently // The source is the transaction's required token, not the selected token if (isPostQuote) { - const { isHyperliquidSource, isPolymarketDepositWallet } = transactionData; + const { isHyperliquidSource, isPolymarketDepositWallet, isQuoteRequired } = + transactionData; const sourceAmounts = calculatePostQuoteSourceAmounts( tokens, paymentToken, isMaxAmount ?? false, isHyperliquidSource, isPolymarketDepositWallet, + isQuoteRequired, ); log('Updated post-quote source amounts', { transactionId, sourceAmounts }); transactionData.sourceAmounts = sourceAmounts; @@ -94,6 +96,7 @@ export function updateSourceAmounts( * @param isMaxAmount - Whether the transaction is a maximum amount transaction. * @param isHyperliquidSource - Whether the source is HyperLiquid (perps withdrawal). * @param isPolymarketDepositWallet - Whether the source is a Polymarket deposit wallet. + * @param isQuoteRequired - When true, a quote is always fetched even when source and target tokens are identical. * @returns Array of source amounts. */ function calculatePostQuoteSourceAmounts( @@ -102,6 +105,7 @@ function calculatePostQuoteSourceAmounts( isMaxAmount: boolean, isHyperliquidSource?: boolean, isPolymarketDepositWallet?: boolean, + isQuoteRequired?: boolean, ): TransactionPaySourceAmount[] { return tokens .filter((token) => { @@ -121,7 +125,8 @@ function calculatePostQuoteSourceAmounts( if ( isSameToken(token, paymentToken) && !isHyperliquidSource && - !isPolymarketDepositWallet + !isPolymarketDepositWallet && + !isQuoteRequired ) { log('Skipping token as same as destination token'); return false; From 65d66781a0c0c810f613d037e57a896e65ab7036 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 16 Jun 2026 19:53:40 +0530 Subject: [PATCH 2/6] update --- packages/transaction-pay-controller/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 17116e6f030..def52850f08 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fix post-quote flow for MM Pay transactions transferring between same chain and tokens when `isQuoteRequired` is set ([#9150](https://github.com/MetaMask/core/pull/9150)) +- Fix Relay quote `user` address to use `txParams.from` instead of `accountOverride` for same-token-same-chain transactions ([#9150](https://github.com/MetaMask/core/pull/9150)) + ## [23.8.0] ### Changed From 8255b8e50da468943977732faf51ca6d858dab48 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Tue, 16 Jun 2026 19:57:46 +0530 Subject: [PATCH 3/6] update --- .../src/strategy/relay/relay-quotes.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 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 e4d7c2ab87e..7794569c4a4 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -1200,8 +1200,12 @@ function getQuoteUser( transaction: TransactionMeta, from: Hex, ): Hex { - const { sourceChainId, sourceTokenAddress, targetChainId, targetTokenAddress } = - request; + const { + sourceChainId, + sourceTokenAddress, + targetChainId, + targetTokenAddress, + } = request; const isSameSourceAndTarget = sourceChainId === targetChainId && From 42f1490ce25754987d1216303bada4db2f2a210a Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Thu, 18 Jun 2026 13:40:43 +0530 Subject: [PATCH 4/6] update --- .../src/strategy/relay/relay-quotes.test.ts | 41 +++++++++++++++++++ .../src/strategy/relay/relay-quotes.ts | 8 +++- 2 files changed, 48 insertions(+), 1 deletion(-) 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 e6b01c16f90..2485154ce6d 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 @@ -1411,6 +1411,47 @@ describe('Relay Quotes Utils', () => { expect(body.user).toBe(txParamsFrom); }); + it('keeps user as from when same token and chain with accountOverride but recipient differs', async () => { + const txParamsFrom = '0xOriginalSender000000000000000000000000' as Hex; + const accountOverride = + '0xOverrideAccount0000000000000000000000000' as Hex; + const externalRecipient = + '0xExternalRecipient000000000000000000000000' as Hex; + const tokenAddress = '0xTokenAddress00000000000000000000000000' as Hex; + const chainId = '0x89' as Hex; + + successfulFetchMock.mockResolvedValue({ + ok: true, + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [ + { + ...QUOTE_REQUEST_MOCK, + from: accountOverride, + recipient: externalRecipient, + sourceChainId: chainId, + sourceTokenAddress: tokenAddress, + targetChainId: chainId, + targetTokenAddress: tokenAddress, + }, + ], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { from: txParamsFrom }, + } as TransactionMeta, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body.user).toBe(accountOverride); + }); + it('keeps user as from when source and destination differ even with accountOverride', async () => { const txParamsFrom = '0xOriginalSender000000000000000000000000' as Hex; const accountOverride = 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 7794569c4a4..f98b14a8f64 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -1215,7 +1215,13 @@ function getQuoteUser( const hasAccountOverride = txParamsFrom && from.toLowerCase() !== txParamsFrom.toLowerCase(); - return isSameSourceAndTarget && hasAccountOverride ? txParamsFrom : from; + const recipient = request.recipient ?? from; + const isRecipientAccountOverride = + recipient.toLowerCase() === from.toLowerCase(); + + return isSameSourceAndTarget && hasAccountOverride && isRecipientAccountOverride + ? txParamsFrom + : from; } function getSubsidizedFeeAmountUsd(quote: RelayQuote): BigNumber { From 85f105f5dca6cab44e49e9ce400a7817cf9bb155 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Thu, 18 Jun 2026 13:54:06 +0530 Subject: [PATCH 5/6] update --- .../src/strategy/relay/relay-quotes.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 f98b14a8f64..48af7c57f3c 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -1219,7 +1219,9 @@ function getQuoteUser( const isRecipientAccountOverride = recipient.toLowerCase() === from.toLowerCase(); - return isSameSourceAndTarget && hasAccountOverride && isRecipientAccountOverride + return isSameSourceAndTarget && + hasAccountOverride && + isRecipientAccountOverride ? txParamsFrom : from; } From d9c808ec9e22bf534d5692769fd5af1e438f08d9 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Thu, 18 Jun 2026 13:59:27 +0530 Subject: [PATCH 6/6] update --- packages/transaction-pay-controller/CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 8fe456c12c9..e6e75255c40 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fix post-quote flow for MM Pay transactions transferring between same chain and tokens when `isQuoteRequired` is set ([#9150](https://github.com/MetaMask/core/pull/9150)) + ## [23.9.0] ### Added @@ -19,7 +23,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fix post-quote flow for MM Pay transactions transferring between same chain and tokens when `isQuoteRequired` is set ([#9150](https://github.com/MetaMask/core/pull/9150)) - Sync transaction metadata when fiat payment is selected but no payment token is present ([#9158](https://github.com/MetaMask/core/pull/9158)) - Fix direct mUSD fiat Money Account deposits by submitting a sponsored Money Account vault batch after fiat settlement instead of requiring Relay execute ([#9161](https://github.com/MetaMask/core/pull/9161))