diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index fcff6929fd..e648f0cf36 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The default `policyOptions.circuitBreakDuration` is now `30` seconds. - The default `pollingInterval` for the block tracker is now `20` seconds. - 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. ### Changed diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index ab34ec507e..549488ebdd 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -739,6 +739,10 @@ export type NetworkControllerOptions = { * The API key for Infura, used to make requests to Infura. */ infuraProjectId: string; + /** + * An optional map of available failover URLs for each chain ID. + */ + failoverUrls?: Record; /** * The desired state with which to initialize this controller. * Missing properties will be filled in with defaults. For instance, if not @@ -1256,6 +1260,8 @@ export class NetworkController extends BaseController< readonly #infuraProjectId: string; + readonly #failoverUrls?: Record; + #previouslySelectedNetworkClientId: string; #providerProxy: ProviderProxy | undefined; @@ -1291,6 +1297,7 @@ export class NetworkController extends BaseController< messenger, state, infuraProjectId, + failoverUrls, log, getRpcServiceOptions, getBlockTrackerOptions, @@ -1333,6 +1340,7 @@ export class NetworkController extends BaseController< }); this.#infuraProjectId = infuraProjectId; + this.#failoverUrls = failoverUrls; this.#log = log; this.#getRpcServiceOptions = getRpcServiceOptions; this.#getBlockTrackerOptions = getBlockTrackerOptions; @@ -2844,6 +2852,7 @@ export class NetworkController extends BaseController< ), ); + const defaultFailoverUrls = this.#failoverUrls?.[networkFields.chainId]; for (const addedRpcEndpoint of addedRpcEndpoints) { if (addedRpcEndpoint.type === RpcEndpointType.Infura) { autoManagedNetworkClientRegistry[NetworkClientType.Infura][ @@ -2854,7 +2863,8 @@ export class NetworkController extends BaseController< type: NetworkClientType.Infura, chainId: networkFields.chainId, network: addedRpcEndpoint.networkClientId, - failoverRpcUrls: addedRpcEndpoint.failoverUrls, + failoverRpcUrls: + defaultFailoverUrls ?? addedRpcEndpoint.failoverUrls, infuraProjectId: this.#infuraProjectId, ticker: networkFields.nativeCurrency, }, @@ -2872,7 +2882,8 @@ export class NetworkController extends BaseController< networkClientConfiguration: { type: NetworkClientType.Custom, chainId: networkFields.chainId, - failoverRpcUrls: addedRpcEndpoint.failoverUrls, + failoverRpcUrls: + defaultFailoverUrls ?? addedRpcEndpoint.failoverUrls, rpcUrl: addedRpcEndpoint.url, ticker: networkFields.nativeCurrency, }, @@ -3023,6 +3034,7 @@ export class NetworkController extends BaseController< const networkClientsWithIds = chainIds.flatMap((chainId) => { const networkConfiguration = this.state.networkConfigurationsByChainId[chainId]; + const defaultFailoverUrls = this.#failoverUrls?.[chainId]; return networkConfiguration.rpcEndpoints.map((rpcEndpoint) => { if (rpcEndpoint.type === RpcEndpointType.Infura) { const infuraNetworkName = deriveInfuraNetworkNameFromRpcEndpointUrl( @@ -3035,7 +3047,8 @@ export class NetworkController extends BaseController< networkClientConfiguration: { type: NetworkClientType.Infura, network: infuraNetworkName, - failoverRpcUrls: rpcEndpoint.failoverUrls, + failoverRpcUrls: + defaultFailoverUrls ?? rpcEndpoint.failoverUrls, infuraProjectId: this.#infuraProjectId, chainId: networkConfiguration.chainId, ticker: networkConfiguration.nativeCurrency, @@ -3055,7 +3068,7 @@ export class NetworkController extends BaseController< networkClientConfiguration: { type: NetworkClientType.Custom, chainId: networkConfiguration.chainId, - failoverRpcUrls: rpcEndpoint.failoverUrls, + failoverRpcUrls: defaultFailoverUrls ?? rpcEndpoint.failoverUrls, rpcUrl: rpcEndpoint.url, ticker: networkConfiguration.nativeCurrency, }, diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 5cfe6692a4..e715e650aa 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1539,6 +1539,115 @@ describe('NetworkController', () => { }); }); }); + + describe('if the controller was initialized with failoverUrls', () => { + it('applies the chain-level failover URLs to an Infura network client, overriding the endpoint value', async () => { + const infuraProjectId = 'some-infura-project-id'; + + await withController( + { + infuraProjectId, + failoverUrls: { + [ChainId[InfuraNetworkType.mainnet]]: ['https://chain.failover'], + }, + }, + async ({ controller }) => { + const networkClient = controller.getNetworkClientById( + NetworkType.mainnet, + ); + + expect(networkClient.configuration).toStrictEqual({ + chainId: ChainId[InfuraNetworkType.mainnet], + failoverRpcUrls: ['https://chain.failover'], + infuraProjectId, + network: InfuraNetworkType.mainnet, + ticker: NetworksTicker[InfuraNetworkType.mainnet], + type: NetworkClientType.Infura, + }); + }, + ); + }); + + it('applies the chain-level failover URLs to a custom network client, overriding the endpoint value', async () => { + await withController( + { + state: + buildNetworkControllerStateWithDefaultSelectedNetworkClientId({ + networkConfigurationsByChainId: { + '0x1337': buildCustomNetworkConfiguration({ + chainId: '0x1337', + nativeCurrency: 'TEST', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + failoverUrls: ['https://endpoint.failover'], + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + url: 'https://test.network', + }), + ], + }), + }, + }), + infuraProjectId: 'some-infura-project-id', + failoverUrls: { + '0x1337': ['https://chain.failover'], + }, + }, + async ({ controller }) => { + const networkClient = controller.getNetworkClientById( + 'AAAA-AAAA-AAAA-AAAA', + ); + + expect(networkClient.configuration).toStrictEqual({ + chainId: '0x1337', + failoverRpcUrls: ['https://chain.failover'], + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }); + }, + ); + }); + + it('falls back to the endpoint failover URLs when no entry exists for the chain', async () => { + await withController( + { + state: + buildNetworkControllerStateWithDefaultSelectedNetworkClientId({ + networkConfigurationsByChainId: { + '0x1337': buildCustomNetworkConfiguration({ + chainId: '0x1337', + nativeCurrency: 'TEST', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + failoverUrls: ['https://endpoint.failover'], + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + url: 'https://test.network', + }), + ], + }), + }, + }), + infuraProjectId: 'some-infura-project-id', + failoverUrls: { + '0x9999': ['https://chain.failover'], + }, + }, + async ({ controller }) => { + const networkClient = controller.getNetworkClientById( + 'AAAA-AAAA-AAAA-AAAA', + ); + + expect(networkClient.configuration).toStrictEqual({ + chainId: '0x1337', + failoverRpcUrls: ['https://endpoint.failover'], + rpcUrl: 'https://test.network', + ticker: 'TEST', + type: NetworkClientType.Custom, + }); + }, + ); + }); + }); }); describe('getNetworkClientRegistry', () => { @@ -1821,6 +1930,65 @@ describe('NetworkController', () => { ); }); }); + + describe('if the controller was initialized with failoverUrls', () => { + it('applies the chain-level failover URLs to every endpoint on a matched chain, keeping endpoint URLs for unmatched chains', async () => { + await withController( + { + state: + buildNetworkControllerStateWithDefaultSelectedNetworkClientId({ + networkConfigurationsByChainId: { + '0x1337': buildCustomNetworkConfiguration({ + chainId: '0x1337', + nativeCurrency: 'TOKEN1', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + failoverUrls: ['https://first.endpoint.failover'], + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + url: 'https://test.network/1', + }), + buildCustomRpcEndpoint({ + failoverUrls: ['https://second.endpoint.failover'], + networkClientId: 'BBBB-BBBB-BBBB-BBBB', + url: 'https://test.network/2', + }), + ], + }), + '0x2448': buildCustomNetworkConfiguration({ + chainId: '0x2448', + nativeCurrency: 'TOKEN2', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + failoverUrls: ['https://third.endpoint.failover'], + networkClientId: 'CCCC-CCCC-CCCC-CCCC', + url: 'https://test.network/3', + }), + ], + }), + }, + }), + failoverUrls: { + '0x1337': ['https://chain.failover'], + }, + }, + async ({ controller }) => { + mockCreateNetworkClient().mockReturnValue(buildFakeClient()); + + const registry = controller.getNetworkClientRegistry(); + + expect( + registry['AAAA-AAAA-AAAA-AAAA'].configuration.failoverRpcUrls, + ).toStrictEqual(['https://chain.failover']); + expect( + registry['BBBB-BBBB-BBBB-BBBB'].configuration.failoverRpcUrls, + ).toStrictEqual(['https://chain.failover']); + expect( + registry['CCCC-CCCC-CCCC-CCCC'].configuration.failoverRpcUrls, + ).toStrictEqual(['https://third.endpoint.failover']); + }, + ); + }); + }); }); describe('lookupNetwork', () => { @@ -4472,6 +4640,90 @@ describe('NetworkController', () => { ); }); + it('overrides the per-endpoint failover URLs with the chain-level failoverUrls when the controller was initialized with them', async () => { + uuidV4Mock + .mockReturnValueOnce('BBBB-BBBB-BBBB-BBBB') + .mockReturnValueOnce('CCCC-CCCC-CCCC-CCCC'); + const createAutoManagedNetworkClientSpy = jest.spyOn( + createAutoManagedNetworkClientModule, + 'createAutoManagedNetworkClient', + ); + const infuraProjectId = 'some-infura-project-id'; + + await withController( + { + state: + buildNetworkControllerStateWithDefaultSelectedNetworkClientId({ + networkConfigurationsByChainId: { + '0x1337': buildCustomNetworkConfiguration({ + chainId: '0x1337', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + url: 'https://test.endpoint/1', + }), + ], + }), + }, + }), + infuraProjectId, + failoverUrls: { + [infuraChainId]: ['https://chain.failover'], + }, + isRpcFailoverEnabled: true, + }, + ({ controller }) => { + const defaultRpcEndpoint: InfuraRpcEndpoint = { + failoverUrls: ['https://first.failover.endpoint'], + name: infuraNetworkNickname, + networkClientId: infuraNetworkType, + type: RpcEndpointType.Infura as const, + url: `https://${infuraNetworkType}.infura.io/v3/{infuraProjectId}` as const, + }; + + controller.addNetwork({ + blockExplorerUrls: [], + chainId: infuraChainId, + defaultRpcEndpointIndex: 1, + name: infuraNetworkType, + nativeCurrency: infuraNativeTokenName, + rpcEndpoints: [ + defaultRpcEndpoint, + { + failoverUrls: ['https://second.failover.endpoint'], + name: 'Test Network 1', + type: RpcEndpointType.Custom, + url: 'https://test.endpoint/2', + }, + ], + }); + + // Skipping the 1st call because it's for the initial state + expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + networkClientId: infuraNetworkType, + networkClientConfiguration: expect.objectContaining({ + failoverRpcUrls: ['https://chain.failover'], + type: NetworkClientType.Infura, + }), + }), + ); + expect(createAutoManagedNetworkClientSpy).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + networkClientId: 'BBBB-BBBB-BBBB-BBBB', + networkClientConfiguration: expect.objectContaining({ + failoverRpcUrls: ['https://chain.failover'], + rpcUrl: 'https://test.endpoint/2', + type: NetworkClientType.Custom, + }), + }), + ); + }, + ); + }); + it('adds the network configuration to state under the chain ID', async () => { uuidV4Mock.mockReturnValueOnce('BBBB-BBBB-BBBB-BBBB'); @@ -5837,6 +6089,87 @@ describe('NetworkController', () => { ); }); + it('overrides the endpoint failover URLs with the chain-level failoverUrls when the controller was initialized with them', async () => { + const createAutoManagedNetworkClientSpy = jest.spyOn( + createAutoManagedNetworkClientModule, + 'createAutoManagedNetworkClient', + ); + const networkConfigurationToUpdate = + buildInfuraNetworkConfiguration(infuraNetworkType, { + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'AAAA-AAAA-AAAA-AAAA', + url: 'https://rpc.network', + }), + ], + }); + const infuraProjectId = 'some-infura-project-id'; + + await withController( + { + state: { + networkConfigurationsByChainId: { + [infuraChainId]: networkConfigurationToUpdate, + '0x9999': buildCustomNetworkConfiguration({ + chainId: '0x9999', + nativeCurrency: 'TEST-9999', + rpcEndpoints: [ + buildCustomRpcEndpoint({ + networkClientId: 'ZZZZ-ZZZZ-ZZZZ-ZZZZ', + url: 'https://selected.endpoint', + }), + ], + }), + }, + selectedNetworkClientId: 'ZZZZ-ZZZZ-ZZZZ-ZZZZ', + }, + infuraProjectId, + failoverUrls: { + [infuraChainId]: ['https://chain.failover'], + }, + isRpcFailoverEnabled: true, + }, + async ({ controller }) => { + const infuraRpcEndpoint: InfuraRpcEndpoint = { + failoverUrls: ['https://failover.endpoint'], + networkClientId: infuraNetworkType, + url: `https://${infuraNetworkType}.infura.io/v3/{infuraProjectId}`, + type: RpcEndpointType.Infura, + }; + + await controller.updateNetwork(infuraChainId, { + ...networkConfigurationToUpdate, + rpcEndpoints: [ + ...networkConfigurationToUpdate.rpcEndpoints, + infuraRpcEndpoint, + ], + }); + + expect( + createAutoManagedNetworkClientSpy, + ).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + networkClientId: infuraNetworkType, + networkClientConfiguration: expect.objectContaining({ + failoverRpcUrls: ['https://chain.failover'], + type: NetworkClientType.Infura, + }), + }), + ); + + const networkConfigurationsByNetworkClientId = + getNetworkConfigurationsByNetworkClientId( + controller.getNetworkClientRegistry(), + ); + expect( + networkConfigurationsByNetworkClientId[infuraNetworkType] + .failoverRpcUrls, + ).toStrictEqual(['https://chain.failover']); + }, + ); + }); + it('stores the network configuration with the new RPC endpoint in state', async () => { const networkConfigurationToUpdate = buildInfuraNetworkConfiguration(infuraNetworkType, {