diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 4d1e2c817a..223cedf180 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 ([#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] 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); +}