From 10be756b96ac1162c24acfb282a9ebd18bbc68b0 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 3 Jun 2026 20:32:06 +0530 Subject: [PATCH 1/3] feat: add flag to exclude networks from using infura for live balance --- .../src/utils/feature-flags.test.ts | 59 +++++++++++++++ .../src/utils/feature-flags.ts | 28 +++++++ .../src/utils/token.test.ts | 73 +++++++++++++++++++ .../src/utils/token.ts | 4 +- 4 files changed, 162 insertions(+), 2 deletions(-) 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 16cb3cdf00..076fd56ed5 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts @@ -23,6 +23,7 @@ import { getRelayOriginGasOverhead, getRelayPollingInterval, getRelayPollingTimeout, + isChainExcludedFromInfura, isEIP7702Chain, isRelayExecuteEnabled, getFeatureFlags, @@ -502,6 +503,64 @@ describe('Feature Flags Utils', () => { }); }); + describe('isChainExcludedFromInfura', () => { + it('returns false when no feature flags are set', () => { + expect(isChainExcludedFromInfura(messenger, CHAIN_ID_MOCK)).toBe(false); + }); + + it('returns false when excludeChainIdsFromInfura is empty', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_extended: { + excludeChainIdsFromInfura: [], + }, + }, + }); + + expect(isChainExcludedFromInfura(messenger, CHAIN_ID_MOCK)).toBe(false); + }); + + it('returns true when chainId is in the exclusion list', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_extended: { + excludeChainIdsFromInfura: [CHAIN_ID_MOCK], + }, + }, + }); + + expect(isChainExcludedFromInfura(messenger, CHAIN_ID_MOCK)).toBe(true); + }); + + it('returns false when chainId is not in the exclusion list', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_extended: { + excludeChainIdsFromInfura: [CHAIN_ID_DIFFERENT_MOCK], + }, + }, + }); + + expect(isChainExcludedFromInfura(messenger, CHAIN_ID_MOCK)).toBe(false); + }); + + it('performs case-insensitive comparison', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_extended: { + excludeChainIdsFromInfura: ['0xA' as Hex], + }, + }, + }); + + expect(isChainExcludedFromInfura(messenger, '0xa' as Hex)).toBe(true); + }); + }); + describe('getRelayOriginGasOverhead', () => { it('returns default when no feature flags are set', () => { expect(getRelayOriginGasOverhead(messenger)).toBe( diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.ts b/packages/transaction-pay-controller/src/utils/feature-flags.ts index 63e29944ef..8ef214c4a6 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.ts @@ -149,6 +149,7 @@ export type PayStrategiesConfigRaw = { }; type FeatureFlagsExtendedRaw = { + excludeChainIdsFromInfura?: Hex[]; payStrategies?: { relay?: { gaslessEnabled?: boolean; @@ -556,6 +557,33 @@ export function isRelayExecuteEnabled( return featureFlags.payStrategies?.relay?.gaslessEnabled ?? false; } +/** + * Whether a chain is excluded from preferring Infura for balance queries. + * + * When a chain ID appears in the `confirmations_pay_extended.excludeChainIdsFromInfura` + * feature flag array, the Infura RPC endpoint should not be forced for that chain. + * + * @param messenger - Controller messenger. + * @param chainId - Chain ID to check. + * @returns True if the chain should skip the Infura preference. + */ +export function isChainExcludedFromInfura( + messenger: TransactionPayControllerMessenger, + chainId: Hex, +): boolean { + const state = messenger.call('RemoteFeatureFlagController:getState'); + const featureFlags = + (state.remoteFeatureFlags?.confirmations_pay_extended as + | FeatureFlagsExtendedRaw + | undefined) ?? {}; + + const excludedChains = featureFlags.excludeChainIdsFromInfura ?? []; + + return excludedChains.some( + (excluded) => excluded.toLowerCase() === chainId.toLowerCase(), + ); +} + /** * Get the origin gas overhead to include in Relay quote requests * for EIP-7702 chains. diff --git a/packages/transaction-pay-controller/src/utils/token.test.ts b/packages/transaction-pay-controller/src/utils/token.test.ts index 0de1a6b779..e23dbd9e48 100644 --- a/packages/transaction-pay-controller/src/utils/token.test.ts +++ b/packages/transaction-pay-controller/src/utils/token.test.ts @@ -773,6 +773,79 @@ describe('Token Utils', () => { NETWORK_CLIENT_ID_MOCK, ); }); + + it('skips Infura when chain is in excludeChainIdsFromInfura flag', async () => { + PROVIDER_MOCK.request.mockResolvedValue('0x4C4B40'); + + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_extended: { + excludeChainIdsFromInfura: [CHAIN_ID_MOCK], + }, + }, + }); + + getNetworkConfigurationByChainIdMock.mockReturnValue({ + rpcEndpoints: [ + { + type: RpcEndpointType.Infura, + networkClientId: INFURA_NETWORK_CLIENT_ID_MOCK, + }, + ], + } as NetworkConfiguration); + + const result = await getLiveTokenBalance( + messenger, + ACCOUNT_MOCK, + CHAIN_ID_MOCK, + ERC20_ADDRESS_MOCK, + ); + + expect(result).toBe('5000000'); + expect(getNetworkConfigurationByChainIdMock).not.toHaveBeenCalled(); + expect(findNetworkClientIdByChainIdMock).toHaveBeenCalledWith( + CHAIN_ID_MOCK, + ); + expect(getNetworkClientByIdMock).toHaveBeenCalledWith( + NETWORK_CLIENT_ID_MOCK, + ); + }); + + it('uses Infura when chain is not in excludeChainIdsFromInfura flag', async () => { + PROVIDER_MOCK.request.mockResolvedValue('0x895440'); + + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_extended: { + excludeChainIdsFromInfura: ['0x89' as Hex], + }, + }, + }); + + getNetworkConfigurationByChainIdMock.mockReturnValue({ + rpcEndpoints: [ + { + type: RpcEndpointType.Infura, + networkClientId: INFURA_NETWORK_CLIENT_ID_MOCK, + }, + ], + } as NetworkConfiguration); + + const result = await getLiveTokenBalance( + messenger, + ACCOUNT_MOCK, + CHAIN_ID_MOCK, + ERC20_ADDRESS_MOCK, + ); + + expect(result).toBe('9000000'); + expect(getNetworkClientByIdMock).toHaveBeenCalledWith( + INFURA_NETWORK_CLIENT_ID_MOCK, + ); + expect(findNetworkClientIdByChainIdMock).not.toHaveBeenCalled(); + }); }); describe('computeTokenAmounts', () => { diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index bf0bf39e88..53250ad64c 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -13,7 +13,7 @@ import { STABLECOINS, } from '../constants'; import type { FiatRates, TransactionPayControllerMessenger } from '../types'; -import { getAssetsUnifyStateFeature } from './feature-flags'; +import { getAssetsUnifyStateFeature, isChainExcludedFromInfura } from './feature-flags'; import { getNetworkClientId, rpcRequest } from './provider'; /** @@ -322,7 +322,7 @@ export async function getLiveTokenBalance( chainId: Hex, tokenAddress: Hex, ): Promise { - const options = { preferInfura: true }; + const options = { preferInfura: !isChainExcludedFromInfura(messenger, chainId) }; const isNative = tokenAddress.toLowerCase() === getNativeToken(chainId).toLowerCase(); From 0662158959bfdbc091a380bd20f67a0bf514bab5 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 3 Jun 2026 20:34:34 +0530 Subject: [PATCH 2/3] update --- 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 546ba3fbb6..ddf5264f04 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Live token balance queries now respect the `confirmations_pay_extended.excludeChainIdsFromInfura` feature flag, skipping the Infura endpoint preference for excluded chains ([#8992](https://github.com/MetaMask/core/pull/8992)) - Bump `@metamask/assets-controllers` from `^108.3.0` to `^108.4.0` ([#8981](https://github.com/MetaMask/core/pull/8981)) - Bump `@metamask/assets-controller` from `^8.0.2` to `^8.3.1` ([#8981](https://github.com/MetaMask/core/pull/8981), [#8985](https://github.com/MetaMask/core/pull/8985)) - Bump `@metamask/remote-feature-flag-controller` from `^4.2.1` to `^4.2.2` ([#8986](https://github.com/MetaMask/core/pull/8986)) From e244127fb2cb50a4c9211ef3640106198bca56cc Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Wed, 3 Jun 2026 20:37:39 +0530 Subject: [PATCH 3/3] update --- packages/transaction-pay-controller/src/utils/token.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index 53250ad64c..2a0e3d032e 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -13,7 +13,10 @@ import { STABLECOINS, } from '../constants'; import type { FiatRates, TransactionPayControllerMessenger } from '../types'; -import { getAssetsUnifyStateFeature, isChainExcludedFromInfura } from './feature-flags'; +import { + getAssetsUnifyStateFeature, + isChainExcludedFromInfura, +} from './feature-flags'; import { getNetworkClientId, rpcRequest } from './provider'; /** @@ -322,7 +325,9 @@ export async function getLiveTokenBalance( chainId: Hex, tokenAddress: Hex, ): Promise { - const options = { preferInfura: !isChainExcludedFromInfura(messenger, chainId) }; + const options = { + preferInfura: !isChainExcludedFromInfura(messenger, chainId), + }; const isNative = tokenAddress.toLowerCase() === getNativeToken(chainId).toLowerCase();