From f7aad33a6c1db32276f4fcf0e0761676f3420210 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 11 Jun 2026 21:20:20 +0100 Subject: [PATCH 1/2] fix(transaction-pay-controller): block raw publish without pay quote --- .../transaction-pay-controller/CHANGELOG.md | 1 + .../helpers/TransactionPayPublishHook.test.ts | 58 +++++++++++++++++++ .../src/helpers/TransactionPayPublishHook.ts | 26 ++++++++- 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 4d1e2c817a..977e8a32ca 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Prevent Pay-configured transactions from falling back to raw publish when no Pay quote is available ([#0000](https://github.com/MetaMask/core/pull/0000)) - Clear stale `fiatPayment.rampsQuote` when a fiat quote fetch fails, preventing the "No quotes" alert from being silently suppressed after a prior successful fetch ([#9073](https://github.com/MetaMask/core/pull/9073)) ## [23.5.1] diff --git a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts index ff6a4eade9..f12fec1f93 100644 --- a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts +++ b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.test.ts @@ -6,6 +6,7 @@ import type { import { TransactionPayStrategy } from '..'; import { getMessengerMock } from '../tests/messenger-mock'; import type { + TransactionData, TransactionPayControllerState, TransactionPayQuote, } from '../types'; @@ -25,6 +26,22 @@ const QUOTE_MOCK = { strategy: TransactionPayStrategy.Test, } as TransactionPayQuote; +const PAYMENT_TOKEN_MOCK = { + address: '0xdef', + balanceFiat: '1', + balanceHuman: '1', + balanceRaw: '1000000', + balanceUsd: '1', + chainId: '0x1', + decimals: 6, + symbol: 'TEST', +}; + +const EMPTY_TRANSACTION_DATA_MOCK = { + isLoading: false, + tokens: [], +} as TransactionData; + describe('TransactionPayPublishHook', () => { const isSmartTransactionMock = jest.fn(); const executeMock = jest.fn(); @@ -115,6 +132,47 @@ describe('TransactionPayPublishHook', () => { expect(executeMock).not.toHaveBeenCalled(); }); + it('does nothing if no quotes exist for transaction data without explicit pay config', async () => { + getControllerStateMock.mockReturnValue({ + transactionData: { + [TRANSACTION_META_MOCK.id]: EMPTY_TRANSACTION_DATA_MOCK, + }, + }); + + await runHook(); + + expect(executeMock).not.toHaveBeenCalled(); + }); + + it.each([ + ['account override', { accountOverride: '0xdef' }], + [ + 'fiat payment method', + { fiatPayment: { selectedPaymentMethodId: 'test-payment-method' } }, + ], + ['payment override', { paymentOverride: {} }], + ['payment token', { paymentToken: PAYMENT_TOKEN_MOCK }], + ['post-quote flag', { isPostQuote: true }], + ] as [string, Partial][])( + 'throws if no quotes exist for transaction data with %s', + async (_description, transactionData) => { + getControllerStateMock.mockReturnValue({ + transactionData: { + [TRANSACTION_META_MOCK.id]: { + ...EMPTY_TRANSACTION_DATA_MOCK, + ...transactionData, + }, + }, + }); + + await expect(runHook()).rejects.toThrow( + 'MetaMask Pay: No pay quote available', + ); + + expect(executeMock).not.toHaveBeenCalled(); + }, + ); + it('sets submittedTime on the transaction before executing strategy', async () => { await runHook(); diff --git a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts index 48d616fa0c..2b613049ce 100644 --- a/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts +++ b/packages/transaction-pay-controller/src/helpers/TransactionPayPublishHook.ts @@ -6,6 +6,7 @@ import { createModuleLogger } from '@metamask/utils'; import { projectLogger } from '../logger'; import type { + TransactionData, TransactionPayControllerMessenger, TransactionPayQuote, } from '../types'; @@ -19,6 +20,8 @@ const EMPTY_RESULT = { transactionHash: undefined, }; +const ERROR_NO_PAY_QUOTE = 'No pay quote available'; + export class TransactionPayPublishHook { readonly #isSmartTransaction: (chainId: Hex) => boolean; @@ -62,11 +65,16 @@ export class TransactionPayPublishHook { 'TransactionPayController:getState', ); + const transactionData = controllerState.transactionData?.[transactionId]; + const quotes = - (controllerState.transactionData?.[transactionId] - ?.quotes as TransactionPayQuote[]) ?? []; + (transactionData?.quotes as TransactionPayQuote[]) ?? []; if (!quotes?.length) { + if (hasExplicitPayConfig(transactionData)) { + throw new Error(ERROR_NO_PAY_QUOTE); + } + log('Skipping as no quotes found'); return EMPTY_RESULT; } @@ -94,3 +102,17 @@ export class TransactionPayPublishHook { }); } } + +function hasExplicitPayConfig(transactionData?: TransactionData): boolean { + if (!transactionData) { + return false; + } + + return [ + transactionData.accountOverride, + transactionData.fiatPayment?.selectedPaymentMethodId, + transactionData.isPostQuote, + transactionData.paymentOverride, + transactionData.paymentToken, + ].some(Boolean); +} From c22574bfd522c7ec8513593ba02f8e8c2a074e4b Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 11 Jun 2026 21:24:59 +0100 Subject: [PATCH 2/2] chore(transaction-pay-controller): update changelog PR link --- 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 977e8a32ca..223cedf180 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Prevent Pay-configured transactions from falling back to raw publish when no Pay quote is available ([#0000](https://github.com/MetaMask/core/pull/0000)) +- Prevent Pay-configured transactions from falling back to raw publish when no Pay quote is available ([#9101](https://github.com/MetaMask/core/pull/9101)) - Clear stale `fiatPayment.rampsQuote` when a fiat quote fetch fails, preventing the "No quotes" alert from being silently suppressed after a prior successful fetch ([#9073](https://github.com/MetaMask/core/pull/9073)) ## [23.5.1]