diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 8e4f609561..c2fbde7f2b 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 fiat strategy never being selected by routing fiat payment method through `getStrategyOrder` and allowing quote retrieval when no crypto payment token is set ([#8720](https://github.com/MetaMask/core/pull/8720)) + ## [21.1.0] ### Added diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index bd8e5a4cfe..1e18803732 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -540,6 +540,48 @@ describe('TransactionPayController', () => { CHAIN_ID_MOCK, TOKEN_ADDRESS_MOCK, 'perpsDeposit', + undefined, + ); + }); + + it('passes fiat payment method ID into getStrategyOrder', async () => { + const controller = createController(); + + controller.updatePaymentToken({ + transactionId: TRANSACTION_ID_MOCK, + tokenAddress: TOKEN_ADDRESS_MOCK, + chainId: CHAIN_ID_MOCK, + }); + + const { updateTransactionData } = updatePaymentTokenMock.mock.calls[0][1]; + + updateTransactionData(TRANSACTION_ID_MOCK, (data) => { + data.paymentToken = { + address: TOKEN_ADDRESS_MOCK, + balanceFiat: '1', + balanceHuman: '1', + balanceRaw: '1', + balanceUsd: '1', + chainId: CHAIN_ID_MOCK, + decimals: 6, + symbol: 'USDC', + }; + data.fiatPayment = { selectedPaymentMethodId: 'card-123' }; + }); + + const transactionMeta = { + id: TRANSACTION_ID_MOCK, + type: 'perpsDeposit', + } as TransactionMeta; + + messenger.call('TransactionPayController:getStrategy', transactionMeta); + + expect(getStrategyOrderMock).toHaveBeenCalledWith( + messenger, + CHAIN_ID_MOCK, + TOKEN_ADDRESS_MOCK, + 'perpsDeposit', + 'card-123', ); }); }); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index e1b7670379..61cb6b4790 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -324,14 +324,15 @@ export class TransactionPayController extends BaseController< return validStrategies; } - const paymentToken = - this.state.transactionData[transaction.id]?.paymentToken; + const transactionData = this.state.transactionData[transaction.id]; + const paymentToken = transactionData?.paymentToken; return getStrategyOrder( this.messenger, paymentToken?.chainId, paymentToken?.address, transaction.type, + transactionData?.fiatPayment?.selectedPaymentMethodId, ); } } diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts index 6e16c174cb..2257127c07 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts @@ -809,6 +809,49 @@ describe('Feature Flags Utils', () => { TransactionPayStrategy.Relay, ]); }); + + it('returns only Fiat strategy when fiatPaymentMethodId is provided', () => { + const strategyOrder = getStrategyOrder( + messenger, + undefined, + undefined, + undefined, + 'card-123', + ); + + expect(strategyOrder).toStrictEqual([TransactionPayStrategy.Fiat]); + }); + + it('returns only Fiat strategy regardless of other routing config when fiatPaymentMethodId is provided', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + strategyOrder: [ + TransactionPayStrategy.Relay, + TransactionPayStrategy.Across, + ], + strategyOverrides: { + default: { + chains: { + [CHAIN_ID_MOCK]: [TransactionPayStrategy.Bridge], + }, + }, + }, + }, + }, + }); + + const strategyOrder = getStrategyOrder( + messenger, + CHAIN_ID_MOCK, + TOKEN_ADDRESS_MOCK, + 'perpsDeposit', + '/payments/debit-credit-card', + ); + + expect(strategyOrder).toStrictEqual([TransactionPayStrategy.Fiat]); + }); }); describe('getStrategyOrder route-aware resolution', () => { diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.ts b/packages/transaction-pay-controller/src/utils/feature-flags.ts index 911c5bc605..491853bfdf 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.ts @@ -312,6 +312,7 @@ function getDefaultOverrideStrategies( * @param tokenAddress - Optional token address used to match route overrides. * @param transactionType - Optional transaction type used to match route * overrides. + * @param fiatPaymentMethodId - Optional fiat payment method ID used to match route overrides. * @returns Ordered strategy list. */ export function getStrategyOrder( @@ -319,7 +320,13 @@ export function getStrategyOrder( chainId?: Hex, tokenAddress?: Hex, transactionType?: string, + fiatPaymentMethodId?: string, ): StrategyOrder { + // If fiat payment method is selected, use Fiat strategy only + if (fiatPaymentMethodId) { + return [TransactionPayStrategy.Fiat]; + } + const routingConfig = getStrategyRoutingConfig(messenger); const normalizedChainId = normalizeHex(chainId); const normalizedTokenAddress = normalizeHex(tokenAddress); diff --git a/packages/transaction-pay-controller/src/utils/quotes.test.ts b/packages/transaction-pay-controller/src/utils/quotes.test.ts index c0a9eabcf3..30e37f1040 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.test.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.test.ts @@ -589,6 +589,42 @@ describe('Quotes Utils', () => { }); }); + it('still invokes strategies when no payment token but fiat payment method is set', async () => { + await run({ + transactionData: { + ...TRANSACTION_DATA_MOCK, + paymentToken: undefined, + fiatPayment: { selectedPaymentMethodId: 'card-123' }, + }, + }); + + expect(getQuotesMock).toHaveBeenCalled(); + }); + + it('does not invoke strategies when no payment token and no fiat payment method', async () => { + await run({ + transactionData: { + ...TRANSACTION_DATA_MOCK, + paymentToken: undefined, + fiatPayment: {}, + }, + }); + + const transactionDataMock = { + quotes: [QUOTE_MOCK], + quotesLastUpdated: undefined, + }; + + updateTransactionDataMock.mock.calls.map((call) => + call[1](transactionDataMock), + ); + + expect(transactionDataMock).toMatchObject({ + quotes: [], + quotesLastUpdated: expect.any(Number), + }); + }); + it('gets quotes from strategy', async () => { await run(); diff --git a/packages/transaction-pay-controller/src/utils/quotes.ts b/packages/transaction-pay-controller/src/utils/quotes.ts index 3ac79bd2a3..e6d47df328 100644 --- a/packages/transaction-pay-controller/src/utils/quotes.ts +++ b/packages/transaction-pay-controller/src/utils/quotes.ts @@ -571,7 +571,7 @@ async function getQuotes( }, ); - if (!requests?.length) { + if (!requests?.length && !fiatPaymentMethod) { return { batchTransactions: [], quotes: [],