From 23d240117d5d54858c12973eaef77881c6b03683 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Mon, 21 Sep 2026 12:51:49 +0100 Subject: [PATCH 1/4] feat(transaction-pay-controller): gate server strategy per transaction type `ServerStrategy.supports` previously returned the `payStrategies.server.enabled` feature flag directly, so enabling the strategy selected it for every Pay flow at once, including flows it does not yet implement. Add a `server-support` module with two independent gating layers: - An allowlist of transaction types, sourced from the new `payStrategies.server.enabledTransactionTypes` remote feature flag, so flows can be enabled individually. - Capability guards for request shapes the strategy does not implement, which the feature flag cannot override. Both layers match nested transactions via `hasTransactionType`, since a transaction can carry several types. --- .../transaction-pay-controller/CHANGELOG.md | 4 + .../strategy/server/ServerStrategy.test.ts | 63 ++++-- .../src/strategy/server/ServerStrategy.ts | 31 ++- .../strategy/server/server-support.test.ts | 179 ++++++++++++++++++ .../src/strategy/server/server-support.ts | 144 ++++++++++++++ .../src/utils/feature-flags.test.ts | 39 ++++ .../src/utils/feature-flags.ts | 33 ++++ 7 files changed, 480 insertions(+), 13 deletions(-) create mode 100644 packages/transaction-pay-controller/src/strategy/server/server-support.test.ts create mode 100644 packages/transaction-pay-controller/src/strategy/server/server-support.ts diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 7c31c190875..3c985ce3da7 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Gate the server pay strategy per transaction type so flows can be enabled individually + - `ServerStrategy.supports` now additionally requires the transaction, or one of its nested transactions, to have a type listed in the new `payStrategies.server.enabledTransactionTypes` remote feature flag, which defaults to an empty list; enabling `payStrategies.server.enabled` alone no longer selects the strategy for any flow. + - `ServerStrategy.supports` now also declines requests that use capabilities the strategy does not implement, regardless of the feature flag: non-atomic execution, maximum amounts, HyperLiquid activation fees, Polymarket deposit wallets, direct mUSD Money Account quotes, and transactions requiring exact-output pricing. + - `PayStrategiesConfig['server']` gains a required `enabledTransactionTypes` property. - Support subsidized max Relay deposits using atomic `EXACT_OUTPUT` quotes with transaction calls embedded, gated by `payStrategies.relay.atomicMaxEnabled` (disabled by default, with per-transaction-type overrides). ([#10224](https://github.com/MetaMask/core/pull/10224)) - Honor the client's `atomic` hint: atomic max quotes use the source-token budget adjusted to destination decimals for 1:1 subsidized stablecoin routes, without a discovery quote or reusing the original deposit amount. Unsubsidized responses are re-quoted non-atomically. - Non-atomic hints start with `EXACT_INPUT` and upgrade to atomic execution when subsidized. diff --git a/packages/transaction-pay-controller/src/strategy/server/ServerStrategy.test.ts b/packages/transaction-pay-controller/src/strategy/server/ServerStrategy.test.ts index ea705d35182..b58934797f4 100644 --- a/packages/transaction-pay-controller/src/strategy/server/ServerStrategy.test.ts +++ b/packages/transaction-pay-controller/src/strategy/server/ServerStrategy.test.ts @@ -1,7 +1,12 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; +import { TransactionType } from '@metamask/transaction-controller'; import type { TransactionPayControllerMessenger } from '../../index.js'; -import type { TransactionPayQuote } from '../../types.js'; +import type { + PayStrategyGetQuotesRequest, + QuoteRequest, + TransactionPayQuote, +} from '../../types.js'; import { getPayStrategiesConfig } from '../../utils/feature-flags.js'; import { getServerQuotes } from './server-quotes.js'; import { submitServerQuotes } from './server-submit.js'; @@ -16,6 +21,23 @@ const QUOTE_MOCK = { estimatedDuration: 5, } as TransactionPayQuote; +const QUOTE_REQUEST_MOCK = { + from: '0x1234567890123456789012345678901234567890', + sourceBalanceRaw: '1000000', + sourceChainId: '0x1', + sourceTokenAddress: '0x0000000000000000000000000000000000000001', + sourceTokenAmount: '1000', + targetAmountMinimum: '900', + targetChainId: '0xa4b1', + targetTokenAddress: '0x0000000000000000000000000000000000000002', +} as QuoteRequest; + +const SUPPORTS_REQUEST_MOCK = { + messenger: {} as TransactionPayControllerMessenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { type: TransactionType.perpsDeposit } as TransactionMeta, +} as PayStrategyGetQuotesRequest; + describe('ServerStrategy', () => { const getServerQuotesMock = jest.mocked(getServerQuotes); const submitServerQuotesMock = jest.mocked(submitServerQuotes); @@ -25,30 +47,49 @@ describe('ServerStrategy', () => { jest.resetAllMocks(); getServerQuotesMock.mockResolvedValue([QUOTE_MOCK]); getPayStrategiesConfigMock.mockReturnValue({ - server: { enabled: true }, + server: { + enabled: true, + enabledTransactionTypes: [TransactionType.perpsDeposit], + }, relay: { enabled: false }, } as ReturnType); }); describe('supports', () => { - it('returns true when server strategy is enabled', () => { - expect( - new ServerStrategy().supports({ - messenger: {} as TransactionPayControllerMessenger, - } as never), - ).toBe(true); + it('returns true when enabled and the flow is allowlisted', () => { + expect(new ServerStrategy().supports(SUPPORTS_REQUEST_MOCK)).toBe(true); }); it('returns false when server strategy is disabled', () => { getPayStrategiesConfigMock.mockReturnValue({ - server: { enabled: false }, + server: { + enabled: false, + enabledTransactionTypes: [TransactionType.perpsDeposit], + }, relay: { enabled: true }, } as ReturnType); + expect(new ServerStrategy().supports(SUPPORTS_REQUEST_MOCK)).toBe(false); + }); + + it('returns false when the flow is not allowlisted', () => { + getPayStrategiesConfigMock.mockReturnValue({ + server: { + enabled: true, + enabledTransactionTypes: [] as TransactionType[], + }, + relay: { enabled: false }, + } as ReturnType); + + expect(new ServerStrategy().supports(SUPPORTS_REQUEST_MOCK)).toBe(false); + }); + + it('returns false when a request uses an unsupported capability', () => { expect( new ServerStrategy().supports({ - messenger: {} as TransactionPayControllerMessenger, - } as never), + ...SUPPORTS_REQUEST_MOCK, + requests: [{ ...QUOTE_REQUEST_MOCK, isMaxAmount: true }], + }), ).toBe(false); }); }); diff --git a/packages/transaction-pay-controller/src/strategy/server/ServerStrategy.ts b/packages/transaction-pay-controller/src/strategy/server/ServerStrategy.ts index 398ed12fc91..197c96191d1 100644 --- a/packages/transaction-pay-controller/src/strategy/server/ServerStrategy.ts +++ b/packages/transaction-pay-controller/src/strategy/server/ServerStrategy.ts @@ -1,3 +1,6 @@ +import { createModuleLogger } from '@metamask/utils'; + +import { projectLogger } from '../../logger.js'; import type { PayStrategy, PayStrategyExecuteRequest, @@ -7,18 +10,42 @@ import type { import { getPayStrategiesConfig } from '../../utils/feature-flags.js'; import { getServerQuotes } from './server-quotes.js'; import { submitServerQuotes } from './server-submit.js'; +import { getServerUnsupportedReason } from './server-support.js'; import type { ServerQuote } from './types.js'; +const log = createModuleLogger(projectLogger, 'server-strategy'); + /** * Pay strategy that routes quote, submit, and status requests through the * MetaMask intents API. Supports both gasless (delegated server-execute) and * non-gasless (TransactionController-submitted) execution paths. Gated by the - * `payStrategies.server.enabled` remote feature flag. + * `payStrategies.server.enabled` remote feature flag, then narrowed per flow by + * `payStrategies.server.enabledTransactionTypes`. */ export class ServerStrategy implements PayStrategy { supports(request: PayStrategyGetQuotesRequest): boolean { const config = getPayStrategiesConfig(request.messenger); - return config.server.enabled; + + if (!config.server.enabled) { + return false; + } + + const reason = getServerUnsupportedReason({ + enabledTransactionTypes: config.server.enabledTransactionTypes, + requests: request.requests, + transaction: request.transaction, + }); + + if (reason) { + log('Unsupported request', { + reason, + transactionType: request.transaction?.type, + }); + + return false; + } + + return true; } async getQuotes( diff --git a/packages/transaction-pay-controller/src/strategy/server/server-support.test.ts b/packages/transaction-pay-controller/src/strategy/server/server-support.test.ts new file mode 100644 index 00000000000..fa41ec15322 --- /dev/null +++ b/packages/transaction-pay-controller/src/strategy/server/server-support.test.ts @@ -0,0 +1,179 @@ +import type { TransactionMeta } from '@metamask/transaction-controller'; +import { TransactionType } from '@metamask/transaction-controller'; + +import type { QuoteRequest } from '../../types.js'; +import { + DEFAULT_SERVER_ENABLED_TRANSACTION_TYPES, + getServerUnsupportedReason, + ServerUnsupportedReason, +} from './server-support.js'; + +const REQUEST_MOCK: QuoteRequest = { + from: '0x1234567890123456789012345678901234567890', + sourceBalanceRaw: '1000000', + sourceChainId: '0x1', + sourceTokenAddress: '0x0000000000000000000000000000000000000001', + sourceTokenAmount: '1000', + targetAmountMinimum: '900', + targetChainId: '0xa4b1', + targetTokenAddress: '0x0000000000000000000000000000000000000002', +}; + +const TRANSACTION_MOCK = { + type: TransactionType.perpsDeposit, +} as TransactionMeta; + +/** + * Invoke the util with sensible defaults for a supported perps deposit. + * + * @param overrides - Values to override on the default arguments. + * @param overrides.enabledTransactionTypes - Allowlisted transaction types. + * @param overrides.requests - Quote requests. + * @param overrides.transaction - Parent transaction. + * @returns The unsupported reason, if any. + */ +function getReason({ + enabledTransactionTypes = [TransactionType.perpsDeposit], + requests = [REQUEST_MOCK], + transaction = TRANSACTION_MOCK, +}: { + enabledTransactionTypes?: TransactionType[]; + requests?: QuoteRequest[]; + transaction?: TransactionMeta; +} = {}): ServerUnsupportedReason | undefined { + return getServerUnsupportedReason({ + enabledTransactionTypes, + requests, + transaction, + }); +} + +describe('server-support', () => { + describe('DEFAULT_SERVER_ENABLED_TRANSACTION_TYPES', () => { + it('is empty so enabling the strategy alone diverts no traffic', () => { + expect(DEFAULT_SERVER_ENABLED_TRANSACTION_TYPES).toStrictEqual([]); + }); + }); + + describe('getServerUnsupportedReason', () => { + it('returns undefined when the flow is allowlisted and uses no unsupported capability', () => { + expect(getReason()).toBeUndefined(); + }); + + it('returns undefined when there are no requests', () => { + expect(getReason({ requests: [] })).toBeUndefined(); + }); + + it('supports a flow only once it is added to the allowlist', () => { + expect(getReason({ enabledTransactionTypes: [] })).toBe( + ServerUnsupportedReason.DisabledTransactionType, + ); + + expect( + getReason({ enabledTransactionTypes: [TransactionType.perpsDeposit] }), + ).toBeUndefined(); + }); + + it('returns disabled transaction type when the flow is not allowlisted', () => { + expect( + getReason({ enabledTransactionTypes: [TransactionType.predictDeposit] }), + ).toBe(ServerUnsupportedReason.DisabledTransactionType); + }); + + it('returns disabled transaction type when the transaction has no type', () => { + expect(getReason({ transaction: {} as TransactionMeta })).toBe( + ServerUnsupportedReason.DisabledTransactionType, + ); + }); + + it('returns undefined when a nested transaction has an allowlisted type', () => { + expect( + getReason({ + transaction: { + nestedTransactions: [{ type: TransactionType.perpsDeposit }], + type: TransactionType.batch, + } as TransactionMeta, + }), + ).toBeUndefined(); + }); + + it.each([ + TransactionType.perpsDepositAndOrder, + TransactionType.predictDepositAndOrder, + ])( + 'returns exact output for %s even when explicitly allowlisted', + (transactionType) => { + expect( + getReason({ + enabledTransactionTypes: [transactionType], + transaction: { type: transactionType } as TransactionMeta, + }), + ).toBe(ServerUnsupportedReason.ExactOutput); + }, + ); + + it('returns exact output when a nested transaction requires it', () => { + expect( + getReason({ + transaction: { + nestedTransactions: [ + { type: TransactionType.perpsDepositAndOrder }, + ], + type: TransactionType.perpsDeposit, + } as TransactionMeta, + }), + ).toBe(ServerUnsupportedReason.ExactOutput); + }); + + it.each([ + ['atomic', { atomic: false }, ServerUnsupportedReason.NonAtomic], + [ + 'hyperliquidActivationFeeUsd', + { hyperliquidActivationFeeUsd: '1' }, + ServerUnsupportedReason.HyperliquidActivationFee, + ], + [ + 'isDirectMusdMoneyAccount', + { isDirectMusdMoneyAccount: true }, + ServerUnsupportedReason.DirectMusdMoneyAccount, + ], + ['isMaxAmount', { isMaxAmount: true }, ServerUnsupportedReason.MaxAmount], + [ + 'isPolymarketDepositWallet', + { isPolymarketDepositWallet: true }, + ServerUnsupportedReason.PolymarketDepositWallet, + ], + ] as const)( + 'returns %s reason for an unsupported capability', + (_name, overrides, expected) => { + expect( + getReason({ requests: [{ ...REQUEST_MOCK, ...overrides }] }), + ).toBe(expected); + }, + ); + + it('returns a reason when any request is unsupported', () => { + expect( + getReason({ + requests: [REQUEST_MOCK, { ...REQUEST_MOCK, isMaxAmount: true }], + }), + ).toBe(ServerUnsupportedReason.MaxAmount); + }); + + it('ignores capability flags that are explicitly false or undefined', () => { + expect( + getReason({ + requests: [ + { + ...REQUEST_MOCK, + atomic: true, + isDirectMusdMoneyAccount: false, + isMaxAmount: false, + isPolymarketDepositWallet: false, + }, + ], + }), + ).toBeUndefined(); + }); + }); +}); diff --git a/packages/transaction-pay-controller/src/strategy/server/server-support.ts b/packages/transaction-pay-controller/src/strategy/server/server-support.ts new file mode 100644 index 00000000000..e0b4e33d177 --- /dev/null +++ b/packages/transaction-pay-controller/src/strategy/server/server-support.ts @@ -0,0 +1,144 @@ +import type { TransactionMeta } from '@metamask/transaction-controller'; +import { + hasTransactionType, + TransactionType, +} from '@metamask/transaction-controller'; + +import type { QuoteRequest } from '../../types.js'; + +/** + * Parent transaction types enabled for the server strategy by default. + * + * Intentionally empty so that turning on `payStrategies.server.enabled` alone + * never diverts live traffic away from the relay strategy. Flows are opted in + * one at a time via the `payStrategies.server.enabledTransactionTypes` remote + * feature flag, which lets a single flow be validated in isolation before the + * next one is added. + */ +export const DEFAULT_SERVER_ENABLED_TRANSACTION_TYPES: TransactionType[] = []; + +/** + * Transaction types whose quote must be priced as an exact output. + * + * The transaction embeds a call that transfers exactly `targetAmountMinimum`, + * so a quote priced on expected output underfunds the transfer and reverts. + * The server strategy cannot express this yet because {@link ServerTradeType} + * has no exact-output member, and these flows route to HyperCore, which + * suppresses the embedded calls that would otherwise let the backend infer it. + * + * Enforced independently of the remote flag so the flag cannot enable a flow + * that is known to misprice. + */ +const EXACT_OUTPUT_TRANSACTION_TYPES: TransactionType[] = [ + TransactionType.perpsDepositAndOrder, + TransactionType.predictDepositAndOrder, +]; + +/** Why the server strategy declined to handle a request. */ +export enum ServerUnsupportedReason { + /** The direct mUSD Money Account fiat flow is not implemented. */ + DirectMusdMoneyAccount = 'directMusdMoneyAccount', + + /** The parent transaction type is not in the remote-flag allowlist. */ + DisabledTransactionType = 'disabledTransactionType', + + /** The flow requires exact-output pricing, which is not implemented. */ + ExactOutput = 'exactOutput', + + /** Reserving the HyperLiquid activation fee is not implemented. */ + HyperliquidActivationFee = 'hyperliquidActivationFee', + + /** Two-phase max-amount gas station probing is not implemented. */ + MaxAmount = 'maxAmount', + + /** Non-atomic multi-leg submission is not implemented. */ + NonAtomic = 'nonAtomic', + + /** Polymarket deposit-wallet routing is not implemented. */ + PolymarketDepositWallet = 'polymarketDepositWallet', +} + +/** + * Determine whether the server strategy can handle a set of quote requests. + * + * Gating has two independent layers: + * + * 1. A remote-flag allowlist of transaction types, so flows can be rolled out + * and rolled back one at a time. + * 2. Code-level capability guards for request shapes the server strategy does + * not implement yet. These cannot be overridden by the flag; each one is + * deleted as the corresponding capability lands. + * + * A transaction can carry several types across its nested transactions, so + * both layers match against the transaction and its nested transactions. + * + * @param options - Options bag. + * @param options.enabledTransactionTypes - Transaction types opted in via + * remote feature flag. + * @param options.requests - Quote requests for required tokens. + * @param options.transaction - Metadata of the original target transaction. + * @returns The first reason the requests are unsupported, or `undefined` if + * every request is supported. + */ +export function getServerUnsupportedReason({ + enabledTransactionTypes, + requests, + transaction, +}: { + enabledTransactionTypes: TransactionType[]; + requests: QuoteRequest[]; + transaction?: TransactionMeta; +}): ServerUnsupportedReason | undefined { + if (!hasTransactionType(transaction, enabledTransactionTypes)) { + return ServerUnsupportedReason.DisabledTransactionType; + } + + // Checked after the allowlist so that a transaction combining an allowlisted + // type with an exact-output type is still declined. + if (hasTransactionType(transaction, EXACT_OUTPUT_TRANSACTION_TYPES)) { + return ServerUnsupportedReason.ExactOutput; + } + + for (const request of requests) { + const reason = getRequestUnsupportedReason(request); + + if (reason) { + return reason; + } + } + + return undefined; +} + +/** + * Determine whether a single quote request uses a capability the server + * strategy has not implemented yet. + * + * @param request - Quote request for a required token. + * @returns The reason the request is unsupported, or `undefined`. + */ +function getRequestUnsupportedReason( + request: QuoteRequest, +): ServerUnsupportedReason | undefined { + if (request.atomic === false) { + return ServerUnsupportedReason.NonAtomic; + } + + if (request.hyperliquidActivationFeeUsd !== undefined) { + return ServerUnsupportedReason.HyperliquidActivationFee; + } + + if (request.isDirectMusdMoneyAccount === true) { + return ServerUnsupportedReason.DirectMusdMoneyAccount; + } + + if (request.isMaxAmount === true) { + return ServerUnsupportedReason.MaxAmount; + } + + if (request.isPolymarketDepositWallet === true) { + return ServerUnsupportedReason.PolymarketDepositWallet; + } + + return undefined; +} 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 79acd41b901..2fe672bcb6c 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts @@ -1113,12 +1113,51 @@ describe('Feature Flags Utils', () => { expect(config.server).toStrictEqual({ enabled: false, + enabledTransactionTypes: [], baseUrl: DEFAULT_SERVER_BASE_URL, pollingInterval: 2000, pollingTimeout: undefined, }); }); + it.each([ + ['the flag is absent', undefined, []], + ['the flag is not an array', 'perpsDeposit', []], + [ + 'the flag lists known types', + ['perpsDeposit', 'perpsWithdraw'], + [TransactionType.perpsDeposit, TransactionType.perpsWithdraw], + ], + [ + 'the flag lists unknown types', + ['perpsDeposit', 'notATransactionType'], + [TransactionType.perpsDeposit], + ], + [ + 'the flag lists duplicates', + ['perpsDeposit', 'perpsDeposit'], + [TransactionType.perpsDeposit], + ], + ])( + 'returns enabled transaction types when %s', + (_name, enabledTransactionTypes, expected) => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_extended: { + payStrategies: { + server: { enabledTransactionTypes }, + }, + }, + }, + }); + + expect( + getPayStrategiesConfig(messenger).server.enabledTransactionTypes, + ).toStrictEqual(expected); + }, + ); + it('returns enabled: true when the flag enables server', () => { getRemoteFeatureFlagControllerStateMock.mockReturnValue({ ...getDefaultRemoteFeatureFlagControllerState(), diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.ts b/packages/transaction-pay-controller/src/utils/feature-flags.ts index f88d26e8dd8..d28d3890201 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.ts @@ -29,6 +29,7 @@ import { SERVER_POLLING_INTERVAL, SERVER_URL_BASE, } from '../strategy/server/constants.js'; +import { DEFAULT_SERVER_ENABLED_TRANSACTION_TYPES } from '../strategy/server/server-support.js'; import type { TransactionPayControllerMessenger } from '../types.js'; const log = createModuleLogger(projectLogger, 'feature-flags'); @@ -210,6 +211,7 @@ type FeatureFlagsExtendedRaw = { }; server?: { enabled?: boolean; + enabledTransactionTypes?: string[]; baseUrl?: string; pollingInterval?: number; pollingTimeout?: number; @@ -226,6 +228,12 @@ export type PayStrategiesConfig = { across: AcrossConfig; server: { enabled: boolean; + /** + * Parent transaction types the server strategy is allowed to handle. + * Lets flows be enabled one at a time while the strategy reaches parity + * with the relay strategy. + */ + enabledTransactionTypes: TransactionType[]; baseUrl: string; pollingInterval: number; pollingTimeout?: number; @@ -239,6 +247,28 @@ function normalizeHex(value: string | undefined): Hex | undefined { return value?.toLowerCase() as Hex | undefined; } +/** + * Convert raw remote-flag transaction type strings into known transaction + * types, discarding any value the client does not recognise. + * + * @param values - Raw transaction type strings from the remote feature flag. + * @returns Recognised transaction types, or the default allowlist when the + * flag is absent. + */ +function normalizeTransactionTypes( + values: string[] | undefined, +): TransactionType[] { + if (!Array.isArray(values)) { + return DEFAULT_SERVER_ENABLED_TRANSACTION_TYPES; + } + + const knownTypes = Object.values(TransactionType) as string[]; + + return uniq(values.filter((value) => knownTypes.includes(value))).map( + (value) => value as TransactionType, + ); +} + function normalizeStrategy( strategy: unknown, ): TransactionPayStrategy | undefined { @@ -618,6 +648,9 @@ export function getPayStrategiesConfig( const server = { enabled: serverRaw.enabled ?? false, + enabledTransactionTypes: normalizeTransactionTypes( + serverRaw.enabledTransactionTypes, + ), baseUrl: serverRaw.baseUrl ?? DEFAULT_SERVER_BASE_URL, pollingInterval: serverRaw.pollingInterval ?? SERVER_POLLING_INTERVAL, pollingTimeout: serverRaw.pollingTimeout, From f7ec391af595a2c1b55ffc3dfcbd1ec0cfadf5c2 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Mon, 21 Sep 2026 12:52:21 +0100 Subject: [PATCH 2/4] chore: link changelog entry to PR --- 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 3c985ce3da7..98e22143e86 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Gate the server pay strategy per transaction type so flows can be enabled individually +- Gate the server pay strategy per transaction type so flows can be enabled individually ([#10312](https://github.com/MetaMask/core/pull/10312)) - `ServerStrategy.supports` now additionally requires the transaction, or one of its nested transactions, to have a type listed in the new `payStrategies.server.enabledTransactionTypes` remote feature flag, which defaults to an empty list; enabling `payStrategies.server.enabled` alone no longer selects the strategy for any flow. - `ServerStrategy.supports` now also declines requests that use capabilities the strategy does not implement, regardless of the feature flag: non-atomic execution, maximum amounts, HyperLiquid activation fees, Polymarket deposit wallets, direct mUSD Money Account quotes, and transactions requiring exact-output pricing. - `PayStrategiesConfig['server']` gains a required `enabledTransactionTypes` property. From ed5312cdf81a4476c20a7ab9215edcd331eb1972 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Mon, 21 Sep 2026 16:11:11 +0100 Subject: [PATCH 3/4] chore: satisfy oxfmt formatting check --- .../src/strategy/server/server-support.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/src/strategy/server/server-support.test.ts b/packages/transaction-pay-controller/src/strategy/server/server-support.test.ts index fa41ec15322..886351a66a4 100644 --- a/packages/transaction-pay-controller/src/strategy/server/server-support.test.ts +++ b/packages/transaction-pay-controller/src/strategy/server/server-support.test.ts @@ -76,7 +76,9 @@ describe('server-support', () => { it('returns disabled transaction type when the flow is not allowlisted', () => { expect( - getReason({ enabledTransactionTypes: [TransactionType.predictDeposit] }), + getReason({ + enabledTransactionTypes: [TransactionType.predictDeposit], + }), ).toBe(ServerUnsupportedReason.DisabledTransactionType); }); From fa495afa9441989c33dc0a3d7018b2ef98dc1aeb Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Mon, 21 Sep 2026 16:43:47 +0100 Subject: [PATCH 4/4] chore: simplify changelog entry and drop property JSDoc --- packages/transaction-pay-controller/CHANGELOG.md | 5 ++--- .../transaction-pay-controller/src/utils/feature-flags.ts | 5 ----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 98e22143e86..999d0131779 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -10,9 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Gate the server pay strategy per transaction type so flows can be enabled individually ([#10312](https://github.com/MetaMask/core/pull/10312)) - - `ServerStrategy.supports` now additionally requires the transaction, or one of its nested transactions, to have a type listed in the new `payStrategies.server.enabledTransactionTypes` remote feature flag, which defaults to an empty list; enabling `payStrategies.server.enabled` alone no longer selects the strategy for any flow. - - `ServerStrategy.supports` now also declines requests that use capabilities the strategy does not implement, regardless of the feature flag: non-atomic execution, maximum amounts, HyperLiquid activation fees, Polymarket deposit wallets, direct mUSD Money Account quotes, and transactions requiring exact-output pricing. - - `PayStrategiesConfig['server']` gains a required `enabledTransactionTypes` property. + - `ServerStrategy.supports` now also requires a transaction type listed in the new `payStrategies.server.enabledTransactionTypes` remote feature flag, which defaults to empty. + - `ServerStrategy.supports` now declines flows using capabilities the strategy does not implement yet, regardless of the flag. - Support subsidized max Relay deposits using atomic `EXACT_OUTPUT` quotes with transaction calls embedded, gated by `payStrategies.relay.atomicMaxEnabled` (disabled by default, with per-transaction-type overrides). ([#10224](https://github.com/MetaMask/core/pull/10224)) - Honor the client's `atomic` hint: atomic max quotes use the source-token budget adjusted to destination decimals for 1:1 subsidized stablecoin routes, without a discovery quote or reusing the original deposit amount. Unsubsidized responses are re-quoted non-atomically. - Non-atomic hints start with `EXACT_INPUT` and upgrade to atomic execution when subsidized. diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.ts b/packages/transaction-pay-controller/src/utils/feature-flags.ts index d28d3890201..61eb679831a 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.ts @@ -228,11 +228,6 @@ export type PayStrategiesConfig = { across: AcrossConfig; server: { enabled: boolean; - /** - * Parent transaction types the server strategy is allowed to handle. - * Lets flows be enabled one at a time while the strategy reaches parity - * with the relay strategy. - */ enabledTransactionTypes: TransactionType[]; baseUrl: string; pollingInterval: number;