diff --git a/README.md b/README.md index c1880364cce..7ea1324a138 100644 --- a/README.md +++ b/README.md @@ -487,6 +487,7 @@ linkStyle default opacity:0.5 network_connection_banner_controller --> network_enablement_controller; network_controller --> analytics_controller; network_controller --> base_controller; + network_controller --> config_registry_controller; network_controller --> connectivity_controller; network_controller --> controller_utils; network_controller --> eth_block_tracker; diff --git a/package.json b/package.json index 78172396b4f..2a58eb3ac5b 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,9 @@ "eslint-plugin-import-x>unrs-resolver": false, "simple-git-hooks": false, "tsx>esbuild": false, - "jest>@jest/core>jest-resolve>unrs-resolver": false + "jest>@jest/core>jest-resolve>unrs-resolver": false, + "@metamask/network-controller>@metamask/config-registry-controller>@metamask/keyring-controller>ethereumjs-wallet>ethereum-cryptography>keccak": false, + "@metamask/network-controller>@metamask/config-registry-controller>@metamask/keyring-controller>ethereumjs-wallet>ethereum-cryptography>secp256k1": false } } } diff --git a/packages/assets-controllers/src/AssetsContractController.test.ts b/packages/assets-controllers/src/AssetsContractController.test.ts index 8033c412094..e218c1157ac 100644 --- a/packages/assets-controllers/src/AssetsContractController.test.ts +++ b/packages/assets-controllers/src/AssetsContractController.test.ts @@ -111,6 +111,15 @@ async function setupAssetContractControllers({ }), ); + messenger.registerActionHandler('ConfigRegistryController:getState', () => ({ + configs: { + networks: {}, + }, + lastFetched: 0, + etag: '', + version: '1', + })); + const networkControllerMessenger: NetworkControllerMessenger = new Messenger({ namespace: 'NetworkController', parent: messenger, @@ -119,6 +128,8 @@ async function setupAssetContractControllers({ messenger.delegate({ messenger: networkControllerMessenger, actions: [ + 'ConfigRegistryController:getNetworkConfigByCaip2ChainId', + 'ConfigRegistryController:getState', 'ConnectivityController:getState', 'RemoteFeatureFlagController:getState', ], diff --git a/packages/config-registry-controller/CHANGELOG.md b/packages/config-registry-controller/CHANGELOG.md index 92b376cba49..fb4df0e5c42 100644 --- a/packages/config-registry-controller/CHANGELOG.md +++ b/packages/config-registry-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add optional `isAutoEnabled?: boolean` property to `RegistryNetworkConfig.config` ([#9879](https://github.com/MetaMask/core/pull/9879)) +- Add `selectEvmAutoEnabledNetworksChainIds` selector to retrieve the CAIP-2 chain IDs of all EVM networks that are auto-enabled ([#9879](https://github.com/MetaMask/core/pull/9879)) + ## [3.0.0] ### Added diff --git a/packages/config-registry-controller/src/config-registry-api-service/types.ts b/packages/config-registry-controller/src/config-registry-api-service/types.ts index d1604ff8554..18343ffba07 100644 --- a/packages/config-registry-controller/src/config-registry-api-service/types.ts +++ b/packages/config-registry-controller/src/config-registry-api-service/types.ts @@ -48,6 +48,7 @@ const ChainConfigSchema = type({ isFeatured: boolean(), isDeprecated: boolean(), isDeletable: boolean(), + isAutoEnabled: optional(boolean()), priority: number(), }); diff --git a/packages/config-registry-controller/src/index.ts b/packages/config-registry-controller/src/index.ts index 646387691db..4e9cd1096fa 100644 --- a/packages/config-registry-controller/src/index.ts +++ b/packages/config-registry-controller/src/index.ts @@ -3,6 +3,7 @@ export type { ConfigRegistryControllerOptions, ConfigRegistryControllerActions, ConfigRegistryControllerGetStateAction, + ConfigRegistryControllerStateChangedEvent, ConfigRegistryControllerEvents, ConfigRegistryControllerMessenger, } from './ConfigRegistryController.js'; @@ -15,7 +16,11 @@ export { ConfigRegistryController, DEFAULT_POLLING_INTERVAL, } from './ConfigRegistryController.js'; -export { selectFeaturedNetworks, selectNetworks } from './selectors.js'; +export { + selectFeaturedNetworks, + selectNetworks, + selectEvmAutoEnabledNetworksChainIds, +} from './selectors.js'; export type { FetchConfigOptions, FetchConfigResult, diff --git a/packages/config-registry-controller/src/selectors.test.ts b/packages/config-registry-controller/src/selectors.test.ts index 1120c15eb55..536d6412411 100644 --- a/packages/config-registry-controller/src/selectors.test.ts +++ b/packages/config-registry-controller/src/selectors.test.ts @@ -1,5 +1,10 @@ import { createMockNetworkConfig } from '../tests/helpers.js'; -import { selectFeaturedNetworks, selectNetworks } from './selectors.js'; +import { ConfigRegistryControllerState } from './ConfigRegistryController.js'; +import { + selectEvmAutoEnabledNetworksChainIds, + selectFeaturedNetworks, + selectNetworks, +} from './selectors.js'; describe('selectors', () => { describe('selectNetworks', () => { @@ -79,4 +84,99 @@ describe('selectors', () => { expect(Object.keys(featured)).toHaveLength(0); }); }); + + describe('selectEvmAutoEnabledNetworksChainIds', () => { + it('returns the list of CAIP-2 chain IDs for auto-enabled EVM networks', () => { + const state: ConfigRegistryControllerState = { + configs: { + networks: { + 'eip155:1': createMockNetworkConfig({ + chainId: 'eip155:1', + config: { + isAutoEnabled: true, + isActive: true, + isDeprecated: false, + }, + }), + 'eip155:3': createMockNetworkConfig({ + chainId: 'eip155:3', + config: { + isAutoEnabled: false, + isActive: true, + isDeprecated: false, + }, + }), + 'eip155:4': createMockNetworkConfig({ + chainId: 'eip155:4', + config: { + isAutoEnabled: true, + isActive: false, + isDeprecated: false, + }, + }), + 'eip155:5': createMockNetworkConfig({ + chainId: 'eip155:5', + config: { + isAutoEnabled: true, + isActive: true, + isDeprecated: true, + }, + }), + 'eip155:6': createMockNetworkConfig({ + chainId: 'eip155:6', + config: { + isAutoEnabled: true, + isActive: true, + isDeprecated: false, + }, + }), + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': createMockNetworkConfig({ + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + config: { + isAutoEnabled: true, + isActive: true, + isDeprecated: false, + }, + }), + }, + }, + version: '1.0.0', + lastFetched: Date.now(), + etag: null, + }; + + const result = selectEvmAutoEnabledNetworksChainIds(state); + expect(result).toStrictEqual(['eip155:1', 'eip155:6']); + }); + + it('returns the same array reference when the chain IDs have not changed', () => { + const networks = { + 'eip155:1': createMockNetworkConfig({ + chainId: 'eip155:1', + config: { isAutoEnabled: true, isActive: true, isDeprecated: false }, + }), + }; + const state: ConfigRegistryControllerState = { + configs: { networks }, + version: '1.0.0', + lastFetched: 1, + etag: null, + }; + + const first = selectEvmAutoEnabledNetworksChainIds(state); + // Unrelated state change, same `networks` object. + const second = selectEvmAutoEnabledNetworksChainIds({ + ...state, + lastFetched: 2, + }); + // New `networks` object with the same auto-enabled chain IDs. + const third = selectEvmAutoEnabledNetworksChainIds({ + ...state, + configs: { networks: { ...networks } }, + }); + + expect(second).toBe(first); + expect(third).toBe(first); + }); + }); }); diff --git a/packages/config-registry-controller/src/selectors.ts b/packages/config-registry-controller/src/selectors.ts index 5c8c608dd33..030fc10b875 100644 --- a/packages/config-registry-controller/src/selectors.ts +++ b/packages/config-registry-controller/src/selectors.ts @@ -1,3 +1,4 @@ +import { CaipChainId, KnownCaipNamespace } from '@metamask/utils'; import { createSelector } from 'reselect'; import { filterNetworks } from './config-registry-api-service/filters.js'; @@ -37,3 +38,32 @@ export const selectFeaturedNetworks = createSelector( return result; }, ); + +/** + * Returns the list of CAIP-2 chain IDs for networks that are auto-enabled in the + * config registry. + * + * @param state - The config registry controller state. + * @returns The list of CAIP-2 chain IDs for auto-enabled networks. + */ +export const selectEvmAutoEnabledNetworksChainIds = createSelector( + selectNetworks, + (networks): CaipChainId[] => + Object.values(networks) + .filter( + ({ chainId, config }) => + chainId.startsWith(KnownCaipNamespace.Eip155) && + config.isAutoEnabled && + config.isActive && + !config.isDeprecated, + ) + .map((config) => config.chainId), + { + // Messenger selector subscriptions only skip work when the result is + // referentially equal, so keep the previous array when the IDs are the same. + memoizeOptions: { + resultEqualityCheck: (a: CaipChainId[], b: CaipChainId[]) => + a.length === b.length && a.every((chainId, i) => chainId === b[i]), + }, + }, +); diff --git a/packages/gas-fee-controller/src/GasFeeController.test.ts b/packages/gas-fee-controller/src/GasFeeController.test.ts index 17fb08fac35..00c67aa6265 100644 --- a/packages/gas-fee-controller/src/GasFeeController.test.ts +++ b/packages/gas-fee-controller/src/GasFeeController.test.ts @@ -79,6 +79,16 @@ const getRootMessenger = (): RootMessenger => { }), ); + rootMessenger.registerActionHandler( + 'ConfigRegistryController:getState', + () => ({ + configs: { networks: {} }, + lastFetched: 0, + etag: '', + version: '1', + }), + ); + return rootMessenger; }; @@ -105,6 +115,7 @@ const setupNetworkController = async ({ rootMessenger.delegate({ messenger: networkControllerMessenger, actions: [ + 'ConfigRegistryController:getState', 'ConnectivityController:getState', 'RemoteFeatureFlagController:getState', ], diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index 6e97a2dc8d6..5824737c576 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** `NetworkControllerMessenger` now requires the `ConfigRegistryController:stateChanged` event and `ConfigRegistryController:getNetworkConfigByCaip2ChainId` action to be delegated from the root messenger ([#9879](https://github.com/MetaMask/core/pull/9879)) + - `NetworkController` now depends on the `ConfigRegistryController` to auto-register default networks from the registry. +- Update `init` to auto-register default networks from `ConfigRegistryController` ([#9879](https://github.com/MetaMask/core/pull/9879)) + ## [35.0.1] ### Changed diff --git a/packages/network-controller/jest.config.js b/packages/network-controller/jest.config.js index 245fc38807a..e20eff6e4a0 100644 --- a/packages/network-controller/jest.config.js +++ b/packages/network-controller/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 93.42, - functions: 98, - lines: 97.67, - statements: 97.56, + branches: 93.8, + functions: 98.14, + lines: 97.94, + statements: 97.83, }, }, diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index 06e583d125a..9471ef06d7f 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -57,6 +57,7 @@ "dependencies": { "@metamask/analytics-controller": "^2.0.0", "@metamask/base-controller": "^9.1.0", + "@metamask/config-registry-controller": "^3.0.0", "@metamask/connectivity-controller": "^0.3.0", "@metamask/controller-utils": "^12.3.0", "@metamask/eth-block-tracker": "^15.0.1", diff --git a/packages/network-controller/src/NetworkController.ts b/packages/network-controller/src/NetworkController.ts index 13d91c8b3f0..5f0cbc17be1 100644 --- a/packages/network-controller/src/NetworkController.ts +++ b/packages/network-controller/src/NetworkController.ts @@ -7,6 +7,12 @@ import type { ControllerStateChangeEvent, } from '@metamask/base-controller'; import { BaseController } from '@metamask/base-controller'; +import type { + ConfigRegistryControllerStateChangedEvent, + ConfigRegistryControllerGetNetworkConfigByCaip2ChainIdAction, + ConfigRegistryControllerGetStateAction, +} from '@metamask/config-registry-controller'; +import { selectEvmAutoEnabledNetworksChainIds } from '@metamask/config-registry-controller'; import type { ConnectivityControllerGetStateAction } from '@metamask/connectivity-controller'; import type { Partialize } from '@metamask/controller-utils'; import { @@ -35,8 +41,14 @@ import { createSwappableProxy, } from '@metamask/swappable-obj-proxy'; import type { SwappableProxy } from '@metamask/swappable-obj-proxy'; -import type { Hex } from '@metamask/utils'; -import { hasProperty, isPlainObject, isStrictHexString } from '@metamask/utils'; +import type { CaipChainId, Hex } from '@metamask/utils'; +import { + hasProperty, + isPlainObject, + isStrictHexString, + numberToHex, + parseCaipChainId, +} from '@metamask/utils'; import deepEqual from 'fast-deep-equal'; import type { Draft } from 'immer'; import { produce } from 'immer'; @@ -680,7 +692,9 @@ export type NetworkControllerEvents = /** * All events that {@link NetworkController} calls internally. */ -type AllowedEvents = RemoteFeatureFlagControllerStateChangeEvent; +type AllowedEvents = + | RemoteFeatureFlagControllerStateChangeEvent + | ConfigRegistryControllerStateChangedEvent; const MESSENGER_EXPOSED_METHODS = [ 'addNetwork', @@ -723,6 +737,8 @@ export type NetworkControllerActions = * All actions that {@link NetworkController} calls internally. */ type AllowedActions = + | ConfigRegistryControllerGetNetworkConfigByCaip2ChainIdAction + | ConfigRegistryControllerGetStateAction | ConnectivityControllerGetStateAction | RemoteFeatureFlagControllerGetStateAction | AnalyticsControllerGetStateAction @@ -1418,6 +1434,12 @@ export class NetworkController extends BaseController< }, getRpcFailoverMode, ); + + this.messenger.subscribe( + 'ConfigRegistryController:stateChanged', + (caipChainIds) => this.#autoAddNetworksFromConfigRegistry(caipChainIds), + selectEvmAutoEnabledNetworksChainIds, + ); } /** @@ -1617,15 +1639,22 @@ export class NetworkController extends BaseController< } /** - * Initialize the NetworkController, applying the RPC failover mode from the - * `corePlatformRpcFailoverMode` remote feature flag and applying the network - * selection. + * Initialize the NetworkController: + * - Apply the RPC failover mode from the `corePlatformRpcFailoverMode` remote feature flag; + * - Apply the network selection. + * - Auto-add networks for any chains that are configured to be auto-enabled in ConfigRegistryController. */ init(): void { const state = this.messenger.call('RemoteFeatureFlagController:getState'); this.#updateRpcFailover(getRpcFailoverMode(state)); this.#applyNetworkSelection(this.state.selectedNetworkClientId); + + this.#autoAddNetworksFromConfigRegistry( + selectEvmAutoEnabledNetworksChainIds( + this.messenger.call('ConfigRegistryController:getState'), + ), + ); } /** @@ -3133,4 +3162,65 @@ export class NetworkController extends BaseController< this.#ethQuery = new EthQuery(this.#providerProxy); } + + /** + * Adds networks to state and registers network clients for the given CAIP-2 + * chain IDs if they are not already present. Configurations for these + * networks are retrieved from ConfigRegistryController. + * + * @param caipChainIds - The CAIP-2 chain IDs of the networks to enable. + */ + #autoAddNetworksFromConfigRegistry(caipChainIds: CaipChainId[]): void { + for (const caipChainId of caipChainIds) { + try { + const registryNetworkConfig = this.messenger.call( + 'ConfigRegistryController:getNetworkConfigByCaip2ChainId', + caipChainId, + ); + const hexChainId = numberToHex( + Number(parseCaipChainId(caipChainId).reference), + ); + + if ( + !registryNetworkConfig || + this.state.networkConfigurationsByChainId[hexChainId] + ) { + continue; + } + + const rpcEndpoint: + | InfuraRpcEndpoint + | AddNetworkCustomRpcEndpointFields = + registryNetworkConfig.rpcProviders.default.type === 'infura' + ? { + type: RpcEndpointType.Infura, + networkClientId: + registryNetworkConfig.rpcProviders.default.networkClientId, + url: registryNetworkConfig.rpcProviders.default + .url as InfuraRpcEndpoint['url'], + } + : { + type: RpcEndpointType.Custom, + url: registryNetworkConfig.rpcProviders.default.url, + }; + + this.addNetwork({ + chainId: hexChainId, + name: registryNetworkConfig.name, + nativeCurrency: registryNetworkConfig.assets.native.symbol, + rpcEndpoints: [rpcEndpoint], + defaultRpcEndpointIndex: 0, + blockExplorerUrls: [registryNetworkConfig.blockExplorerUrls.default], + defaultBlockExplorerUrlIndex: 0, + }); + } catch (error) { + const capturedError = + error instanceof Error ? error : new Error(String(error)); + this.#log?.error( + `Failed to auto-enable network for chain ID ${caipChainId}: ${capturedError}`, + ); + this.messenger.captureException?.(capturedError); + } + } + } } diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 34b0ae744dd..5ef7bc6b351 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -57,6 +57,7 @@ import { buildInfuraNetworkClientConfiguration, buildInfuraNetworkConfiguration, buildInfuraRpcEndpoint, + buildMockConfigRegistryControllerNetwork, buildNetworkConfiguration, buildNetworkControllerMessenger, buildRootMessenger, @@ -996,6 +997,242 @@ describe('NetworkController', () => { }); }); + describe('init', () => { + it('auto-enables networks that are set as auto-enabled in the config registry', async () => { + const networkConfig = buildMockConfigRegistryControllerNetwork({ + chainId: 'eip155:9999', + config: { + ...buildMockConfigRegistryControllerNetwork().config, + isAutoEnabled: true, + }, + }); + await withController( + { + initializeController: false, + configRegistryNetworkConfigs: [networkConfig], + }, + ({ controller }) => { + expect( + controller.state.networkConfigurationsByChainId, + ).not.toHaveProperty('0x270f'); + + controller.init(); + + expect( + controller.state.networkConfigurationsByChainId, + ).toHaveProperty( + '0x270f', + expect.objectContaining({ + chainId: '0x270f', + name: networkConfig.name, + nativeCurrency: networkConfig.assets.native.symbol, + blockExplorerUrls: [networkConfig.blockExplorerUrls.default], + defaultBlockExplorerUrlIndex: 0, + rpcEndpoints: [ + expect.objectContaining({ + networkClientId: + networkConfig.rpcProviders.default.networkClientId, + url: networkConfig.rpcProviders.default.url, + type: networkConfig.rpcProviders.default.type, + }), + ], + defaultRpcEndpointIndex: 0, + }), + ); + }, + ); + }); + }); + + describe('ConfigRegistryController:stateChanged', () => { + it('enables Infura chains that are set as auto-enabled in the config registry', async () => { + const networkConfig = buildMockConfigRegistryControllerNetwork({ + chainId: 'eip155:9999', + config: { + ...buildMockConfigRegistryControllerNetwork().config, + isAutoEnabled: true, + }, + }); + await withController( + { configRegistryNetworkConfigs: [networkConfig] }, + async ({ controller, messenger }) => { + messenger.publish( + 'ConfigRegistryController:stateChanged', + { + configs: { networks: { 'eip155:9999': networkConfig } }, + lastFetched: 0, + etag: 'etag', + version: '1', + }, + [], + ); + + expect( + controller.state.networkConfigurationsByChainId, + ).toHaveProperty( + '0x270f', + expect.objectContaining({ + chainId: '0x270f', + name: networkConfig.name, + nativeCurrency: networkConfig.assets.native.symbol, + blockExplorerUrls: [networkConfig.blockExplorerUrls.default], + defaultBlockExplorerUrlIndex: 0, + rpcEndpoints: [ + expect.objectContaining({ + networkClientId: + networkConfig.rpcProviders.default.networkClientId, + url: networkConfig.rpcProviders.default.url, + type: networkConfig.rpcProviders.default.type, + }), + ], + defaultRpcEndpointIndex: 0, + }), + ); + }, + ); + }); + + it('enables custom networks that are set as auto-enabled in the config registry', async () => { + const networkConfig = buildMockConfigRegistryControllerNetwork({ + chainId: 'eip155:9999', + rpcProviders: { + default: buildCustomRpcEndpoint({ + url: 'https://test.network/1', + type: RpcEndpointType.Custom, + }), + fallbacks: [], + }, + config: { + ...buildMockConfigRegistryControllerNetwork().config, + isAutoEnabled: true, + }, + }); + await withController( + { configRegistryNetworkConfigs: [networkConfig] }, + async ({ controller, messenger }) => { + messenger.publish( + 'ConfigRegistryController:stateChanged', + { + configs: { networks: { 'eip155:9999': networkConfig } }, + lastFetched: 0, + etag: 'etag', + version: '1', + }, + [], + ); + + expect( + controller.state.networkConfigurationsByChainId, + ).toHaveProperty( + '0x270f', + expect.objectContaining({ + chainId: '0x270f', + name: networkConfig.name, + nativeCurrency: networkConfig.assets.native.symbol, + blockExplorerUrls: [networkConfig.blockExplorerUrls.default], + defaultBlockExplorerUrlIndex: 0, + rpcEndpoints: [ + expect.objectContaining({ + url: networkConfig.rpcProviders.default.url, + type: networkConfig.rpcProviders.default.type, + }), + ], + defaultRpcEndpointIndex: 0, + }), + ); + }, + ); + }); + + it('gracefully handles errors thrown when enabling networks from the config registry', async () => { + const networkConfigs = [ + buildMockConfigRegistryControllerNetwork({ + chainId: 'eip155:9997', + rpcProviders: { + default: buildCustomRpcEndpoint({ + url: 'https://test.network/2', + type: RpcEndpointType.Custom, + }), + fallbacks: [], + }, + config: { + ...buildMockConfigRegistryControllerNetwork().config, + isAutoEnabled: true, + }, + }), + buildMockConfigRegistryControllerNetwork({ + chainId: 'eip155:9998', + rpcProviders: { + default: buildCustomRpcEndpoint({ + url: 'https://test.network/2', + type: RpcEndpointType.Custom, + }), + fallbacks: [], + }, + config: { + ...buildMockConfigRegistryControllerNetwork().config, + isAutoEnabled: true, + }, + }), + buildMockConfigRegistryControllerNetwork({ + chainId: 'eip155:9999', + rpcProviders: { + default: buildCustomRpcEndpoint({ + url: 'https://test.network/1', + type: RpcEndpointType.Custom, + }), + fallbacks: [], + }, + config: { + ...buildMockConfigRegistryControllerNetwork().config, + isAutoEnabled: true, + }, + }), + ]; + await withController( + { configRegistryNetworkConfigs: networkConfigs }, + async ({ controller, messenger, networkControllerMessenger }) => { + messenger.publish( + 'ConfigRegistryController:stateChanged', + { + configs: { networks: { 'eip155:9999': networkConfigs[1] } }, + lastFetched: 0, + etag: 'etag', + version: '1', + }, + [], + ); + + expect( + networkControllerMessenger.captureException, + ).toHaveBeenCalled(); + expect( + controller.state.networkConfigurationsByChainId, + ).not.toHaveProperty('0x270e'); + expect( + controller.state.networkConfigurationsByChainId, + ).toHaveProperty( + '0x270f', + expect.objectContaining({ + chainId: '0x270f', + name: networkConfigs[2].name, + nativeCurrency: networkConfigs[2].assets.native.symbol, + blockExplorerUrls: [networkConfigs[2].blockExplorerUrls.default], + defaultBlockExplorerUrlIndex: 0, + rpcEndpoints: [ + expect.objectContaining({ + url: networkConfigs[2].rpcProviders.default.url, + type: networkConfigs[2].rpcProviders.default.type, + }), + ], + defaultRpcEndpointIndex: 0, + }), + ); + }, + ); + }); + }); + describe('destroy', () => { it('does not throw if called before the provider is initialized', async () => { await withController(async ({ controller }) => { diff --git a/packages/network-controller/tests/helpers.ts b/packages/network-controller/tests/helpers.ts index c522757f098..5717a8f9d2e 100644 --- a/packages/network-controller/tests/helpers.ts +++ b/packages/network-controller/tests/helpers.ts @@ -1,4 +1,5 @@ import { getDefaultAnalyticsControllerState } from '@metamask/analytics-controller'; +import { RegistryNetworkConfig } from '@metamask/config-registry-controller'; import { CONNECTIVITY_STATUSES } from '@metamask/connectivity-controller'; import type { ConnectivityStatus } from '@metamask/connectivity-controller'; import { @@ -15,7 +16,7 @@ import type { MessengerActions, MessengerEvents, } from '@metamask/messenger'; -import type { Hex } from '@metamask/utils'; +import type { CaipChainId, Hex } from '@metamask/utils'; import { v4 as uuidV4 } from 'uuid'; import { FakeBlockTracker } from '../../../tests/fake-block-tracker.js'; @@ -97,6 +98,9 @@ export const TESTNET = { * @param options.trackEvent - The handler registered for * `AnalyticsController:trackEvent`. Defaults to a Jest mock so tests can assert * on it. + * @param options.configRegistryNetworkConfigs - The network config that + * `ConfigRegistryController:getNetworkConfigByCaip2ChainId` returns by default. Defaults to + * a mock network config for the chain ID `eip155:9999`. * @returns The messenger. */ export function buildRootMessenger({ @@ -104,11 +108,13 @@ export function buildRootMessenger({ rpcFailoverMode = 'disabled', analyticsId = '11111111-1111-4111-8111-111111111111', trackEvent = jest.fn(), + configRegistryNetworkConfigs = [buildMockConfigRegistryControllerNetwork()], }: { connectivityStatus?: ConnectivityStatus; rpcFailoverMode?: RpcFailoverMode; analyticsId?: string; trackEvent?: jest.Mock; + configRegistryNetworkConfigs?: RegistryNetworkConfig[]; } = {}): RootMessenger { const rootMessenger = new Messenger< MockAnyNamespace, @@ -143,6 +149,31 @@ export function buildRootMessenger({ trackEvent, ); + rootMessenger.registerActionHandler( + 'ConfigRegistryController:getNetworkConfigByCaip2ChainId', + (caipChainId) => + configRegistryNetworkConfigs.find( + (config) => config.chainId === caipChainId, + ), + ); + + rootMessenger.registerActionHandler( + 'ConfigRegistryController:getState', + () => ({ + configs: { + networks: configRegistryNetworkConfigs.reduce< + Record + >((acc, config) => { + acc[config.chainId] = config; + return acc; + }, {}), + }, + version: '0', + lastFetched: 0, + etag: '', + }), + ); + return rootMessenger; } @@ -168,13 +199,18 @@ export function buildNetworkControllerMessenger( rootMessenger.delegate({ messenger: networkControllerMessenger, actions: [ + 'ConfigRegistryController:getNetworkConfigByCaip2ChainId', + 'ConfigRegistryController:getState', 'ConnectivityController:getState', 'RemoteFeatureFlagController:getState', 'AnalyticsController:getState', 'AnalyticsController:trackEvent', ], - // eslint-disable-next-line no-restricted-syntax - events: ['RemoteFeatureFlagController:stateChange'], + events: [ + // eslint-disable-next-line no-restricted-syntax + 'RemoteFeatureFlagController:stateChange', + 'ConfigRegistryController:stateChanged', + ], }); return networkControllerMessenger; @@ -645,6 +681,55 @@ function generateCustomRpcEndpointUrl(): string { return url; } +/** + * Builds a mock RegistryNetworkConfig object for use in testing, providing defaults + * and allowing properties to be overridden at will. + * + * @param override - The properties to override the new RegistryNetworkConfig with. + * @returns The complete RegistryNetworkConfig object. + */ +export function buildMockConfigRegistryControllerNetwork( + override: Partial = {}, +): RegistryNetworkConfig { + return { + chainId: 'eip155:9999', + imageUrl: 'https://example.com/network-logo.png', + coingeckoPlatformId: 'ethereum', + name: 'Ethereum Mainnet', + assets: { + native: { + assetId: 'eip155:1/slip44:60', + imageUrl: 'https://example.com/eth-logo.png', + name: 'Ether', + symbol: 'ETH', + decimals: 18, + }, + }, + rpcProviders: { + default: { + type: RpcEndpointType.Infura, + url: 'https://my-network.infura.io/v3/{infuraProjectId}', + networkClientId: 'my-network', + }, + fallbacks: [], + }, + blockExplorerUrls: { + default: 'https://etherscan.io', + fallbacks: [], + }, + config: { + isActive: true, + isTestnet: false, + isDefault: false, + isDeprecated: false, + isDeletable: true, + isFeatured: false, + priority: 0, + }, + ...override, + }; +} + type WithControllerCallback = ({ controller, }: { @@ -655,6 +740,7 @@ type WithControllerCallback = ({ type WithControllerOptions = Partial & { rpcFailoverMode?: RpcFailoverMode; + configRegistryNetworkConfigs?: RegistryNetworkConfig[]; initializeController?: boolean; }; @@ -679,10 +765,12 @@ export async function withController( const { rpcFailoverMode, initializeController = true, + configRegistryNetworkConfigs = [buildMockConfigRegistryControllerNetwork()], ...controllerOptions } = rest; const messenger = buildRootMessenger({ rpcFailoverMode, + configRegistryNetworkConfigs, }); const networkControllerMessenger = buildNetworkControllerMessenger(messenger); const controller = new NetworkController({ diff --git a/packages/network-controller/tsconfig.build.json b/packages/network-controller/tsconfig.build.json index eaab8d986d3..0ce20a4381b 100644 --- a/packages/network-controller/tsconfig.build.json +++ b/packages/network-controller/tsconfig.build.json @@ -8,6 +8,7 @@ "references": [ { "path": "../analytics-controller/tsconfig.build.json" }, { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../config-registry-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, { "path": "../connectivity-controller/tsconfig.build.json" }, { "path": "../eth-block-tracker/tsconfig.build.json" }, diff --git a/packages/network-controller/tsconfig.json b/packages/network-controller/tsconfig.json index 292dec5bbc8..543c1616011 100644 --- a/packages/network-controller/tsconfig.json +++ b/packages/network-controller/tsconfig.json @@ -7,6 +7,7 @@ "references": [ { "path": "../analytics-controller" }, { "path": "../base-controller" }, + { "path": "../config-registry-controller" }, { "path": "../controller-utils" }, { "path": "../connectivity-controller" }, { "path": "../eth-block-tracker" }, diff --git a/packages/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js index 0a7110162b2..636e85e9785 100644 --- a/packages/transaction-controller/jest.config.js +++ b/packages/transaction-controller/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 93.49, - functions: 93.77, - lines: 97.15, - statements: 97.12, + branches: 94.04, + functions: 93.9, + lines: 97.29, + statements: 97.26, }, }, diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index c815c116a6e..d4bb64265c2 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -27,8 +27,7 @@ import { } from '@metamask/network-controller'; import type { NetworkClientConfiguration, - NetworkControllerActions, - NetworkControllerEvents, + NetworkControllerMessenger, NetworkControllerOptions, } from '@metamask/network-controller'; import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; @@ -79,7 +78,7 @@ type AllTransactionControllerEvents = type AllActions = | AllTransactionControllerActions - | NetworkControllerActions + | MessengerActions | ConnectivityControllerGetStateAction | ApprovalControllerActions | AccountsControllerActions @@ -89,7 +88,7 @@ type AllActions = type AllEvents = | AllTransactionControllerEvents - | NetworkControllerEvents + | MessengerEvents | ApprovalControllerEvents; type RootMessenger = Messenger; @@ -210,10 +209,8 @@ const setupController = async ( const networkControllerMessenger = new Messenger< 'NetworkController', - | NetworkControllerActions - | ConnectivityControllerGetStateAction - | RemoteFeatureFlagControllerGetStateAction, - NetworkControllerEvents, + MessengerActions, + MessengerEvents, typeof rootMessenger >({ namespace: 'NetworkController', @@ -222,10 +219,20 @@ const setupController = async ( rootMessenger.delegate({ messenger: networkControllerMessenger, actions: [ + 'ConfigRegistryController:getState', 'ConnectivityController:getState', 'RemoteFeatureFlagController:getState', ], }); + rootMessenger.registerActionHandler( + 'ConfigRegistryController:getState', + () => ({ + configs: { networks: {} }, + version: '1', + etag: '', + lastFetched: 0, + }), + ); const networkController = new NetworkController({ messenger: networkControllerMessenger, infuraProjectId, diff --git a/packages/wallet/src/initialization/instances/network-controller/network-controller.ts b/packages/wallet/src/initialization/instances/network-controller/network-controller.ts index db61fabb905..5557dace48f 100644 --- a/packages/wallet/src/initialization/instances/network-controller/network-controller.ts +++ b/packages/wallet/src/initialization/instances/network-controller/network-controller.ts @@ -31,11 +31,14 @@ export const networkController: InitializationConfiguration< actions: [ 'AnalyticsController:getState', 'AnalyticsController:trackEvent', + 'ConfigRegistryController:getState', + 'ConfigRegistryController:getNetworkConfigByCaip2ChainId', 'ConnectivityController:getState', 'RemoteFeatureFlagController:getState', ], events: [ + 'ConfigRegistryController:stateChanged', // eslint-disable-next-line no-restricted-syntax 'RemoteFeatureFlagController:stateChange', ], diff --git a/yarn.lock b/yarn.lock index 6e2fb03f824..369b4c66f3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8159,6 +8159,7 @@ __metadata: "@metamask/analytics-controller": "npm:^2.0.0" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" + "@metamask/config-registry-controller": "npm:^3.0.0" "@metamask/connectivity-controller": "npm:^0.3.0" "@metamask/controller-utils": "npm:^12.3.0" "@metamask/eth-block-tracker": "npm:^15.0.1"