From ce206bd3fde65987a2f463cb914a092c6198f747 Mon Sep 17 00:00:00 2001 From: mikesposito Date: Wed, 8 Jul 2026 21:23:01 +0200 Subject: [PATCH 1/3] feat!: allow dynamic infura networks --- packages/network-controller/CHANGELOG.md | 11 ++ .../src/NetworkController.ts | 67 ++----- .../src/create-network-client.ts | 18 +- packages/network-controller/src/types.ts | 3 +- .../tests/NetworkController.test.ts | 163 +----------------- 5 files changed, 39 insertions(+), 223 deletions(-) diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index e36c2e857a..8bad562eac 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING:** `BuiltInNetworkClientId` is now `string` instead of `InfuraNetworkType` ([#9432](https://github.com/MetaMask/core/pull/9432)) + - This type was previously constrained to known Infura network names (e.g. `"mainnet"`, `"sepolia"`). It is now `string` to allow dynamically configured Infura networks whose names are not bundled into the package. + - If you have code that narrows on `BuiltInNetworkClientId`, you will need to remove the narrowing or check the network client type via `NetworkClientType` instead. +- **BREAKING:** `InfuraNetworkClientConfiguration.network` is now `string` instead of `InfuraNetworkType` ([#9432](https://github.com/MetaMask/core/pull/9432)) + - Previously only known Infura network names were accepted. Any valid Infura subdomain string is now accepted. +- **BREAKING:** `NetworkController.getNetworkClientById` no longer distinguishes between Infura and custom network clients when the provided ID is not found ([#9432](https://github.com/MetaMask/core/pull/9432)) + - The method now checks the Infura registry first and falls back to the custom registry, without using `isInfuraNetworkType` to decide which registry to consult. + - The error message when no client is found has changed from `"No Infura network client was found with the ID \"\"."` / `"No custom network client was found with the ID \"\"."` to `"No network client was found with ID \"\"."`. +- Remove validation that prevented a custom RPC endpoint from having a `networkClientId` that matches a known Infura network name ([#9432](https://github.com/MetaMask/core/pull/9432)) +- Remove validation that required an Infura RPC endpoint URL's implied chain ID to match the chain ID of the network configuration it belongs to ([#9432](https://github.com/MetaMask/core/pull/9432)) + - This allows Infura-backed networks to be added and updated dynamically without being constrained to the known Infura network list bundled in `@metamask/controller-utils`. - Bump `@metamask/messenger` from `^1.2.0` to `^2.0.0` ([#9392](https://github.com/MetaMask/core/pull/9392)) ## [34.0.0] diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 5ad7b5443c..544ded37f3 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -326,7 +326,7 @@ function isErrorWithCode(error: unknown): error is { code: string | number } { /** * The string that uniquely identifies an Infura network client. */ -export type BuiltInNetworkClientId = InfuraNetworkType; +export type BuiltInNetworkClientId = string; /** * The string that uniquely identifies a custom network client. @@ -1074,20 +1074,15 @@ function isValidUrl(url: string): boolean { * * @param rpcEndpointUrl - The URL to operate on. * @returns The Infura network name that the URL references. - * @throws if the URL is not an Infura API URL, or if an Infura network is not - * present in the URL. + * @throws if no Infura network is present in the URL. */ function deriveInfuraNetworkNameFromRpcEndpointUrl( rpcEndpointUrl: string, -): InfuraNetworkType { +): string { const match = INFURA_URL_REGEX.exec(rpcEndpointUrl); if (match?.groups) { - if (isInfuraNetworkType(match.groups.networkName)) { - return match.groups.networkName; - } - - throw new Error(`Unknown Infura network '${match.groups.networkName}'`); + return match.groups.networkName; } throw new Error('Could not derive Infura network from RPC endpoint URL'); @@ -1529,18 +1524,11 @@ export class NetworkController extends BaseController< const autoManagedNetworkClientRegistry = this.#ensureAutoManagedNetworkClientRegistryPopulated(); - if (isInfuraNetworkType(networkClientId)) { - const infuraNetworkClient = - autoManagedNetworkClientRegistry[NetworkClientType.Infura][ - networkClientId - ]; - // This is impossible to reach - /* istanbul ignore if */ - if (!infuraNetworkClient) { - throw new Error( - `No Infura network client was found with the ID "${networkClientId}".`, - ); - } + const infuraNetworkClient = + autoManagedNetworkClientRegistry[NetworkClientType.Infura][ + networkClientId + ]; + if (infuraNetworkClient) { return infuraNetworkClient; } @@ -1550,7 +1538,7 @@ export class NetworkController extends BaseController< ]; if (!customNetworkClient) { throw new Error( - `No custom network client was found with the ID "${networkClientId}".`, + `No network client was found with ID "${networkClientId}".`, ); } return customNetworkClient; @@ -1620,10 +1608,7 @@ export class NetworkController extends BaseController< | NetworkStatus.Blocked; isEIP1559Compatible: undefined | boolean; }> { - // Force TypeScript to use one of the two overloads explicitly - const networkClient = isInfuraNetworkType(networkClientId) - ? this.getNetworkClientById(networkClientId) - : this.getNetworkClientById(networkClientId); + const networkClient = this.getNetworkClientById(networkClientId); const isInfura = networkClient.configuration.type === NetworkClientType.Infura; @@ -2603,16 +2588,6 @@ export class NetworkController extends BaseController< ? rpcEndpointFields.networkClientId : undefined; - if ( - rpcEndpointFields.type === RpcEndpointType.Custom && - networkClientId !== undefined && - isInfuraNetworkType(networkClientId) - ) { - throw new Error( - `${errorMessagePrefix}: Custom RPC endpoint '${rpcEndpointFields.url}' has invalid network client ID '${networkClientId}'`, - ); - } - if ( mode === 'update' && networkClientId !== undefined && @@ -2700,22 +2675,6 @@ export class NetworkController extends BaseController< ); } - const soleInfuraRpcEndpoint = infuraRpcEndpoints[0]; - if (soleInfuraRpcEndpoint) { - const infuraNetworkName = deriveInfuraNetworkNameFromRpcEndpointUrl( - soleInfuraRpcEndpoint.url, - ); - const infuraNetworkNickname = NetworkNickname[infuraNetworkName]; - const infuraChainId = ChainId[infuraNetworkName]; - if (networkFields.chainId !== infuraChainId) { - throw new Error( - mode === 'add' - ? `Could not add network with chain ID ${networkFields.chainId} and Infura RPC endpoint for '${infuraNetworkNickname}' which represents ${infuraChainId}, as the two conflict` - : `Could not update network with chain ID ${networkFields.chainId} and Infura RPC endpoint for '${infuraNetworkNickname}' which represents ${infuraChainId}, as the two conflict`, - ); - } - } - if ( networkFields.rpcEndpoints[networkFields.defaultRpcEndpointIndex] === undefined @@ -3097,6 +3056,8 @@ export class NetworkController extends BaseController< updateState?: (state: Draft) => void; } = {}, ): void { + const networkClient = this.getNetworkClientById(networkClientId); + const autoManagedNetworkClientRegistry = this.#ensureAutoManagedNetworkClientRegistryPopulated(); @@ -3104,7 +3065,7 @@ export class NetworkController extends BaseController< | AutoManagedNetworkClient | AutoManagedNetworkClient; - if (isInfuraNetworkType(networkClientId)) { + if (networkClient.configuration.type === NetworkClientType.Infura) { const possibleAutoManagedNetworkClient = autoManagedNetworkClientRegistry[NetworkClientType.Infura][ networkClientId diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index f79ef7b2ca..7a2b3ed839 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -200,7 +200,7 @@ export function createNetworkClient({ configuration.type === NetworkClientType.Infura ? createInfuraNetworkMiddleware({ blockTracker, - network: configuration.network, + chainId: configuration.chainId, rpcProvider, rpcApiMiddleware, }) @@ -556,19 +556,19 @@ function createBlockTracker({ * * @param args - The arguments. * @param args.blockTracker - The block tracker to use. - * @param args.network - The Infura network to use. + * @param args.chainId - The chain id to use. * @param args.rpcProvider - The RPC provider to use. * @param args.rpcApiMiddleware - Additional middleware. * @returns The collection of middleware that makes up the Infura client. */ function createInfuraNetworkMiddleware({ blockTracker, - network, + chainId, rpcProvider, rpcApiMiddleware, }: { blockTracker: PollingBlockTracker; - network: InfuraNetworkType; + chainId: string; rpcProvider: InternalProvider; rpcApiMiddleware: RpcApiMiddleware; }): JsonRpcMiddleware< @@ -578,7 +578,7 @@ function createInfuraNetworkMiddleware({ > { return JsonRpcEngineV2.create({ middleware: [ - createNetworkAndChainIdMiddleware({ network }), + createNetworkAndChainIdMiddleware({ chainId }), createBlockCacheMiddleware({ blockTracker }), createInflightCacheMiddleware(), createBlockRefMiddleware({ blockTracker, provider: rpcProvider }), @@ -593,16 +593,16 @@ function createInfuraNetworkMiddleware({ * Creates static method middleware. * * @param args - The Arguments. - * @param args.network - The Infura network to use. + * @param args.chainId - The chain id to use. * @returns The middleware that implements the eth_chainId method. */ function createNetworkAndChainIdMiddleware({ - network, + chainId, }: { - network: InfuraNetworkType; + chainId: string; }): JsonRpcMiddleware { return createScaffoldMiddleware({ - eth_chainId: ChainId[network], + eth_chainId: chainId, }); } diff --git a/packages/network-controller/src/types.ts b/packages/network-controller/src/types.ts index 90b1655d3b..92ed2126ac 100644 --- a/packages/network-controller/src/types.ts +++ b/packages/network-controller/src/types.ts @@ -1,4 +1,3 @@ -import type { InfuraNetworkType } from '@metamask/controller-utils'; import type { BlockTracker as BaseBlockTracker } from '@metamask/eth-block-tracker'; import type { InternalProvider } from '@metamask/eth-json-rpc-provider'; import type { MiddlewareContext } from '@metamask/json-rpc-engine/v2'; @@ -47,7 +46,7 @@ export type CustomNetworkClientConfiguration = */ export type InfuraNetworkClientConfiguration = CommonNetworkClientConfiguration & { - network: InfuraNetworkType; + network: string; infuraProjectId: string; type: NetworkClientType.Infura; }; diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 35fa8fe8b0..8f29604edc 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1352,7 +1352,7 @@ describe('NetworkController', () => { expect(() => controller.getNetworkClientById(NetworkType.mainnet), ).toThrow( - 'No Infura network client was found with the ID "mainnet".', + 'No network client was found with ID "mainnet".', ); }, ); @@ -1415,7 +1415,7 @@ describe('NetworkController', () => { }, async ({ controller }) => { expect(() => controller.getNetworkClientById('0x1337')).toThrow( - 'No custom network client was found with the ID "0x1337".', + 'No network client was found with ID "0x1337".', ); }, ); @@ -1898,7 +1898,7 @@ describe('NetworkController', () => { await expect(() => controller.lookupNetwork('non-existent-network-id'), ).rejects.toThrow( - 'No custom network client was found with the ID "non-existent-network-id".', + 'No network client was found with ID "non-existent-network-id".', ); }); }); @@ -3056,7 +3056,7 @@ describe('NetworkController', () => { controller.setActiveNetwork('invalid-network-client-id'), ).rejects.toThrow( new Error( - "No network client found with ID 'invalid-network-client-id'", + 'No network client was found with ID "invalid-network-client-id".', ), ); }); @@ -4785,52 +4785,6 @@ describe('NetworkController', () => { } describe('given the ID of a non-Infura-supported chain', () => { - it('throws (albeit for a different reason) if rpcEndpoints contains an Infura RPC endpoint that represents a different chain that the one being added', async () => { - uuidV4Mock - .mockReturnValueOnce('AAAA-AAAA-AAAA-AAAA') - .mockReturnValueOnce('BBBB-BBBB-BBBB-BBBB'); - const defaultRpcEndpoint = buildInfuraRpcEndpoint( - InfuraNetworkType.mainnet, - ); - - await withController( - { - state: { - selectedNetworkClientId: TESTNET.networkType, - networkConfigurationsByChainId: { - [TESTNET.chainId]: buildInfuraNetworkConfiguration( - TESTNET.networkType, - ), - }, - }, - }, - ({ controller }) => { - expect(() => - controller.addNetwork({ - blockExplorerUrls: [], - chainId: '0x1337', - defaultRpcEndpointIndex: 0, - name: 'Some Network', - nativeCurrency: 'TOKEN', - rpcEndpoints: [ - defaultRpcEndpoint, - { - failoverUrls: [], - name: 'Test Network 2', - type: RpcEndpointType.Custom, - url: 'https://test.endpoint/2', - }, - ], - }), - ).toThrow( - new Error( - "Could not add network with chain ID 0x1337 and Infura RPC endpoint for 'Ethereum' which represents 0x1, as the two conflict", - ), - ); - }, - ); - }); - it('creates a new network client for each given RPC endpoint', async () => { uuidV4Mock .mockReturnValueOnce('AAAA-AAAA-AAAA-AAAA') @@ -5264,39 +5218,6 @@ describe('NetworkController', () => { ); }); - it('throws if one of the new rpcEndpoints is custom and uses an Infura network name for networkClientId', async () => { - const networkConfigurationToUpdate = buildCustomNetworkConfiguration({ - chainId: '0x1337', - }); - - await withController( - { - state: buildNetworkControllerStateWithDefaultSelectedNetworkClientId({ - networkConfigurationsByChainId: { - '0x1337': networkConfigurationToUpdate, - }, - }), - }, - async ({ controller }) => { - await expect( - controller.updateNetwork('0x1337', { - ...networkConfigurationToUpdate, - rpcEndpoints: [ - buildUpdateNetworkCustomRpcEndpointFields({ - networkClientId: InfuraNetworkType.mainnet, - url: 'https://test.network', - }), - ], - }), - ).rejects.toThrow( - new Error( - "Could not update network: Custom RPC endpoint 'https://test.network' has invalid network client ID 'mainnet'", - ), - ); - }, - ); - }); - it('throws if one of the new rpcEndpoints has an invalid url property', async () => { const networkConfigurationToUpdate = buildCustomNetworkConfiguration({ chainId: '0x1337', @@ -10641,44 +10562,6 @@ describe('NetworkController', () => { ); }); - it('throws if the existing Infura RPC endpoint is not removed in the process of changing the chain ID', async () => { - const networkConfigurationToUpdate = - buildInfuraNetworkConfiguration(infuraNetworkType); - - 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', - }, - }, - async ({ controller }) => { - await expect( - controller.updateNetwork(infuraChainId, { - ...networkConfigurationToUpdate, - chainId: '0x1337', - }), - ).rejects.toThrow( - new Error( - `Could not update network with chain ID 0x1337 and Infura RPC endpoint for '${infuraNetworkNickname}' which represents ${infuraChainId}, as the two conflict`, - ), - ); - }, - ); - }); - it('re-files the existing network configuration from under the old chain ID to under the new one, regenerating network client IDs for each custom RPC endpoint', async () => { uuidV4Mock .mockReturnValueOnce('CCCC-CCCC-CCCC-CCCC') @@ -11373,44 +11256,6 @@ describe('NetworkController', () => { ); }); - it('throws if the existing Infura RPC endpoint is not updated in the process of changing the chain ID', async () => { - const networkConfigurationToUpdate = - buildInfuraNetworkConfiguration(infuraNetworkType); - - 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', - }, - }, - async ({ controller }) => { - await expect( - controller.updateNetwork(infuraChainId, { - ...networkConfigurationToUpdate, - chainId: anotherInfuraChainId, - }), - ).rejects.toThrow( - new Error( - `Could not update network with chain ID ${anotherInfuraChainId} and Infura RPC endpoint for '${infuraNetworkNickname}' which represents ${infuraChainId}, as the two conflict`, - ), - ); - }, - ); - }); - it('re-files the existing network configuration from under the old chain ID to under the new one, regenerating network client IDs for each custom RPC endpoint', async () => { uuidV4Mock .mockReturnValueOnce('CCCC-CCCC-CCCC-CCCC') From 70b106ff1e3a5f1e05e3ae9121ecfc02c97617e5 Mon Sep 17 00:00:00 2001 From: mikesposito Date: Thu, 9 Jul 2026 10:03:23 +0200 Subject: [PATCH 2/3] update tests --- ...tractControllerWithNetworkClientId.test.ts | 24 +++++++++++++------ .../tests/NetworkController.test.ts | 4 +--- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/assets-controllers/src/AssetsContractControllerWithNetworkClientId.test.ts b/packages/assets-controllers/src/AssetsContractControllerWithNetworkClientId.test.ts index cd5ffe1a18..6d96391ee9 100644 --- a/packages/assets-controllers/src/AssetsContractControllerWithNetworkClientId.test.ts +++ b/packages/assets-controllers/src/AssetsContractControllerWithNetworkClientId.test.ts @@ -31,7 +31,7 @@ describe('AssetsContractController with NetworkClientId', () => { 'invalidNetworkClientId', ), ).rejects.toThrow( - `No custom network client was found with the ID "invalidNetworkClientId".`, + `No network client was found with ID "invalidNetworkClientId".`, ); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); @@ -46,7 +46,7 @@ describe('AssetsContractController with NetworkClientId', () => { 'invalidNetworkClientId', ), ).rejects.toThrow( - `No custom network client was found with the ID "invalidNetworkClientId".`, + `No network client was found with ID "invalidNetworkClientId".`, ); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); @@ -154,7 +154,9 @@ describe('AssetsContractController with NetworkClientId', () => { undefined, 'invalidNetworkClientId', ), - ).rejects.toThrow('No custom network client was found'); + ).rejects.toThrow( + 'No network client was found with ID "invalidNetworkClientId".', + ); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); @@ -582,7 +584,9 @@ describe('AssetsContractController with NetworkClientId', () => { ERC721_GODS_ADDRESS, 'invalidNetworkClientId', ), - ).rejects.toThrow('No custom network client was found'); + ).rejects.toThrow( + 'No network client was found with ID "invalidNetworkClientId".', + ); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); @@ -698,7 +702,9 @@ describe('AssetsContractController with NetworkClientId', () => { '148332', 'invalidNetworkClientId', ), - ).rejects.toThrow('No custom network client was found'); + ).rejects.toThrow( + 'No network client was found with ID "invalidNetworkClientId".', + ); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); @@ -823,7 +829,9 @@ describe('AssetsContractController with NetworkClientId', () => { '1', 'invalidNetworkClientId', ), - ).rejects.toThrow('No custom network client was found'); + ).rejects.toThrow( + 'No network client was found with ID "invalidNetworkClientId".', + ); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); @@ -873,7 +881,9 @@ describe('AssetsContractController with NetworkClientId', () => { ERC1155_ID, 'invalidNetworkClientId', ), - ).rejects.toThrow('No custom network client was found'); + ).rejects.toThrow( + 'No network client was found with ID "invalidNetworkClientId".', + ); messenger.clearEventSubscriptions('NetworkController:networkDidChange'); }); diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 8f29604edc..82a54df1c6 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -1351,9 +1351,7 @@ describe('NetworkController', () => { async ({ controller }) => { expect(() => controller.getNetworkClientById(NetworkType.mainnet), - ).toThrow( - 'No network client was found with ID "mainnet".', - ); + ).toThrow('No network client was found with ID "mainnet".'); }, ); }); From 0e180da1e28cf91b13b7679368465de5882665f6 Mon Sep 17 00:00:00 2001 From: mikesposito Date: Thu, 9 Jul 2026 10:17:36 +0200 Subject: [PATCH 3/3] fix lint --- packages/network-controller/src/NetworkController.ts | 6 +----- packages/network-controller/src/create-network-client.ts | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 544ded37f3..2e3a2549b3 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -1404,11 +1404,7 @@ export class NetworkController extends BaseController< 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 - ]; + const networkClient = networkClientsById[networkClientId]; if ( networkClient.configuration.failoverRpcUrls && networkClient.configuration.failoverRpcUrls.length > 0 diff --git a/packages/network-controller/src/create-network-client.ts b/packages/network-controller/src/create-network-client.ts index 7a2b3ed839..1ce4021354 100644 --- a/packages/network-controller/src/create-network-client.ts +++ b/packages/network-controller/src/create-network-client.ts @@ -1,10 +1,6 @@ import { CONNECTIVITY_STATUSES } from '@metamask/connectivity-controller'; -import type { - CockatielFailureReason, - InfuraNetworkType, -} from '@metamask/controller-utils'; +import type { CockatielFailureReason } from '@metamask/controller-utils'; import { - ChainId, DEFAULT_MAX_CONSECUTIVE_FAILURES, DEFAULT_MAX_RETRIES, } from '@metamask/controller-utils';