Skip to content

Commit 0c25600

Browse files
committed
Integrate RPC failover into NetworkController
When configuring an Infura network, a list of failover endpoint URLs can now be supplied. If Infura is perceived to be down (after making 15 attempts to make a request, all of which either throw an error, return a non-200 response, or return a non-parseable response) then the request will be forwarded automatically to the first failover URL (using subsequent URLs as failovers for previous URLs). By default, all Infura networks will use Quicknode as a failover. Under the hood, the list of failover URLs are loaded into an RpcServiceChain (added in a previous commit) and this is then passed to the Infura middleware.
1 parent 48b1b68 commit 0c25600

File tree

16 files changed

+2339
-907
lines changed

16 files changed

+2339
-907
lines changed

packages/assets-controllers/src/AssetsContractController.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
NetworkClientId,
1414
NetworkControllerActions,
1515
NetworkControllerEvents,
16+
InfuraNetworkClientConfiguration,
1617
} from '@metamask/network-controller';
1718
import {
1819
NetworkController,
@@ -69,13 +70,14 @@ async function setupAssetContractControllers({
6970
useNetworkControllerProvider?: boolean;
7071
infuraProjectId?: string;
7172
} = {}) {
72-
const networkClientConfiguration = {
73+
const networkClientConfiguration: InfuraNetworkClientConfiguration = {
7374
type: NetworkClientType.Infura,
7475
network: NetworkType.mainnet,
76+
failoverEndpointUrls: [],
7577
infuraProjectId,
7678
chainId: BUILT_IN_NETWORKS.mainnet.chainId,
7779
ticker: BUILT_IN_NETWORKS.mainnet.ticker,
78-
} as const;
80+
};
7981
let provider: Provider;
8082

8183
const messenger = new Messenger<
@@ -1091,6 +1093,7 @@ describe('AssetsContractController', () => {
10911093
ticker: BUILT_IN_NETWORKS.sepolia.ticker,
10921094
type: NetworkClientType.Infura,
10931095
network: 'sepolia',
1096+
failoverEndpointUrls: [],
10941097
infuraProjectId: networkClientConfiguration.infuraProjectId,
10951098
},
10961099
mocks: [

packages/assets-controllers/src/AssetsContractControllerWithNetworkClientId.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,7 @@ describe('AssetsContractController with NetworkClientId', () => {
757757
ticker: BUILT_IN_NETWORKS.sepolia.ticker,
758758
type: NetworkClientType.Infura,
759759
network: 'sepolia',
760+
failoverEndpointUrls: [],
760761
infuraProjectId: networkClientConfiguration.infuraProjectId,
761762
},
762763
mocks: [

packages/network-controller/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- The request returns a non-200 response
1818
- Use exponential backoff / jitter when retrying requests to Infura and custom RPC endpoints ([#5290](https://github.com/MetaMask/core/pull/5290))
1919
- As requests are retried, the delay between retries will increase exponentially (using random variance to prevent bursts)
20+
- Add support for automatic failover when Infura is down ([#5630](https://github.com/MetaMask/core/pull/5630))
21+
- An Infura RPC endpoint can now be configured with a list of failover URLs (see "Changed" for details).
22+
- If, after many attempts, an Infura network is perceived to be down, the list of failover URLs will be tried in turn.
23+
- All Infura RPC endpoints are configured to use Quicknode by default.
2024

2125
### Changed
2226

2327
- **BREAKING:** `NetworkController` constructor now takes two required options, `fetch` and `btoa` ([#5290](https://github.com/MetaMask/core/pull/5290))
2428
- These are passed along to functions that create the JSON-RPC middleware
29+
- **BREAKING:** Add required property `failoverEndpointUrls` to `InfuraRpcEndpoint` ([#5630](https://github.com/MetaMask/core/pull/5630))
30+
- **BREAKING:** Add required property `failoverEndpointUrls` to `InfuraNetworkClientConfiguration` ([#5630](https://github.com/MetaMask/core/pull/5630))
2531
- Synchronize retry logic and error handling behavior between Infura and custom RPC endpoints ([#5290](https://github.com/MetaMask/core/pull/5290))
2632
- A request to a custom endpoint that returns a 418 response will no longer return a JSON-RPC response with the error "Request is being rate limited"
2733
- A request to a custom endpoint that returns a 429 response now returns a JSON-RPC response with the error "Request is being rate limited"

packages/network-controller/src/NetworkController.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ import { createSelector } from 'reselect';
2828
import * as URI from 'uri-js';
2929
import { v4 as uuidV4 } from 'uuid';
3030

31-
import { INFURA_BLOCKED_KEY, NetworkStatus } from './constants';
31+
import {
32+
DEFAULT_INFURA_FAILOVER_URL,
33+
INFURA_BLOCKED_KEY,
34+
NetworkStatus,
35+
} from './constants';
3236
import type {
3337
AutoManagedNetworkClient,
3438
ProxyWithAccessibleTarget,
@@ -95,6 +99,10 @@ export enum RpcEndpointType {
9599
* separate type.
96100
*/
97101
export type InfuraRpcEndpoint = {
102+
/**
103+
* Alternate RPC endpoints to use when this endpoint is down.
104+
*/
105+
failoverUrls: string[];
98106
/**
99107
* The optional user-facing nickname of the endpoint.
100108
*/
@@ -569,6 +577,7 @@ function getDefaultNetworkConfigurationsByChainId(): Record<
569577
nativeCurrency: NetworksTicker[infuraNetworkType],
570578
rpcEndpoints: [
571579
{
580+
failoverUrls: [DEFAULT_INFURA_FAILOVER_URL],
572581
networkClientId: infuraNetworkType,
573582
type: RpcEndpointType.Infura,
574583
url: rpcEndpointUrl,
@@ -2446,6 +2455,7 @@ export class NetworkController extends BaseController<
24462455
type: NetworkClientType.Infura,
24472456
chainId: networkFields.chainId,
24482457
network: addedRpcEndpoint.networkClientId,
2458+
failoverEndpointUrls: addedRpcEndpoint.failoverUrls,
24492459
infuraProjectId: this.#infuraProjectId,
24502460
ticker: networkFields.nativeCurrency,
24512461
},
@@ -2617,6 +2627,7 @@ export class NetworkController extends BaseController<
26172627
networkClientConfiguration: {
26182628
type: NetworkClientType.Infura,
26192629
network: infuraNetworkName,
2630+
failoverEndpointUrls: rpcEndpoint.failoverUrls,
26202631
infuraProjectId: this.#infuraProjectId,
26212632
chainId: networkConfiguration.chainId,
26222633
ticker: networkConfiguration.nativeCurrency,

packages/network-controller/src/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,8 @@ export enum NetworkStatus {
2626
}
2727

2828
export const INFURA_BLOCKED_KEY = 'countryBlocked';
29+
30+
/**
31+
* The default endpoint URL to hit when Infura is down.
32+
*/
33+
export const DEFAULT_INFURA_FAILOVER_URL = 'https://quicknode.com';

packages/network-controller/src/create-auto-managed-network-client.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@ describe('createAutoManagedNetworkClient', () => {
1919
rpcUrl: 'https://test.chain',
2020
chainId: '0x1337',
2121
ticker: 'ETH',
22-
} as const,
22+
},
2323
{
2424
type: NetworkClientType.Infura,
2525
network: NetworkType.mainnet,
2626
chainId: BUILT_IN_NETWORKS[NetworkType.mainnet].chainId,
2727
infuraProjectId: 'some-infura-project-id',
2828
ticker: BUILT_IN_NETWORKS[NetworkType.mainnet].ticker,
29-
} as const,
29+
failoverEndpointUrls: [],
30+
},
3031
];
3132
for (const networkClientConfiguration of networkClientConfigurations) {
3233
describe(`given configuration for a ${networkClientConfiguration.type} network client`, () => {

packages/network-controller/src/create-network-client.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine';
2626
import type { Hex, Json, JsonRpcParams } from '@metamask/utils';
2727

28-
import { RpcService } from './rpc-service/rpc-service';
28+
import { RpcServiceChain } from './rpc-service/rpc-service-chain';
2929
import type {
3030
BlockTracker,
3131
NetworkClientConfiguration,
@@ -66,18 +66,21 @@ export function createNetworkClient({
6666
fetch: typeof fetch;
6767
btoa: typeof btoa;
6868
}): NetworkClient {
69-
const rpcService =
69+
const primaryEndpointUrl =
7070
configuration.type === NetworkClientType.Infura
71-
? new RpcService({
72-
fetch: givenFetch,
73-
btoa: givenBtoa,
74-
endpointUrl: `https://${configuration.network}.infura.io/v3/${configuration.infuraProjectId}`,
75-
})
76-
: new RpcService({
77-
fetch: givenFetch,
78-
btoa: givenBtoa,
79-
endpointUrl: configuration.rpcUrl,
80-
});
71+
? `https://${configuration.network}.infura.io/v3/${configuration.infuraProjectId}`
72+
: configuration.rpcUrl;
73+
const failoverEndpointUrls =
74+
configuration.type === NetworkClientType.Infura
75+
? configuration.failoverEndpointUrls
76+
: [];
77+
const rpcService = new RpcServiceChain({
78+
fetch: givenFetch,
79+
btoa: givenBtoa,
80+
serviceConfigurations: [primaryEndpointUrl, ...failoverEndpointUrls].map(
81+
(endpointUrl) => ({ endpointUrl }),
82+
),
83+
});
8184

8285
const rpcApiMiddleware =
8386
configuration.type === NetworkClientType.Infura

packages/network-controller/src/types.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,40 @@ export enum NetworkClientType {
1818
}
1919

2020
/**
21-
* A configuration object that can be used to create a client for a custom
22-
* network.
21+
* A configuration object that can be used to create a client for a network.
2322
*/
24-
export type CustomNetworkClientConfiguration = {
23+
type CommonNetworkClientConfiguration = {
24+
/**
25+
* The chain ID of the network.
26+
*/
2527
chainId: Hex;
26-
rpcUrl: string;
28+
/**
29+
* The name of the currency to use for the chain.
30+
*/
2731
ticker: string;
28-
type: NetworkClientType.Custom;
2932
};
3033

34+
/**
35+
* A configuration object that can be used to create a client for a custom
36+
* network.
37+
*/
38+
export type CustomNetworkClientConfiguration =
39+
CommonNetworkClientConfiguration & {
40+
rpcUrl: string;
41+
type: NetworkClientType.Custom;
42+
};
43+
3144
/**
3245
* A configuration object that can be used to create a client for an Infura
3346
* network.
3447
*/
35-
export type InfuraNetworkClientConfiguration = {
36-
chainId: Hex;
37-
network: InfuraNetworkType;
38-
infuraProjectId: string;
39-
ticker: string;
40-
type: NetworkClientType.Infura;
41-
};
48+
export type InfuraNetworkClientConfiguration =
49+
CommonNetworkClientConfiguration & {
50+
network: InfuraNetworkType;
51+
infuraProjectId: string;
52+
failoverEndpointUrls: string[];
53+
type: NetworkClientType.Infura;
54+
};
4255

4356
/**
4457
* A configuration object that can be used to create a client for a network.

0 commit comments

Comments
 (0)