From c3ab29b9ef4686c34a40c3b47481f136609dde2e Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 17 Jun 2026 13:24:06 +0200 Subject: [PATCH 01/28] feat(network-controller): add getIsRpcFailoverForced selector --- .../network-controller/src/selectors.test.ts | 38 +++++++++++++++++++ packages/network-controller/src/selectors.ts | 9 +++++ 2 files changed, 47 insertions(+) create mode 100644 packages/network-controller/src/selectors.test.ts diff --git a/packages/network-controller/src/selectors.test.ts b/packages/network-controller/src/selectors.test.ts new file mode 100644 index 0000000000..fa290523d1 --- /dev/null +++ b/packages/network-controller/src/selectors.test.ts @@ -0,0 +1,38 @@ +import { getIsRpcFailoverForced } from './selectors'; + +describe('getIsRpcFailoverForced', () => { + it('returns true when the flag is true', () => { + const state = { + remoteFeatureFlags: { + 'wallet-framework-rpc-failover-force-enabled': true, + }, + cacheTimestamp: 0, + }; + expect(getIsRpcFailoverForced(state as never)).toBe(true); + }); + + it('returns false when the flag is false', () => { + const state = { + remoteFeatureFlags: { + 'wallet-framework-rpc-failover-force-enabled': false, + }, + cacheTimestamp: 0, + }; + expect(getIsRpcFailoverForced(state as never)).toBe(false); + }); + + it('returns false when the flag is absent', () => { + const state = { remoteFeatureFlags: {}, cacheTimestamp: 0 }; + expect(getIsRpcFailoverForced(state as never)).toBe(false); + }); + + it('returns false when the flag is a non-boolean value', () => { + const state = { + remoteFeatureFlags: { + 'wallet-framework-rpc-failover-force-enabled': 'yes', + }, + cacheTimestamp: 0, + }; + expect(getIsRpcFailoverForced(state as never)).toBe('yes'); + }); +}); diff --git a/packages/network-controller/src/selectors.ts b/packages/network-controller/src/selectors.ts index 016c8a66b0..af08bb4d99 100644 --- a/packages/network-controller/src/selectors.ts +++ b/packages/network-controller/src/selectors.ts @@ -7,3 +7,12 @@ export function getIsRpcFailoverEnabled( .walletFrameworkRpcFailoverEnabled as boolean | undefined; return walletFrameworkRpcFailoverEnabled ?? false; } + +export function getIsRpcFailoverForced( + state: RemoteFeatureFlagControllerState, +): boolean { + const forceEnabled = state.remoteFeatureFlags[ + 'wallet-framework-rpc-failover-force-enabled' + ] as boolean | undefined; + return forceEnabled ?? false; +} From 003b255287dcaba1895f2be1183cf38103560147 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 17 Jun 2026 13:32:30 +0200 Subject: [PATCH 02/28] test(network-controller): clarify non-boolean selector test name --- packages/network-controller/src/selectors.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/network-controller/src/selectors.test.ts b/packages/network-controller/src/selectors.test.ts index fa290523d1..f75f010bb6 100644 --- a/packages/network-controller/src/selectors.test.ts +++ b/packages/network-controller/src/selectors.test.ts @@ -26,7 +26,7 @@ describe('getIsRpcFailoverForced', () => { expect(getIsRpcFailoverForced(state as never)).toBe(false); }); - it('returns false when the flag is a non-boolean value', () => { + it('passes through non-boolean values without coercion', () => { const state = { remoteFeatureFlags: { 'wallet-framework-rpc-failover-force-enabled': 'yes', From e51b6252c1b00d703ac9805f2565cdb583a4c82c Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 17 Jun 2026 14:34:59 +0200 Subject: [PATCH 03/28] feat(network-controller): force Infura traffic to failover when forced Thread isRpcFailoverForced through createNetworkClient, createRpcServiceChain, and createAutoManagedNetworkClient. When the force flag is on for an Infura endpoint that has failover URLs, the endpoint chain is built from failovers only, bypassing Infura entirely. --- .../src/NetworkController.ts | 6 + ...create-auto-managed-network-client.test.ts | 101 +++++++++++++ .../src/create-auto-managed-network-client.ts | 20 +++ .../rpc-endpoint-failover.test.ts | 138 ++++++++++++++++++ .../src/create-network-client.ts | 33 +++-- .../tests/network-client/helpers.ts | 3 + 6 files changed, 292 insertions(+), 9 deletions(-) create mode 100644 packages/network-controller/src/create-network-client-tests/rpc-endpoint-failover.test.ts diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 549488ebdd..b56a0c149b 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -1287,6 +1287,8 @@ export class NetworkController extends BaseController< #isRpcFailoverEnabled = false; + #isRpcFailoverForced = false; + /** * Constructs a NetworkController. * @@ -2872,6 +2874,7 @@ export class NetworkController extends BaseController< getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, isRpcFailoverEnabled: this.#isRpcFailoverEnabled, + isRpcFailoverForced: this.#isRpcFailoverForced, logger: this.#log, }); } else { @@ -2891,6 +2894,7 @@ export class NetworkController extends BaseController< getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, isRpcFailoverEnabled: this.#isRpcFailoverEnabled, + isRpcFailoverForced: this.#isRpcFailoverForced, logger: this.#log, }); } @@ -3057,6 +3061,7 @@ export class NetworkController extends BaseController< getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, isRpcFailoverEnabled: this.#isRpcFailoverEnabled, + isRpcFailoverForced: this.#isRpcFailoverForced, logger: this.#log, }), ] as const; @@ -3076,6 +3081,7 @@ export class NetworkController extends BaseController< getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, isRpcFailoverEnabled: this.#isRpcFailoverEnabled, + isRpcFailoverForced: this.#isRpcFailoverForced, logger: this.#log, }), ] as const; diff --git a/packages/network-controller/src/create-auto-managed-network-client.test.ts b/packages/network-controller/src/create-auto-managed-network-client.test.ts index eea5e94d8f..a96145ea82 100644 --- a/packages/network-controller/src/create-auto-managed-network-client.test.ts +++ b/packages/network-controller/src/create-auto-managed-network-client.test.ts @@ -46,6 +46,7 @@ describe('createAutoManagedNetworkClient', () => { }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, + isRpcFailoverForced: false, }); expect(configuration).toStrictEqual(networkClientConfiguration); @@ -64,6 +65,7 @@ describe('createAutoManagedNetworkClient', () => { }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, + isRpcFailoverForced: false, }); }).not.toThrow(); }); @@ -79,6 +81,7 @@ describe('createAutoManagedNetworkClient', () => { }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, + isRpcFailoverForced: false, }); // This also tests the `has` trap in the proxy @@ -114,6 +117,7 @@ describe('createAutoManagedNetworkClient', () => { }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, + isRpcFailoverForced: false, }); const result = await provider.request({ @@ -165,6 +169,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, }); await provider.request({ @@ -187,6 +192,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, }); }); @@ -230,6 +236,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: false, + isRpcFailoverForced: false, }); const { provider } = autoManagedNetworkClient; @@ -254,6 +261,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: false, + isRpcFailoverForced: false, }); expect(createNetworkClientMock).toHaveBeenNthCalledWith(2, { id: 'some-network-client-id', @@ -262,6 +270,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, }); }); @@ -305,6 +314,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, }); const { provider } = autoManagedNetworkClient; @@ -329,6 +339,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, }); expect(createNetworkClientMock).toHaveBeenNthCalledWith(2, { id: 'some-network-client-id', @@ -337,6 +348,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: false, + isRpcFailoverForced: false, }); }); }); @@ -352,6 +364,7 @@ describe('createAutoManagedNetworkClient', () => { }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, + isRpcFailoverForced: false, }); // This also tests the `has` trap in the proxy @@ -413,6 +426,7 @@ describe('createAutoManagedNetworkClient', () => { }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, + isRpcFailoverForced: false, }); const blockNumberViaLatest = await new Promise((resolve) => { @@ -487,6 +501,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, }); await new Promise((resolve) => { @@ -505,6 +520,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, }); }); @@ -548,6 +564,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: false, + isRpcFailoverForced: false, }); const { blockTracker } = autoManagedNetworkClient; @@ -566,6 +583,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: false, + isRpcFailoverForced: false, }); expect(createNetworkClientMock).toHaveBeenNthCalledWith(2, { id: 'some-network-client-id', @@ -574,6 +592,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, }); }); @@ -617,6 +636,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, }); const { blockTracker } = autoManagedNetworkClient; @@ -635,6 +655,7 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, }); expect(createNetworkClientMock).toHaveBeenNthCalledWith(2, { id: 'some-network-client-id', @@ -643,11 +664,90 @@ describe('createAutoManagedNetworkClient', () => { getBlockTrackerOptions, messenger, isRpcFailoverEnabled: false, + isRpcFailoverForced: false, }); }); }); }); + it('allows for enabling the forced RPC failover behavior, even after having already accessed the provider', async () => { + mockNetwork({ + networkClientConfiguration, + mocks: [ + { + request: { + method: 'test_method', + params: [], + }, + response: { + result: 'test response', + }, + discardAfterMatching: false, + }, + ], + }); + const createNetworkClientMock = jest.spyOn( + createNetworkClientModule, + 'createNetworkClient', + ); + const getRpcServiceOptions = (): Omit< + RpcServiceOptions, + 'failoverService' | 'endpointUrl' + > => ({ + btoa, + fetch, + isOffline: (): boolean => false, + }); + const getBlockTrackerOptions = (): PollingBlockTrackerOptions => ({ + pollingInterval: 5000, + }); + const messenger = buildNetworkControllerMessenger(); + + const autoManagedNetworkClient = createAutoManagedNetworkClient({ + networkClientId: 'some-network-client-id', + networkClientConfiguration, + getRpcServiceOptions, + getBlockTrackerOptions, + messenger, + isRpcFailoverEnabled: false, + isRpcFailoverForced: false, + }); + const { provider } = autoManagedNetworkClient; + + await provider.request({ + id: 1, + jsonrpc: '2.0', + method: 'test_method', + params: [], + }); + autoManagedNetworkClient.enableRpcFailoverForced(); + await provider.request({ + id: 1, + jsonrpc: '2.0', + method: 'test_method', + params: [], + }); + + expect(createNetworkClientMock).toHaveBeenNthCalledWith(1, { + id: 'some-network-client-id', + configuration: networkClientConfiguration, + getRpcServiceOptions, + getBlockTrackerOptions, + messenger, + isRpcFailoverEnabled: false, + isRpcFailoverForced: false, + }); + expect(createNetworkClientMock).toHaveBeenNthCalledWith(2, { + id: 'some-network-client-id', + configuration: networkClientConfiguration, + getRpcServiceOptions, + getBlockTrackerOptions, + messenger, + isRpcFailoverEnabled: false, + isRpcFailoverForced: true, + }); + }); + it('destroys the block tracker when destroyed', () => { mockNetwork({ networkClientConfiguration, @@ -673,6 +773,7 @@ describe('createAutoManagedNetworkClient', () => { }), messenger: buildNetworkControllerMessenger(), isRpcFailoverEnabled: false, + isRpcFailoverForced: false, }); // Start the block tracker blockTracker.on('latest', () => { diff --git a/packages/network-controller/src/create-auto-managed-network-client.ts b/packages/network-controller/src/create-auto-managed-network-client.ts index 3259700d5f..8eb8dbcb2b 100644 --- a/packages/network-controller/src/create-auto-managed-network-client.ts +++ b/packages/network-controller/src/create-auto-managed-network-client.ts @@ -49,6 +49,8 @@ export type AutoManagedNetworkClient< destroy: () => void; enableRpcFailover: () => void; disableRpcFailover: () => void; + enableRpcFailoverForced: () => void; + disableRpcFailoverForced: () => void; }; /** @@ -96,6 +98,7 @@ export function createAutoManagedNetworkClient< > => ({}), messenger, isRpcFailoverEnabled: givenIsRpcFailoverEnabled, + isRpcFailoverForced: givenIsRpcFailoverForced, logger, }: { networkClientId: NetworkClientId; @@ -108,9 +111,11 @@ export function createAutoManagedNetworkClient< ) => Omit; messenger: NetworkControllerMessenger; isRpcFailoverEnabled: boolean; + isRpcFailoverForced: boolean; logger?: Logger; }): AutoManagedNetworkClient { let isRpcFailoverEnabled = givenIsRpcFailoverEnabled; + let isRpcFailoverForced = givenIsRpcFailoverForced; let networkClient: NetworkClient | undefined; const ensureNetworkClientCreated = (): NetworkClient => { @@ -121,6 +126,7 @@ export function createAutoManagedNetworkClient< getBlockTrackerOptions, messenger, isRpcFailoverEnabled, + isRpcFailoverForced, logger, }); @@ -246,6 +252,18 @@ export function createAutoManagedNetworkClient< networkClient = undefined; }; + const enableRpcFailoverForced = (): void => { + isRpcFailoverForced = true; + destroy(); + networkClient = undefined; + }; + + const disableRpcFailoverForced = (): void => { + isRpcFailoverForced = false; + destroy(); + networkClient = undefined; + }; + return { configuration: networkClientConfiguration, provider: providerProxy, @@ -253,5 +271,7 @@ export function createAutoManagedNetworkClient< destroy, enableRpcFailover, disableRpcFailover, + enableRpcFailoverForced, + disableRpcFailoverForced, }; } diff --git a/packages/network-controller/src/create-network-client-tests/rpc-endpoint-failover.test.ts b/packages/network-controller/src/create-network-client-tests/rpc-endpoint-failover.test.ts new file mode 100644 index 0000000000..d42a7fdb07 --- /dev/null +++ b/packages/network-controller/src/create-network-client-tests/rpc-endpoint-failover.test.ts @@ -0,0 +1,138 @@ +import { buildRootMessenger } from '../../tests/helpers'; +import { + withMockedCommunications, + withNetworkClient, +} from '../../tests/network-client/helpers'; + +describe('createNetworkClient - RPC endpoint failover (forced)', () => { + describe('when isRpcFailoverForced is true and providerType is infura', () => { + it('routes requests to the failover endpoint instead of Infura when failover URLs are provided', async () => { + const failoverUrl = 'https://failover.example.com'; + + // Only mock the failover URL — if Infura is hit, nock will throw because + // there is no matching mock for it. + // eth_gasPrice is not served by local middleware so it actually reaches + // the RPC endpoint, letting us confirm which host received the request. + await withMockedCommunications( + { + providerType: 'custom', + customRpcUrl: failoverUrl, + }, + async (failoverComms) => { + failoverComms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); + failoverComms.mockRpcCall({ + request: { method: 'eth_gasPrice', params: [] }, + response: { result: '0xabc' }, + }); + + const messenger = buildRootMessenger(); + + const result = await withNetworkClient( + { + providerType: 'infura', + failoverRpcUrls: [failoverUrl], + isRpcFailoverForced: true, + isRpcFailoverEnabled: false, + messenger, + getRpcServiceOptions: () => ({ + fetch, + btoa, + isOffline: (): boolean => false, + }), + }, + async ({ makeRpcCall }) => { + return await makeRpcCall({ method: 'eth_gasPrice', params: [] }); + }, + ); + + expect(result).toBe('0xabc'); + }, + ); + }); + + it('falls back to Infura when no failover URLs are provided', async () => { + // Only mock Infura — if any failover were hit, nock would throw. + await withMockedCommunications( + { + providerType: 'infura', + }, + async (infuraComms) => { + infuraComms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); + infuraComms.mockRpcCall({ + request: { method: 'eth_gasPrice', params: [] }, + response: { result: '0xdef' }, + }); + + const messenger = buildRootMessenger(); + + const result = await withNetworkClient( + { + providerType: 'infura', + failoverRpcUrls: [], + isRpcFailoverForced: true, + isRpcFailoverEnabled: false, + messenger, + getRpcServiceOptions: () => ({ + fetch, + btoa, + isOffline: (): boolean => false, + }), + }, + async ({ makeRpcCall }) => { + return await makeRpcCall({ method: 'eth_gasPrice', params: [] }); + }, + ); + + expect(result).toBe('0xdef'); + }, + ); + }); + }); + + describe('when isRpcFailoverForced is true and providerType is custom', () => { + it('still routes requests to the custom primary endpoint, not the failover', async () => { + const customRpcUrl = 'https://custom.example.com'; + const failoverUrl = 'https://failover.example.com'; + + // Only mock the custom URL — if failover is hit, nock will throw. + // eth_gasPrice is not served by local middleware so it actually reaches + // the RPC endpoint, letting us confirm which host received the request. + await withMockedCommunications( + { + providerType: 'custom', + customRpcUrl, + }, + async (customComms) => { + customComms.mockNextBlockTrackerRequest({ blockNumber: '0x1' }); + customComms.mockRpcCall({ + request: { method: 'eth_gasPrice', params: [] }, + response: { result: '0xabc' }, + }); + + const messenger = buildRootMessenger(); + + const result = await withNetworkClient( + { + providerType: 'custom', + customRpcUrl, + failoverRpcUrls: [failoverUrl], + isRpcFailoverForced: true, + isRpcFailoverEnabled: false, + messenger, + getRpcServiceOptions: () => ({ + fetch, + btoa, + isOffline: (): boolean => false, + }), + }, + async ({ makeRpcCall }) => { + return await makeRpcCall({ method: 'eth_gasPrice', params: [] }); + }, + ); + + expect(result).toBe('0xabc'); + }, + ); + }); + }); +}); diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index cc8967cf36..1109d79118 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -145,6 +145,7 @@ export function createNetworkClient({ getBlockTrackerOptions, messenger, isRpcFailoverEnabled, + isRpcFailoverForced, logger, }: { id: NetworkClientId; @@ -157,6 +158,7 @@ export function createNetworkClient({ ) => Omit; messenger: NetworkControllerMessenger; isRpcFailoverEnabled: boolean; + isRpcFailoverForced: boolean; logger?: Logger; }): NetworkClient { const primaryEndpointUrl = @@ -170,6 +172,7 @@ export function createNetworkClient({ getRpcServiceOptions, messenger, isRpcFailoverEnabled, + isRpcFailoverForced, logger, }); @@ -252,6 +255,7 @@ function createRpcServiceChain({ getRpcServiceOptions, messenger, isRpcFailoverEnabled, + isRpcFailoverForced, logger, }: { id: NetworkClientId; @@ -262,17 +266,28 @@ function createRpcServiceChain({ ) => RpcServiceOptionsWithDefaults; messenger: NetworkControllerMessenger; isRpcFailoverEnabled: boolean; + isRpcFailoverForced: boolean; logger?: Logger; }): RpcServiceChain { - const availableEndpoints = isRpcFailoverEnabled - ? [ - { url: primaryEndpointUrl, isFailover: false }, - ...(configuration.failoverRpcUrls ?? []).map((url) => ({ - url, - isFailover: true, - })), - ] - : [{ url: primaryEndpointUrl, isFailover: false }]; + const failoverEndpoints = (configuration.failoverRpcUrls ?? []).map((url) => ({ + url, + isFailover: true, + })); + const isInfuraEndpoint = configuration.type === NetworkClientType.Infura; + + let availableEndpoints: { url: string; isFailover: boolean }[]; + if (isRpcFailoverForced && isInfuraEndpoint && failoverEndpoints.length > 0) { + // Force flag is on for an Infura endpoint with failovers: bypass Infura + // entirely and route all traffic (including block polling) to failovers. + availableEndpoints = failoverEndpoints; + } else if (isRpcFailoverEnabled) { + availableEndpoints = [ + { url: primaryEndpointUrl, isFailover: false }, + ...failoverEndpoints, + ]; + } else { + availableEndpoints = [{ url: primaryEndpointUrl, isFailover: false }]; + } const isOffline = (): boolean => { const connectivityState = messenger.call('ConnectivityController:getState'); diff --git a/packages/network-controller/tests/network-client/helpers.ts b/packages/network-controller/tests/network-client/helpers.ts index c5e6de192e..79ccac7792 100644 --- a/packages/network-controller/tests/network-client/helpers.ts +++ b/packages/network-controller/tests/network-client/helpers.ts @@ -334,6 +334,7 @@ export type MockOptions = { messenger?: RootMessenger; networkClientId?: NetworkClientId; isRpcFailoverEnabled?: boolean; + isRpcFailoverForced?: boolean; }; export type MockCommunications = { @@ -502,6 +503,7 @@ export async function withNetworkClient( messenger = buildRootMessenger(), networkClientId = 'some-network-client-id', isRpcFailoverEnabled = false, + isRpcFailoverForced = false, }: MockOptions, fn: (client: MockNetworkClient) => Promise, ): Promise { @@ -554,6 +556,7 @@ export async function withNetworkClient( getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled, + isRpcFailoverForced, }); /* eslint-disable-next-line n/no-process-env */ process.env.IN_TEST = inTest; From 4792d927e8286c007a36efac1234dd7008bf2bad Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 17 Jun 2026 14:41:39 +0200 Subject: [PATCH 04/28] test(network-controller): cover disableRpcFailoverForced and document param Add a disableRpcFailoverForced reconstruction test mirroring the sibling, and add the missing isRpcFailoverForced JSDoc tags. --- ...create-auto-managed-network-client.test.ts | 78 +++++++++++++++++++ .../src/create-auto-managed-network-client.ts | 3 + .../src/create-network-client.ts | 6 ++ .../tests/network-client/helpers.ts | 3 + 4 files changed, 90 insertions(+) diff --git a/packages/network-controller/src/create-auto-managed-network-client.test.ts b/packages/network-controller/src/create-auto-managed-network-client.test.ts index a96145ea82..24fc2ee08f 100644 --- a/packages/network-controller/src/create-auto-managed-network-client.test.ts +++ b/packages/network-controller/src/create-auto-managed-network-client.test.ts @@ -748,6 +748,84 @@ describe('createAutoManagedNetworkClient', () => { }); }); + it('allows for disabling the forced RPC failover behavior, even after having accessed the provider', async () => { + mockNetwork({ + networkClientConfiguration, + mocks: [ + { + request: { + method: 'test_method', + params: [], + }, + response: { + result: 'test response', + }, + discardAfterMatching: false, + }, + ], + }); + const createNetworkClientMock = jest.spyOn( + createNetworkClientModule, + 'createNetworkClient', + ); + const getRpcServiceOptions = (): Omit< + RpcServiceOptions, + 'failoverService' | 'endpointUrl' + > => ({ + btoa, + fetch, + isOffline: (): boolean => false, + }); + const getBlockTrackerOptions = (): PollingBlockTrackerOptions => ({ + pollingInterval: 5000, + }); + const messenger = buildNetworkControllerMessenger(); + + const autoManagedNetworkClient = createAutoManagedNetworkClient({ + networkClientId: 'some-network-client-id', + networkClientConfiguration, + getRpcServiceOptions, + getBlockTrackerOptions, + messenger, + isRpcFailoverEnabled: false, + isRpcFailoverForced: true, + }); + const { provider } = autoManagedNetworkClient; + + await provider.request({ + id: 1, + jsonrpc: '2.0', + method: 'test_method', + params: [], + }); + autoManagedNetworkClient.disableRpcFailoverForced(); + await provider.request({ + id: 1, + jsonrpc: '2.0', + method: 'test_method', + params: [], + }); + + expect(createNetworkClientMock).toHaveBeenNthCalledWith(1, { + id: 'some-network-client-id', + configuration: networkClientConfiguration, + getRpcServiceOptions, + getBlockTrackerOptions, + messenger, + isRpcFailoverEnabled: false, + isRpcFailoverForced: true, + }); + expect(createNetworkClientMock).toHaveBeenNthCalledWith(2, { + id: 'some-network-client-id', + configuration: networkClientConfiguration, + getRpcServiceOptions, + getBlockTrackerOptions, + messenger, + isRpcFailoverEnabled: false, + isRpcFailoverForced: false, + }); + }); + it('destroys the block tracker when destroyed', () => { mockNetwork({ networkClientConfiguration, diff --git a/packages/network-controller/src/create-auto-managed-network-client.ts b/packages/network-controller/src/create-auto-managed-network-client.ts index 8eb8dbcb2b..adcd516c67 100644 --- a/packages/network-controller/src/create-auto-managed-network-client.ts +++ b/packages/network-controller/src/create-auto-managed-network-client.ts @@ -83,6 +83,9 @@ const UNINITIALIZED_TARGET = { __UNINITIALIZED__: true }; * @param args.isRpcFailoverEnabled - Whether or not requests sent to the * primary RPC endpoint for this network should be automatically diverted to * provided failover endpoints if the primary is unavailable. + * @param args.isRpcFailoverForced - Whether or not to force all traffic for + * Infura endpoints that have failover URLs to those failover URLs, bypassing + * Infura entirely. * @param args.logger - A `loglevel` logger. * @returns The auto-managed network client. */ diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index 1109d79118..9a4bd5196d 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -135,6 +135,9 @@ type RpcApiMiddleware = JsonRpcMiddleware< * provided failover endpoints if the primary is unavailable. This effectively * causes the `failoverRpcUrls` property of the network client configuration * to be honored or ignored. + * @param args.isRpcFailoverForced - Whether or not to force all traffic for + * Infura endpoints that have failover URLs to those failover URLs, bypassing + * Infura entirely. * @param args.logger - A `loglevel` logger. * @returns The network client. */ @@ -245,6 +248,9 @@ export function createNetworkClient({ * provided failover endpoints if the primary is unavailable. This effectively * causes the `failoverRpcUrls` property of the network client configuration * to be honored or ignored. + * @param args.isRpcFailoverForced - Whether or not to force all traffic for + * Infura endpoints that have failover URLs to those failover URLs, bypassing + * Infura entirely. * @param args.logger - A `loglevel` logger. * @returns The RPC service chain. */ diff --git a/packages/network-controller/tests/network-client/helpers.ts b/packages/network-controller/tests/network-client/helpers.ts index 79ccac7792..7a3593b7c5 100644 --- a/packages/network-controller/tests/network-client/helpers.ts +++ b/packages/network-controller/tests/network-client/helpers.ts @@ -483,6 +483,9 @@ export async function waitForPromiseToBeFulfilledAfterRunningAllTimers( * @param options.networkClientId - The ID of the new network client. * @param options.isRpcFailoverEnabled - Whether or not the RPC failover * functionality is enabled. + * @param options.isRpcFailoverForced - Whether or not to force all traffic for + * Infura endpoints that have failover URLs to those failover URLs, bypassing + * Infura entirely. * @param fn - A function which will be called with an object that allows * interaction with the network client. * @returns The return value of the given function. From 6a4e3c172e55482ae4b133fec7f68024ef2ee3f8 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 17 Jun 2026 16:38:58 +0200 Subject: [PATCH 05/28] feat(network-controller): react to wallet-framework-rpc-failover-force-enabled flag Subscribe to RemoteFeatureFlagController state changes and read the forced failover flag on init, reconstructing affected network clients. Add public enableRpcFailoverForced/disableRpcFailoverForced methods and their messenger action types. Update existing tests for the new createAutoManagedNetworkClient argument and the new auto-managed client methods. --- .../NetworkController-method-action-types.ts | 21 + .../src/NetworkController.ts | 70 ++- .../tests/NetworkController.test.ts | 556 ++++++++++++++++++ packages/network-controller/tests/helpers.ts | 8 +- 4 files changed, 653 insertions(+), 2 deletions(-) diff --git a/packages/network-controller/src/NetworkController-method-action-types.ts b/packages/network-controller/src/NetworkController-method-action-types.ts index c07de322ba..a1cd8f33c9 100644 --- a/packages/network-controller/src/NetworkController-method-action-types.ts +++ b/packages/network-controller/src/NetworkController-method-action-types.ts @@ -36,6 +36,25 @@ export type NetworkControllerDisableRpcFailoverAction = { handler: NetworkController['disableRpcFailover']; }; +/** + * Forces RPC failover for Infura endpoints. When enabled, any Infura endpoint + * configured with failover URLs will route all traffic to those failover URLs, + * bypassing Infura entirely. + */ +export type NetworkControllerEnableRpcFailoverForcedAction = { + type: `NetworkController:enableRpcFailoverForced`; + handler: NetworkController['enableRpcFailoverForced']; +}; + +/** + * Stops forcing RPC failover for Infura endpoints, restoring normal + * automatic-failover behavior. + */ +export type NetworkControllerDisableRpcFailoverForcedAction = { + type: `NetworkController:disableRpcFailoverForced`; + handler: NetworkController['disableRpcFailoverForced']; +}; + /** * Accesses the provider and block tracker for the currently selected network. * @@ -311,6 +330,8 @@ export type NetworkControllerMethodActions = | NetworkControllerGetEthQueryAction | NetworkControllerEnableRpcFailoverAction | NetworkControllerDisableRpcFailoverAction + | NetworkControllerEnableRpcFailoverForcedAction + | NetworkControllerDisableRpcFailoverForcedAction | NetworkControllerGetProviderAndBlockTrackerAction | NetworkControllerGetSelectedNetworkClientAction | NetworkControllerGetSelectedChainIdAction diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index b56a0c149b..2007f4ebfd 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -59,7 +59,7 @@ import type { NetworkControllerMethodActions, } from './NetworkController-method-action-types'; import type { RpcServiceOptionsWithDefaults } from './rpc-service/rpc-service'; -import { getIsRpcFailoverEnabled } from './selectors'; +import { getIsRpcFailoverEnabled, getIsRpcFailoverForced } from './selectors'; import { NetworkClientType } from './types'; import type { BlockTracker, @@ -672,7 +672,9 @@ type AllowedEvents = RemoteFeatureFlagControllerStateChangeEvent; const MESSENGER_EXPOSED_METHODS = [ 'addNetwork', 'disableRpcFailover', + 'disableRpcFailoverForced', 'enableRpcFailover', + 'enableRpcFailoverForced', 'findNetworkClientIdByChainId', 'get1559CompatibilityWithNetworkClientId', 'getEIP1559Compatibility', @@ -1392,6 +1394,15 @@ export class NetworkController extends BaseController< }, getIsRpcFailoverEnabled, ); + + this.messenger.subscribe( + // eslint-disable-next-line no-restricted-syntax + 'RemoteFeatureFlagController:stateChange', + (isRpcFailoverForced) => { + this.#updateRpcFailoverForced(isRpcFailoverForced); + }, + getIsRpcFailoverForced, + ); } /** @@ -1422,6 +1433,24 @@ export class NetworkController extends BaseController< this.#updateRpcFailoverEnabled(false); } + /** + * Forces RPC failover for Infura endpoints. When enabled, any Infura endpoint + * configured with failover URLs will route all traffic to those failover URLs, + * bypassing Infura entirely. Infura endpoints without failover URLs continue to + * use Infura. Custom endpoints are unaffected. + */ + enableRpcFailoverForced(): void { + this.#updateRpcFailoverForced(true); + } + + /** + * Stops forcing RPC failover for Infura endpoints, restoring the normal + * automatic-failover behavior governed by {@link enableRpcFailover}. + */ + disableRpcFailoverForced(): void { + this.#updateRpcFailoverForced(false); + } + /** * Enables or disables the RPC failover functionality, depending on the * boolean given. This is done by reconstructing all network clients that were @@ -1463,6 +1492,44 @@ export class NetworkController extends BaseController< this.#isRpcFailoverEnabled = newIsRpcFailoverEnabled; } + /** + * Enables or disables forced RPC failover, depending on the boolean given. + * This reconstructs all network clients that were configured with failover + * URLs so the new value takes effect. Network client IDs are preserved. + * + * @param newIsRpcFailoverForced - Whether or not to force RPC failover. + */ + #updateRpcFailoverForced(newIsRpcFailoverForced: boolean): void { + if (this.#isRpcFailoverForced === newIsRpcFailoverForced) { + return; + } + + const autoManagedNetworkClientRegistry = + this.#ensureAutoManagedNetworkClientRegistryPopulated(); + + for (const networkClientsById of Object.values( + autoManagedNetworkClientRegistry, + )) { + for (const networkClientId of Object.keys(networkClientsById)) { + // Type assertion: We can assume that `networkClientId` is valid here. + const networkClient = + networkClientsById[ + networkClientId as keyof typeof networkClientsById + ]; + if ( + networkClient.configuration.failoverRpcUrls && + networkClient.configuration.failoverRpcUrls.length > 0 + ) { + newIsRpcFailoverForced + ? networkClient.enableRpcFailoverForced() + : networkClient.disableRpcFailoverForced(); + } + } + } + + this.#isRpcFailoverForced = newIsRpcFailoverForced; + } + /** * Accesses the provider and block tracker for the currently selected network. * @@ -1630,6 +1697,7 @@ export class NetworkController extends BaseController< init(): void { const state = this.messenger.call('RemoteFeatureFlagController:getState'); this.#updateRpcFailoverEnabled(getIsRpcFailoverEnabled(state)); + this.#updateRpcFailoverForced(getIsRpcFailoverForced(state)); this.#applyNetworkSelection(this.state.selectedNetworkClientId); } diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index e715e650aa..2586183f61 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1113,6 +1113,496 @@ describe('NetworkController', () => { }); }); + describe('enableRpcFailoverForced', () => { + describe('if the controller was initialized with isRpcFailoverForced = false', () => { + it('calls enableRpcFailoverForced on only the network clients whose RPC endpoints have configured failover URLs', async () => { + const originalCreateAutoManagedNetworkClient = + createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; + const autoManagedNetworkClients: AutoManagedNetworkClient[] = + []; + jest + .spyOn( + createAutoManagedNetworkClientModule, + 'createAutoManagedNetworkClient', + ) + .mockImplementation((...args) => { + const autoManagedNetworkClient = + originalCreateAutoManagedNetworkClient(...args); + jest.spyOn(autoManagedNetworkClient, 'enableRpcFailoverForced'); + autoManagedNetworkClients.push(autoManagedNetworkClient); + return autoManagedNetworkClient; + }); + + await withController( + { + isRpcFailoverForced: false, + state: { + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurationsByChainId: { + [ChainId.mainnet]: buildInfuraNetworkConfiguration( + InfuraNetworkType.mainnet, + { + rpcEndpoints: [ + buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { + failoverUrls: [], + }), + ], + }, + ), + '0x200': buildCustomNetworkConfiguration({ + chainId: '0x200', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + url: 'https://test.network/1', + failoverUrls: ['https://failover.endpoint/1'], + }), + ], + }), + '0x300': buildCustomNetworkConfiguration({ + chainId: '0x300', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'BBBB-BBBB-BBBB-BBBB', + url: 'https://test.network/2', + failoverUrls: ['https://failover.endpoint/2'], + }), + ], + }), + }, + }, + }, + async ({ controller }) => { + controller.enableRpcFailoverForced(); + + expect(autoManagedNetworkClients).toHaveLength(3); + expect( + autoManagedNetworkClients[0].enableRpcFailoverForced, + ).not.toHaveBeenCalled(); + expect( + autoManagedNetworkClients[1].enableRpcFailoverForced, + ).toHaveBeenCalled(); + expect( + autoManagedNetworkClients[2].enableRpcFailoverForced, + ).toHaveBeenCalled(); + }, + ); + }); + }); + + describe('if the controller was initialized with isRpcFailoverForced = true', () => { + it('does not call createAutoManagedNetworkClient at all', async () => { + await withController( + { + isRpcFailoverForced: true, + state: { + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurationsByChainId: { + [ChainId.mainnet]: buildInfuraNetworkConfiguration( + InfuraNetworkType.mainnet, + { + rpcEndpoints: [ + buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { + failoverUrls: [], + }), + ], + }, + ), + '0x200': buildCustomNetworkConfiguration({ + chainId: '0x200', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + url: 'https://test.network/1', + failoverUrls: ['https://failover.endpoint/1'], + }), + ], + }), + '0x300': buildCustomNetworkConfiguration({ + chainId: '0x300', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'BBBB-BBBB-BBBB-BBBB', + url: 'https://test.network/2', + failoverUrls: ['https://failover.endpoint/2'], + }), + ], + }), + }, + }, + }, + async ({ controller }) => { + const originalCreateAutoManagedNetworkClient = + createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; + const autoManagedNetworkClients: AutoManagedNetworkClient[] = + []; + jest + .spyOn( + createAutoManagedNetworkClientModule, + 'createAutoManagedNetworkClient', + ) + .mockImplementation((...args) => { + const autoManagedNetworkClient = + originalCreateAutoManagedNetworkClient(...args); + jest.spyOn(autoManagedNetworkClient, 'enableRpcFailoverForced'); + autoManagedNetworkClients.push(autoManagedNetworkClient); + return autoManagedNetworkClient; + }); + + controller.enableRpcFailoverForced(); + + expect(autoManagedNetworkClients).toHaveLength(0); + }, + ); + }); + }); + }); + + describe('disableRpcFailoverForced', () => { + describe('if the controller was initialized with isRpcFailoverForced = true', () => { + it('calls disableRpcFailoverForced on only the network clients whose RPC endpoints have configured failover URLs', async () => { + const originalCreateAutoManagedNetworkClient = + createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; + const autoManagedNetworkClients: AutoManagedNetworkClient[] = + []; + jest + .spyOn( + createAutoManagedNetworkClientModule, + 'createAutoManagedNetworkClient', + ) + .mockImplementation((...args) => { + const autoManagedNetworkClient = + originalCreateAutoManagedNetworkClient(...args); + jest.spyOn(autoManagedNetworkClient, 'disableRpcFailoverForced'); + autoManagedNetworkClients.push(autoManagedNetworkClient); + return autoManagedNetworkClient; + }); + + await withController( + { + isRpcFailoverForced: true, + state: { + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurationsByChainId: { + [ChainId.mainnet]: buildInfuraNetworkConfiguration( + InfuraNetworkType.mainnet, + { + rpcEndpoints: [ + buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { + failoverUrls: [], + }), + ], + }, + ), + '0x200': buildCustomNetworkConfiguration({ + chainId: '0x200', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + url: 'https://test.network/1', + failoverUrls: ['https://failover.endpoint/1'], + }), + ], + }), + '0x300': buildCustomNetworkConfiguration({ + chainId: '0x300', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'BBBB-BBBB-BBBB-BBBB', + url: 'https://test.network/2', + failoverUrls: ['https://failover.endpoint/2'], + }), + ], + }), + }, + }, + }, + async ({ controller }) => { + controller.disableRpcFailoverForced(); + + expect(autoManagedNetworkClients).toHaveLength(3); + expect( + autoManagedNetworkClients[0].disableRpcFailoverForced, + ).not.toHaveBeenCalled(); + expect( + autoManagedNetworkClients[1].disableRpcFailoverForced, + ).toHaveBeenCalled(); + expect( + autoManagedNetworkClients[2].disableRpcFailoverForced, + ).toHaveBeenCalled(); + }, + ); + }); + }); + + describe('if the controller was initialized with isRpcFailoverForced = false', () => { + it('does not call createAutoManagedNetworkClient at all', async () => { + await withController( + { + isRpcFailoverForced: false, + state: { + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurationsByChainId: { + [ChainId.mainnet]: buildInfuraNetworkConfiguration( + InfuraNetworkType.mainnet, + { + rpcEndpoints: [ + buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { + failoverUrls: [], + }), + ], + }, + ), + '0x200': buildCustomNetworkConfiguration({ + chainId: '0x200', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + url: 'https://test.network/1', + failoverUrls: ['https://failover.endpoint/1'], + }), + ], + }), + '0x300': buildCustomNetworkConfiguration({ + chainId: '0x300', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'BBBB-BBBB-BBBB-BBBB', + url: 'https://test.network/2', + failoverUrls: ['https://failover.endpoint/2'], + }), + ], + }), + }, + }, + }, + async ({ controller }) => { + const originalCreateAutoManagedNetworkClient = + createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; + const autoManagedNetworkClients: AutoManagedNetworkClient[] = + []; + jest + .spyOn( + createAutoManagedNetworkClientModule, + 'createAutoManagedNetworkClient', + ) + .mockImplementation((...args) => { + const autoManagedNetworkClient = + originalCreateAutoManagedNetworkClient(...args); + jest.spyOn( + autoManagedNetworkClient, + 'disableRpcFailoverForced', + ); + autoManagedNetworkClients.push(autoManagedNetworkClient); + return autoManagedNetworkClient; + }); + + controller.disableRpcFailoverForced(); + + expect(autoManagedNetworkClients).toHaveLength(0); + }, + ); + }); + }); + }); + + describe('RemoteFeatureFlagController:stateChange (isRpcFailoverForced)', () => { + it('calls enableRpcFailoverForced on clients with failover URLs when the flag turns true', async () => { + const originalCreateAutoManagedNetworkClient = + createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; + const autoManagedNetworkClients: AutoManagedNetworkClient[] = + []; + jest + .spyOn( + createAutoManagedNetworkClientModule, + 'createAutoManagedNetworkClient', + ) + .mockImplementation((...args) => { + const autoManagedNetworkClient = + originalCreateAutoManagedNetworkClient(...args); + jest.spyOn(autoManagedNetworkClient, 'enableRpcFailoverForced'); + autoManagedNetworkClients.push(autoManagedNetworkClient); + return autoManagedNetworkClient; + }); + + await withController( + { + isRpcFailoverForced: false, + state: { + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurationsByChainId: { + [ChainId.mainnet]: buildInfuraNetworkConfiguration( + InfuraNetworkType.mainnet, + { + rpcEndpoints: [ + buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { + failoverUrls: [], + }), + ], + }, + ), + '0x200': buildCustomNetworkConfiguration({ + chainId: '0x200', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + url: 'https://test.network/1', + failoverUrls: ['https://failover.endpoint/1'], + }), + ], + }), + }, + }, + }, + async ({ messenger }) => { + messenger.publish( + // eslint-disable-next-line no-restricted-syntax + 'RemoteFeatureFlagController:stateChange', + { + remoteFeatureFlags: { + walletFrameworkRpcFailoverEnabled: false, + 'wallet-framework-rpc-failover-force-enabled': true, + }, + cacheTimestamp: 0, + }, + [], + ); + + expect(autoManagedNetworkClients).toHaveLength(2); + expect( + autoManagedNetworkClients[0].enableRpcFailoverForced, + ).not.toHaveBeenCalled(); + expect( + autoManagedNetworkClients[1].enableRpcFailoverForced, + ).toHaveBeenCalled(); + }, + ); + }); + + it('picks up the initial forced value during init()', async () => { + const originalCreateAutoManagedNetworkClient = + createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; + const autoManagedNetworkClients: AutoManagedNetworkClient[] = + []; + jest + .spyOn( + createAutoManagedNetworkClientModule, + 'createAutoManagedNetworkClient', + ) + .mockImplementation((...args) => { + const autoManagedNetworkClient = + originalCreateAutoManagedNetworkClient(...args); + jest.spyOn(autoManagedNetworkClient, 'enableRpcFailoverForced'); + autoManagedNetworkClients.push(autoManagedNetworkClient); + return autoManagedNetworkClient; + }); + + await withController( + { + isRpcFailoverForced: true, + state: { + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurationsByChainId: { + [ChainId.mainnet]: buildInfuraNetworkConfiguration( + InfuraNetworkType.mainnet, + { + rpcEndpoints: [ + buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { + failoverUrls: ['https://failover.endpoint/1'], + }), + ], + }, + ), + '0x200': buildCustomNetworkConfiguration({ + chainId: '0x200', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + url: 'https://test.network/1', + failoverUrls: [], + }), + ], + }), + }, + }, + }, + async () => { + expect(autoManagedNetworkClients).toHaveLength(2); + expect( + autoManagedNetworkClients[0].enableRpcFailoverForced, + ).toHaveBeenCalled(); + expect( + autoManagedNetworkClients[1].enableRpcFailoverForced, + ).not.toHaveBeenCalled(); + }, + ); + }); + + it('calls enableRpcFailoverForced but not enableRpcFailover when only the forced flag is true', async () => { + const originalCreateAutoManagedNetworkClient = + createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; + const autoManagedNetworkClients: AutoManagedNetworkClient[] = + []; + jest + .spyOn( + createAutoManagedNetworkClientModule, + 'createAutoManagedNetworkClient', + ) + .mockImplementation((...args) => { + const autoManagedNetworkClient = + originalCreateAutoManagedNetworkClient(...args); + jest.spyOn(autoManagedNetworkClient, 'enableRpcFailover'); + jest.spyOn(autoManagedNetworkClient, 'enableRpcFailoverForced'); + autoManagedNetworkClients.push(autoManagedNetworkClient); + return autoManagedNetworkClient; + }); + + await withController( + { + isRpcFailoverEnabled: false, + isRpcFailoverForced: false, + state: { + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurationsByChainId: { + '0x200': buildCustomNetworkConfiguration({ + chainId: '0x200', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + url: 'https://test.network/1', + failoverUrls: ['https://failover.endpoint/1'], + }), + ], + }), + }, + }, + }, + async ({ messenger }) => { + messenger.publish( + // eslint-disable-next-line no-restricted-syntax + 'RemoteFeatureFlagController:stateChange', + { + remoteFeatureFlags: { + walletFrameworkRpcFailoverEnabled: false, + 'wallet-framework-rpc-failover-force-enabled': true, + }, + cacheTimestamp: 0, + }, + [], + ); + + expect(autoManagedNetworkClients).toHaveLength(1); + expect( + autoManagedNetworkClients[0].enableRpcFailoverForced, + ).toHaveBeenCalled(); + expect( + autoManagedNetworkClients[0].enableRpcFailover, + ).not.toHaveBeenCalled(); + }, + ); + }); + }); + describe('destroy', () => { it('does not throw if called before the provider is initialized', async () => { await withController(async ({ controller }) => { @@ -1677,6 +2167,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, 'base-mainnet': { blockTracker: expect.anything(), @@ -1692,6 +2184,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, 'bsc-mainnet': { blockTracker: expect.anything(), @@ -1707,6 +2201,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, 'linea-mainnet': { blockTracker: expect.anything(), @@ -1722,6 +2218,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, 'linea-sepolia': { blockTracker: expect.anything(), @@ -1737,6 +2235,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, mainnet: { blockTracker: expect.anything(), @@ -1752,6 +2252,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, 'megaeth-testnet': { blockTracker: expect.anything(), @@ -1766,6 +2268,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, 'megaeth-testnet-v2': { blockTracker: expect.anything(), @@ -1780,6 +2284,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, 'monad-mainnet': { blockTracker: expect.anything(), @@ -1795,6 +2301,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, 'monad-testnet': { blockTracker: expect.anything(), @@ -1809,6 +2317,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, 'optimism-mainnet': { blockTracker: expect.anything(), @@ -1824,6 +2334,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, 'polygon-mainnet': { blockTracker: expect.anything(), @@ -1839,6 +2351,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, sepolia: { blockTracker: expect.anything(), @@ -1854,6 +2368,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, }); }, @@ -1910,6 +2426,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, 'BBBB-BBBB-BBBB-BBBB': { blockTracker: expect.anything(), @@ -1924,6 +2442,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, }); }, @@ -4568,6 +5088,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( @@ -4585,6 +5107,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( @@ -4602,6 +5126,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); const networkConfigurationsByNetworkClientId = @@ -6069,6 +6595,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByNetworkClientId = @@ -6385,6 +6913,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( createAutoManagedNetworkClientSpy, @@ -6401,6 +6931,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByNetworkClientId = @@ -7378,6 +7910,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByNetworkClientId = getNetworkConfigurationsByNetworkClientId( @@ -8253,6 +8787,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( @@ -8270,6 +8806,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); @@ -9260,6 +9798,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( getNetworkConfigurationsByNetworkClientId( @@ -10421,6 +10961,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( createAutoManagedNetworkClientSpy, @@ -10437,6 +10979,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByNetworkClientId = @@ -11143,6 +11687,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ networkClientId: 'DDDD-DDDD-DDDD-DDDD', @@ -11157,6 +11703,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( @@ -11878,6 +12426,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( createAutoManagedNetworkClientSpy, @@ -11894,6 +12444,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByChainId = @@ -12581,6 +13133,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( @@ -12598,6 +13152,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); diff --git a/packages/network-controller/tests/helpers.ts b/packages/network-controller/tests/helpers.ts index adfa80aeb9..f2f715054b 100644 --- a/packages/network-controller/tests/helpers.ts +++ b/packages/network-controller/tests/helpers.ts @@ -90,14 +90,17 @@ export const TESTNET = { * @param options.connectivityStatus - The connectivity status to return by default. * If not provided, defaults to Online. * @param options.isRpcFailoverEnabled - The RPC failover feature flag to return, defaults to false. + * @param options.isRpcFailoverForced - The forced RPC failover feature flag to return, defaults to false. * @returns The messenger. */ export function buildRootMessenger({ connectivityStatus = CONNECTIVITY_STATUSES.Online, isRpcFailoverEnabled = false, + isRpcFailoverForced = false, }: { connectivityStatus?: ConnectivityStatus; isRpcFailoverEnabled?: boolean; + isRpcFailoverForced?: boolean; } = {}): RootMessenger { const rootMessenger = new Messenger< MockAnyNamespace, @@ -117,6 +120,7 @@ export function buildRootMessenger({ () => ({ remoteFeatureFlags: { walletFrameworkRpcFailoverEnabled: isRpcFailoverEnabled, + 'wallet-framework-rpc-failover-force-enabled': isRpcFailoverForced, }, cacheTimestamp: 0, }), @@ -632,6 +636,7 @@ type WithControllerCallback = ({ type WithControllerOptions = Partial & { isRpcFailoverEnabled?: boolean; + isRpcFailoverForced?: boolean; initializeController?: boolean; }; @@ -655,10 +660,11 @@ export async function withController( const [{ ...rest }, fn] = args.length === 2 ? args : [{}, args[0]]; const { isRpcFailoverEnabled, + isRpcFailoverForced, initializeController = true, ...controllerOptions } = rest; - const messenger = buildRootMessenger({ isRpcFailoverEnabled }); + const messenger = buildRootMessenger({ isRpcFailoverEnabled, isRpcFailoverForced }); const networkControllerMessenger = buildNetworkControllerMessenger(messenger); const controller = new NetworkController({ messenger: networkControllerMessenger, From 9beb97d5da8b844487e226330769aab409d6efd2 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 17 Jun 2026 18:45:53 +0200 Subject: [PATCH 06/28] fix(network-controller): export forced failover action types Export NetworkControllerEnableRpcFailoverForcedAction and NetworkControllerDisableRpcFailoverForcedAction, and update the init JSDoc to mention both failover flags. --- packages/network-controller/src/NetworkController.ts | 5 +++-- packages/network-controller/src/index.ts | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 2007f4ebfd..86d22b555f 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -1691,8 +1691,9 @@ export class NetworkController extends BaseController< } /** - * Initialize the NetworkController, updating the RPC failover feature flag - * and applying the network selection. + * Initialize the NetworkController, updating the RPC failover feature flags + * (`isRpcFailoverEnabled` and `isRpcFailoverForced`) and applying the network + * selection. */ init(): void { const state = this.messenger.call('RemoteFeatureFlagController:getState'); diff --git a/packages/network-controller/src/index.ts b/packages/network-controller/src/index.ts index 9b9179fc8a..6cf60d6a52 100644 --- a/packages/network-controller/src/index.ts +++ b/packages/network-controller/src/index.ts @@ -71,6 +71,8 @@ export type { NetworkControllerUpdateNetworkAction, NetworkControllerEnableRpcFailoverAction, NetworkControllerDisableRpcFailoverAction, + NetworkControllerEnableRpcFailoverForcedAction, + NetworkControllerDisableRpcFailoverForcedAction, NetworkControllerGetProviderAndBlockTrackerAction, NetworkControllerGetNetworkClientRegistryAction, NetworkControllerLookupNetworkAction, From 938e1bde5387cf434922cdd73baf9f8ec47c67f7 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 17 Jun 2026 18:47:49 +0200 Subject: [PATCH 07/28] docs(network-controller): changelog for forced RPC failover --- packages/network-controller/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index a203ae6398..5d829cb9e0 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -18,6 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The default `retryTimeout` for the block tracker is now `20` seconds. - Add `failoverUrls` constructor argument ([#9140](https://github.com/MetaMask/core/pull/9140)) - These will override `failoverUrls` from state during network client creation. +- Add forced RPC failover for Infura endpoints, driven by the `wallet-framework-rpc-failover-force-enabled` remote feature flag ([#9170](https://github.com/MetaMask/core/pull/9170)) + - When enabled, Infura endpoints configured with failover URLs route all traffic to those failover URLs, bypassing Infura entirely. Infura endpoints without failover URLs continue to use Infura, and custom RPC endpoints are unaffected. + - Adds the `NetworkController.enableRpcFailoverForced` and `NetworkController.disableRpcFailoverForced` methods, along with the `NetworkControllerEnableRpcFailoverForcedAction` and `NetworkControllerDisableRpcFailoverForcedAction` messenger actions. ### Changed From ba7ad247bc57e573a73e486961573bfd55d6c31d Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 17 Jun 2026 19:08:33 +0200 Subject: [PATCH 08/28] docs(network-controller): point changelog at the real PR number --- packages/network-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 5d829cb9e0..b4a59af0ef 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The default `retryTimeout` for the block tracker is now `20` seconds. - Add `failoverUrls` constructor argument ([#9140](https://github.com/MetaMask/core/pull/9140)) - These will override `failoverUrls` from state during network client creation. -- Add forced RPC failover for Infura endpoints, driven by the `wallet-framework-rpc-failover-force-enabled` remote feature flag ([#9170](https://github.com/MetaMask/core/pull/9170)) +- Add forced RPC failover for Infura endpoints, driven by the `wallet-framework-rpc-failover-force-enabled` remote feature flag ([#9175](https://github.com/MetaMask/core/pull/9175)) - When enabled, Infura endpoints configured with failover URLs route all traffic to those failover URLs, bypassing Infura entirely. Infura endpoints without failover URLs continue to use Infura, and custom RPC endpoints are unaffected. - Adds the `NetworkController.enableRpcFailoverForced` and `NetworkController.disableRpcFailoverForced` methods, along with the `NetworkControllerEnableRpcFailoverForcedAction` and `NetworkControllerDisableRpcFailoverForcedAction` messenger actions. From 51ec73529bfaf448130cb679f016b2d75b7e4e23 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 17 Jun 2026 19:18:08 +0200 Subject: [PATCH 09/28] docs(network-controller): note positional primary under forced failover --- packages/network-controller/src/create-network-client.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index 9a4bd5196d..2ed0b99f64 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -285,6 +285,9 @@ function createRpcServiceChain({ if (isRpcFailoverForced && isInfuraEndpoint && failoverEndpoints.length > 0) { // Force flag is on for an Infura endpoint with failovers: bypass Infura // entirely and route all traffic (including block polling) to failovers. + // The first failover becomes the positional primary of the chain, so + // availability/degraded events will report that failover URL as the + // primary endpoint (there is no Infura primary in this mode). availableEndpoints = failoverEndpoints; } else if (isRpcFailoverEnabled) { availableEndpoints = [ From 4bc51fef9caf14eac34fd93f42fff8ae833efdf5 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 17 Jun 2026 19:27:28 +0200 Subject: [PATCH 10/28] fix: run lint:misc --- .../network-controller/src/create-network-client.ts | 10 ++++++---- packages/network-controller/tests/helpers.ts | 5 ++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index 2ed0b99f64..078ebea5d6 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -275,10 +275,12 @@ function createRpcServiceChain({ isRpcFailoverForced: boolean; logger?: Logger; }): RpcServiceChain { - const failoverEndpoints = (configuration.failoverRpcUrls ?? []).map((url) => ({ - url, - isFailover: true, - })); + const failoverEndpoints = (configuration.failoverRpcUrls ?? []).map( + (url) => ({ + url, + isFailover: true, + }), + ); const isInfuraEndpoint = configuration.type === NetworkClientType.Infura; let availableEndpoints: { url: string; isFailover: boolean }[]; diff --git a/packages/network-controller/tests/helpers.ts b/packages/network-controller/tests/helpers.ts index f2f715054b..5982b6646f 100644 --- a/packages/network-controller/tests/helpers.ts +++ b/packages/network-controller/tests/helpers.ts @@ -664,7 +664,10 @@ export async function withController( initializeController = true, ...controllerOptions } = rest; - const messenger = buildRootMessenger({ isRpcFailoverEnabled, isRpcFailoverForced }); + const messenger = buildRootMessenger({ + isRpcFailoverEnabled, + isRpcFailoverForced, + }); const networkControllerMessenger = buildNetworkControllerMessenger(messenger); const controller = new NetworkController({ messenger: networkControllerMessenger, From 18ef4dcb4423ee4beb180cd377213c63809c5473 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 17 Jun 2026 19:32:55 +0200 Subject: [PATCH 11/28] fix: messenger types --- .../src/NetworkController-method-action-types.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/network-controller/src/NetworkController-method-action-types.ts b/packages/network-controller/src/NetworkController-method-action-types.ts index a1cd8f33c9..45d5e94499 100644 --- a/packages/network-controller/src/NetworkController-method-action-types.ts +++ b/packages/network-controller/src/NetworkController-method-action-types.ts @@ -39,7 +39,8 @@ export type NetworkControllerDisableRpcFailoverAction = { /** * Forces RPC failover for Infura endpoints. When enabled, any Infura endpoint * configured with failover URLs will route all traffic to those failover URLs, - * bypassing Infura entirely. + * bypassing Infura entirely. Infura endpoints without failover URLs continue to + * use Infura. Custom endpoints are unaffected. */ export type NetworkControllerEnableRpcFailoverForcedAction = { type: `NetworkController:enableRpcFailoverForced`; @@ -47,8 +48,8 @@ export type NetworkControllerEnableRpcFailoverForcedAction = { }; /** - * Stops forcing RPC failover for Infura endpoints, restoring normal - * automatic-failover behavior. + * Stops forcing RPC failover for Infura endpoints, restoring the normal + * automatic-failover behavior governed by {@link enableRpcFailover}. */ export type NetworkControllerDisableRpcFailoverForcedAction = { type: `NetworkController:disableRpcFailoverForced`; From fe94fab2b117aed5889c35a2cdd1fe0d027007da Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 17 Jun 2026 19:41:39 +0200 Subject: [PATCH 12/28] fix: remove no-restricted-syntax --- packages/network-controller/tests/NetworkController.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 2586183f61..c006436493 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1456,7 +1456,6 @@ describe('NetworkController', () => { }, async ({ messenger }) => { messenger.publish( - // eslint-disable-next-line no-restricted-syntax 'RemoteFeatureFlagController:stateChange', { remoteFeatureFlags: { @@ -1579,7 +1578,6 @@ describe('NetworkController', () => { }, async ({ messenger }) => { messenger.publish( - // eslint-disable-next-line no-restricted-syntax 'RemoteFeatureFlagController:stateChange', { remoteFeatureFlags: { From 0bd55665a0fbcc8f1458075a922dc92d0b1ca162 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 17 Jun 2026 21:38:38 +0200 Subject: [PATCH 13/28] refactor(network-controller): rename force-failover flag to core-platform prefix Rename the remote flag key from wallet-framework-rpc-failover-force-enabled to core-platform-rpc-failover-force-enabled to match the team's new name. --- packages/network-controller/CHANGELOG.md | 2 +- packages/network-controller/src/selectors.test.ts | 6 +++--- packages/network-controller/src/selectors.ts | 2 +- packages/network-controller/tests/NetworkController.test.ts | 4 ++-- packages/network-controller/tests/helpers.ts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index b4a59af0ef..9653ce90a4 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The default `retryTimeout` for the block tracker is now `20` seconds. - Add `failoverUrls` constructor argument ([#9140](https://github.com/MetaMask/core/pull/9140)) - These will override `failoverUrls` from state during network client creation. -- Add forced RPC failover for Infura endpoints, driven by the `wallet-framework-rpc-failover-force-enabled` remote feature flag ([#9175](https://github.com/MetaMask/core/pull/9175)) +- Add forced RPC failover for Infura endpoints, driven by the `core-platform-rpc-failover-force-enabled` remote feature flag ([#9175](https://github.com/MetaMask/core/pull/9175)) - When enabled, Infura endpoints configured with failover URLs route all traffic to those failover URLs, bypassing Infura entirely. Infura endpoints without failover URLs continue to use Infura, and custom RPC endpoints are unaffected. - Adds the `NetworkController.enableRpcFailoverForced` and `NetworkController.disableRpcFailoverForced` methods, along with the `NetworkControllerEnableRpcFailoverForcedAction` and `NetworkControllerDisableRpcFailoverForcedAction` messenger actions. diff --git a/packages/network-controller/src/selectors.test.ts b/packages/network-controller/src/selectors.test.ts index f75f010bb6..232d82c41a 100644 --- a/packages/network-controller/src/selectors.test.ts +++ b/packages/network-controller/src/selectors.test.ts @@ -4,7 +4,7 @@ describe('getIsRpcFailoverForced', () => { it('returns true when the flag is true', () => { const state = { remoteFeatureFlags: { - 'wallet-framework-rpc-failover-force-enabled': true, + 'core-platform-rpc-failover-force-enabled': true, }, cacheTimestamp: 0, }; @@ -14,7 +14,7 @@ describe('getIsRpcFailoverForced', () => { it('returns false when the flag is false', () => { const state = { remoteFeatureFlags: { - 'wallet-framework-rpc-failover-force-enabled': false, + 'core-platform-rpc-failover-force-enabled': false, }, cacheTimestamp: 0, }; @@ -29,7 +29,7 @@ describe('getIsRpcFailoverForced', () => { it('passes through non-boolean values without coercion', () => { const state = { remoteFeatureFlags: { - 'wallet-framework-rpc-failover-force-enabled': 'yes', + 'core-platform-rpc-failover-force-enabled': 'yes', }, cacheTimestamp: 0, }; diff --git a/packages/network-controller/src/selectors.ts b/packages/network-controller/src/selectors.ts index af08bb4d99..8c6afa6f12 100644 --- a/packages/network-controller/src/selectors.ts +++ b/packages/network-controller/src/selectors.ts @@ -12,7 +12,7 @@ export function getIsRpcFailoverForced( state: RemoteFeatureFlagControllerState, ): boolean { const forceEnabled = state.remoteFeatureFlags[ - 'wallet-framework-rpc-failover-force-enabled' + 'core-platform-rpc-failover-force-enabled' ] as boolean | undefined; return forceEnabled ?? false; } diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index c006436493..c22f0671c2 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1460,7 +1460,7 @@ describe('NetworkController', () => { { remoteFeatureFlags: { walletFrameworkRpcFailoverEnabled: false, - 'wallet-framework-rpc-failover-force-enabled': true, + 'core-platform-rpc-failover-force-enabled': true, }, cacheTimestamp: 0, }, @@ -1582,7 +1582,7 @@ describe('NetworkController', () => { { remoteFeatureFlags: { walletFrameworkRpcFailoverEnabled: false, - 'wallet-framework-rpc-failover-force-enabled': true, + 'core-platform-rpc-failover-force-enabled': true, }, cacheTimestamp: 0, }, diff --git a/packages/network-controller/tests/helpers.ts b/packages/network-controller/tests/helpers.ts index 5982b6646f..45a233e547 100644 --- a/packages/network-controller/tests/helpers.ts +++ b/packages/network-controller/tests/helpers.ts @@ -120,7 +120,7 @@ export function buildRootMessenger({ () => ({ remoteFeatureFlags: { walletFrameworkRpcFailoverEnabled: isRpcFailoverEnabled, - 'wallet-framework-rpc-failover-force-enabled': isRpcFailoverForced, + 'core-platform-rpc-failover-force-enabled': isRpcFailoverForced, }, cacheTimestamp: 0, }), From bc8dce770b08098cc15ea23a21ae6ad68269538e Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Mon, 22 Jun 2026 12:28:23 +0200 Subject: [PATCH 14/28] fix: changelog --- packages/network-controller/CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index a03f475e42..d5b2ac79a0 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add forced RPC failover for Infura endpoints, driven by the `core-platform-rpc-failover-force-enabled` remote feature flag ([#9175](https://github.com/MetaMask/core/pull/9175)) + - When enabled, Infura endpoints configured with failover URLs route all traffic to those failover URLs, bypassing Infura entirely. Infura endpoints without failover URLs continue to use Infura, and custom RPC endpoints are unaffected. + - Adds the `NetworkController.enableRpcFailoverForced` and `NetworkController.disableRpcFailoverForced` methods, along with the `NetworkControllerEnableRpcFailoverForcedAction` and `NetworkControllerDisableRpcFailoverForcedAction` messenger actions. + ## [33.0.0] ### Added @@ -20,9 +26,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The default `retryTimeout` for the block tracker is now `20` seconds. - Add `failoverUrls` constructor argument ([#9140](https://github.com/MetaMask/core/pull/9140)) - These will override `failoverUrls` from state during network client creation. -- Add forced RPC failover for Infura endpoints, driven by the `core-platform-rpc-failover-force-enabled` remote feature flag ([#9175](https://github.com/MetaMask/core/pull/9175)) - - When enabled, Infura endpoints configured with failover URLs route all traffic to those failover URLs, bypassing Infura entirely. Infura endpoints without failover URLs continue to use Infura, and custom RPC endpoints are unaffected. - - Adds the `NetworkController.enableRpcFailoverForced` and `NetworkController.disableRpcFailoverForced` methods, along with the `NetworkControllerEnableRpcFailoverForcedAction` and `NetworkControllerDisableRpcFailoverForcedAction` messenger actions. ### Changed @@ -1223,7 +1226,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial release + - As a result of converting our shared controllers repo into a monorepo ([#831](https://github.com/MetaMask/core/pull/831)), we've created this package from select parts of [`@metamask/controllers` v33.0.0](https://github.com/MetaMask/core/tree/v33.0.0), namely: + - Everything in `src/network` (minus `NetworkType` and `NetworksChainId`, which were placed in `@metamask/controller-utils`) All changes listed after this point were applied to this package following the monorepo conversion. From 2a256aff24916c7c641b35c27c47ee6ea0f15d10 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Mon, 22 Jun 2026 12:39:57 +0200 Subject: [PATCH 15/28] fix: lint:misc --- packages/network-controller/CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index d5b2ac79a0..3bcecdfc30 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -1226,9 +1226,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial release - - As a result of converting our shared controllers repo into a monorepo ([#831](https://github.com/MetaMask/core/pull/831)), we've created this package from select parts of [`@metamask/controllers` v33.0.0](https://github.com/MetaMask/core/tree/v33.0.0), namely: - - Everything in `src/network` (minus `NetworkType` and `NetworksChainId`, which were placed in `@metamask/controller-utils`) All changes listed after this point were applied to this package following the monorepo conversion. From 1aec6fea9aa6b63982eb630ec175529485827b7b Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Tue, 23 Jun 2026 18:12:50 +0200 Subject: [PATCH 16/28] fix(network-controller): read forced failover flag as camelCase key The ClientConfigAPI delivers remote feature flags to clients in camelCase, so the selector must read corePlatformRpcFailoverForceEnabled (matching the sibling walletFrameworkRpcFailoverEnabled), not the kebab-case LaunchDarkly name. --- packages/network-controller/CHANGELOG.md | 2 +- packages/network-controller/src/selectors.test.ts | 6 +++--- packages/network-controller/src/selectors.ts | 7 +++---- .../network-controller/tests/NetworkController.test.ts | 4 ++-- packages/network-controller/tests/helpers.ts | 2 +- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 3bcecdfc30..f86b17a74d 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add forced RPC failover for Infura endpoints, driven by the `core-platform-rpc-failover-force-enabled` remote feature flag ([#9175](https://github.com/MetaMask/core/pull/9175)) +- Add forced RPC failover for Infura endpoints, driven by the `corePlatformRpcFailoverForceEnabled` remote feature flag ([#9175](https://github.com/MetaMask/core/pull/9175)) - When enabled, Infura endpoints configured with failover URLs route all traffic to those failover URLs, bypassing Infura entirely. Infura endpoints without failover URLs continue to use Infura, and custom RPC endpoints are unaffected. - Adds the `NetworkController.enableRpcFailoverForced` and `NetworkController.disableRpcFailoverForced` methods, along with the `NetworkControllerEnableRpcFailoverForcedAction` and `NetworkControllerDisableRpcFailoverForcedAction` messenger actions. diff --git a/packages/network-controller/src/selectors.test.ts b/packages/network-controller/src/selectors.test.ts index 232d82c41a..d5a7c9261a 100644 --- a/packages/network-controller/src/selectors.test.ts +++ b/packages/network-controller/src/selectors.test.ts @@ -4,7 +4,7 @@ describe('getIsRpcFailoverForced', () => { it('returns true when the flag is true', () => { const state = { remoteFeatureFlags: { - 'core-platform-rpc-failover-force-enabled': true, + corePlatformRpcFailoverForceEnabled: true, }, cacheTimestamp: 0, }; @@ -14,7 +14,7 @@ describe('getIsRpcFailoverForced', () => { it('returns false when the flag is false', () => { const state = { remoteFeatureFlags: { - 'core-platform-rpc-failover-force-enabled': false, + corePlatformRpcFailoverForceEnabled: false, }, cacheTimestamp: 0, }; @@ -29,7 +29,7 @@ describe('getIsRpcFailoverForced', () => { it('passes through non-boolean values without coercion', () => { const state = { remoteFeatureFlags: { - 'core-platform-rpc-failover-force-enabled': 'yes', + corePlatformRpcFailoverForceEnabled: 'yes', }, cacheTimestamp: 0, }; diff --git a/packages/network-controller/src/selectors.ts b/packages/network-controller/src/selectors.ts index 8c6afa6f12..9dc157dfa5 100644 --- a/packages/network-controller/src/selectors.ts +++ b/packages/network-controller/src/selectors.ts @@ -11,8 +11,7 @@ export function getIsRpcFailoverEnabled( export function getIsRpcFailoverForced( state: RemoteFeatureFlagControllerState, ): boolean { - const forceEnabled = state.remoteFeatureFlags[ - 'core-platform-rpc-failover-force-enabled' - ] as boolean | undefined; - return forceEnabled ?? false; + const corePlatformRpcFailoverForceEnabled = state.remoteFeatureFlags + .corePlatformRpcFailoverForceEnabled as boolean | undefined; + return corePlatformRpcFailoverForceEnabled ?? false; } diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 0d3175f0cb..d763cbace0 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1442,7 +1442,7 @@ describe('NetworkController', () => { { remoteFeatureFlags: { walletFrameworkRpcFailoverEnabled: false, - 'core-platform-rpc-failover-force-enabled': true, + corePlatformRpcFailoverForceEnabled: true, }, cacheTimestamp: 0, }, @@ -1564,7 +1564,7 @@ describe('NetworkController', () => { { remoteFeatureFlags: { walletFrameworkRpcFailoverEnabled: false, - 'core-platform-rpc-failover-force-enabled': true, + corePlatformRpcFailoverForceEnabled: true, }, cacheTimestamp: 0, }, diff --git a/packages/network-controller/tests/helpers.ts b/packages/network-controller/tests/helpers.ts index 45a233e547..f3a95e58e6 100644 --- a/packages/network-controller/tests/helpers.ts +++ b/packages/network-controller/tests/helpers.ts @@ -120,7 +120,7 @@ export function buildRootMessenger({ () => ({ remoteFeatureFlags: { walletFrameworkRpcFailoverEnabled: isRpcFailoverEnabled, - 'core-platform-rpc-failover-force-enabled': isRpcFailoverForced, + corePlatformRpcFailoverForceEnabled: isRpcFailoverForced, }, cacheTimestamp: 0, }), From 4efa475122927afdc34943db03ef0c286d149b89 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 24 Jun 2026 14:55:33 +0200 Subject: [PATCH 17/28] refactor(network-controller): remove unused forced failover public API Drop the NetworkController enableRpcFailoverForced/disableRpcFailoverForced methods, their messenger actions, and exports. Forced failover is driven entirely by the remote feature flag; nothing calls these imperatively. --- packages/network-controller/CHANGELOG.md | 1 - .../NetworkController-method-action-types.ts | 22 -- .../src/NetworkController.ts | 20 -- packages/network-controller/src/index.ts | 2 - .../tests/NetworkController.test.ts | 293 ------------------ 5 files changed, 338 deletions(-) diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index f86b17a74d..9c204968a5 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -11,7 +11,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add forced RPC failover for Infura endpoints, driven by the `corePlatformRpcFailoverForceEnabled` remote feature flag ([#9175](https://github.com/MetaMask/core/pull/9175)) - When enabled, Infura endpoints configured with failover URLs route all traffic to those failover URLs, bypassing Infura entirely. Infura endpoints without failover URLs continue to use Infura, and custom RPC endpoints are unaffected. - - Adds the `NetworkController.enableRpcFailoverForced` and `NetworkController.disableRpcFailoverForced` methods, along with the `NetworkControllerEnableRpcFailoverForcedAction` and `NetworkControllerDisableRpcFailoverForcedAction` messenger actions. ## [33.0.0] diff --git a/packages/network-controller/src/NetworkController-method-action-types.ts b/packages/network-controller/src/NetworkController-method-action-types.ts index 45d5e94499..c07de322ba 100644 --- a/packages/network-controller/src/NetworkController-method-action-types.ts +++ b/packages/network-controller/src/NetworkController-method-action-types.ts @@ -36,26 +36,6 @@ export type NetworkControllerDisableRpcFailoverAction = { handler: NetworkController['disableRpcFailover']; }; -/** - * Forces RPC failover for Infura endpoints. When enabled, any Infura endpoint - * configured with failover URLs will route all traffic to those failover URLs, - * bypassing Infura entirely. Infura endpoints without failover URLs continue to - * use Infura. Custom endpoints are unaffected. - */ -export type NetworkControllerEnableRpcFailoverForcedAction = { - type: `NetworkController:enableRpcFailoverForced`; - handler: NetworkController['enableRpcFailoverForced']; -}; - -/** - * Stops forcing RPC failover for Infura endpoints, restoring the normal - * automatic-failover behavior governed by {@link enableRpcFailover}. - */ -export type NetworkControllerDisableRpcFailoverForcedAction = { - type: `NetworkController:disableRpcFailoverForced`; - handler: NetworkController['disableRpcFailoverForced']; -}; - /** * Accesses the provider and block tracker for the currently selected network. * @@ -331,8 +311,6 @@ export type NetworkControllerMethodActions = | NetworkControllerGetEthQueryAction | NetworkControllerEnableRpcFailoverAction | NetworkControllerDisableRpcFailoverAction - | NetworkControllerEnableRpcFailoverForcedAction - | NetworkControllerDisableRpcFailoverForcedAction | NetworkControllerGetProviderAndBlockTrackerAction | NetworkControllerGetSelectedNetworkClientAction | NetworkControllerGetSelectedChainIdAction diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index d9c1569de8..7f9063cc75 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -669,9 +669,7 @@ type AllowedEvents = RemoteFeatureFlagControllerStateChangeEvent; const MESSENGER_EXPOSED_METHODS = [ 'addNetwork', 'disableRpcFailover', - 'disableRpcFailoverForced', 'enableRpcFailover', - 'enableRpcFailoverForced', 'findNetworkClientIdByChainId', 'get1559CompatibilityWithNetworkClientId', 'getEIP1559Compatibility', @@ -1421,24 +1419,6 @@ export class NetworkController extends BaseController< this.#updateRpcFailoverEnabled(false); } - /** - * Forces RPC failover for Infura endpoints. When enabled, any Infura endpoint - * configured with failover URLs will route all traffic to those failover URLs, - * bypassing Infura entirely. Infura endpoints without failover URLs continue to - * use Infura. Custom endpoints are unaffected. - */ - enableRpcFailoverForced(): void { - this.#updateRpcFailoverForced(true); - } - - /** - * Stops forcing RPC failover for Infura endpoints, restoring the normal - * automatic-failover behavior governed by {@link enableRpcFailover}. - */ - disableRpcFailoverForced(): void { - this.#updateRpcFailoverForced(false); - } - /** * Enables or disables the RPC failover functionality, depending on the * boolean given. This is done by reconstructing all network clients that were diff --git a/packages/network-controller/src/index.ts b/packages/network-controller/src/index.ts index 330c3e624c..85d940d7c9 100644 --- a/packages/network-controller/src/index.ts +++ b/packages/network-controller/src/index.ts @@ -70,8 +70,6 @@ export type { NetworkControllerUpdateNetworkAction, NetworkControllerEnableRpcFailoverAction, NetworkControllerDisableRpcFailoverAction, - NetworkControllerEnableRpcFailoverForcedAction, - NetworkControllerDisableRpcFailoverForcedAction, NetworkControllerGetProviderAndBlockTrackerAction, NetworkControllerGetNetworkClientRegistryAction, NetworkControllerLookupNetworkAction, diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index d763cbace0..a16ce60b3a 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1095,299 +1095,6 @@ describe('NetworkController', () => { }); }); - describe('enableRpcFailoverForced', () => { - describe('if the controller was initialized with isRpcFailoverForced = false', () => { - it('calls enableRpcFailoverForced on only the network clients whose RPC endpoints have configured failover URLs', async () => { - const originalCreateAutoManagedNetworkClient = - createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; - const autoManagedNetworkClients: AutoManagedNetworkClient[] = - []; - jest - .spyOn( - createAutoManagedNetworkClientModule, - 'createAutoManagedNetworkClient', - ) - .mockImplementation((...args) => { - const autoManagedNetworkClient = - originalCreateAutoManagedNetworkClient(...args); - jest.spyOn(autoManagedNetworkClient, 'enableRpcFailoverForced'); - autoManagedNetworkClients.push(autoManagedNetworkClient); - return autoManagedNetworkClient; - }); - - await withController( - { - isRpcFailoverForced: false, - state: { - selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', - networkConfigurationsByChainId: { - [ChainId.mainnet]: buildInfuraNetworkConfiguration( - InfuraNetworkType.mainnet, - { - rpcEndpoints: [ - buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { - failoverUrls: [], - }), - ], - }, - ), - '0x200': buildCustomNetworkConfiguration({ - chainId: '0x200', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'AAAA-AAAA-AAAA-AAAA', - url: 'https://test.network/1', - failoverUrls: ['https://failover.endpoint/1'], - }), - ], - }), - '0x300': buildCustomNetworkConfiguration({ - chainId: '0x300', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'BBBB-BBBB-BBBB-BBBB', - url: 'https://test.network/2', - failoverUrls: ['https://failover.endpoint/2'], - }), - ], - }), - }, - }, - }, - async ({ controller }) => { - controller.enableRpcFailoverForced(); - - expect(autoManagedNetworkClients).toHaveLength(3); - expect( - autoManagedNetworkClients[0].enableRpcFailoverForced, - ).not.toHaveBeenCalled(); - expect( - autoManagedNetworkClients[1].enableRpcFailoverForced, - ).toHaveBeenCalled(); - expect( - autoManagedNetworkClients[2].enableRpcFailoverForced, - ).toHaveBeenCalled(); - }, - ); - }); - }); - - describe('if the controller was initialized with isRpcFailoverForced = true', () => { - it('does not call createAutoManagedNetworkClient at all', async () => { - await withController( - { - isRpcFailoverForced: true, - state: { - selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', - networkConfigurationsByChainId: { - [ChainId.mainnet]: buildInfuraNetworkConfiguration( - InfuraNetworkType.mainnet, - { - rpcEndpoints: [ - buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { - failoverUrls: [], - }), - ], - }, - ), - '0x200': buildCustomNetworkConfiguration({ - chainId: '0x200', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'AAAA-AAAA-AAAA-AAAA', - url: 'https://test.network/1', - failoverUrls: ['https://failover.endpoint/1'], - }), - ], - }), - '0x300': buildCustomNetworkConfiguration({ - chainId: '0x300', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'BBBB-BBBB-BBBB-BBBB', - url: 'https://test.network/2', - failoverUrls: ['https://failover.endpoint/2'], - }), - ], - }), - }, - }, - }, - async ({ controller }) => { - const originalCreateAutoManagedNetworkClient = - createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; - const autoManagedNetworkClients: AutoManagedNetworkClient[] = - []; - jest - .spyOn( - createAutoManagedNetworkClientModule, - 'createAutoManagedNetworkClient', - ) - .mockImplementation((...args) => { - const autoManagedNetworkClient = - originalCreateAutoManagedNetworkClient(...args); - jest.spyOn(autoManagedNetworkClient, 'enableRpcFailoverForced'); - autoManagedNetworkClients.push(autoManagedNetworkClient); - return autoManagedNetworkClient; - }); - - controller.enableRpcFailoverForced(); - - expect(autoManagedNetworkClients).toHaveLength(0); - }, - ); - }); - }); - }); - - describe('disableRpcFailoverForced', () => { - describe('if the controller was initialized with isRpcFailoverForced = true', () => { - it('calls disableRpcFailoverForced on only the network clients whose RPC endpoints have configured failover URLs', async () => { - const originalCreateAutoManagedNetworkClient = - createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; - const autoManagedNetworkClients: AutoManagedNetworkClient[] = - []; - jest - .spyOn( - createAutoManagedNetworkClientModule, - 'createAutoManagedNetworkClient', - ) - .mockImplementation((...args) => { - const autoManagedNetworkClient = - originalCreateAutoManagedNetworkClient(...args); - jest.spyOn(autoManagedNetworkClient, 'disableRpcFailoverForced'); - autoManagedNetworkClients.push(autoManagedNetworkClient); - return autoManagedNetworkClient; - }); - - await withController( - { - isRpcFailoverForced: true, - state: { - selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', - networkConfigurationsByChainId: { - [ChainId.mainnet]: buildInfuraNetworkConfiguration( - InfuraNetworkType.mainnet, - { - rpcEndpoints: [ - buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { - failoverUrls: [], - }), - ], - }, - ), - '0x200': buildCustomNetworkConfiguration({ - chainId: '0x200', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'AAAA-AAAA-AAAA-AAAA', - url: 'https://test.network/1', - failoverUrls: ['https://failover.endpoint/1'], - }), - ], - }), - '0x300': buildCustomNetworkConfiguration({ - chainId: '0x300', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'BBBB-BBBB-BBBB-BBBB', - url: 'https://test.network/2', - failoverUrls: ['https://failover.endpoint/2'], - }), - ], - }), - }, - }, - }, - async ({ controller }) => { - controller.disableRpcFailoverForced(); - - expect(autoManagedNetworkClients).toHaveLength(3); - expect( - autoManagedNetworkClients[0].disableRpcFailoverForced, - ).not.toHaveBeenCalled(); - expect( - autoManagedNetworkClients[1].disableRpcFailoverForced, - ).toHaveBeenCalled(); - expect( - autoManagedNetworkClients[2].disableRpcFailoverForced, - ).toHaveBeenCalled(); - }, - ); - }); - }); - - describe('if the controller was initialized with isRpcFailoverForced = false', () => { - it('does not call createAutoManagedNetworkClient at all', async () => { - await withController( - { - isRpcFailoverForced: false, - state: { - selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', - networkConfigurationsByChainId: { - [ChainId.mainnet]: buildInfuraNetworkConfiguration( - InfuraNetworkType.mainnet, - { - rpcEndpoints: [ - buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { - failoverUrls: [], - }), - ], - }, - ), - '0x200': buildCustomNetworkConfiguration({ - chainId: '0x200', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'AAAA-AAAA-AAAA-AAAA', - url: 'https://test.network/1', - failoverUrls: ['https://failover.endpoint/1'], - }), - ], - }), - '0x300': buildCustomNetworkConfiguration({ - chainId: '0x300', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'BBBB-BBBB-BBBB-BBBB', - url: 'https://test.network/2', - failoverUrls: ['https://failover.endpoint/2'], - }), - ], - }), - }, - }, - }, - async ({ controller }) => { - const originalCreateAutoManagedNetworkClient = - createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; - const autoManagedNetworkClients: AutoManagedNetworkClient[] = - []; - jest - .spyOn( - createAutoManagedNetworkClientModule, - 'createAutoManagedNetworkClient', - ) - .mockImplementation((...args) => { - const autoManagedNetworkClient = - originalCreateAutoManagedNetworkClient(...args); - jest.spyOn( - autoManagedNetworkClient, - 'disableRpcFailoverForced', - ); - autoManagedNetworkClients.push(autoManagedNetworkClient); - return autoManagedNetworkClient; - }); - - controller.disableRpcFailoverForced(); - - expect(autoManagedNetworkClients).toHaveLength(0); - }, - ); - }); - }); - }); - describe('RemoteFeatureFlagController:stateChange (isRpcFailoverForced)', () => { it('calls enableRpcFailoverForced on clients with failover URLs when the flag turns true', async () => { const originalCreateAutoManagedNetworkClient = From c9c9e751e5f089d6bd32ceb0375f258273ecda0f Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 24 Jun 2026 15:53:22 +0200 Subject: [PATCH 18/28] refactor(network-controller): rename forced failover methods to enableForcedRpcFailover --- .../src/NetworkController.ts | 4 +- ...create-auto-managed-network-client.test.ts | 4 +- .../src/create-auto-managed-network-client.ts | 12 +-- .../tests/NetworkController.test.ts | 76 +++++++++---------- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 7f9063cc75..ca20bd135e 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -1489,8 +1489,8 @@ export class NetworkController extends BaseController< networkClient.configuration.failoverRpcUrls.length > 0 ) { newIsRpcFailoverForced - ? networkClient.enableRpcFailoverForced() - : networkClient.disableRpcFailoverForced(); + ? networkClient.enableForcedRpcFailover() + : networkClient.disableForcedRpcFailover(); } } } diff --git a/packages/network-controller/src/create-auto-managed-network-client.test.ts b/packages/network-controller/src/create-auto-managed-network-client.test.ts index 24fc2ee08f..708b2b0330 100644 --- a/packages/network-controller/src/create-auto-managed-network-client.test.ts +++ b/packages/network-controller/src/create-auto-managed-network-client.test.ts @@ -720,7 +720,7 @@ describe('createAutoManagedNetworkClient', () => { method: 'test_method', params: [], }); - autoManagedNetworkClient.enableRpcFailoverForced(); + autoManagedNetworkClient.enableForcedRpcFailover(); await provider.request({ id: 1, jsonrpc: '2.0', @@ -798,7 +798,7 @@ describe('createAutoManagedNetworkClient', () => { method: 'test_method', params: [], }); - autoManagedNetworkClient.disableRpcFailoverForced(); + autoManagedNetworkClient.disableForcedRpcFailover(); await provider.request({ id: 1, jsonrpc: '2.0', diff --git a/packages/network-controller/src/create-auto-managed-network-client.ts b/packages/network-controller/src/create-auto-managed-network-client.ts index adcd516c67..f999a214ff 100644 --- a/packages/network-controller/src/create-auto-managed-network-client.ts +++ b/packages/network-controller/src/create-auto-managed-network-client.ts @@ -49,8 +49,8 @@ export type AutoManagedNetworkClient< destroy: () => void; enableRpcFailover: () => void; disableRpcFailover: () => void; - enableRpcFailoverForced: () => void; - disableRpcFailoverForced: () => void; + enableForcedRpcFailover: () => void; + disableForcedRpcFailover: () => void; }; /** @@ -255,13 +255,13 @@ export function createAutoManagedNetworkClient< networkClient = undefined; }; - const enableRpcFailoverForced = (): void => { + const enableForcedRpcFailover = (): void => { isRpcFailoverForced = true; destroy(); networkClient = undefined; }; - const disableRpcFailoverForced = (): void => { + const disableForcedRpcFailover = (): void => { isRpcFailoverForced = false; destroy(); networkClient = undefined; @@ -274,7 +274,7 @@ export function createAutoManagedNetworkClient< destroy, enableRpcFailover, disableRpcFailover, - enableRpcFailoverForced, - disableRpcFailoverForced, + enableForcedRpcFailover, + disableForcedRpcFailover, }; } diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index a16ce60b3a..e7d965a494 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1096,7 +1096,7 @@ describe('NetworkController', () => { }); describe('RemoteFeatureFlagController:stateChange (isRpcFailoverForced)', () => { - it('calls enableRpcFailoverForced on clients with failover URLs when the flag turns true', async () => { + it('calls enableForcedRpcFailover on clients with failover URLs when the flag turns true', async () => { const originalCreateAutoManagedNetworkClient = createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; const autoManagedNetworkClients: AutoManagedNetworkClient[] = @@ -1109,7 +1109,7 @@ describe('NetworkController', () => { .mockImplementation((...args) => { const autoManagedNetworkClient = originalCreateAutoManagedNetworkClient(...args); - jest.spyOn(autoManagedNetworkClient, 'enableRpcFailoverForced'); + jest.spyOn(autoManagedNetworkClient, 'enableForcedRpcFailover'); autoManagedNetworkClients.push(autoManagedNetworkClient); return autoManagedNetworkClient; }); @@ -1158,10 +1158,10 @@ describe('NetworkController', () => { expect(autoManagedNetworkClients).toHaveLength(2); expect( - autoManagedNetworkClients[0].enableRpcFailoverForced, + autoManagedNetworkClients[0].enableForcedRpcFailover, ).not.toHaveBeenCalled(); expect( - autoManagedNetworkClients[1].enableRpcFailoverForced, + autoManagedNetworkClients[1].enableForcedRpcFailover, ).toHaveBeenCalled(); }, ); @@ -1180,7 +1180,7 @@ describe('NetworkController', () => { .mockImplementation((...args) => { const autoManagedNetworkClient = originalCreateAutoManagedNetworkClient(...args); - jest.spyOn(autoManagedNetworkClient, 'enableRpcFailoverForced'); + jest.spyOn(autoManagedNetworkClient, 'enableForcedRpcFailover'); autoManagedNetworkClients.push(autoManagedNetworkClient); return autoManagedNetworkClient; }); @@ -1217,16 +1217,16 @@ describe('NetworkController', () => { async () => { expect(autoManagedNetworkClients).toHaveLength(2); expect( - autoManagedNetworkClients[0].enableRpcFailoverForced, + autoManagedNetworkClients[0].enableForcedRpcFailover, ).toHaveBeenCalled(); expect( - autoManagedNetworkClients[1].enableRpcFailoverForced, + autoManagedNetworkClients[1].enableForcedRpcFailover, ).not.toHaveBeenCalled(); }, ); }); - it('calls enableRpcFailoverForced but not enableRpcFailover when only the forced flag is true', async () => { + it('calls enableForcedRpcFailover but not enableRpcFailover when only the forced flag is true', async () => { const originalCreateAutoManagedNetworkClient = createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; const autoManagedNetworkClients: AutoManagedNetworkClient[] = @@ -1240,7 +1240,7 @@ describe('NetworkController', () => { const autoManagedNetworkClient = originalCreateAutoManagedNetworkClient(...args); jest.spyOn(autoManagedNetworkClient, 'enableRpcFailover'); - jest.spyOn(autoManagedNetworkClient, 'enableRpcFailoverForced'); + jest.spyOn(autoManagedNetworkClient, 'enableForcedRpcFailover'); autoManagedNetworkClients.push(autoManagedNetworkClient); return autoManagedNetworkClient; }); @@ -1280,7 +1280,7 @@ describe('NetworkController', () => { expect(autoManagedNetworkClients).toHaveLength(1); expect( - autoManagedNetworkClients[0].enableRpcFailoverForced, + autoManagedNetworkClients[0].enableForcedRpcFailover, ).toHaveBeenCalled(); expect( autoManagedNetworkClients[0].enableRpcFailover, @@ -1854,8 +1854,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'base-mainnet': { blockTracker: expect.anything(), @@ -1871,8 +1871,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'bsc-mainnet': { blockTracker: expect.anything(), @@ -1888,8 +1888,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'linea-mainnet': { blockTracker: expect.anything(), @@ -1905,8 +1905,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'linea-sepolia': { blockTracker: expect.anything(), @@ -1922,8 +1922,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, mainnet: { blockTracker: expect.anything(), @@ -1939,8 +1939,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'megaeth-testnet-v2': { blockTracker: expect.anything(), @@ -1955,8 +1955,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'monad-mainnet': { blockTracker: expect.anything(), @@ -1972,8 +1972,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'monad-testnet': { blockTracker: expect.anything(), @@ -1988,8 +1988,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'optimism-mainnet': { blockTracker: expect.anything(), @@ -2005,8 +2005,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'polygon-mainnet': { blockTracker: expect.anything(), @@ -2022,8 +2022,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, sepolia: { blockTracker: expect.anything(), @@ -2039,8 +2039,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, }); }, @@ -2097,8 +2097,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'BBBB-BBBB-BBBB-BBBB': { blockTracker: expect.anything(), @@ -2113,8 +2113,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), - enableRpcFailoverForced: expect.any(Function), - disableRpcFailoverForced: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, }); }, From 064fe2d992ba2d60ddcf81c2e26597af9b02f8b5 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 24 Jun 2026 16:03:29 +0200 Subject: [PATCH 19/28] refactor(network-controller): extract endpoint selection into getAvailableEndpoints --- .../src/create-network-client.ts | 77 +++++++++++++------ 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index 8bbb389ca7..a6e2db0a8f 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -231,6 +231,52 @@ export function createNetworkClient({ return { configuration, provider, blockTracker, destroy }; } +/** + * Determines the ordered list of endpoints that make up the RPC service chain + * for a network, honoring the RPC failover flags. + * + * @param args - The arguments. + * @param args.primaryEndpointUrl - The primary endpoint URL for the network. + * @param args.failoverRpcUrls - The configured failover URLs, if any. + * @param args.isRpcFailoverEnabled - Whether requests to the primary endpoint + * should automatically divert to the failover URLs when the primary is + * unavailable. + * @param args.isRpcFailoverForced - Whether to force all traffic for Infura + * endpoints that have failover URLs to those failover URLs, bypassing Infura. + * @returns The endpoints to use, each flagged as primary or failover. + */ +function getAvailableEndpoints({ + primaryEndpointUrl, + failoverRpcUrls, + isRpcFailoverEnabled, + isRpcFailoverForced, +}: { + primaryEndpointUrl: string; + failoverRpcUrls: string[] | undefined; + isRpcFailoverEnabled: boolean; + isRpcFailoverForced: boolean; +}): { url: string; isFailover: boolean }[] { + const failoverEndpoints = (failoverRpcUrls ?? []).map((url) => ({ + url, + isFailover: true, + })); + // We explicitly check the URL since some networks have been added with invalid configuration types in the past. + const isInfura = new URL(primaryEndpointUrl).hostname.endsWith('.infura.io'); + + if (isRpcFailoverForced && isInfura && failoverEndpoints.length > 0) { + // Force flag is on for an Infura endpoint with failovers: bypass Infura + // entirely and route all traffic (including block polling) to failovers. + // The first failover becomes the positional primary of the chain, so + // availability/degraded events will report that failover URL as the + // primary endpoint (there is no Infura primary in this mode). + return failoverEndpoints; + } + if (isRpcFailoverEnabled && isInfura) { + return [{ url: primaryEndpointUrl, isFailover: false }, ...failoverEndpoints]; + } + return [{ url: primaryEndpointUrl, isFailover: false }]; +} + /** * Creates an RPC service chain, which represents the primary endpoint URL for * the network as well as its failover URLs. @@ -275,31 +321,12 @@ function createRpcServiceChain({ isRpcFailoverForced: boolean; logger?: Logger; }): RpcServiceChain { - const failoverEndpoints = (configuration.failoverRpcUrls ?? []).map( - (url) => ({ - url, - isFailover: true, - }), - ); - // We explicitly check the URL since some networks have been added with invalid configuration types in the past. - const isInfura = new URL(primaryEndpointUrl).hostname.endsWith('.infura.io'); - - let availableEndpoints: { url: string; isFailover: boolean }[]; - if (isRpcFailoverForced && isInfura && failoverEndpoints.length > 0) { - // Force flag is on for an Infura endpoint with failovers: bypass Infura - // entirely and route all traffic (including block polling) to failovers. - // The first failover becomes the positional primary of the chain, so - // availability/degraded events will report that failover URL as the - // primary endpoint (there is no Infura primary in this mode). - availableEndpoints = failoverEndpoints; - } else if (isRpcFailoverEnabled && isInfura) { - availableEndpoints = [ - { url: primaryEndpointUrl, isFailover: false }, - ...failoverEndpoints, - ]; - } else { - availableEndpoints = [{ url: primaryEndpointUrl, isFailover: false }]; - } + const availableEndpoints = getAvailableEndpoints({ + primaryEndpointUrl, + failoverRpcUrls: configuration.failoverRpcUrls, + isRpcFailoverEnabled, + isRpcFailoverForced, + }); const isOffline = (): boolean => { const connectivityState = messenger.call('ConnectivityController:getState'); From c7bcaa1e8751ded2a2594fed7d7c5b7cf742197a Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 24 Jun 2026 16:09:39 +0200 Subject: [PATCH 20/28] test(network-controller): drop selector non-boolean passthrough test --- packages/network-controller/src/selectors.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/network-controller/src/selectors.test.ts b/packages/network-controller/src/selectors.test.ts index d5a7c9261a..30fe02b998 100644 --- a/packages/network-controller/src/selectors.test.ts +++ b/packages/network-controller/src/selectors.test.ts @@ -25,14 +25,4 @@ describe('getIsRpcFailoverForced', () => { const state = { remoteFeatureFlags: {}, cacheTimestamp: 0 }; expect(getIsRpcFailoverForced(state as never)).toBe(false); }); - - it('passes through non-boolean values without coercion', () => { - const state = { - remoteFeatureFlags: { - corePlatformRpcFailoverForceEnabled: 'yes', - }, - cacheTimestamp: 0, - }; - expect(getIsRpcFailoverForced(state as never)).toBe('yes'); - }); }); From ceabdd3dea735d90f4236e2fd5766f0278cb95c4 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 24 Jun 2026 16:21:36 +0200 Subject: [PATCH 21/28] refactor(network-controller): share failover client walk via helper --- .../src/NetworkController.ts | 78 ++++++++++--------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index ca20bd135e..60e18e9f07 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -1420,20 +1420,19 @@ export class NetworkController extends BaseController< } /** - * Enables or disables the RPC failover functionality, depending on the - * boolean given. This is done by reconstructing all network clients that were - * originally configured with failover URLs so that those URLs are either - * honored or ignored. Network client IDs will be preserved so as not to - * invalidate state in other controllers. + * Invokes the given callback for each auto-managed network client that was + * configured with failover URLs. Used to reconstruct those clients when an + * RPC failover setting changes. * - * @param newIsRpcFailoverEnabled - Whether or not to enable or disable the - * RPC failover functionality. - */ - #updateRpcFailoverEnabled(newIsRpcFailoverEnabled: boolean): void { - if (this.#isRpcFailoverEnabled === newIsRpcFailoverEnabled) { - return; - } - + * @param callback - Called with each network client that has failover URLs. + */ + #forEachNetworkClientWithFailover( + callback: ( + networkClient: + | AutoManagedNetworkClient + | AutoManagedNetworkClient, + ) => void, + ): void { const autoManagedNetworkClientRegistry = this.#ensureAutoManagedNetworkClientRegistryPopulated(); @@ -1450,12 +1449,32 @@ export class NetworkController extends BaseController< networkClient.configuration.failoverRpcUrls && networkClient.configuration.failoverRpcUrls.length > 0 ) { - newIsRpcFailoverEnabled - ? networkClient.enableRpcFailover() - : networkClient.disableRpcFailover(); + callback(networkClient); } } } + } + + /** + * Enables or disables the RPC failover functionality, depending on the + * boolean given. This is done by reconstructing all network clients that were + * originally configured with failover URLs so that those URLs are either + * honored or ignored. Network client IDs will be preserved so as not to + * invalidate state in other controllers. + * + * @param newIsRpcFailoverEnabled - Whether or not to enable or disable the + * RPC failover functionality. + */ + #updateRpcFailoverEnabled(newIsRpcFailoverEnabled: boolean): void { + if (this.#isRpcFailoverEnabled === newIsRpcFailoverEnabled) { + return; + } + + this.#forEachNetworkClientWithFailover((networkClient) => { + newIsRpcFailoverEnabled + ? networkClient.enableRpcFailover() + : networkClient.disableRpcFailover(); + }); this.#isRpcFailoverEnabled = newIsRpcFailoverEnabled; } @@ -1472,28 +1491,11 @@ export class NetworkController extends BaseController< return; } - const autoManagedNetworkClientRegistry = - this.#ensureAutoManagedNetworkClientRegistryPopulated(); - - for (const networkClientsById of Object.values( - autoManagedNetworkClientRegistry, - )) { - for (const networkClientId of Object.keys(networkClientsById)) { - // Type assertion: We can assume that `networkClientId` is valid here. - const networkClient = - networkClientsById[ - networkClientId as keyof typeof networkClientsById - ]; - if ( - networkClient.configuration.failoverRpcUrls && - networkClient.configuration.failoverRpcUrls.length > 0 - ) { - newIsRpcFailoverForced - ? networkClient.enableForcedRpcFailover() - : networkClient.disableForcedRpcFailover(); - } - } - } + this.#forEachNetworkClientWithFailover((networkClient) => { + newIsRpcFailoverForced + ? networkClient.enableForcedRpcFailover() + : networkClient.disableForcedRpcFailover(); + }); this.#isRpcFailoverForced = newIsRpcFailoverForced; } From fec431abd6accdef72dedf716237264ef1c68731 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 24 Jun 2026 16:27:51 +0200 Subject: [PATCH 22/28] fix: lint misc --- packages/network-controller/src/create-network-client.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index a6e2db0a8f..9f91d6f758 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -272,7 +272,10 @@ function getAvailableEndpoints({ return failoverEndpoints; } if (isRpcFailoverEnabled && isInfura) { - return [{ url: primaryEndpointUrl, isFailover: false }, ...failoverEndpoints]; + return [ + { url: primaryEndpointUrl, isFailover: false }, + ...failoverEndpoints, + ]; } return [{ url: primaryEndpointUrl, isFailover: false }]; } From f69f85947a2a2e2a490839af28804d5d3c2873a6 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 24 Jun 2026 16:52:42 +0200 Subject: [PATCH 23/28] feat(network-controller)!: drive RPC failover from corePlatformRpcFailoverMode Replace the boolean walletFrameworkRpcFailoverEnabled and the forced flag with a single corePlatformRpcFailoverMode flag (disabled/enabled/forced). The selectors derive both isRpcFailoverEnabled and isRpcFailoverForced from it, defaulting to disabled. NetworkController no longer reads walletFrameworkRpcFailoverEnabled. --- packages/network-controller/CHANGELOG.md | 7 +- .../network-controller/src/selectors.test.ts | 69 +++++++++++++------ packages/network-controller/src/selectors.ts | 51 ++++++++++++-- .../tests/NetworkController.test.ts | 6 +- packages/network-controller/tests/helpers.ts | 9 ++- 5 files changed, 106 insertions(+), 36 deletions(-) diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 9c204968a5..dc13811d7a 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -7,10 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Added +### Changed -- Add forced RPC failover for Infura endpoints, driven by the `corePlatformRpcFailoverForceEnabled` remote feature flag ([#9175](https://github.com/MetaMask/core/pull/9175)) - - When enabled, Infura endpoints configured with failover URLs route all traffic to those failover URLs, bypassing Infura entirely. Infura endpoints without failover URLs continue to use Infura, and custom RPC endpoints are unaffected. +- **BREAKING:** Drive RPC failover from the single `corePlatformRpcFailoverMode` remote feature flag ([#9175](https://github.com/MetaMask/core/pull/9175)) + - The flag is a string with three values: `disabled` (failover off), `enabled` (divert to failover URLs when the primary endpoint is unavailable), and `forced` (Infura endpoints that have failover URLs route all traffic to those URLs, bypassing Infura entirely). Custom endpoints are unaffected, and the value defaults to `disabled` when the flag is absent or unrecognized. + - `NetworkController` no longer reads the `walletFrameworkRpcFailoverEnabled` flag; the `enabled` mode replaces it. Update your remote feature flag configuration to set `corePlatformRpcFailoverMode`. ## [33.0.0] diff --git a/packages/network-controller/src/selectors.test.ts b/packages/network-controller/src/selectors.test.ts index 30fe02b998..595d568d38 100644 --- a/packages/network-controller/src/selectors.test.ts +++ b/packages/network-controller/src/selectors.test.ts @@ -1,28 +1,55 @@ -import { getIsRpcFailoverForced } from './selectors'; +import { getIsRpcFailoverEnabled, getIsRpcFailoverForced } from './selectors'; + +/** + * Builds a remote feature flag controller state with the given failover mode. + * + * @param mode - The value to set for `corePlatformRpcFailoverMode`, if any. + * @returns The state object. + */ +function buildState(mode?: unknown) { + return { + remoteFeatureFlags: + mode === undefined ? {} : { corePlatformRpcFailoverMode: mode }, + cacheTimestamp: 0, + }; +} + +describe('getIsRpcFailoverEnabled', () => { + it('returns true when the mode is "enabled"', () => { + expect(getIsRpcFailoverEnabled(buildState('enabled') as never)).toBe(true); + }); + + it('returns false when the mode is "forced"', () => { + expect(getIsRpcFailoverEnabled(buildState('forced') as never)).toBe(false); + }); + + it('returns false when the mode is "disabled"', () => { + expect(getIsRpcFailoverEnabled(buildState('disabled') as never)).toBe(false); + }); + + it('returns false when the flag is absent', () => { + expect(getIsRpcFailoverEnabled(buildState() as never)).toBe(false); + }); + + it('returns false when the flag is an unrecognized value', () => { + expect(getIsRpcFailoverEnabled(buildState('yes') as never)).toBe(false); + }); +}); describe('getIsRpcFailoverForced', () => { - it('returns true when the flag is true', () => { - const state = { - remoteFeatureFlags: { - corePlatformRpcFailoverForceEnabled: true, - }, - cacheTimestamp: 0, - }; - expect(getIsRpcFailoverForced(state as never)).toBe(true); - }); - - it('returns false when the flag is false', () => { - const state = { - remoteFeatureFlags: { - corePlatformRpcFailoverForceEnabled: false, - }, - cacheTimestamp: 0, - }; - expect(getIsRpcFailoverForced(state as never)).toBe(false); + it('returns true when the mode is "forced"', () => { + expect(getIsRpcFailoverForced(buildState('forced') as never)).toBe(true); + }); + + it('returns false when the mode is "enabled"', () => { + expect(getIsRpcFailoverForced(buildState('enabled') as never)).toBe(false); + }); + + it('returns false when the mode is "disabled"', () => { + expect(getIsRpcFailoverForced(buildState('disabled') as never)).toBe(false); }); it('returns false when the flag is absent', () => { - const state = { remoteFeatureFlags: {}, cacheTimestamp: 0 }; - expect(getIsRpcFailoverForced(state as never)).toBe(false); + expect(getIsRpcFailoverForced(buildState() as never)).toBe(false); }); }); diff --git a/packages/network-controller/src/selectors.ts b/packages/network-controller/src/selectors.ts index 9dc157dfa5..7bf4e7140b 100644 --- a/packages/network-controller/src/selectors.ts +++ b/packages/network-controller/src/selectors.ts @@ -1,17 +1,56 @@ import { RemoteFeatureFlagControllerState } from '@metamask/remote-feature-flag-controller'; +/** + * The RPC failover behavior for Infura networks, controlled by the + * `corePlatformRpcFailoverMode` remote feature flag. + * + * - `disabled`: failover URLs are ignored; traffic stays on the primary + * endpoint. + * - `enabled`: traffic automatically diverts to failover URLs when the primary + * endpoint is unavailable. + * - `forced`: Infura endpoints that have failover URLs route all traffic to + * those failover URLs, bypassing Infura entirely. + */ +type RpcFailoverMode = 'disabled' | 'enabled' | 'forced'; + +/** + * Reads the RPC failover mode from the remote feature flags, defaulting to + * `disabled` when the flag is absent or not a recognized value. + * + * @param state - The remote feature flag controller state. + * @returns The RPC failover mode. + */ +function getRpcFailoverMode( + state: RemoteFeatureFlagControllerState, +): RpcFailoverMode { + const mode = state.remoteFeatureFlags.corePlatformRpcFailoverMode; + return mode === 'enabled' || mode === 'forced' ? mode : 'disabled'; +} + +/** + * Whether normal RPC failover is active, i.e. traffic should divert to the + * configured failover URLs when the primary endpoint is unavailable. Only true + * for the `enabled` mode; the `forced` mode is handled by + * {@link getIsRpcFailoverForced}. + * + * @param state - The remote feature flag controller state. + * @returns Whether RPC failover is enabled. + */ export function getIsRpcFailoverEnabled( state: RemoteFeatureFlagControllerState, ): boolean { - const walletFrameworkRpcFailoverEnabled = state.remoteFeatureFlags - .walletFrameworkRpcFailoverEnabled as boolean | undefined; - return walletFrameworkRpcFailoverEnabled ?? false; + return getRpcFailoverMode(state) === 'enabled'; } +/** + * Whether RPC failover is forced for Infura endpoints, routing all traffic to + * configured failover URLs and bypassing Infura entirely. + * + * @param state - The remote feature flag controller state. + * @returns Whether forced RPC failover is enabled. + */ export function getIsRpcFailoverForced( state: RemoteFeatureFlagControllerState, ): boolean { - const corePlatformRpcFailoverForceEnabled = state.remoteFeatureFlags - .corePlatformRpcFailoverForceEnabled as boolean | undefined; - return corePlatformRpcFailoverForceEnabled ?? false; + return getRpcFailoverMode(state) === 'forced'; } diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index e7d965a494..b51efe2c2f 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1148,8 +1148,7 @@ describe('NetworkController', () => { 'RemoteFeatureFlagController:stateChange', { remoteFeatureFlags: { - walletFrameworkRpcFailoverEnabled: false, - corePlatformRpcFailoverForceEnabled: true, + corePlatformRpcFailoverMode: 'forced', }, cacheTimestamp: 0, }, @@ -1270,8 +1269,7 @@ describe('NetworkController', () => { 'RemoteFeatureFlagController:stateChange', { remoteFeatureFlags: { - walletFrameworkRpcFailoverEnabled: false, - corePlatformRpcFailoverForceEnabled: true, + corePlatformRpcFailoverMode: 'forced', }, cacheTimestamp: 0, }, diff --git a/packages/network-controller/tests/helpers.ts b/packages/network-controller/tests/helpers.ts index f3a95e58e6..d4ce021df1 100644 --- a/packages/network-controller/tests/helpers.ts +++ b/packages/network-controller/tests/helpers.ts @@ -115,12 +115,17 @@ export function buildRootMessenger({ }), ); + // eslint-disable-next-line no-nested-ternary + const corePlatformRpcFailoverMode = isRpcFailoverForced + ? 'forced' + : isRpcFailoverEnabled + ? 'enabled' + : 'disabled'; rootMessenger.registerActionHandler( 'RemoteFeatureFlagController:getState', () => ({ remoteFeatureFlags: { - walletFrameworkRpcFailoverEnabled: isRpcFailoverEnabled, - corePlatformRpcFailoverForceEnabled: isRpcFailoverForced, + corePlatformRpcFailoverMode, }, cacheTimestamp: 0, }), From a23fb7f1ad4dd27e567d8e8e0ea002741d7a2dd6 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 24 Jun 2026 16:56:27 +0200 Subject: [PATCH 24/28] fix: lint misc --- packages/network-controller/src/selectors.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/network-controller/src/selectors.test.ts b/packages/network-controller/src/selectors.test.ts index 595d568d38..e2acf4c319 100644 --- a/packages/network-controller/src/selectors.test.ts +++ b/packages/network-controller/src/selectors.test.ts @@ -24,7 +24,9 @@ describe('getIsRpcFailoverEnabled', () => { }); it('returns false when the mode is "disabled"', () => { - expect(getIsRpcFailoverEnabled(buildState('disabled') as never)).toBe(false); + expect(getIsRpcFailoverEnabled(buildState('disabled') as never)).toBe( + false, + ); }); it('returns false when the flag is absent', () => { From 5a4eb697a3e5d44d90a426d7fc042842a64af69d Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 24 Jun 2026 17:31:45 +0200 Subject: [PATCH 25/28] fix(network-controller): add return type to selector test helper --- packages/network-controller/src/selectors.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/network-controller/src/selectors.test.ts b/packages/network-controller/src/selectors.test.ts index e2acf4c319..db0b5b172d 100644 --- a/packages/network-controller/src/selectors.test.ts +++ b/packages/network-controller/src/selectors.test.ts @@ -6,7 +6,10 @@ import { getIsRpcFailoverEnabled, getIsRpcFailoverForced } from './selectors'; * @param mode - The value to set for `corePlatformRpcFailoverMode`, if any. * @returns The state object. */ -function buildState(mode?: unknown) { +function buildState(mode?: unknown): { + remoteFeatureFlags: Record; + cacheTimestamp: number; +} { return { remoteFeatureFlags: mode === undefined ? {} : { corePlatformRpcFailoverMode: mode }, From e690b24f88bf3ab1e62c5d0a9c0deae8099ec54e Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 24 Jun 2026 17:50:55 +0200 Subject: [PATCH 26/28] refactor(network-controller): track failover as a single mode in the controller Replace the two #isRpcFailover* booleans and their two RemoteFeatureFlagController subscriptions with a single #rpcFailoverMode field, one subscription via getRpcFailoverMode, and one #updateRpcFailover method. The createAutoManagedNetworkClient call sites derive the enabled/forced booleans from the mode inline. --- .../src/NetworkController.ts | 100 +++++++----------- .../network-controller/src/selectors.test.ts | 44 +++----- packages/network-controller/src/selectors.ts | 32 +----- 3 files changed, 53 insertions(+), 123 deletions(-) diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 60e18e9f07..6148427301 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -56,7 +56,8 @@ import type { DegradedEventType, RetryReason } from './create-network-client'; import { projectLogger, createModuleLogger } from './logger'; import type { NetworkControllerMethodActions } from './NetworkController-method-action-types'; import type { RpcServiceOptionsWithDefaults } from './rpc-service/rpc-service'; -import { getIsRpcFailoverEnabled, getIsRpcFailoverForced } from './selectors'; +import { getRpcFailoverMode } from './selectors'; +import type { RpcFailoverMode } from './selectors'; import { NetworkClientType } from './types'; import type { BlockTracker, @@ -1273,9 +1274,7 @@ export class NetworkController extends BaseController< NetworkConfiguration >; - #isRpcFailoverEnabled = false; - - #isRpcFailoverForced = false; + #rpcFailoverMode: RpcFailoverMode = 'disabled'; /** * Constructs a NetworkController. @@ -1375,19 +1374,10 @@ export class NetworkController extends BaseController< this.messenger.subscribe( // eslint-disable-next-line no-restricted-syntax 'RemoteFeatureFlagController:stateChange', - (isRpcFailoverEnabled) => { - this.#updateRpcFailoverEnabled(isRpcFailoverEnabled); - }, - getIsRpcFailoverEnabled, - ); - - this.messenger.subscribe( - // eslint-disable-next-line no-restricted-syntax - 'RemoteFeatureFlagController:stateChange', - (isRpcFailoverForced) => { - this.#updateRpcFailoverForced(isRpcFailoverForced); + (rpcFailoverMode) => { + this.#updateRpcFailover(rpcFailoverMode); }, - getIsRpcFailoverForced, + getRpcFailoverMode, ); } @@ -1407,7 +1397,7 @@ export class NetworkController extends BaseController< * to them if those RPC endpoints are unavailable. */ enableRpcFailover(): void { - this.#updateRpcFailoverEnabled(true); + this.#updateRpcFailover('enabled'); } /** @@ -1416,7 +1406,7 @@ export class NetworkController extends BaseController< * diverted to them if those RPC endpoints are unavailable. */ disableRpcFailover(): void { - this.#updateRpcFailoverEnabled(false); + this.#updateRpcFailover('disabled'); } /** @@ -1456,48 +1446,37 @@ export class NetworkController extends BaseController< } /** - * Enables or disables the RPC failover functionality, depending on the - * boolean given. This is done by reconstructing all network clients that were - * originally configured with failover URLs so that those URLs are either - * honored or ignored. Network client IDs will be preserved so as not to - * invalidate state in other controllers. + * Applies the given RPC failover mode by reconstructing all network clients + * that were configured with failover URLs so that the new mode takes effect. + * Network client IDs are preserved so as not to invalidate state in other + * controllers. * - * @param newIsRpcFailoverEnabled - Whether or not to enable or disable the - * RPC failover functionality. + * @param newMode - The RPC failover mode to apply. */ - #updateRpcFailoverEnabled(newIsRpcFailoverEnabled: boolean): void { - if (this.#isRpcFailoverEnabled === newIsRpcFailoverEnabled) { + #updateRpcFailover(newMode: RpcFailoverMode): void { + if (this.#rpcFailoverMode === newMode) { return; } - this.#forEachNetworkClientWithFailover((networkClient) => { - newIsRpcFailoverEnabled - ? networkClient.enableRpcFailover() - : networkClient.disableRpcFailover(); - }); - - this.#isRpcFailoverEnabled = newIsRpcFailoverEnabled; - } - - /** - * Enables or disables forced RPC failover, depending on the boolean given. - * This reconstructs all network clients that were configured with failover - * URLs so the new value takes effect. Network client IDs are preserved. - * - * @param newIsRpcFailoverForced - Whether or not to force RPC failover. - */ - #updateRpcFailoverForced(newIsRpcFailoverForced: boolean): void { - if (this.#isRpcFailoverForced === newIsRpcFailoverForced) { - return; - } + const wasEnabled = this.#rpcFailoverMode === 'enabled'; + const wasForced = this.#rpcFailoverMode === 'forced'; + const isEnabled = newMode === 'enabled'; + const isForced = newMode === 'forced'; this.#forEachNetworkClientWithFailover((networkClient) => { - newIsRpcFailoverForced - ? networkClient.enableForcedRpcFailover() - : networkClient.disableForcedRpcFailover(); + if (isEnabled !== wasEnabled) { + isEnabled + ? networkClient.enableRpcFailover() + : networkClient.disableRpcFailover(); + } + if (isForced !== wasForced) { + isForced + ? networkClient.enableForcedRpcFailover() + : networkClient.disableForcedRpcFailover(); + } }); - this.#isRpcFailoverForced = newIsRpcFailoverForced; + this.#rpcFailoverMode = newMode; } /** @@ -1667,8 +1646,7 @@ export class NetworkController extends BaseController< */ init(): void { const state = this.messenger.call('RemoteFeatureFlagController:getState'); - this.#updateRpcFailoverEnabled(getIsRpcFailoverEnabled(state)); - this.#updateRpcFailoverForced(getIsRpcFailoverForced(state)); + this.#updateRpcFailover(getRpcFailoverMode(state)); this.#applyNetworkSelection(this.state.selectedNetworkClientId); } @@ -2912,8 +2890,8 @@ export class NetworkController extends BaseController< getRpcServiceOptions: this.#getRpcServiceOptions, getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, - isRpcFailoverEnabled: this.#isRpcFailoverEnabled, - isRpcFailoverForced: this.#isRpcFailoverForced, + isRpcFailoverEnabled: this.#rpcFailoverMode === 'enabled', + isRpcFailoverForced: this.#rpcFailoverMode === 'forced', logger: this.#log, }); } else { @@ -2932,8 +2910,8 @@ export class NetworkController extends BaseController< getRpcServiceOptions: this.#getRpcServiceOptions, getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, - isRpcFailoverEnabled: this.#isRpcFailoverEnabled, - isRpcFailoverForced: this.#isRpcFailoverForced, + isRpcFailoverEnabled: this.#rpcFailoverMode === 'enabled', + isRpcFailoverForced: this.#rpcFailoverMode === 'forced', logger: this.#log, }); } @@ -3099,8 +3077,8 @@ export class NetworkController extends BaseController< getRpcServiceOptions: this.#getRpcServiceOptions, getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, - isRpcFailoverEnabled: this.#isRpcFailoverEnabled, - isRpcFailoverForced: this.#isRpcFailoverForced, + isRpcFailoverEnabled: this.#rpcFailoverMode === 'enabled', + isRpcFailoverForced: this.#rpcFailoverMode === 'forced', logger: this.#log, }), ] as const; @@ -3119,8 +3097,8 @@ export class NetworkController extends BaseController< getRpcServiceOptions: this.#getRpcServiceOptions, getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, - isRpcFailoverEnabled: this.#isRpcFailoverEnabled, - isRpcFailoverForced: this.#isRpcFailoverForced, + isRpcFailoverEnabled: this.#rpcFailoverMode === 'enabled', + isRpcFailoverForced: this.#rpcFailoverMode === 'forced', logger: this.#log, }), ] as const; diff --git a/packages/network-controller/src/selectors.test.ts b/packages/network-controller/src/selectors.test.ts index db0b5b172d..88d95377c2 100644 --- a/packages/network-controller/src/selectors.test.ts +++ b/packages/network-controller/src/selectors.test.ts @@ -1,4 +1,4 @@ -import { getIsRpcFailoverEnabled, getIsRpcFailoverForced } from './selectors'; +import { getRpcFailoverMode } from './selectors'; /** * Builds a remote feature flag controller state with the given failover mode. @@ -17,44 +17,24 @@ function buildState(mode?: unknown): { }; } -describe('getIsRpcFailoverEnabled', () => { - it('returns true when the mode is "enabled"', () => { - expect(getIsRpcFailoverEnabled(buildState('enabled') as never)).toBe(true); +describe('getRpcFailoverMode', () => { + it('returns "enabled" when the flag is "enabled"', () => { + expect(getRpcFailoverMode(buildState('enabled') as never)).toBe('enabled'); }); - it('returns false when the mode is "forced"', () => { - expect(getIsRpcFailoverEnabled(buildState('forced') as never)).toBe(false); + it('returns "forced" when the flag is "forced"', () => { + expect(getRpcFailoverMode(buildState('forced') as never)).toBe('forced'); }); - it('returns false when the mode is "disabled"', () => { - expect(getIsRpcFailoverEnabled(buildState('disabled') as never)).toBe( - false, - ); + it('returns "disabled" when the flag is "disabled"', () => { + expect(getRpcFailoverMode(buildState('disabled') as never)).toBe('disabled'); }); - it('returns false when the flag is absent', () => { - expect(getIsRpcFailoverEnabled(buildState() as never)).toBe(false); + it('returns "disabled" when the flag is absent', () => { + expect(getRpcFailoverMode(buildState() as never)).toBe('disabled'); }); - it('returns false when the flag is an unrecognized value', () => { - expect(getIsRpcFailoverEnabled(buildState('yes') as never)).toBe(false); - }); -}); - -describe('getIsRpcFailoverForced', () => { - it('returns true when the mode is "forced"', () => { - expect(getIsRpcFailoverForced(buildState('forced') as never)).toBe(true); - }); - - it('returns false when the mode is "enabled"', () => { - expect(getIsRpcFailoverForced(buildState('enabled') as never)).toBe(false); - }); - - it('returns false when the mode is "disabled"', () => { - expect(getIsRpcFailoverForced(buildState('disabled') as never)).toBe(false); - }); - - it('returns false when the flag is absent', () => { - expect(getIsRpcFailoverForced(buildState() as never)).toBe(false); + it('returns "disabled" when the flag is an unrecognized value', () => { + expect(getRpcFailoverMode(buildState('yes') as never)).toBe('disabled'); }); }); diff --git a/packages/network-controller/src/selectors.ts b/packages/network-controller/src/selectors.ts index 7bf4e7140b..60406079d8 100644 --- a/packages/network-controller/src/selectors.ts +++ b/packages/network-controller/src/selectors.ts @@ -11,7 +11,7 @@ import { RemoteFeatureFlagControllerState } from '@metamask/remote-feature-flag- * - `forced`: Infura endpoints that have failover URLs route all traffic to * those failover URLs, bypassing Infura entirely. */ -type RpcFailoverMode = 'disabled' | 'enabled' | 'forced'; +export type RpcFailoverMode = 'disabled' | 'enabled' | 'forced'; /** * Reads the RPC failover mode from the remote feature flags, defaulting to @@ -20,37 +20,9 @@ type RpcFailoverMode = 'disabled' | 'enabled' | 'forced'; * @param state - The remote feature flag controller state. * @returns The RPC failover mode. */ -function getRpcFailoverMode( +export function getRpcFailoverMode( state: RemoteFeatureFlagControllerState, ): RpcFailoverMode { const mode = state.remoteFeatureFlags.corePlatformRpcFailoverMode; return mode === 'enabled' || mode === 'forced' ? mode : 'disabled'; } - -/** - * Whether normal RPC failover is active, i.e. traffic should divert to the - * configured failover URLs when the primary endpoint is unavailable. Only true - * for the `enabled` mode; the `forced` mode is handled by - * {@link getIsRpcFailoverForced}. - * - * @param state - The remote feature flag controller state. - * @returns Whether RPC failover is enabled. - */ -export function getIsRpcFailoverEnabled( - state: RemoteFeatureFlagControllerState, -): boolean { - return getRpcFailoverMode(state) === 'enabled'; -} - -/** - * Whether RPC failover is forced for Infura endpoints, routing all traffic to - * configured failover URLs and bypassing Infura entirely. - * - * @param state - The remote feature flag controller state. - * @returns Whether forced RPC failover is enabled. - */ -export function getIsRpcFailoverForced( - state: RemoteFeatureFlagControllerState, -): boolean { - return getRpcFailoverMode(state) === 'forced'; -} From 4b9faa2ae216d3345c8655636f9a3190f2fc9de3 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 24 Jun 2026 18:18:58 +0200 Subject: [PATCH 27/28] refactor(network-controller)!: remove enableRpcFailover/disableRpcFailover Remove the imperative enableRpcFailover/disableRpcFailover methods, their messenger actions, and exported types. RPC failover is driven entirely by the corePlatformRpcFailoverMode remote feature flag. --- packages/network-controller/CHANGELOG.md | 5 + .../NetworkController-method-action-types.ts | 22 -- .../src/NetworkController.ts | 20 -- packages/network-controller/src/index.ts | 2 - .../tests/NetworkController.test.ts | 290 ------------------ 5 files changed, 5 insertions(+), 334 deletions(-) diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index dc13811d7a..988daeb093 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The flag is a string with three values: `disabled` (failover off), `enabled` (divert to failover URLs when the primary endpoint is unavailable), and `forced` (Infura endpoints that have failover URLs route all traffic to those URLs, bypassing Infura entirely). Custom endpoints are unaffected, and the value defaults to `disabled` when the flag is absent or unrecognized. - `NetworkController` no longer reads the `walletFrameworkRpcFailoverEnabled` flag; the `enabled` mode replaces it. Update your remote feature flag configuration to set `corePlatformRpcFailoverMode`. +### Removed + +- **BREAKING:** Remove the `NetworkController.enableRpcFailover` and `NetworkController.disableRpcFailover` methods, their `NetworkController:enableRpcFailover` / `NetworkController:disableRpcFailover` messenger actions, and the `NetworkControllerEnableRpcFailoverAction` / `NetworkControllerDisableRpcFailoverAction` types ([#9175](https://github.com/MetaMask/core/pull/9175)) + - RPC failover is now driven entirely by the `corePlatformRpcFailoverMode` remote feature flag, so there is no longer an imperative toggle. + ## [33.0.0] ### Added diff --git a/packages/network-controller/src/NetworkController-method-action-types.ts b/packages/network-controller/src/NetworkController-method-action-types.ts index c07de322ba..019b6b225c 100644 --- a/packages/network-controller/src/NetworkController-method-action-types.ts +++ b/packages/network-controller/src/NetworkController-method-action-types.ts @@ -16,26 +16,6 @@ export type NetworkControllerGetEthQueryAction = { handler: NetworkController['getEthQuery']; }; -/** - * Enables the RPC failover functionality. That is, if any RPC endpoints are - * configured with failover URLs, then traffic will automatically be diverted - * to them if those RPC endpoints are unavailable. - */ -export type NetworkControllerEnableRpcFailoverAction = { - type: `NetworkController:enableRpcFailover`; - handler: NetworkController['enableRpcFailover']; -}; - -/** - * Disables the RPC failover functionality. That is, even if any RPC endpoints - * are configured with failover URLs, then traffic will not automatically be - * diverted to them if those RPC endpoints are unavailable. - */ -export type NetworkControllerDisableRpcFailoverAction = { - type: `NetworkController:disableRpcFailover`; - handler: NetworkController['disableRpcFailover']; -}; - /** * Accesses the provider and block tracker for the currently selected network. * @@ -309,8 +289,6 @@ export type NetworkControllerFindNetworkClientIdByChainIdAction = { */ export type NetworkControllerMethodActions = | NetworkControllerGetEthQueryAction - | NetworkControllerEnableRpcFailoverAction - | NetworkControllerDisableRpcFailoverAction | NetworkControllerGetProviderAndBlockTrackerAction | NetworkControllerGetSelectedNetworkClientAction | NetworkControllerGetSelectedChainIdAction diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 6148427301..bf4bbedb85 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -669,8 +669,6 @@ type AllowedEvents = RemoteFeatureFlagControllerStateChangeEvent; const MESSENGER_EXPOSED_METHODS = [ 'addNetwork', - 'disableRpcFailover', - 'enableRpcFailover', 'findNetworkClientIdByChainId', 'get1559CompatibilityWithNetworkClientId', 'getEIP1559Compatibility', @@ -1391,24 +1389,6 @@ export class NetworkController extends BaseController< return this.#ethQuery; } - /** - * Enables the RPC failover functionality. That is, if any RPC endpoints are - * configured with failover URLs, then traffic will automatically be diverted - * to them if those RPC endpoints are unavailable. - */ - enableRpcFailover(): void { - this.#updateRpcFailover('enabled'); - } - - /** - * Disables the RPC failover functionality. That is, even if any RPC endpoints - * are configured with failover URLs, then traffic will not automatically be - * diverted to them if those RPC endpoints are unavailable. - */ - disableRpcFailover(): void { - this.#updateRpcFailover('disabled'); - } - /** * Invokes the given callback for each auto-managed network client that was * configured with failover URLs. Used to reconstruct those clients when an diff --git a/packages/network-controller/src/index.ts b/packages/network-controller/src/index.ts index 85d940d7c9..f1f3a4ecc1 100644 --- a/packages/network-controller/src/index.ts +++ b/packages/network-controller/src/index.ts @@ -68,8 +68,6 @@ export type { NetworkControllerAddNetworkAction, NetworkControllerRemoveNetworkAction, NetworkControllerUpdateNetworkAction, - NetworkControllerEnableRpcFailoverAction, - NetworkControllerDisableRpcFailoverAction, NetworkControllerGetProviderAndBlockTrackerAction, NetworkControllerGetNetworkClientRegistryAction, NetworkControllerLookupNetworkAction, diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index b51efe2c2f..4d321927e1 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -805,296 +805,6 @@ describe('NetworkController', () => { }); }); - describe('enableRpcFailover', () => { - describe('if the controller was initialized with isRpcFailoverEnabled = false', () => { - it('calls enableRpcFailover on only the network clients whose RPC endpoints have configured failover URLs', async () => { - const originalCreateAutoManagedNetworkClient = - createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; - const autoManagedNetworkClients: AutoManagedNetworkClient[] = - []; - jest - .spyOn( - createAutoManagedNetworkClientModule, - 'createAutoManagedNetworkClient', - ) - .mockImplementation((...args) => { - const autoManagedNetworkClient = - originalCreateAutoManagedNetworkClient(...args); - jest.spyOn(autoManagedNetworkClient, 'enableRpcFailover'); - autoManagedNetworkClients.push(autoManagedNetworkClient); - return autoManagedNetworkClient; - }); - - await withController( - { - isRpcFailoverEnabled: false, - state: { - selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', - networkConfigurationsByChainId: { - [ChainId.mainnet]: buildInfuraNetworkConfiguration( - InfuraNetworkType.mainnet, - { - rpcEndpoints: [ - buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { - failoverUrls: [], - }), - ], - }, - ), - '0x200': buildCustomNetworkConfiguration({ - chainId: '0x200', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'AAAA-AAAA-AAAA-AAAA', - url: 'https://test.network/1', - failoverUrls: ['https://failover.endpoint/1'], - }), - ], - }), - '0x300': buildCustomNetworkConfiguration({ - chainId: '0x300', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'BBBB-BBBB-BBBB-BBBB', - url: 'https://test.network/2', - failoverUrls: ['https://failover.endpoint/2'], - }), - ], - }), - }, - }, - }, - async ({ controller }) => { - controller.enableRpcFailover(); - - expect(autoManagedNetworkClients).toHaveLength(3); - expect( - autoManagedNetworkClients[0].enableRpcFailover, - ).not.toHaveBeenCalled(); - expect( - autoManagedNetworkClients[1].enableRpcFailover, - ).toHaveBeenCalled(); - expect( - autoManagedNetworkClients[2].enableRpcFailover, - ).toHaveBeenCalled(); - }, - ); - }); - }); - - describe('if the controller was initialized with isRpcFailoverEnabled = true', () => { - it('does not call createAutoManagedNetworkClient at all', async () => { - await withController( - { - isRpcFailoverEnabled: true, - state: { - selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', - networkConfigurationsByChainId: { - [ChainId.mainnet]: buildInfuraNetworkConfiguration( - InfuraNetworkType.mainnet, - { - rpcEndpoints: [ - buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { - failoverUrls: [], - }), - ], - }, - ), - '0x200': buildCustomNetworkConfiguration({ - chainId: '0x200', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'AAAA-AAAA-AAAA-AAAA', - url: 'https://test.network/1', - failoverUrls: ['https://failover.endpoint/1'], - }), - ], - }), - '0x300': buildCustomNetworkConfiguration({ - chainId: '0x300', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'BBBB-BBBB-BBBB-BBBB', - url: 'https://test.network/2', - failoverUrls: ['https://failover.endpoint/2'], - }), - ], - }), - }, - }, - }, - async ({ controller }) => { - const originalCreateAutoManagedNetworkClient = - createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; - const autoManagedNetworkClients: AutoManagedNetworkClient[] = - []; - jest - .spyOn( - createAutoManagedNetworkClientModule, - 'createAutoManagedNetworkClient', - ) - .mockImplementation((...args) => { - const autoManagedNetworkClient = - originalCreateAutoManagedNetworkClient(...args); - jest.spyOn(autoManagedNetworkClient, 'enableRpcFailover'); - autoManagedNetworkClients.push(autoManagedNetworkClient); - return autoManagedNetworkClient; - }); - - controller.enableRpcFailover(); - - expect(autoManagedNetworkClients).toHaveLength(0); - }, - ); - }); - }); - }); - - describe('disableRpcFailover', () => { - describe('if the controller was initialized with isRpcFailoverEnabled = true', () => { - it('calls disableRpcFailover on only the network clients whose RPC endpoints have configured failover URLs', async () => { - const originalCreateAutoManagedNetworkClient = - createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; - const autoManagedNetworkClients: AutoManagedNetworkClient[] = - []; - jest - .spyOn( - createAutoManagedNetworkClientModule, - 'createAutoManagedNetworkClient', - ) - .mockImplementation((...args) => { - const autoManagedNetworkClient = - originalCreateAutoManagedNetworkClient(...args); - jest.spyOn(autoManagedNetworkClient, 'disableRpcFailover'); - autoManagedNetworkClients.push(autoManagedNetworkClient); - return autoManagedNetworkClient; - }); - - await withController( - { - isRpcFailoverEnabled: true, - state: { - selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', - networkConfigurationsByChainId: { - [ChainId.mainnet]: buildInfuraNetworkConfiguration( - InfuraNetworkType.mainnet, - { - rpcEndpoints: [ - buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { - failoverUrls: [], - }), - ], - }, - ), - '0x200': buildCustomNetworkConfiguration({ - chainId: '0x200', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'AAAA-AAAA-AAAA-AAAA', - url: 'https://test.network/1', - failoverUrls: ['https://failover.endpoint/1'], - }), - ], - }), - '0x300': buildCustomNetworkConfiguration({ - chainId: '0x300', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'BBBB-BBBB-BBBB-BBBB', - url: 'https://test.network/2', - failoverUrls: ['https://failover.endpoint/2'], - }), - ], - }), - }, - }, - }, - async ({ controller }) => { - controller.disableRpcFailover(); - - expect(autoManagedNetworkClients).toHaveLength(3); - expect( - autoManagedNetworkClients[0].disableRpcFailover, - ).not.toHaveBeenCalled(); - expect( - autoManagedNetworkClients[1].disableRpcFailover, - ).toHaveBeenCalled(); - expect( - autoManagedNetworkClients[2].disableRpcFailover, - ).toHaveBeenCalled(); - }, - ); - }); - }); - - describe('if the controller was initialized with isRpcFailoverEnabled = false', () => { - it('does not call createAutoManagedNetworkClient at all', async () => { - await withController( - { - isRpcFailoverEnabled: false, - state: { - selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', - networkConfigurationsByChainId: { - [ChainId.mainnet]: buildInfuraNetworkConfiguration( - InfuraNetworkType.mainnet, - { - rpcEndpoints: [ - buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { - failoverUrls: [], - }), - ], - }, - ), - '0x200': buildCustomNetworkConfiguration({ - chainId: '0x200', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'AAAA-AAAA-AAAA-AAAA', - url: 'https://test.network/1', - failoverUrls: ['https://failover.endpoint/1'], - }), - ], - }), - '0x300': buildCustomNetworkConfiguration({ - chainId: '0x300', - rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'BBBB-BBBB-BBBB-BBBB', - url: 'https://test.network/2', - failoverUrls: ['https://failover.endpoint/2'], - }), - ], - }), - }, - }, - }, - async ({ controller }) => { - const originalCreateAutoManagedNetworkClient = - createAutoManagedNetworkClientModule.createAutoManagedNetworkClient; - const autoManagedNetworkClients: AutoManagedNetworkClient[] = - []; - jest - .spyOn( - createAutoManagedNetworkClientModule, - 'createAutoManagedNetworkClient', - ) - .mockImplementation((...args) => { - const autoManagedNetworkClient = - originalCreateAutoManagedNetworkClient(...args); - jest.spyOn(autoManagedNetworkClient, 'disableRpcFailover'); - autoManagedNetworkClients.push(autoManagedNetworkClient); - return autoManagedNetworkClient; - }); - - controller.disableRpcFailover(); - - expect(autoManagedNetworkClients).toHaveLength(0); - }, - ); - }); - }); - }); - describe('RemoteFeatureFlagController:stateChange (isRpcFailoverForced)', () => { it('calls enableForcedRpcFailover on clients with failover URLs when the flag turns true', async () => { const originalCreateAutoManagedNetworkClient = From 0f314298cba494ea5ed38b595d7d4310da5c1567 Mon Sep 17 00:00:00 2001 From: Salah-Eddine Saakoun Date: Wed, 24 Jun 2026 18:21:54 +0200 Subject: [PATCH 28/28] fix: lint misc --- packages/network-controller/src/selectors.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/network-controller/src/selectors.test.ts b/packages/network-controller/src/selectors.test.ts index 88d95377c2..9e934fdc9e 100644 --- a/packages/network-controller/src/selectors.test.ts +++ b/packages/network-controller/src/selectors.test.ts @@ -27,7 +27,9 @@ describe('getRpcFailoverMode', () => { }); it('returns "disabled" when the flag is "disabled"', () => { - expect(getRpcFailoverMode(buildState('disabled') as never)).toBe('disabled'); + expect(getRpcFailoverMode(buildState('disabled') as never)).toBe( + 'disabled', + ); }); it('returns "disabled" when the flag is absent', () => {