From 52950da00384f848ccc9b482e13ba16dca92cf07 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Mon, 20 Apr 2026 17:59:07 +0100 Subject: [PATCH 1/4] fix(transaction-pay-controller): ignore synthetic across gas legs --- .../strategy/across/AcrossStrategy.test.ts | 36 +++++++++++++++++++ .../src/strategy/across/AcrossStrategy.ts | 19 ++++++---- .../src/strategy/across/across-quotes.ts | 28 +++++++-------- .../src/strategy/across/requests.ts | 9 +++++ 4 files changed, 70 insertions(+), 22 deletions(-) create mode 100644 packages/transaction-pay-controller/src/strategy/across/requests.ts diff --git a/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.test.ts b/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.test.ts index e3620d83ad3..d7107b4e6ee 100644 --- a/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.test.ts @@ -117,6 +117,42 @@ describe('AcrossStrategy', () => { ).toBe(true); }); + it('ignores synthetic gas legs for supported perps direct deposits', () => { + const strategy = new AcrossStrategy(); + expect( + strategy.supports({ + ...baseRequest, + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.perpsDeposit, + } as TransactionMeta, + requests: [ + { + from: '0xabc' as Hex, + sourceBalanceRaw: '100', + sourceChainId: CHAIN_ID_ARBITRUM, + sourceTokenAddress: ARBITRUM_USDC_ADDRESS, + sourceTokenAmount: '100', + targetAmountMinimum: '100', + targetChainId: CHAIN_ID_ARBITRUM, + targetTokenAddress: ARBITRUM_USDC_ADDRESS, + }, + { + from: '0xabc' as Hex, + sourceBalanceRaw: '100', + sourceChainId: CHAIN_ID_ARBITRUM, + sourceTokenAddress: ARBITRUM_USDC_ADDRESS, + sourceTokenAmount: '100', + targetAmountMinimum: '0', + targetChainId: CHAIN_ID_ARBITRUM, + targetTokenAddress: + '0x0000000000000000000000000000000000000000' as Hex, + }, + ], + }), + ).toBe(true); + }); + it('returns false for unsupported perps deposits', () => { const strategy = new AcrossStrategy(); expect( diff --git a/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.ts b/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.ts index 5643e58aff1..09106a05cc2 100644 --- a/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.ts +++ b/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.ts @@ -1,5 +1,10 @@ import { TransactionType } from '@metamask/transaction-controller'; +import { getAcrossQuotes } from './across-quotes'; +import { submitAcrossQuotes } from './across-submit'; +import { isSupportedAcrossPerpsDepositRequest } from './perps'; +import { isAcrossQuoteRequest } from './requests'; +import type { AcrossQuote } from './types'; import type { PayStrategy, PayStrategyExecuteRequest, @@ -7,10 +12,6 @@ import type { TransactionPayQuote, } from '../../types'; import { getPayStrategiesConfig } from '../../utils/feature-flags'; -import { getAcrossQuotes } from './across-quotes'; -import { submitAcrossQuotes } from './across-submit'; -import { isSupportedAcrossPerpsDepositRequest } from './perps'; -import type { AcrossQuote } from './types'; export class AcrossStrategy implements PayStrategy { supports(request: PayStrategyGetQuotesRequest): boolean { @@ -20,8 +21,14 @@ export class AcrossStrategy implements PayStrategy { return false; } + const actionableRequests = request.requests.filter(isAcrossQuoteRequest); + + if (actionableRequests.length === 0) { + return false; + } + if (request.transaction?.type === TransactionType.perpsDeposit) { - return request.requests.every((singleRequest) => + return actionableRequests.every((singleRequest) => isSupportedAcrossPerpsDepositRequest( singleRequest, request.transaction?.type, @@ -30,7 +37,7 @@ export class AcrossStrategy implements PayStrategy { } // Across doesn't support same-chain swaps (e.g. mUSD conversions). - return request.requests.every( + return actionableRequests.every( (singleRequest) => singleRequest.sourceChainId !== singleRequest.targetChainId, ); diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts index ee84439f0f2..d668dfeca1e 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -3,6 +3,17 @@ import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; +import { getAcrossDestination } from './across-actions'; +import { normalizeAcrossRequest } from './perps'; +import { isAcrossQuoteRequest } from './requests'; +import { getAcrossOrderedTransactions } from './transactions'; +import type { + AcrossAction, + AcrossActionRequestBody, + AcrossGasLimits, + AcrossQuote, + AcrossSwapApprovalResponse, +} from './types'; import { TransactionPayStrategy } from '../../constants'; import { projectLogger } from '../../logger'; import type { @@ -18,16 +29,6 @@ import { getPayStrategiesConfig, getSlippage } from '../../utils/feature-flags'; import { calculateGasCost } from '../../utils/gas'; import { estimateQuoteGasLimits } from '../../utils/quote-gas'; import { getTokenFiatRate } from '../../utils/token'; -import { getAcrossDestination } from './across-actions'; -import { normalizeAcrossRequest } from './perps'; -import { getAcrossOrderedTransactions } from './transactions'; -import type { - AcrossAction, - AcrossActionRequestBody, - AcrossGasLimits, - AcrossQuote, - AcrossSwapApprovalResponse, -} from './types'; const log = createModuleLogger(projectLogger, 'across-strategy'); @@ -50,12 +51,7 @@ export async function getAcrossQuotes( log('Fetching quotes', requests); try { - const normalizedRequests = requests.filter( - (singleRequest) => - singleRequest.isMaxAmount === true || - (singleRequest.targetAmountMinimum !== undefined && - singleRequest.targetAmountMinimum !== '0'), - ); + const normalizedRequests = requests.filter(isAcrossQuoteRequest); if (normalizedRequests.length === 0) { return []; diff --git a/packages/transaction-pay-controller/src/strategy/across/requests.ts b/packages/transaction-pay-controller/src/strategy/across/requests.ts new file mode 100644 index 00000000000..77b967af562 --- /dev/null +++ b/packages/transaction-pay-controller/src/strategy/across/requests.ts @@ -0,0 +1,9 @@ +import type { QuoteRequest } from '../../types'; + +export function isAcrossQuoteRequest(request: QuoteRequest): boolean { + return ( + request.isMaxAmount === true || + (request.targetAmountMinimum !== undefined && + request.targetAmountMinimum !== '0') + ); +} From d4cc800c6853c0afd1adbb565f9ad54f3f728591 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Mon, 20 Apr 2026 18:18:50 +0100 Subject: [PATCH 2/4] docs(transaction-pay-controller): add changelog entry for across fix --- packages/transaction-pay-controller/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 9d5911e9fb1..23d8e687e4a 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Ignore synthetic gas legs when determining Across support for perps direct deposits ([#8527](https://github.com/MetaMask/core/pull/8527)) - Route Across status polling through the configured Across API base and support `depositTxnRef`/`fillTxnRef` for Across status responses ([#8512](https://github.com/MetaMask/core/pull/8512)) ## [19.2.1] From eb2a25a0fd03bdbcd259ce2a50306183a9c658b3 Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Mon, 20 Apr 2026 18:36:07 +0100 Subject: [PATCH 3/4] style(transaction-pay-controller): apply oxfmt to across fix --- .../src/strategy/across/AcrossStrategy.ts | 10 ++++----- .../src/strategy/across/across-quotes.ts | 22 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.ts b/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.ts index 09106a05cc2..09d9269b9d5 100644 --- a/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.ts +++ b/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.ts @@ -1,10 +1,5 @@ import { TransactionType } from '@metamask/transaction-controller'; -import { getAcrossQuotes } from './across-quotes'; -import { submitAcrossQuotes } from './across-submit'; -import { isSupportedAcrossPerpsDepositRequest } from './perps'; -import { isAcrossQuoteRequest } from './requests'; -import type { AcrossQuote } from './types'; import type { PayStrategy, PayStrategyExecuteRequest, @@ -12,6 +7,11 @@ import type { TransactionPayQuote, } from '../../types'; import { getPayStrategiesConfig } from '../../utils/feature-flags'; +import { getAcrossQuotes } from './across-quotes'; +import { submitAcrossQuotes } from './across-submit'; +import { isSupportedAcrossPerpsDepositRequest } from './perps'; +import { isAcrossQuoteRequest } from './requests'; +import type { AcrossQuote } from './types'; export class AcrossStrategy implements PayStrategy { supports(request: PayStrategyGetQuotesRequest): boolean { diff --git a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts index d668dfeca1e..b612c92aa0c 100644 --- a/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/across/across-quotes.ts @@ -3,17 +3,6 @@ import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; -import { getAcrossDestination } from './across-actions'; -import { normalizeAcrossRequest } from './perps'; -import { isAcrossQuoteRequest } from './requests'; -import { getAcrossOrderedTransactions } from './transactions'; -import type { - AcrossAction, - AcrossActionRequestBody, - AcrossGasLimits, - AcrossQuote, - AcrossSwapApprovalResponse, -} from './types'; import { TransactionPayStrategy } from '../../constants'; import { projectLogger } from '../../logger'; import type { @@ -29,6 +18,17 @@ import { getPayStrategiesConfig, getSlippage } from '../../utils/feature-flags'; import { calculateGasCost } from '../../utils/gas'; import { estimateQuoteGasLimits } from '../../utils/quote-gas'; import { getTokenFiatRate } from '../../utils/token'; +import { getAcrossDestination } from './across-actions'; +import { normalizeAcrossRequest } from './perps'; +import { isAcrossQuoteRequest } from './requests'; +import { getAcrossOrderedTransactions } from './transactions'; +import type { + AcrossAction, + AcrossActionRequestBody, + AcrossGasLimits, + AcrossQuote, + AcrossSwapApprovalResponse, +} from './types'; const log = createModuleLogger(projectLogger, 'across-strategy'); From d773fd302489d4734eceac39bb012f771c9d816c Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Mon, 20 Apr 2026 18:42:15 +0100 Subject: [PATCH 4/4] test(transaction-pay-controller): cover synthetic across gas leg paths --- .../strategy/across/AcrossStrategy.test.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.test.ts b/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.test.ts index d7107b4e6ee..8d60ba13d94 100644 --- a/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.test.ts +++ b/packages/transaction-pay-controller/src/strategy/across/AcrossStrategy.test.ts @@ -153,6 +153,50 @@ describe('AcrossStrategy', () => { ).toBe(true); }); + it('returns false when all requests are synthetic zero-minimum legs', () => { + const strategy = new AcrossStrategy(); + expect( + strategy.supports({ + ...baseRequest, + requests: [ + { + from: '0xabc' as Hex, + sourceBalanceRaw: '100', + sourceChainId: CHAIN_ID_ARBITRUM, + sourceTokenAddress: ARBITRUM_USDC_ADDRESS, + sourceTokenAmount: '100', + targetAmountMinimum: '0', + targetChainId: CHAIN_ID_ARBITRUM, + targetTokenAddress: + '0x0000000000000000000000000000000000000000' as Hex, + }, + ], + }), + ).toBe(false); + }); + + it('treats max-amount requests as actionable even with zero minimums', () => { + const strategy = new AcrossStrategy(); + expect( + strategy.supports({ + ...baseRequest, + requests: [ + { + from: '0xabc' as Hex, + isMaxAmount: true, + sourceBalanceRaw: '100', + sourceChainId: '0x1' as Hex, + sourceTokenAddress: '0xabc' as Hex, + sourceTokenAmount: '100', + targetAmountMinimum: '0', + targetChainId: '0x2' as Hex, + targetTokenAddress: '0xdef' as Hex, + }, + ], + }), + ).toBe(true); + }); + it('returns false for unsupported perps deposits', () => { const strategy = new AcrossStrategy(); expect(