From eca412d70fb2efa747811aa7e5eeb542f766aa6f Mon Sep 17 00:00:00 2001 From: Pedro Figueiredo Date: Thu, 16 Apr 2026 14:55:47 +0100 Subject: [PATCH] fix(transaction-pay-controller): allow Across perps ETH gas top-up quotes Across perps deposits originating from the PerpsController were being rejected before any quote call when the route included both the USDC deposit leg and the destination-chain ETH gas top-up leg. The existing Across perps support check only allowed the Arbitrum USDC leg, so the ETH leg triggered quotes-strategy-unsupported and the whole route was rejected locally before reaching Across. Widen the perps support gate to allow the native ETH top-up leg while still only normalizing the USDC leg into the HyperCore direct-deposit path. This preserves mixed USDC + ETH perps bundles and adds regression coverage for that route shape. --- .../transaction-pay-controller/CHANGELOG.md | 4 + .../src/strategy/across/perps.test.ts | 116 ++++++++++++++++++ .../src/strategy/across/perps.ts | 45 +++++-- 3 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 packages/transaction-pay-controller/src/strategy/across/perps.test.ts diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 2589b78feaf..2fbdb0e688e 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 + +- Support Across quotes for the Arbitrum native-token gas top-up leg of perps deposits ([#8493](https://github.com/MetaMask/core/pull/8493)) + ## [19.2.0] ### Changed diff --git a/packages/transaction-pay-controller/src/strategy/across/perps.test.ts b/packages/transaction-pay-controller/src/strategy/across/perps.test.ts new file mode 100644 index 00000000000..9896d9f3731 --- /dev/null +++ b/packages/transaction-pay-controller/src/strategy/across/perps.test.ts @@ -0,0 +1,116 @@ +import { TransactionType } from '@metamask/transaction-controller'; +import type { Hex } from '@metamask/utils'; + +import { + ARBITRUM_USDC_ADDRESS, + CHAIN_ID_ARBITRUM, + NATIVE_TOKEN_ADDRESS, +} from '../../constants'; +import type { QuoteRequest } from '../../types'; +import { + ACROSS_HYPERCORE_USDC_PERPS_ADDRESS, + isSupportedAcrossPerpsDepositRequest, + normalizeAcrossRequest, +} from './perps'; + +const REQUEST_MOCK: QuoteRequest = { + from: '0x1234567890123456789012345678901234567890' as Hex, + sourceBalanceRaw: '1000000', + sourceChainId: '0x1' as Hex, + sourceTokenAddress: '0x1111111111111111111111111111111111111111' as Hex, + sourceTokenAmount: '1000000', + targetAmountMinimum: '1000000', + targetChainId: CHAIN_ID_ARBITRUM, + targetTokenAddress: ARBITRUM_USDC_ADDRESS, +}; + +describe('perps', () => { + describe('isSupportedAcrossPerpsDepositRequest', () => { + it('returns true for direct deposit requests on Arbitrum USDC', () => { + expect( + isSupportedAcrossPerpsDepositRequest( + { + ...REQUEST_MOCK, + targetTokenAddress: ARBITRUM_USDC_ADDRESS.toUpperCase() as Hex, + }, + TransactionType.perpsDeposit, + ), + ).toBe(true); + }); + + it('returns true for Arbitrum native-token gas top-up requests', () => { + expect( + isSupportedAcrossPerpsDepositRequest( + { + ...REQUEST_MOCK, + targetTokenAddress: NATIVE_TOKEN_ADDRESS, + }, + TransactionType.perpsDeposit, + ), + ).toBe(true); + }); + + it('returns false for post-quote requests', () => { + expect( + isSupportedAcrossPerpsDepositRequest( + { + ...REQUEST_MOCK, + isPostQuote: true, + }, + TransactionType.perpsDeposit, + ), + ).toBe(false); + }); + + it('returns false for unsupported transaction types and tokens', () => { + expect( + isSupportedAcrossPerpsDepositRequest( + REQUEST_MOCK, + TransactionType.bridge, + ), + ).toBe(false); + + expect( + isSupportedAcrossPerpsDepositRequest( + { + ...REQUEST_MOCK, + targetChainId: '0x1' as Hex, + targetTokenAddress: + '0x2222222222222222222222222222222222222222' as Hex, + }, + TransactionType.perpsDeposit, + ), + ).toBe(false); + }); + }); + + describe('normalizeAcrossRequest', () => { + it('normalizes direct perps deposits to the Across HyperCore route', () => { + expect( + normalizeAcrossRequest(REQUEST_MOCK, TransactionType.perpsDeposit), + ).toStrictEqual({ + ...REQUEST_MOCK, + targetAmountMinimum: '100000000', + targetChainId: '0x539', + targetTokenAddress: ACROSS_HYPERCORE_USDC_PERPS_ADDRESS, + }); + }); + + it('does not normalize gas top-up requests', () => { + const request = { + ...REQUEST_MOCK, + targetTokenAddress: NATIVE_TOKEN_ADDRESS, + }; + + expect( + normalizeAcrossRequest(request, TransactionType.perpsDeposit), + ).toBe(request); + }); + + it('does not normalize non-perps requests', () => { + expect(normalizeAcrossRequest(REQUEST_MOCK, TransactionType.bridge)).toBe( + REQUEST_MOCK, + ); + }); + }); +}); diff --git a/packages/transaction-pay-controller/src/strategy/across/perps.ts b/packages/transaction-pay-controller/src/strategy/across/perps.ts index 0a24e7617f8..64849683106 100644 --- a/packages/transaction-pay-controller/src/strategy/across/perps.ts +++ b/packages/transaction-pay-controller/src/strategy/across/perps.ts @@ -7,6 +7,7 @@ import { CHAIN_ID_ARBITRUM, CHAIN_ID_HYPERCORE, HYPERCORE_USDC_DECIMALS, + NATIVE_TOKEN_ADDRESS, USDC_DECIMALS, } from '../../constants'; import type { QuoteRequest } from '../../types'; @@ -14,18 +15,40 @@ import type { QuoteRequest } from '../../types'; export const ACROSS_HYPERCORE_USDC_PERPS_ADDRESS = '0x2100000000000000000000000000000000000000' as Hex; +function isAcrossPerpsDirectDepositRequest( + request: Pick, +): boolean { + return ( + request.targetChainId === CHAIN_ID_ARBITRUM && + request.targetTokenAddress.toLowerCase() === + ARBITRUM_USDC_ADDRESS.toLowerCase() + ); +} + +function isAcrossPerpsGasTopUpRequest( + request: Pick, +): boolean { + return ( + request.targetChainId === CHAIN_ID_ARBITRUM && + request.targetTokenAddress.toLowerCase() === + NATIVE_TOKEN_ADDRESS.toLowerCase() + ); +} + /** - * Detect the quote-time parent transaction shape that Across can map to the - * new HyperCore USDC-PERPS direct-deposit route. + * Detect the quote-time parent transaction shape that Across can support for + * HyperCore perps deposits. * * The parent transaction remains `perpsDeposit` while quotes are being * selected. `perpsAcrossDeposit` is only assigned later to the generated - * Across submission transaction(s). + * Across submission transaction(s). At quote time Across can support: + * - the direct HyperCore USDC deposit leg + * - the destination-chain native gas top-up leg * * @param request - Transaction pay quote request. * @param parentTransactionType - Parent transaction type before Across * execution. - * @returns Whether the request matches the supported direct-deposit path. + * @returns Whether the request matches a supported perps deposit leg. */ export function isSupportedAcrossPerpsDepositRequest( request: Pick< @@ -37,9 +60,8 @@ export function isSupportedAcrossPerpsDepositRequest( return ( parentTransactionType === TransactionType.perpsDeposit && request.isPostQuote !== true && - request.targetChainId === CHAIN_ID_ARBITRUM && - request.targetTokenAddress.toLowerCase() === - ARBITRUM_USDC_ADDRESS.toLowerCase() + (isAcrossPerpsDirectDepositRequest(request) || + isAcrossPerpsGasTopUpRequest(request)) ); } @@ -48,8 +70,8 @@ export function isSupportedAcrossPerpsDepositRequest( * direct perps deposits. * * Transaction pay starts from the required on-chain asset identity - * (Arbitrum USDC, 6 decimals), while Across now expects the HyperCore - * USDC-PERPS destination token (8 decimals). + * (Arbitrum USDC, 6 decimals), while Across expects the HyperCore + * USDC-PERPS destination token (8 decimals) for the USDC deposit leg. * * @param request - Transaction pay quote request. * @param parentTransactionType - Parent transaction type before Across @@ -60,7 +82,10 @@ export function normalizeAcrossRequest( request: QuoteRequest, parentTransactionType?: TransactionType, ): QuoteRequest { - if (!isSupportedAcrossPerpsDepositRequest(request, parentTransactionType)) { + if ( + parentTransactionType !== TransactionType.perpsDeposit || + !isAcrossPerpsDirectDepositRequest(request) + ) { return request; }