diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 60fb463b16..e76eeb98a9 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 `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 diff --git a/packages/network-controller/src/NetworkController-method-action-types.ts b/packages/network-controller/src/NetworkController-method-action-types.ts index c07de322ba..45d5e94499 100644 --- a/packages/network-controller/src/NetworkController-method-action-types.ts +++ b/packages/network-controller/src/NetworkController-method-action-types.ts @@ -36,6 +36,26 @@ 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. * @@ -311,6 +331,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 09c727ada4..d9c1569de8 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -56,7 +56,7 @@ 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 { getIsRpcFailoverEnabled, getIsRpcFailoverForced } from './selectors'; import { NetworkClientType } from './types'; import type { BlockTracker, @@ -669,7 +669,9 @@ type AllowedEvents = RemoteFeatureFlagControllerStateChangeEvent; const MESSENGER_EXPOSED_METHODS = [ 'addNetwork', 'disableRpcFailover', + 'disableRpcFailoverForced', 'enableRpcFailover', + 'enableRpcFailoverForced', 'findNetworkClientIdByChainId', 'get1559CompatibilityWithNetworkClientId', 'getEIP1559Compatibility', @@ -1275,6 +1277,8 @@ export class NetworkController extends BaseController< #isRpcFailoverEnabled = false; + #isRpcFailoverForced = false; + /** * Constructs a NetworkController. * @@ -1378,6 +1382,15 @@ export class NetworkController extends BaseController< }, getIsRpcFailoverEnabled, ); + + this.messenger.subscribe( + // eslint-disable-next-line no-restricted-syntax + 'RemoteFeatureFlagController:stateChange', + (isRpcFailoverForced) => { + this.#updateRpcFailoverForced(isRpcFailoverForced); + }, + getIsRpcFailoverForced, + ); } /** @@ -1408,6 +1421,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 @@ -1449,6 +1480,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. * @@ -1610,12 +1679,14 @@ 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.#updateRpcFailoverForced(getIsRpcFailoverForced(state)); this.#applyNetworkSelection(this.state.selectedNetworkClientId); } @@ -2860,6 +2931,7 @@ export class NetworkController extends BaseController< getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, isRpcFailoverEnabled: this.#isRpcFailoverEnabled, + isRpcFailoverForced: this.#isRpcFailoverForced, logger: this.#log, }); } else { @@ -2879,6 +2951,7 @@ export class NetworkController extends BaseController< getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, isRpcFailoverEnabled: this.#isRpcFailoverEnabled, + isRpcFailoverForced: this.#isRpcFailoverForced, logger: this.#log, }); } @@ -3045,6 +3118,7 @@ export class NetworkController extends BaseController< getBlockTrackerOptions: this.#getBlockTrackerOptions, messenger: this.messenger, isRpcFailoverEnabled: this.#isRpcFailoverEnabled, + isRpcFailoverForced: this.#isRpcFailoverForced, logger: this.#log, }), ] as const; @@ -3064,6 +3138,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..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 @@ -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.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('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, @@ -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..adcd516c67 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; }; /** @@ -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 enableRpcFailoverForced = (): void => { + isRpcFailoverForced = true; + destroy(); + networkClient = undefined; + }; + + const disableRpcFailoverForced = (): void => { + isRpcFailoverForced = false; + destroy(); + networkClient = undefined; + }; + return { configuration: networkClientConfiguration, provider: providerProxy, @@ -253,5 +274,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 8fc02a6f92..8bbb389ca7 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, }); @@ -242,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. */ @@ -252,6 +261,7 @@ function createRpcServiceChain({ getRpcServiceOptions, messenger, isRpcFailoverEnabled, + isRpcFailoverForced, logger, }: { id: NetworkClientId; @@ -262,21 +272,34 @@ function createRpcServiceChain({ ) => RpcServiceOptionsWithDefaults; messenger: NetworkControllerMessenger; isRpcFailoverEnabled: boolean; + 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'); - const availableEndpoints = - isRpcFailoverEnabled && isInfura - ? [ - { url: primaryEndpointUrl, isFailover: false }, - ...(configuration.failoverRpcUrls ?? []).map((url) => ({ - url, - isFailover: true, - })), - ] - : [{ url: primaryEndpointUrl, isFailover: false }]; + 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 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..330c3e624c 100644 --- a/packages/network-controller/src/index.ts +++ b/packages/network-controller/src/index.ts @@ -70,6 +70,8 @@ export type { NetworkControllerUpdateNetworkAction, NetworkControllerEnableRpcFailoverAction, NetworkControllerDisableRpcFailoverAction, + NetworkControllerEnableRpcFailoverForcedAction, + NetworkControllerDisableRpcFailoverForcedAction, 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..232d82c41a --- /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: { + 'core-platform-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: { + 'core-platform-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('passes through non-boolean values without coercion', () => { + const state = { + remoteFeatureFlags: { + 'core-platform-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..8c6afa6f12 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[ + '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 65ee190f87..0d3175f0cb 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1095,6 +1095,494 @@ 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( + 'RemoteFeatureFlagController:stateChange', + { + remoteFeatureFlags: { + walletFrameworkRpcFailoverEnabled: false, + 'core-platform-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( + 'RemoteFeatureFlagController:stateChange', + { + remoteFeatureFlags: { + walletFrameworkRpcFailoverEnabled: false, + 'core-platform-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 }) => { @@ -1659,6 +2147,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(), @@ -1674,6 +2164,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(), @@ -1689,6 +2181,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(), @@ -1704,6 +2198,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(), @@ -1719,6 +2215,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(), @@ -1734,6 +2232,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(), @@ -1748,6 +2248,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(), @@ -1763,6 +2265,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(), @@ -1777,6 +2281,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(), @@ -1792,6 +2298,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(), @@ -1807,6 +2315,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(), @@ -1822,6 +2332,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, }); }, @@ -1878,6 +2390,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(), @@ -1892,6 +2406,8 @@ describe('NetworkController', () => { destroy: expect.any(Function), enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), + enableRpcFailoverForced: expect.any(Function), + disableRpcFailoverForced: expect.any(Function), }, }); }, @@ -4536,6 +5052,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( @@ -4553,6 +5071,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( @@ -4570,6 +5090,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); const networkConfigurationsByNetworkClientId = @@ -6037,6 +6559,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByNetworkClientId = @@ -6353,6 +6877,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( createAutoManagedNetworkClientSpy, @@ -6369,6 +6895,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByNetworkClientId = @@ -7346,6 +7874,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByNetworkClientId = getNetworkConfigurationsByNetworkClientId( @@ -8221,6 +8751,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( @@ -8238,6 +8770,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); @@ -9228,6 +9762,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( getNetworkConfigurationsByNetworkClientId( @@ -10389,6 +10925,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( createAutoManagedNetworkClientSpy, @@ -10405,6 +10943,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByNetworkClientId = @@ -11111,6 +11651,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect(createAutoManagedNetworkClientSpy).toHaveBeenCalledWith({ networkClientId: 'DDDD-DDDD-DDDD-DDDD', @@ -11125,6 +11667,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( @@ -11846,6 +12390,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); expect( createAutoManagedNetworkClientSpy, @@ -11862,6 +12408,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }); const networkConfigurationsByChainId = @@ -12549,6 +13097,8 @@ describe('NetworkController', () => { getBlockTrackerOptions, messenger: networkControllerMessenger, isRpcFailoverEnabled: true, + isRpcFailoverForced: false, + logger: undefined, }, ); expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( @@ -12566,6 +13116,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..45a233e547 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, + 'core-platform-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,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;