diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 25869ad7ba..988daeb093 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **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`. + +### 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 09c727ada4..bf4bbedb85 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 } from './selectors'; +import { getRpcFailoverMode } from './selectors'; +import type { RpcFailoverMode } from './selectors'; import { NetworkClientType } from './types'; import type { BlockTracker, @@ -668,8 +669,6 @@ type AllowedEvents = RemoteFeatureFlagControllerStateChangeEvent; const MESSENGER_EXPOSED_METHODS = [ 'addNetwork', - 'disableRpcFailover', - 'enableRpcFailover', 'findNetworkClientIdByChainId', 'get1559CompatibilityWithNetworkClientId', 'getEIP1559Compatibility', @@ -1273,7 +1272,7 @@ export class NetworkController extends BaseController< NetworkConfiguration >; - #isRpcFailoverEnabled = false; + #rpcFailoverMode: RpcFailoverMode = 'disabled'; /** * Constructs a NetworkController. @@ -1373,10 +1372,10 @@ export class NetworkController extends BaseController< this.messenger.subscribe( // eslint-disable-next-line no-restricted-syntax 'RemoteFeatureFlagController:stateChange', - (isRpcFailoverEnabled) => { - this.#updateRpcFailoverEnabled(isRpcFailoverEnabled); + (rpcFailoverMode) => { + this.#updateRpcFailover(rpcFailoverMode); }, - getIsRpcFailoverEnabled, + getRpcFailoverMode, ); } @@ -1391,38 +1390,19 @@ export class NetworkController extends BaseController< } /** - * 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.#updateRpcFailoverEnabled(true); - } - - /** - * 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.#updateRpcFailoverEnabled(false); - } - - /** - * 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(); @@ -1439,14 +1419,44 @@ export class NetworkController extends BaseController< networkClient.configuration.failoverRpcUrls && networkClient.configuration.failoverRpcUrls.length > 0 ) { - newIsRpcFailoverEnabled - ? networkClient.enableRpcFailover() - : networkClient.disableRpcFailover(); + callback(networkClient); } } } + } + + /** + * 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 newMode - The RPC failover mode to apply. + */ + #updateRpcFailover(newMode: RpcFailoverMode): void { + if (this.#rpcFailoverMode === newMode) { + return; + } + + const wasEnabled = this.#rpcFailoverMode === 'enabled'; + const wasForced = this.#rpcFailoverMode === 'forced'; + const isEnabled = newMode === 'enabled'; + const isForced = newMode === 'forced'; + + this.#forEachNetworkClientWithFailover((networkClient) => { + if (isEnabled !== wasEnabled) { + isEnabled + ? networkClient.enableRpcFailover() + : networkClient.disableRpcFailover(); + } + if (isForced !== wasForced) { + isForced + ? networkClient.enableForcedRpcFailover() + : networkClient.disableForcedRpcFailover(); + } + }); - this.#isRpcFailoverEnabled = newIsRpcFailoverEnabled; + this.#rpcFailoverMode = newMode; } /** @@ -1610,12 +1620,13 @@ 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'); - this.#updateRpcFailoverEnabled(getIsRpcFailoverEnabled(state)); + this.#updateRpcFailover(getRpcFailoverMode(state)); this.#applyNetworkSelection(this.state.selectedNetworkClientId); } @@ -2859,7 +2870,8 @@ export class NetworkController extends BaseController< getRpcServiceOptions: this.#getRpcServiceOptions, getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, - isRpcFailoverEnabled: this.#isRpcFailoverEnabled, + isRpcFailoverEnabled: this.#rpcFailoverMode === 'enabled', + isRpcFailoverForced: this.#rpcFailoverMode === 'forced', logger: this.#log, }); } else { @@ -2878,7 +2890,8 @@ export class NetworkController extends BaseController< getRpcServiceOptions: this.#getRpcServiceOptions, getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, - isRpcFailoverEnabled: this.#isRpcFailoverEnabled, + isRpcFailoverEnabled: this.#rpcFailoverMode === 'enabled', + isRpcFailoverForced: this.#rpcFailoverMode === 'forced', logger: this.#log, }); } @@ -3044,7 +3057,8 @@ export class NetworkController extends BaseController< getRpcServiceOptions: this.#getRpcServiceOptions, getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, - isRpcFailoverEnabled: this.#isRpcFailoverEnabled, + isRpcFailoverEnabled: this.#rpcFailoverMode === 'enabled', + isRpcFailoverForced: this.#rpcFailoverMode === 'forced', logger: this.#log, }), ] as const; @@ -3063,7 +3077,8 @@ export class NetworkController extends BaseController< getRpcServiceOptions: this.#getRpcServiceOptions, getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, - isRpcFailoverEnabled: this.#isRpcFailoverEnabled, + isRpcFailoverEnabled: this.#rpcFailoverMode === 'enabled', + isRpcFailoverForced: this.#rpcFailoverMode === 'forced', 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..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 @@ -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,168 @@ 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.enableForcedRpcFailover(); + 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('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.disableForcedRpcFailover(); + 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, @@ -673,6 +851,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..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,6 +49,8 @@ export type AutoManagedNetworkClient< destroy: () => void; enableRpcFailover: () => void; disableRpcFailover: () => void; + enableForcedRpcFailover: () => void; + disableForcedRpcFailover: () => void; }; /** @@ -81,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. */ @@ -96,6 +101,7 @@ export function createAutoManagedNetworkClient< > => ({}), messenger, isRpcFailoverEnabled: givenIsRpcFailoverEnabled, + isRpcFailoverForced: givenIsRpcFailoverForced, logger, }: { networkClientId: NetworkClientId; @@ -108,9 +114,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 +129,7 @@ export function createAutoManagedNetworkClient< getBlockTrackerOptions, messenger, isRpcFailoverEnabled, + isRpcFailoverForced, logger, }); @@ -246,6 +255,18 @@ export function createAutoManagedNetworkClient< networkClient = undefined; }; + const enableForcedRpcFailover = (): void => { + isRpcFailoverForced = true; + destroy(); + networkClient = undefined; + }; + + const disableForcedRpcFailover = (): void => { + isRpcFailoverForced = false; + destroy(); + networkClient = undefined; + }; + return { configuration: networkClientConfiguration, provider: providerProxy, @@ -253,5 +274,7 @@ export function createAutoManagedNetworkClient< destroy, enableRpcFailover, disableRpcFailover, + enableForcedRpcFailover, + disableForcedRpcFailover, }; } 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 8fc02a6f92..9f91d6f758 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. */ @@ -145,6 +148,7 @@ export function createNetworkClient({ getBlockTrackerOptions, messenger, isRpcFailoverEnabled, + isRpcFailoverForced, logger, }: { id: NetworkClientId; @@ -157,6 +161,7 @@ export function createNetworkClient({ ) => Omit; messenger: NetworkControllerMessenger; isRpcFailoverEnabled: boolean; + isRpcFailoverForced: boolean; logger?: Logger; }): NetworkClient { const primaryEndpointUrl = @@ -170,6 +175,7 @@ export function createNetworkClient({ getRpcServiceOptions, messenger, isRpcFailoverEnabled, + isRpcFailoverForced, logger, }); @@ -225,6 +231,55 @@ 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. @@ -242,6 +297,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. */ @@ -252,6 +310,7 @@ function createRpcServiceChain({ getRpcServiceOptions, messenger, isRpcFailoverEnabled, + isRpcFailoverForced, logger, }: { id: NetworkClientId; @@ -262,21 +321,15 @@ function createRpcServiceChain({ ) => RpcServiceOptionsWithDefaults; messenger: NetworkControllerMessenger; isRpcFailoverEnabled: boolean; + isRpcFailoverForced: boolean; logger?: Logger; }): RpcServiceChain { - // 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'); - - const availableEndpoints = - isRpcFailoverEnabled && isInfura - ? [ - { url: primaryEndpointUrl, isFailover: false }, - ...(configuration.failoverRpcUrls ?? []).map((url) => ({ - url, - isFailover: true, - })), - ] - : [{ url: primaryEndpointUrl, isFailover: false }]; + const availableEndpoints = getAvailableEndpoints({ + primaryEndpointUrl, + failoverRpcUrls: configuration.failoverRpcUrls, + isRpcFailoverEnabled, + isRpcFailoverForced, + }); const isOffline = (): boolean => { const connectivityState = messenger.call('ConnectivityController:getState'); 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/src/selectors.test.ts b/packages/network-controller/src/selectors.test.ts new file mode 100644 index 0000000000..9e934fdc9e --- /dev/null +++ b/packages/network-controller/src/selectors.test.ts @@ -0,0 +1,42 @@ +import { getRpcFailoverMode } 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): { + remoteFeatureFlags: Record; + cacheTimestamp: number; +} { + return { + remoteFeatureFlags: + mode === undefined ? {} : { corePlatformRpcFailoverMode: mode }, + cacheTimestamp: 0, + }; +} + +describe('getRpcFailoverMode', () => { + it('returns "enabled" when the flag is "enabled"', () => { + expect(getRpcFailoverMode(buildState('enabled') as never)).toBe('enabled'); + }); + + it('returns "forced" when the flag is "forced"', () => { + expect(getRpcFailoverMode(buildState('forced') as never)).toBe('forced'); + }); + + it('returns "disabled" when the flag is "disabled"', () => { + expect(getRpcFailoverMode(buildState('disabled') as never)).toBe( + 'disabled', + ); + }); + + it('returns "disabled" when the flag is absent', () => { + expect(getRpcFailoverMode(buildState() as never)).toBe('disabled'); + }); + + 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 016c8a66b0..60406079d8 100644 --- a/packages/network-controller/src/selectors.ts +++ b/packages/network-controller/src/selectors.ts @@ -1,9 +1,28 @@ import { RemoteFeatureFlagControllerState } from '@metamask/remote-feature-flag-controller'; -export function getIsRpcFailoverEnabled( +/** + * 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. + */ +export 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. + */ +export function getRpcFailoverMode( state: RemoteFeatureFlagControllerState, -): boolean { - const walletFrameworkRpcFailoverEnabled = state.remoteFeatureFlags - .walletFrameworkRpcFailoverEnabled as boolean | undefined; - return walletFrameworkRpcFailoverEnabled ?? false; +): RpcFailoverMode { + const mode = state.remoteFeatureFlags.corePlatformRpcFailoverMode; + return mode === 'enabled' || mode === 'forced' ? mode : 'disabled'; } diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 65ee190f87..4d321927e1 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -805,293 +805,196 @@ 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; - }); + describe('RemoteFeatureFlagController:stateChange (isRpcFailoverForced)', () => { + it('calls enableForcedRpcFailover 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, 'enableForcedRpcFailover'); + 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', + await withController( + { + isRpcFailoverForced: false, + state: { + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurationsByChainId: { + [ChainId.mainnet]: buildInfuraNetworkConfiguration( + InfuraNetworkType.mainnet, + { rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'BBBB-BBBB-BBBB-BBBB', - url: 'https://test.network/2', - failoverUrls: ['https://failover.endpoint/2'], + 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 ({ 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 ({ messenger }) => { + messenger.publish( + 'RemoteFeatureFlagController:stateChange', + { + remoteFeatureFlags: { + corePlatformRpcFailoverMode: 'forced', }, + cacheTimestamp: 0, }, - }, - 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); - }, - ); - }); + expect(autoManagedNetworkClients).toHaveLength(2); + expect( + autoManagedNetworkClients[0].enableForcedRpcFailover, + ).not.toHaveBeenCalled(); + expect( + autoManagedNetworkClients[1].enableForcedRpcFailover, + ).toHaveBeenCalled(); + }, + ); }); - }); - 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; - }); + 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, 'enableForcedRpcFailover'); + 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', + await withController( + { + isRpcFailoverForced: true, + state: { + selectedNetworkClientId: 'AAAA-AAAA-AAAA-AAAA', + networkConfigurationsByChainId: { + [ChainId.mainnet]: buildInfuraNetworkConfiguration( + InfuraNetworkType.mainnet, + { rpcEndpoints: [ - buildCustomRpcEndpoint({ - networkClientId: 'AAAA-AAAA-AAAA-AAAA', - url: 'https://test.network/1', + buildInfuraRpcEndpoint(InfuraNetworkType.mainnet, { 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'], - }), - ], - }), - }, + }, + ), + '0x200': buildCustomNetworkConfiguration({ + chainId: '0x200', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + url: 'https://test.network/1', + failoverUrls: [], + }), + ], + }), }, }, - async ({ controller }) => { - controller.disableRpcFailover(); - - expect(autoManagedNetworkClients).toHaveLength(3); - expect( - autoManagedNetworkClients[0].disableRpcFailover, - ).not.toHaveBeenCalled(); - expect( - autoManagedNetworkClients[1].disableRpcFailover, - ).toHaveBeenCalled(); - expect( - autoManagedNetworkClients[2].disableRpcFailover, - ).toHaveBeenCalled(); - }, - ); - }); + }, + async () => { + expect(autoManagedNetworkClients).toHaveLength(2); + expect( + autoManagedNetworkClients[0].enableForcedRpcFailover, + ).toHaveBeenCalled(); + expect( + autoManagedNetworkClients[1].enableForcedRpcFailover, + ).not.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'], - }), - ], - }), - }, + it('calls enableForcedRpcFailover 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, 'enableForcedRpcFailover'); + 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 ({ 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(); + }, + async ({ messenger }) => { + messenger.publish( + 'RemoteFeatureFlagController:stateChange', + { + remoteFeatureFlags: { + corePlatformRpcFailoverMode: 'forced', + }, + cacheTimestamp: 0, + }, + [], + ); - expect(autoManagedNetworkClients).toHaveLength(0); - }, - ); - }); + expect(autoManagedNetworkClients).toHaveLength(1); + expect( + autoManagedNetworkClients[0].enableForcedRpcFailover, + ).toHaveBeenCalled(); + expect( + autoManagedNetworkClients[0].enableRpcFailover, + ).not.toHaveBeenCalled(); + }, + ); }); }); @@ -1659,6 +1562,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'base-mainnet': { blockTracker: expect.anything(), @@ -1674,6 +1579,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'bsc-mainnet': { blockTracker: expect.anything(), @@ -1689,6 +1596,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'linea-mainnet': { blockTracker: expect.anything(), @@ -1704,6 +1613,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'linea-sepolia': { blockTracker: expect.anything(), @@ -1719,6 +1630,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, mainnet: { blockTracker: expect.anything(), @@ -1734,6 +1647,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'megaeth-testnet-v2': { blockTracker: expect.anything(), @@ -1748,6 +1663,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'monad-mainnet': { blockTracker: expect.anything(), @@ -1763,6 +1680,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'monad-testnet': { blockTracker: expect.anything(), @@ -1777,6 +1696,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'optimism-mainnet': { blockTracker: expect.anything(), @@ -1792,6 +1713,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'polygon-mainnet': { blockTracker: expect.anything(), @@ -1807,6 +1730,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, sepolia: { blockTracker: expect.anything(), @@ -1822,6 +1747,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, }); }, @@ -1878,6 +1805,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, 'BBBB-BBBB-BBBB-BBBB': { blockTracker: expect.anything(), @@ -1892,6 +1821,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableForcedRpcFailover: expect.any(Function), + disableForcedRpcFailover: expect.any(Function), }, }); }, @@ -4536,6 +4467,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( @@ -4553,6 +4486,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( @@ -4570,6 +4505,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); const networkConfigurationsByNetworkClientId = @@ -6037,6 +5974,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByNetworkClientId = @@ -6353,6 +6292,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( createAutoManagedNetworkClientSpy, @@ -6369,6 +6310,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByNetworkClientId = @@ -7346,6 +7289,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByNetworkClientId = getNetworkConfigurationsByNetworkClientId( @@ -8221,6 +8166,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( @@ -8238,6 +8185,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); @@ -9228,6 +9177,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( getNetworkConfigurationsByNetworkClientId( @@ -10389,6 +10340,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( createAutoManagedNetworkClientSpy, @@ -10405,6 +10358,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByNetworkClientId = @@ -11111,6 +11066,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ networkClientId: 'DDDD-DDDD-DDDD-DDDD', @@ -11125,6 +11082,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( @@ -11846,6 +11805,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( createAutoManagedNetworkClientSpy, @@ -11862,6 +11823,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByChainId = @@ -12549,6 +12512,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( @@ -12566,6 +12531,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..d4ce021df1 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, @@ -112,11 +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, + corePlatformRpcFailoverMode, }, cacheTimestamp: 0, }), @@ -632,6 +641,7 @@ type WithControllerCallback = ({ type WithControllerOptions = Partial & { isRpcFailoverEnabled?: boolean; + isRpcFailoverForced?: boolean; initializeController?: boolean; }; @@ -655,10 +665,14 @@ 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, diff --git a/packages/network-controller/tests/network-client/helpers.ts b/packages/network-controller/tests/network-client/helpers.ts index c5e6de192e..7a3593b7c5 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 = { @@ -482,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. @@ -502,6 +506,7 @@ export async function withNetworkClient( messenger = buildRootMessenger(), networkClientId = 'some-network-client-id', isRpcFailoverEnabled = false, + isRpcFailoverForced = false, }: MockOptions, fn: (client: MockNetworkClient) => Promise, ): Promise { @@ -554,6 +559,7 @@ export async function withNetworkClient( getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled, + isRpcFailoverForced, }); /* eslint-disable-next-line n/no-process-env */ process.env.IN_TEST = inTest;