From ea55ac9e781443052100f0bd2dadc0ad98932f21 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 5 Jun 2026 10:33:41 +0200 Subject: [PATCH 1/6] feat: Use RemoteFeatureFlagController for isRpcFailoverEnabled --- packages/network-controller/package.json | 1 + .../src/NetworkController.ts | 39 ++++++++++++------- packages/network-controller/src/selectors.ts | 9 +++++ .../network-controller/tsconfig.build.json | 3 +- packages/network-controller/tsconfig.json | 3 +- yarn.lock | 1 + 6 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 packages/network-controller/src/selectors.ts diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index d48d99f683..9089ebb71e 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -63,6 +63,7 @@ "@metamask/eth-query": "^4.0.0", "@metamask/json-rpc-engine": "^10.5.0", "@metamask/messenger": "^1.2.0", + "@metamask/remote-feature-flag-controller": "^4.2.2", "@metamask/rpc-errors": "^7.0.2", "@metamask/swappable-obj-proxy": "^2.3.0", "@metamask/utils": "^11.9.0", diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 4cc4397e7a..96a06d3603 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -21,6 +21,10 @@ import { import type { PollingBlockTrackerOptions } from '@metamask/eth-block-tracker'; import EthQuery from '@metamask/eth-query'; import type { Messenger } from '@metamask/messenger'; +import { + RemoteFeatureFlagControllerGetStateAction, + RemoteFeatureFlagControllerStateChangeEvent, +} from '@metamask/remote-feature-flag-controller'; import { errorCodes } from '@metamask/rpc-errors'; import { createEventEmitterProxy, @@ -55,6 +59,7 @@ import type { NetworkControllerMethodActions, } from './NetworkController-method-action-types'; import type { RpcServiceOptionsWithDefaults } from './rpc-service/rpc-service'; +import { getIsRpcFailoverEnabled } from './selectors'; import { NetworkClientType } from './types'; import type { BlockTracker, @@ -663,7 +668,7 @@ export type NetworkControllerEvents = /** * All events that {@link NetworkController} calls internally. */ -type AllowedEvents = never; +type AllowedEvents = RemoteFeatureFlagControllerStateChangeEvent; const MESSENGER_EXPOSED_METHODS = [ 'addNetwork', @@ -714,7 +719,9 @@ export type NetworkControllerGetNetworkConfigurationByNetworkClientId = /** * All actions that {@link NetworkController} calls internally. */ -type AllowedActions = ConnectivityControllerGetStateAction; +type AllowedActions = + | ConnectivityControllerGetStateAction + | RemoteFeatureFlagControllerGetStateAction; export type NetworkControllerMessenger = Messenger< typeof controllerName, @@ -767,11 +774,6 @@ export type NetworkControllerOptions = { * An array of Hex Chain IDs representing the additional networks to be included as default. */ additionalDefaultNetworks?: AdditionalDefaultNetwork[]; - /** - * Whether or not requests sent to unavailable RPC endpoints should be - * automatically diverted to configured failover RPC endpoints. - */ - isRpcFailoverEnabled?: boolean; }; /** @@ -1295,10 +1297,7 @@ export class NetworkController extends BaseController< NetworkConfiguration >; - #isRpcFailoverEnabled: Exclude< - NetworkControllerOptions['isRpcFailoverEnabled'], - undefined - >; + #isRpcFailoverEnabled = false; /** * Constructs a NetworkController. @@ -1314,7 +1313,6 @@ export class NetworkController extends BaseController< getRpcServiceOptions, getBlockTrackerOptions, additionalDefaultNetworks, - isRpcFailoverEnabled = false, } = options; const initialState = { ...getDefaultNetworkControllerState(additionalDefaultNetworks), @@ -1357,7 +1355,6 @@ export class NetworkController extends BaseController< this.#log = log; this.#getRpcServiceOptions = getRpcServiceOptions; this.#getBlockTrackerOptions = getBlockTrackerOptions; - this.#isRpcFailoverEnabled = isRpcFailoverEnabled; this.#previouslySelectedNetworkClientId = this.state.selectedNetworkClientId; @@ -1395,6 +1392,14 @@ export class NetworkController extends BaseController< }); }, ); + + this.messenger.subscribe( + 'RemoteFeatureFlagController:stateChange', + (isRpcFailoverEnabled) => { + this.#updateRpcFailoverEnabled(isRpcFailoverEnabled); + }, + getIsRpcFailoverEnabled, + ); } /** @@ -1626,6 +1631,14 @@ export class NetworkController extends BaseController< await this.lookupNetwork(); } + /** + * Initialize the NetworkController, updating the RPC failover feature flag. + */ + init() { + const state = this.messenger.call('RemoteFeatureFlagController:getState'); + this.#updateRpcFailoverEnabled(getIsRpcFailoverEnabled(state)); + } + /** * Creates proxies for accessing the global network client and its block * tracker. You must call this method in order to use diff --git a/packages/network-controller/src/selectors.ts b/packages/network-controller/src/selectors.ts new file mode 100644 index 0000000000..8bdb1c3fe9 --- /dev/null +++ b/packages/network-controller/src/selectors.ts @@ -0,0 +1,9 @@ +import { RemoteFeatureFlagControllerState } from '@metamask/remote-feature-flag-controller'; + +export function getIsRpcFailoverEnabled( + state: RemoteFeatureFlagControllerState, +) { + const walletFrameworkRpcFailoverEnabled = state.remoteFeatureFlags + .walletFrameworkRpcFailoverEnabled as boolean | undefined; + return walletFrameworkRpcFailoverEnabled ?? false; +} diff --git a/packages/network-controller/tsconfig.build.json b/packages/network-controller/tsconfig.build.json index 0ebc69f899..8a9702017f 100644 --- a/packages/network-controller/tsconfig.build.json +++ b/packages/network-controller/tsconfig.build.json @@ -13,7 +13,8 @@ { "path": "../eth-json-rpc-middleware/tsconfig.build.json" }, { "path": "../eth-json-rpc-provider/tsconfig.build.json" }, { "path": "../json-rpc-engine/tsconfig.build.json" }, - { "path": "../messenger/tsconfig.build.json" } + { "path": "../messenger/tsconfig.build.json" }, + { "path": "../remote-feature-flag-controller/tsconfig.build.json" }, ], "include": ["../../types", "./src"] } diff --git a/packages/network-controller/tsconfig.json b/packages/network-controller/tsconfig.json index 37d8a33ef5..55c2197ab2 100644 --- a/packages/network-controller/tsconfig.json +++ b/packages/network-controller/tsconfig.json @@ -12,7 +12,8 @@ { "path": "../eth-json-rpc-middleware" }, { "path": "../eth-json-rpc-provider" }, { "path": "../json-rpc-engine" }, - { "path": "../messenger" } + { "path": "../messenger" }, + { "path": "../remote-feature-flag-controller" }, ], "include": ["../../types", "../../tests", "./src", "./tests"] } diff --git a/yarn.lock b/yarn.lock index 4d08c1d875..d4405d370f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7658,6 +7658,7 @@ __metadata: "@metamask/eth-query": "npm:^4.0.0" "@metamask/json-rpc-engine": "npm:^10.5.0" "@metamask/messenger": "npm:^1.2.0" + "@metamask/remote-feature-flag-controller": "npm:^4.2.2" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/swappable-obj-proxy": "npm:^2.3.0" "@metamask/utils": "npm:^11.9.0" From 338b64bac0aa78a00bc6fe20e19d594312bb1036 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 5 Jun 2026 12:45:11 +0200 Subject: [PATCH 2/6] Fix tests --- .../tests/NetworkController.test.ts | 34 +++++++++---------- packages/network-controller/tests/helpers.ts | 29 +++++++++++++--- .../network-controller/tsconfig.build.json | 2 +- packages/network-controller/tsconfig.json | 2 +- 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 487f87d4b2..cb36cd2edc 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1131,6 +1131,23 @@ describe('NetworkController', () => { 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, @@ -1171,23 +1188,6 @@ describe('NetworkController', () => { }, }, 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(3); diff --git a/packages/network-controller/tests/helpers.ts b/packages/network-controller/tests/helpers.ts index 860eb2a534..ec32a677b1 100644 --- a/packages/network-controller/tests/helpers.ts +++ b/packages/network-controller/tests/helpers.ts @@ -93,8 +93,10 @@ export const TESTNET = { */ export function buildRootMessenger({ connectivityStatus = CONNECTIVITY_STATUSES.Online, + isRpcFailoverEnabled = false, }: { connectivityStatus?: ConnectivityStatus; + isRpcFailoverEnabled?: boolean; } = {}): RootMessenger { const rootMessenger = new Messenger< MockAnyNamespace, @@ -109,6 +111,16 @@ export function buildRootMessenger({ }), ); + rootMessenger.registerActionHandler( + 'RemoteFeatureFlagController:getState', + () => ({ + remoteFeatureFlags: { + walletFrameworkRpcFailoverEnabled: isRpcFailoverEnabled, + }, + cacheTimestamp: 0, + }), + ); + return rootMessenger; } @@ -133,7 +145,10 @@ export function buildNetworkControllerMessenger( rootMessenger.delegate({ messenger: networkControllerMessenger, - actions: ['ConnectivityController:getState'], + actions: [ + 'ConnectivityController:getState', + 'RemoteFeatureFlagController:getState', + ], }); return networkControllerMessenger; @@ -612,7 +627,9 @@ type WithControllerCallback = ({ networkControllerMessenger: NetworkControllerMessenger; }) => Promise | ReturnValue; -type WithControllerOptions = Partial; +type WithControllerOptions = Partial & { + isRpcFailoverEnabled?: boolean; +}; type WithControllerArgs = | [WithControllerCallback] @@ -632,7 +649,8 @@ export async function withController( ...args: WithControllerArgs ): Promise { const [{ ...rest }, fn] = args.length === 2 ? args : [{}, args[0]]; - const messenger = buildRootMessenger(); + const { isRpcFailoverEnabled, ...controllerOptions } = rest; + const messenger = buildRootMessenger({ isRpcFailoverEnabled }); const networkControllerMessenger = buildNetworkControllerMessenger(messenger); const controller = new NetworkController({ messenger: networkControllerMessenger, @@ -645,8 +663,11 @@ export async function withController( btoa, isOffline: (): boolean => false, }), - ...rest, + ...controllerOptions, }); + + controller.init(); + try { return await fn({ controller, messenger, networkControllerMessenger }); } finally { diff --git a/packages/network-controller/tsconfig.build.json b/packages/network-controller/tsconfig.build.json index 8a9702017f..5767f283bb 100644 --- a/packages/network-controller/tsconfig.build.json +++ b/packages/network-controller/tsconfig.build.json @@ -14,7 +14,7 @@ { "path": "../eth-json-rpc-provider/tsconfig.build.json" }, { "path": "../json-rpc-engine/tsconfig.build.json" }, { "path": "../messenger/tsconfig.build.json" }, - { "path": "../remote-feature-flag-controller/tsconfig.build.json" }, + { "path": "../remote-feature-flag-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/network-controller/tsconfig.json b/packages/network-controller/tsconfig.json index 55c2197ab2..87d6f24f68 100644 --- a/packages/network-controller/tsconfig.json +++ b/packages/network-controller/tsconfig.json @@ -13,7 +13,7 @@ { "path": "../eth-json-rpc-provider" }, { "path": "../json-rpc-engine" }, { "path": "../messenger" }, - { "path": "../remote-feature-flag-controller" }, + { "path": "../remote-feature-flag-controller" } ], "include": ["../../types", "../../tests", "./src", "./tests"] } From 49b669a99b0cfcc6c0b37bb510c1eae0806943b6 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 5 Jun 2026 12:53:41 +0200 Subject: [PATCH 3/6] Update CHANGELOG and README --- README.md | 1 + packages/network-controller/CHANGELOG.md | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index 54235840db..dff073e7f2 100644 --- a/README.md +++ b/README.md @@ -424,6 +424,7 @@ linkStyle default opacity:0.5 network_controller --> eth_json_rpc_provider; network_controller --> json_rpc_engine; network_controller --> messenger; + network_controller --> remote_feature_flag_controller; network_enablement_controller --> base_controller; network_enablement_controller --> controller_utils; network_enablement_controller --> messenger; diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 14e4113c13..4d23194807 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** Automatically populate `isRpcFailoverEnabled` using `RemoteFeatureFlagController` ([#9013](https://github.com/MetaMask/core/pull/9013)) + - `NetworkController.init` must now be called to fully initialize the controller. + - The constructor argument `isRpcFailoverEnabled` is no longer available. + - `RemoteFeatureFlagController:stateChange` and `RemoteFeatureFlagController:getState` are now required. + ### Added - Add defaults for policy and block tracker options ([#9002](https://github.com/MetaMask/core/pull/9002)) From 59d9374df64feec48a8b203cfe02a53b2e6bc051 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 5 Jun 2026 12:57:55 +0200 Subject: [PATCH 4/6] Fix CHANGELOG format --- packages/network-controller/CHANGELOG.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 4d23194807..6c4b4eee3a 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -7,13 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Changed - -- **BREAKING:** Automatically populate `isRpcFailoverEnabled` using `RemoteFeatureFlagController` ([#9013](https://github.com/MetaMask/core/pull/9013)) - - `NetworkController.init` must now be called to fully initialize the controller. - - The constructor argument `isRpcFailoverEnabled` is no longer available. - - `RemoteFeatureFlagController:stateChange` and `RemoteFeatureFlagController:getState` are now required. - ### Added - Add defaults for policy and block tracker options ([#9002](https://github.com/MetaMask/core/pull/9002)) @@ -24,6 +17,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The default `pollingInterval` for the block tracker is now `20` seconds. - The default `retryTimeout` for the block tracker is now `20` seconds. +### Changed + +- **BREAKING:** Automatically populate `isRpcFailoverEnabled` using `RemoteFeatureFlagController` ([#9013](https://github.com/MetaMask/core/pull/9013)) + - `NetworkController.init` must now be called to fully initialize the controller. + - The constructor argument `isRpcFailoverEnabled` is no longer available. + - `RemoteFeatureFlagController:stateChange` and `RemoteFeatureFlagController:getState` are now required. + ### Fixed - Add defaults for `fetch`, `btoa` and `isOffline` in `RpcServiceOptions` ([#9000](https://github.com/MetaMask/core/pull/9000)) From ca1f7a1b0cb21095387eed11aa807cdfb64d1baf Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 5 Jun 2026 13:09:02 +0200 Subject: [PATCH 5/6] Add delegation for state change --- packages/network-controller/tests/helpers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/network-controller/tests/helpers.ts b/packages/network-controller/tests/helpers.ts index ec32a677b1..89a4315473 100644 --- a/packages/network-controller/tests/helpers.ts +++ b/packages/network-controller/tests/helpers.ts @@ -149,6 +149,7 @@ export function buildNetworkControllerMessenger( 'ConnectivityController:getState', 'RemoteFeatureFlagController:getState', ], + events: ['RemoteFeatureFlagController:stateChange'], }); return networkControllerMessenger; From 5ffe1c5b18d9f7edcda4e0febc091b51fe705cc7 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 5 Jun 2026 13:22:44 +0200 Subject: [PATCH 6/6] Fix lint --- packages/network-controller/src/NetworkController.ts | 3 ++- packages/network-controller/src/selectors.ts | 2 +- packages/network-controller/tests/helpers.ts | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 96a06d3603..2cc779140f 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -1394,6 +1394,7 @@ export class NetworkController extends BaseController< ); this.messenger.subscribe( + // eslint-disable-next-line no-restricted-syntax 'RemoteFeatureFlagController:stateChange', (isRpcFailoverEnabled) => { this.#updateRpcFailoverEnabled(isRpcFailoverEnabled); @@ -1634,7 +1635,7 @@ export class NetworkController extends BaseController< /** * Initialize the NetworkController, updating the RPC failover feature flag. */ - init() { + init(): void { const state = this.messenger.call('RemoteFeatureFlagController:getState'); this.#updateRpcFailoverEnabled(getIsRpcFailoverEnabled(state)); } diff --git a/packages/network-controller/src/selectors.ts b/packages/network-controller/src/selectors.ts index 8bdb1c3fe9..016c8a66b0 100644 --- a/packages/network-controller/src/selectors.ts +++ b/packages/network-controller/src/selectors.ts @@ -2,7 +2,7 @@ import { RemoteFeatureFlagControllerState } from '@metamask/remote-feature-flag- export function getIsRpcFailoverEnabled( state: RemoteFeatureFlagControllerState, -) { +): boolean { const walletFrameworkRpcFailoverEnabled = state.remoteFeatureFlags .walletFrameworkRpcFailoverEnabled as boolean | undefined; return walletFrameworkRpcFailoverEnabled ?? false; diff --git a/packages/network-controller/tests/helpers.ts b/packages/network-controller/tests/helpers.ts index 89a4315473..fb9f80cda5 100644 --- a/packages/network-controller/tests/helpers.ts +++ b/packages/network-controller/tests/helpers.ts @@ -89,6 +89,7 @@ export const TESTNET = { * @param options - Optional configuration. * @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. * @returns The messenger. */ export function buildRootMessenger({ @@ -149,6 +150,7 @@ export function buildNetworkControllerMessenger( 'ConnectivityController:getState', 'RemoteFeatureFlagController:getState', ], + // eslint-disable-next-line no-restricted-syntax events: ['RemoteFeatureFlagController:stateChange'], });