diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index ead04dc76fe..477f6a94e9f 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] +### Changed + +- Add route-based `confirmations_pay` strategy resolution ([#8282](https://github.com/MetaMask/core/pull/8282)) + ## [19.0.0] ### Added diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index f19bac3064e..7f669956e50 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -365,6 +365,45 @@ describe('TransactionPayController', () => { ), ).toBe(TransactionPayStrategy.Test); }); + + it('passes payment token route args into feature flag fallback', 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', + }; + }); + + 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', + ); + }); }); describe('transaction data update', () => { diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 37ce1a19744..a1e48b3a2ca 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -272,8 +272,18 @@ export class TransactionPayController extends BaseController< isTransactionPayStrategy(strategy), ); - return validStrategies.length - ? validStrategies - : getStrategyOrder(this.messenger); + if (validStrategies.length) { + return validStrategies; + } + + const paymentToken = + this.state.transactionData[transaction.id]?.paymentToken; + + return getStrategyOrder( + this.messenger, + paymentToken?.chainId, + paymentToken?.address, + transaction.type, + ); } } 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 18e287ffe6b..2d7554743ad 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts @@ -8,7 +8,6 @@ import { DEFAULT_RELAY_ORIGIN_GAS_OVERHEAD, DEFAULT_RELAY_QUOTE_URL, DEFAULT_SLIPPAGE, - DEFAULT_STRATEGY_ORDER, getAssetsUnifyStateFeature, getFallbackGas, DEFAULT_RELAY_EXECUTE_URL, @@ -21,8 +20,10 @@ import { getGasBuffer, getPayStrategiesConfig, getSlippage, + getStrategy, getStrategyOrder, } from './feature-flags'; +import * as featureFlagsModule from './feature-flags'; import { getDefaultRemoteFeatureFlagControllerState } from '../../../remote-feature-flag-controller/src/remote-feature-flag-controller'; import { TransactionPayStrategy } from '../constants'; import { getMessengerMock } from '../tests/messenger-mock'; @@ -52,6 +53,24 @@ describe('Feature Flags Utils', () => { }); }); + describe('module surface', () => { + it('does not expose raw confirmations_pay feature flags', () => { + expect(featureFlagsModule).not.toHaveProperty( + 'getConfirmationsPayFeatureFlags', + ); + }); + + it('does not expose route resolution from raw feature flags', () => { + expect(featureFlagsModule).not.toHaveProperty( + 'getStrategyOrderForRouteFromFeatureFlags', + ); + }); + + it('does not expose the old route helper name', () => { + expect(featureFlagsModule).not.toHaveProperty('getStrategiesForRoute'); + }); + }); + describe('getFeatureFlags', () => { it('returns default feature flags when none are set', () => { const featureFlags = getFeatureFlags(messenger); @@ -708,10 +727,10 @@ describe('Feature Flags Utils', () => { }); describe('getStrategyOrder', () => { - it('returns default strategy order when none is set', () => { + it('returns enabled default strategy order when none is set', () => { const strategyOrder = getStrategyOrder(messenger); - expect(strategyOrder).toStrictEqual(DEFAULT_STRATEGY_ORDER); + expect(strategyOrder).toStrictEqual([TransactionPayStrategy.Relay]); }); it('returns strategy order from feature flags', () => { @@ -760,7 +779,7 @@ describe('Feature Flags Utils', () => { ]); }); - it('falls back to default strategy order when all entries are invalid', () => { + it('falls back to the enabled default strategy order when all entries are invalid', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), remoteFeatureFlags: { @@ -772,7 +791,381 @@ describe('Feature Flags Utils', () => { const strategyOrder = getStrategyOrder(messenger); - expect(strategyOrder).toStrictEqual(DEFAULT_STRATEGY_ORDER); + expect(strategyOrder).toStrictEqual([TransactionPayStrategy.Relay]); + }); + + it('supports undefined local overrides when remote feature flags provide strategy order', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + localOverrides: undefined as never, + remoteFeatureFlags: { + confirmations_pay: { + strategyOrder: [TransactionPayStrategy.Relay], + }, + }, + }); + + expect(getStrategyOrder(messenger)).toStrictEqual([ + TransactionPayStrategy.Relay, + ]); + }); + }); + + describe('getStrategyOrder route-aware resolution', () => { + it('uses default routing config when confirmations_pay flags are absent', () => { + expect(getStrategyOrder(messenger)).toStrictEqual([ + TransactionPayStrategy.Relay, + ]); + }); + + it('filters invalid strategy override config and dedupes strategies', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + strategyOrder: [123, 'relay', 'relay'], + payStrategies: { + across: { enabled: true }, + relay: { enabled: false }, + }, + strategyOverrides: { + transactionTypes: { + perpsDeposit: { + default: [123, 'invalid'], + chains: { + '0xa4b1': [123], + '0xa4b2': ['relay'], + }, + tokens: { + '0xa4b1': undefined, + '0xa4b2': { + '0xabc': [123], + '0xdef': ['across'], + }, + }, + }, + }, + }, + }, + }, + }); + + expect( + getStrategyOrder(messenger, '0xa4b2', '0xdef', 'perpsDeposit'), + ).toStrictEqual([TransactionPayStrategy.Across]); + }); + + it('resolves strategy overrides in transaction-type token, chain, global token, global chain, transaction-type default, global default precedence', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + payStrategies: { + across: { enabled: true }, + relay: { enabled: true }, + }, + strategyOverrides: { + default: { + default: ['across'], + chains: { + '0x89': ['across'], + }, + tokens: { + '0x1': { + '0xdef': ['relay', 'across'], + }, + }, + }, + transactionTypes: { + perpsDeposit: { + default: ['relay'], + chains: { + '0xa4b1': ['across'], + }, + tokens: { + '0xa4b1': { + '0xabc': ['relay', 'across'], + }, + }, + }, + }, + }, + strategyOrder: ['relay', 'across'], + }, + }, + }); + + expect( + getStrategyOrder(messenger, '0xa4b1', '0xabc', 'perpsDeposit'), + ).toStrictEqual([ + TransactionPayStrategy.Relay, + TransactionPayStrategy.Across, + ]); + + expect( + getStrategyOrder(messenger, '0xa4b1', '0xdef', 'perpsDeposit'), + ).toStrictEqual([TransactionPayStrategy.Across]); + + expect( + getStrategyOrder(messenger, '0x1', '0xdef', 'perpsDeposit'), + ).toStrictEqual([ + TransactionPayStrategy.Relay, + TransactionPayStrategy.Across, + ]); + + expect( + getStrategyOrder(messenger, '0x89', '0xdef', 'perpsDeposit'), + ).toStrictEqual([TransactionPayStrategy.Across]); + + expect( + getStrategyOrder(messenger, '0x2', '0xdef', 'perpsDeposit'), + ).toStrictEqual([TransactionPayStrategy.Relay]); + + expect(getStrategyOrder(messenger, '0x1', '0xdef')).toStrictEqual([ + TransactionPayStrategy.Relay, + TransactionPayStrategy.Across, + ]); + + expect(getStrategyOrder(messenger, '0x2', '0xabc')).toStrictEqual([ + TransactionPayStrategy.Across, + ]); + }); + + it('uses default override scope when no transaction-type-specific override matches', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + payStrategies: { + across: { enabled: true }, + relay: { enabled: true }, + }, + strategyOverrides: { + default: { + chains: { + '0xa4b1': ['across'], + }, + }, + transactionTypes: { + perpsDeposit: { + default: ['relay'], + }, + }, + }, + strategyOrder: ['relay', 'across'], + }, + }, + }); + + expect(getStrategyOrder(messenger, '0xa4b1', '0xabc')).toStrictEqual([ + TransactionPayStrategy.Across, + ]); + + expect( + getStrategyOrder(messenger, '0x1', '0xabc', 'unknownType'), + ).toStrictEqual([ + TransactionPayStrategy.Relay, + TransactionPayStrategy.Across, + ]); + }); + + it('lets blanket global chain overrides beat transaction-type defaults', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + payStrategies: { + across: { enabled: true }, + relay: { enabled: true }, + }, + strategyOverrides: { + default: { + chains: { + '0xa4b1': ['across'], + }, + }, + transactionTypes: { + perpsDeposit: { + default: ['relay'], + }, + }, + }, + strategyOrder: ['relay'], + }, + }, + }); + + expect( + getStrategyOrder(messenger, '0xa4b1', '0xabc', 'perpsDeposit'), + ).toStrictEqual([TransactionPayStrategy.Across]); + }); + + it('matches mixed-case route context hex values against normalized overrides', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + payStrategies: { + across: { enabled: true }, + relay: { enabled: true }, + }, + strategyOverrides: { + transactionTypes: { + perpsDeposit: { + default: ['relay'], + chains: { + '0xa4b1': ['across'], + }, + tokens: { + '0xa4b1': { + '0xabc': ['relay'], + }, + }, + }, + }, + }, + strategyOrder: ['relay', 'across'], + }, + }, + }); + + expect( + getStrategyOrder(messenger, '0xA4B1', '0xAbC', 'perpsDeposit'), + ).toStrictEqual([TransactionPayStrategy.Relay]); + + expect( + getStrategyOrder(messenger, '0xA4B1', '0xDef', 'perpsDeposit'), + ).toStrictEqual([TransactionPayStrategy.Across]); + }); + + it('does not fall back when a matched override resolves only to disabled strategies', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + payStrategies: { + across: { enabled: false }, + relay: { enabled: true }, + }, + strategyOverrides: { + transactionTypes: { + perpsDeposit: { + chains: { + '0xa4b1': ['across'], + }, + default: ['relay'], + }, + }, + }, + strategyOrder: ['across', 'relay'], + }, + }, + }); + + expect( + getStrategyOrder(messenger, '0xa4b1', '0xabc', 'perpsDeposit'), + ).toStrictEqual([]); + }); + + it('ignores empty override entries and falls back to the global order', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + payStrategies: { + across: { enabled: true }, + relay: { enabled: true }, + }, + strategyOverrides: { + transactionTypes: { + perpsDeposit: undefined, + }, + }, + strategyOrder: ['relay'], + }, + }, + }); + + expect( + getStrategyOrder(messenger, '0xa4b1', '0xabc', 'perpsDeposit'), + ).toStrictEqual([TransactionPayStrategy.Relay]); + }); + + it('returns an empty strategy list when no enabled strategies remain', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + payStrategies: { + across: { enabled: false }, + relay: { enabled: false }, + }, + strategyOrder: ['relay', 'across'], + }, + }, + }); + + expect(getStrategyOrder(messenger)).toStrictEqual([]); + }); + }); + + describe('getStrategyOrder with remote feature flag controller state', () => { + it('falls back to defaults when remote feature flag maps are undefined', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + localOverrides: undefined as never, + remoteFeatureFlags: undefined as never, + }); + + expect(getStrategyOrder(messenger)).toStrictEqual([ + TransactionPayStrategy.Relay, + ]); + }); + }); + + describe('getStrategy', () => { + it('returns the first applicable strategy for a route', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + payStrategies: { + across: { enabled: true }, + relay: { enabled: true }, + }, + strategyOverrides: { + transactionTypes: { + perpsDeposit: { + chains: { + '0xa4b1': ['across', 'relay'], + }, + }, + }, + }, + }, + }, + }); + + expect(getStrategy(messenger, '0xa4b1', '0xabc', 'perpsDeposit')).toBe( + TransactionPayStrategy.Across, + ); + }); + + it('returns undefined when no enabled strategy remains', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay: { + payStrategies: { + across: { enabled: false }, + relay: { enabled: false }, + }, + strategyOrder: ['relay', 'across'], + }, + }, + }); + + expect(getStrategy(messenger)).toBeUndefined(); }); }); }); diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.ts b/packages/transaction-pay-controller/src/utils/feature-flags.ts index b8528a8904c..0ae78aadfa8 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.ts @@ -2,7 +2,6 @@ import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { uniq } from 'lodash'; -import type { TransactionPayControllerMessenger } from '..'; import { isTransactionPayStrategy, TransactionPayStrategy } from '../constants'; import { projectLogger } from '../logger'; import { @@ -10,10 +9,11 @@ import { RELAY_POLLING_INTERVAL, RELAY_QUOTE_URL, } from '../strategy/relay/constants'; +import type { TransactionPayControllerMessenger } from '../types'; const log = createModuleLogger(projectLogger, 'feature-flags'); -type StrategyOrder = [TransactionPayStrategy, ...TransactionPayStrategy[]]; +type StrategyOrder = TransactionPayStrategy[]; export const DEFAULT_GAS_BUFFER = 1.0; export const DEFAULT_FALLBACK_GAS_ESTIMATE = 900000; @@ -49,9 +49,45 @@ type FeatureFlagsRaw = { slippage?: number; slippageTokens?: Record>; strategyOrder?: string[]; + strategyOverrides?: StrategyOverridesRaw; payStrategies?: PayStrategiesConfigRaw; }; +type StrategyOverrideRaw = { + default?: unknown; + chains?: Record; + tokens?: Record>; +}; + +type StrategyOverridesRaw = { + default?: StrategyOverrideRaw; + transactionTypes?: Record; +}; + +type StrategyOverride = { + chains: Record; + default?: TransactionPayStrategy[]; + tokens: Record>; +}; + +type StrategyOverrides = { + default?: StrategyOverride; + transactionTypes: Record; +}; + +type StrategyRoutingConfig = { + payStrategies: { + across: { + enabled: boolean; + }; + relay: { + enabled: boolean; + }; + }; + strategyOverrides: StrategyOverrides; + strategyOrder: TransactionPayStrategy[]; +}; + export type FeatureFlags = { relayDisabledGasStationChains: Hex[]; relayExecuteUrl: string; @@ -99,32 +135,247 @@ export type PayStrategiesConfig = { }; }; +function normalizeHex(value: string | undefined): Hex | undefined { + return value?.toLowerCase() as Hex | undefined; +} + +function normalizeStrategy( + strategy: unknown, +): TransactionPayStrategy | undefined { + if (typeof strategy !== 'string') { + return undefined; + } + + const normalizedStrategy = strategy.toLowerCase() as TransactionPayStrategy; + + return isTransactionPayStrategy(normalizedStrategy) + ? normalizedStrategy + : undefined; +} + +function normalizeStrategyList(strategies: unknown): TransactionPayStrategy[] { + if (!Array.isArray(strategies)) { + return []; + } + + return uniq( + strategies + .map((strategy) => normalizeStrategy(strategy)) + .filter( + (strategy): strategy is TransactionPayStrategy => + strategy !== undefined, + ), + ); +} + +function normalizeStrategyOverride( + override: StrategyOverrideRaw | undefined, +): StrategyOverride { + const chains = Object.entries(override?.chains ?? {}).reduce< + Record + >((result, [chainId, strategies]) => { + const normalizedStrategies = normalizeStrategyList(strategies); + + if (normalizedStrategies.length) { + result[normalizeHex(chainId) as Hex] = normalizedStrategies; + } + + return result; + }, {}); + + const tokens = Object.entries(override?.tokens ?? {}).reduce< + Record> + >((result, [chainId, tokenOverrides]) => { + const normalizedTokenOverrides = Object.entries( + tokenOverrides ?? {}, + ).reduce>( + (tokenResult, [tokenAddress, strategies]) => { + const normalizedStrategies = normalizeStrategyList(strategies); + + if (normalizedStrategies.length) { + tokenResult[normalizeHex(tokenAddress) as Hex] = normalizedStrategies; + } + + return tokenResult; + }, + {}, + ); + + if (Object.keys(normalizedTokenOverrides).length) { + result[normalizeHex(chainId) as Hex] = normalizedTokenOverrides; + } + + return result; + }, {}); + + const defaultStrategies = normalizeStrategyList(override?.default); + + return { + chains, + default: defaultStrategies.length ? defaultStrategies : undefined, + tokens, + }; +} + +function normalizeStrategyRoutingConfig( + featureFlags: FeatureFlagsRaw, +): StrategyRoutingConfig { + const strategyOrder = normalizeStrategyList(featureFlags.strategyOrder); + + return { + payStrategies: { + across: { + enabled: featureFlags.payStrategies?.across?.enabled ?? false, + }, + relay: { + enabled: featureFlags.payStrategies?.relay?.enabled ?? true, + }, + }, + strategyOverrides: { + default: featureFlags.strategyOverrides?.default + ? normalizeStrategyOverride(featureFlags.strategyOverrides.default) + : undefined, + transactionTypes: Object.entries( + featureFlags.strategyOverrides?.transactionTypes ?? {}, + ).reduce>((result, [type, override]) => { + result[type] = normalizeStrategyOverride(override); + return result; + }, {}), + }, + strategyOrder: + strategyOrder.length > 0 ? strategyOrder : [...DEFAULT_STRATEGY_ORDER], + }; +} + +function getStrategyRoutingConfig( + messenger: TransactionPayControllerMessenger, +): StrategyRoutingConfig { + const state = messenger.call('RemoteFeatureFlagController:getState'); + const featureFlags = state.remoteFeatureFlags?.confirmations_pay as + | FeatureFlagsRaw + | undefined; + + return normalizeStrategyRoutingConfig(featureFlags ?? {}); +} + +function filterEnabledStrategies( + strategies: readonly TransactionPayStrategy[], + routingConfig: StrategyRoutingConfig, +): TransactionPayStrategy[] { + return strategies.filter((strategy) => { + if (strategy === TransactionPayStrategy.Across) { + return routingConfig.payStrategies.across.enabled; + } + + if (strategy === TransactionPayStrategy.Relay) { + return routingConfig.payStrategies.relay.enabled; + } + + return true; + }); +} + +function getTokenOverrideStrategies( + override: StrategyOverride | undefined, + normalizedChainId: Hex | undefined, + normalizedTokenAddress: Hex | undefined, +): readonly TransactionPayStrategy[] | undefined { + if (!override || !normalizedChainId || !normalizedTokenAddress) { + return undefined; + } + + return override.tokens[normalizedChainId]?.[normalizedTokenAddress]; +} + +function getChainOverrideStrategies( + override: StrategyOverride | undefined, + normalizedChainId: Hex | undefined, +): readonly TransactionPayStrategy[] | undefined { + if (!override || !normalizedChainId) { + return undefined; + } + + return override.chains[normalizedChainId]; +} + +function getDefaultOverrideStrategies( + override: StrategyOverride | undefined, +): readonly TransactionPayStrategy[] | undefined { + return override?.default; +} + /** - * Get ordered list of strategies to try. + * Get ordered list of strategies to try for a route. * * @param messenger - Controller messenger. + * @param chainId - Optional chain ID used to match route overrides. + * @param tokenAddress - Optional token address used to match route overrides. + * @param transactionType - Optional transaction type used to match route + * overrides. * @returns Ordered strategy list. */ export function getStrategyOrder( messenger: TransactionPayControllerMessenger, + chainId?: Hex, + tokenAddress?: Hex, + transactionType?: string, ): StrategyOrder { - const { strategyOrder: strategyPriority } = getFeatureFlagsRaw(messenger); - - if (!Array.isArray(strategyPriority)) { - return [...DEFAULT_STRATEGY_ORDER]; - } - - const validStrategyPriority = uniq( - strategyPriority.filter((strategy): strategy is TransactionPayStrategy => - isTransactionPayStrategy(strategy), + const routingConfig = getStrategyRoutingConfig(messenger); + const normalizedChainId = normalizeHex(chainId); + const normalizedTokenAddress = normalizeHex(tokenAddress); + const transactionTypeOverride = transactionType + ? routingConfig.strategyOverrides.transactionTypes[transactionType] + : undefined; + + const candidates: (readonly TransactionPayStrategy[] | undefined)[] = [ + getTokenOverrideStrategies( + transactionTypeOverride, + normalizedChainId, + normalizedTokenAddress, ), - ); - - if (!validStrategyPriority.length) { - return [...DEFAULT_STRATEGY_ORDER]; + getChainOverrideStrategies(transactionTypeOverride, normalizedChainId), + getTokenOverrideStrategies( + routingConfig.strategyOverrides.default, + normalizedChainId, + normalizedTokenAddress, + ), + getChainOverrideStrategies( + routingConfig.strategyOverrides.default, + normalizedChainId, + ), + getDefaultOverrideStrategies(transactionTypeOverride), + getDefaultOverrideStrategies(routingConfig.strategyOverrides.default), + ]; + + // Overrides are authoritative. Once a route matches a specific override + // scope, disabled strategies do not inherit candidates from lower-precedence + // scopes. + for (const strategies of candidates) { + if (strategies) { + return filterEnabledStrategies(strategies, routingConfig); + } } - return validStrategyPriority as StrategyOrder; + return filterEnabledStrategies(routingConfig.strategyOrder, routingConfig); +} + +/** + * Get the preferred strategy for a route. + * + * @param messenger - Controller messenger. + * @param chainId - Optional chain ID used to match route overrides. + * @param tokenAddress - Optional token address used to match route overrides. + * @param transactionType - Optional transaction type used to match route + * overrides. + * @returns The preferred strategy, if any. + */ +export function getStrategy( + messenger: TransactionPayControllerMessenger, + chainId?: Hex, + tokenAddress?: Hex, + transactionType?: string, +): TransactionPayStrategy | undefined { + return getStrategyOrder(messenger, chainId, tokenAddress, transactionType)[0]; } /** @@ -136,7 +387,11 @@ export function getStrategyOrder( export function getFeatureFlags( messenger: TransactionPayControllerMessenger, ): FeatureFlags { - const featureFlags = getFeatureFlagsRaw(messenger); + const state = messenger.call('RemoteFeatureFlagController:getState'); + const featureFlags = + (state.remoteFeatureFlags?.confirmations_pay as + | FeatureFlagsRaw + | undefined) ?? {}; const estimate = featureFlags.relayFallbackGas?.estimate ?? DEFAULT_FALLBACK_GAS_ESTIMATE; @@ -178,7 +433,11 @@ export function getFeatureFlags( export function getPayStrategiesConfig( messenger: TransactionPayControllerMessenger, ): PayStrategiesConfig { - const featureFlags = getFeatureFlagsRaw(messenger); + const state = messenger.call('RemoteFeatureFlagController:getState'); + const featureFlags = + (state.remoteFeatureFlags?.confirmations_pay as + | FeatureFlagsRaw + | undefined) ?? {}; const payStrategies = featureFlags.payStrategies ?? {}; const acrossRaw = payStrategies.across ?? {}; @@ -213,7 +472,11 @@ export function getPayStrategiesConfig( export function isRelayExecuteEnabled( messenger: TransactionPayControllerMessenger, ): boolean { - const featureFlags = getFeatureFlagsRaw(messenger); + const state = messenger.call('RemoteFeatureFlagController:getState'); + const featureFlags = + (state.remoteFeatureFlags?.confirmations_pay as + | FeatureFlagsRaw + | undefined) ?? {}; return featureFlags.payStrategies?.relay?.executeEnabled ?? false; } @@ -227,7 +490,11 @@ export function isRelayExecuteEnabled( export function getRelayOriginGasOverhead( messenger: TransactionPayControllerMessenger, ): string { - const featureFlags = getFeatureFlagsRaw(messenger); + const state = messenger.call('RemoteFeatureFlagController:getState'); + const featureFlags = + (state.remoteFeatureFlags?.confirmations_pay as + | FeatureFlagsRaw + | undefined) ?? {}; return ( featureFlags.payStrategies?.relay?.originGasOverhead ?? DEFAULT_RELAY_ORIGIN_GAS_OVERHEAD @@ -244,7 +511,11 @@ export function getRelayOriginGasOverhead( export function getRelayPollingInterval( messenger: TransactionPayControllerMessenger, ): number { - const featureFlags = getFeatureFlagsRaw(messenger); + const state = messenger.call('RemoteFeatureFlagController:getState'); + const featureFlags = + (state.remoteFeatureFlags?.confirmations_pay as + | FeatureFlagsRaw + | undefined) ?? {}; return ( featureFlags.payStrategies?.relay?.pollingInterval ?? RELAY_POLLING_INTERVAL ); @@ -260,7 +531,11 @@ export function getRelayPollingInterval( export function getRelayPollingTimeout( messenger: TransactionPayControllerMessenger, ): number | undefined { - const featureFlags = getFeatureFlagsRaw(messenger); + const state = messenger.call('RemoteFeatureFlagController:getState'); + const featureFlags = + (state.remoteFeatureFlags?.confirmations_pay as + | FeatureFlagsRaw + | undefined) ?? {}; return featureFlags.payStrategies?.relay?.pollingTimeout; } @@ -287,7 +562,11 @@ export function getGasBuffer( messenger: TransactionPayControllerMessenger, chainId: Hex, ): number { - const featureFlags = getFeatureFlagsRaw(messenger); + const state = messenger.call('RemoteFeatureFlagController:getState'); + const featureFlags = + (state.remoteFeatureFlags?.confirmations_pay as + | FeatureFlagsRaw + | undefined) ?? {}; return ( featureFlags.gasBuffer?.perChainConfig?.[chainId]?.buffer ?? @@ -310,7 +589,11 @@ export function getSlippage( chainId: Hex, tokenAddress: Hex, ): number { - const featureFlags = getFeatureFlagsRaw(messenger); + const state = messenger.call('RemoteFeatureFlagController:getState'); + const featureFlags = + (state.remoteFeatureFlags?.confirmations_pay as + | FeatureFlagsRaw + | undefined) ?? {}; const { slippageTokens } = featureFlags; const tokenMap = getCaseInsensitive(slippageTokens, chainId); @@ -400,16 +683,3 @@ export function isEIP7702Chain( (supported) => supported.toLowerCase() === chainId.toLowerCase(), ); } - -/** - * Get the raw feature flags from the remote feature flag controller. - * - * @param messenger - Controller messenger. - * @returns Raw feature flags. - */ -function getFeatureFlagsRaw( - messenger: TransactionPayControllerMessenger, -): FeatureFlagsRaw { - const state = messenger.call('RemoteFeatureFlagController:getState'); - return (state.remoteFeatureFlags.confirmations_pay as FeatureFlagsRaw) ?? {}; -}