diff --git a/README.md b/README.md index e03df83f857..ef8ba1ea4c1 100644 --- a/README.md +++ b/README.md @@ -488,6 +488,7 @@ linkStyle default opacity:0.5 network_controller --> messenger; network_controller --> remote_feature_flag_controller; network_enablement_controller --> base_controller; + network_enablement_controller --> config_registry_controller; network_enablement_controller --> controller_utils; network_enablement_controller --> messenger; network_enablement_controller --> multichain_network_controller; diff --git a/packages/network-enablement-controller/CHANGELOG.md b/packages/network-enablement-controller/CHANGELOG.md index aa616603fac..d72bd1b99a2 100644 --- a/packages/network-enablement-controller/CHANGELOG.md +++ b/packages/network-enablement-controller/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING:** Popular-network classification is now augmented by `ConfigRegistryController` ([#9611](https://github.com/MetaMask/core/pull/9611)) + - `NetworkEnablementControllerMessenger` now requires the `ConfigRegistryController:getState` action to be available. - Bump `@metamask/transaction-controller` from `^69.0.0` to `^69.2.1` ([#9568](https://github.com/MetaMask/core/pull/9568), [#9589](https://github.com/MetaMask/core/pull/9589), [#9593](https://github.com/MetaMask/core/pull/9593)) ## [5.6.0] diff --git a/packages/network-enablement-controller/jest.config.js b/packages/network-enablement-controller/jest.config.js index dca7067e851..8df1ea4fd9d 100644 --- a/packages/network-enablement-controller/jest.config.js +++ b/packages/network-enablement-controller/jest.config.js @@ -17,7 +17,7 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 94.8, + branches: 95.18, functions: 100, lines: 100, statements: 100, diff --git a/packages/network-enablement-controller/package.json b/packages/network-enablement-controller/package.json index 91148d965e3..5ae393f2e25 100644 --- a/packages/network-enablement-controller/package.json +++ b/packages/network-enablement-controller/package.json @@ -56,6 +56,7 @@ }, "dependencies": { "@metamask/base-controller": "^9.1.0", + "@metamask/config-registry-controller": "^0.4.1", "@metamask/controller-utils": "^12.3.0", "@metamask/keyring-api": "^23.5.0", "@metamask/messenger": "^2.0.0", diff --git a/packages/network-enablement-controller/src/NetworkEnablementController-method-action-types.ts b/packages/network-enablement-controller/src/NetworkEnablementController-method-action-types.ts index 4301ca30cad..165113b5d2f 100644 --- a/packages/network-enablement-controller/src/NetworkEnablementController-method-action-types.ts +++ b/packages/network-enablement-controller/src/NetworkEnablementController-method-action-types.ts @@ -140,8 +140,8 @@ export type NetworkEnablementControllerIsNetworkEnabledAction = { /** * Returns popular EVM network chain IDs in hex form, restricted to networks - * that exist in NetworkController (networkConfigurationsByChainId). Source list - * is POPULAR_NETWORKS. + * that exist in NetworkController (networkConfigurationsByChainId). Source is + * the bundled `POPULAR_NETWORKS` unioned with registry-featured EVM chains. * * @returns Hex chain IDs for popular EVM networks that are configured. */ diff --git a/packages/network-enablement-controller/src/NetworkEnablementController.test.ts b/packages/network-enablement-controller/src/NetworkEnablementController.test.ts index 23731730bbb..4fbbd7e23dd 100644 --- a/packages/network-enablement-controller/src/NetworkEnablementController.test.ts +++ b/packages/network-enablement-controller/src/NetworkEnablementController.test.ts @@ -1,4 +1,8 @@ import { deriveStateFromMetadata } from '@metamask/base-controller'; +import { + ConfigRegistryControllerGetStateAction, + RegistryNetworkConfig, +} from '@metamask/config-registry-controller'; import { BuiltInNetworkName, ChainId } from '@metamask/controller-utils'; import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; @@ -7,8 +11,14 @@ import type { MessengerEvents, MockAnyNamespace, } from '@metamask/messenger'; -import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; -import { RpcEndpointType } from '@metamask/network-controller'; +import { + MultichainNetworkControllerGetStateAction, + toEvmCaipChainId, +} from '@metamask/multichain-network-controller'; +import { + NetworkControllerGetStateAction, + RpcEndpointType, +} from '@metamask/network-controller'; import { TransactionStatus } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; import { KnownCaipNamespace } from '@metamask/utils'; @@ -71,37 +81,85 @@ function getRootMessenger(): RootMessenger { }); } -type MultichainGetStateReturn = { - multichainNetworkConfigurationsByChainId: Record; - selectedMultichainNetworkChainId: string; - isEvmSelected: boolean; - networksWithTransactionActivity: Record; -}; - -const defaultMultichainGetState = (): MultichainGetStateReturn => ({ - multichainNetworkConfigurationsByChainId: { - [BtcScope.Mainnet]: { chainId: BtcScope.Mainnet, name: 'Bitcoin' }, - [SolScope.Mainnet]: { chainId: SolScope.Mainnet, name: 'Solana' }, - [TrxScope.Mainnet]: { chainId: TrxScope.Mainnet, name: 'Tron' }, - [XlmScope.Pubnet]: { chainId: XlmScope.Pubnet, name: 'Stellar' }, - }, - selectedMultichainNetworkChainId: 'eip155:1', - isEvmSelected: true, - networksWithTransactionActivity: {}, -}); +/** + * Creates a mock RegistryNetworkConfig object with default values, which can be overridden by the provided `overrides` parameter. + * + * @param overrides - Optional properties to override in the default config. + * @returns A mock RegistryNetworkConfig object. + */ +function createMockRegistryNetworkConfig( + overrides: Partial = {}, +): RegistryNetworkConfig { + const base: RegistryNetworkConfig = { + chainId: 'eip155:1', + name: 'Ethereum Mainnet', + imageUrl: + 'https://token.api.cx.metamask.io/assets/networkLogos/ethereum.svg', + coingeckoPlatformId: 'ethereum', + geckoTerminalPlatformId: 'eth', + assets: { + listUrl: 'https://tokens.api.cx.metamask.io/v3/chains/eip155:1/assets', + native: { + assetId: 'eip155:1/slip44:60', + imageUrl: + 'https://static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/slip44/60.png', + name: 'Ether', + symbol: 'ETH', + decimals: 18, + coingeckoCoinId: 'ethereum', + }, + }, + rpcProviders: { + default: { + url: 'https://mainnet.infura.io/v3/{infuraProjectId}', + type: 'infura', + networkClientId: 'mainnet', + }, + fallbacks: [], + }, + blockExplorerUrls: { + default: 'https://etherscan.io', + fallbacks: [], + }, + config: { + isActive: true, + isTestnet: false, + isDefault: true, + isFeatured: true, + isDeprecated: false, + isDeletable: false, + priority: 0, + }, + }; + const { config: configOverride, ...rest } = overrides; + return { + ...base, + ...rest, + config: configOverride + ? { ...base.config, ...configOverride } + : base.config, + }; +} const setupController = ({ config, - multichainGetState = defaultMultichainGetState, }: { config?: Partial< ConstructorParameters[0] >; - multichainGetState?: () => MultichainGetStateReturn; } = {}): { controller: NetworkEnablementController; rootMessenger: RootMessenger; messenger: NetworkEnablementControllerMessenger; + configRegistryControllerGetStateMock: jest.MockedFunction< + ConfigRegistryControllerGetStateAction['handler'] + >; + networkControllerGetStateMock: jest.MockedFunction< + NetworkControllerGetStateAction['handler'] + >; + multichainNetworkControllerGetStateMock: jest.MockedFunction< + MultichainNetworkControllerGetStateAction['handler'] + >; } => { const rootMessenger = getRootMessenger(); @@ -118,6 +176,7 @@ const setupController = ({ rootMessenger.delegate({ messenger: networkEnablementControllerMessenger, actions: [ + 'ConfigRegistryController:getState', 'NetworkController:getState', 'MultichainNetworkController:getState', ], @@ -129,29 +188,70 @@ const setupController = ({ ], }); + const networkControllerGetStateMock: jest.MockedFunction< + NetworkControllerGetStateAction['handler'] + > = jest.fn().mockImplementation(() => ({ + networkConfigurationsByChainId: { + '0x1': { + defaultRpcEndpointIndex: 0, + rpcEndpoints: [{}], + }, + '0xe708': { + defaultRpcEndpointIndex: 0, + rpcEndpoints: [{}], + }, + '0x2105': { + defaultRpcEndpointIndex: 0, + rpcEndpoints: [{}], + }, + }, + })); + rootMessenger.registerActionHandler( 'NetworkController:getState', - jest.fn().mockImplementation(() => ({ - networkConfigurationsByChainId: { - '0x1': { - defaultRpcEndpointIndex: 0, - rpcEndpoints: [{}], - }, - '0xe708': { - defaultRpcEndpointIndex: 0, - rpcEndpoints: [{}], - }, - '0x2105': { - defaultRpcEndpointIndex: 0, - rpcEndpoints: [{}], - }, - }, - })), + networkControllerGetStateMock, ); + const multichainNetworkControllerGetStateMock: jest.MockedFunction< + MultichainNetworkControllerGetStateAction['handler'] + > = jest.fn().mockImplementation(() => ({ + multichainNetworkConfigurationsByChainId: { + [BtcScope.Mainnet]: { chainId: BtcScope.Mainnet, name: 'Bitcoin' }, + [SolScope.Mainnet]: { chainId: SolScope.Mainnet, name: 'Solana' }, + [TrxScope.Mainnet]: { chainId: TrxScope.Mainnet, name: 'Tron' }, + [XlmScope.Pubnet]: { chainId: XlmScope.Pubnet, name: 'Stellar' }, + }, + selectedMultichainNetworkChainId: 'eip155:1', + isEvmSelected: true, + networksWithTransactionActivity: {}, + })); + rootMessenger.registerActionHandler( 'MultichainNetworkController:getState', - jest.fn().mockImplementation(multichainGetState), + jest.fn().mockImplementation(multichainNetworkControllerGetStateMock), + ); + + const configRegistryControllerGetStateMock: jest.MockedFunction< + ConfigRegistryControllerGetStateAction['handler'] + > = jest.fn().mockImplementation(() => ({ + configs: { + networks: { + 'eip155:9999': { + chainId: 'eip155:9999', + config: { + isDefault: false, + isFeatured: true, + isActive: true, + isTestnet: false, + }, + }, + }, + }, + })); + + rootMessenger.registerActionHandler( + 'ConfigRegistryController:getState', + configRegistryControllerGetStateMock, ); const controller = new NetworkEnablementController({ @@ -163,6 +263,9 @@ const setupController = ({ controller, rootMessenger, messenger: networkEnablementControllerMessenger, + configRegistryControllerGetStateMock, + multichainNetworkControllerGetStateMock, + networkControllerGetStateMock, }; }; @@ -1153,65 +1256,73 @@ describe('NetworkEnablementController', () => { describe('enableAllPopularNetworks', () => { it('enables all popular networks that exist in controller configurations and Solana mainnet', () => { - const { controller, messenger } = setupController(); - - // Mock the network configurations - jest - .spyOn(messenger, 'call') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation((actionType: string, ..._args: any[]): any => { - if (actionType === 'NetworkController:getState') { - return { - selectedNetworkClientId: 'mainnet', - networkConfigurationsByChainId: { - '0x1': { - chainId: '0x1', - name: 'Ethereum Mainnet', - nativeCurrency: 'ETH', - }, - '0xe708': { - chainId: '0xe708', - name: 'Linea Mainnet', - nativeCurrency: 'ETH', - }, - '0x2105': { - chainId: '0x2105', - name: 'Base Mainnet', - nativeCurrency: 'ETH', - }, - }, - networksMetadata: {}, - }; - } - if (actionType === 'MultichainNetworkController:getState') { - return { - multichainNetworkConfigurationsByChainId: { - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { - chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - name: 'Solana Mainnet', - nativeCurrency: 'SOL', - }, - [BtcScope.Mainnet]: { - chainId: BtcScope.Mainnet, - name: 'Bitcoin Mainnet', - }, - [TrxScope.Mainnet]: { - chainId: TrxScope.Mainnet, - name: 'Tron Mainnet', - }, - [XlmScope.Pubnet]: { - chainId: XlmScope.Pubnet, - name: 'Stellar Mainnet', - }, - }, - selectedMultichainNetworkChainId: - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - isEvmSelected: false, - networksWithTransactionActivity: {}, - }; - } - throw new Error(`Unexpected action type: ${actionType}`); - }); + const { + controller, + networkControllerGetStateMock, + multichainNetworkControllerGetStateMock, + } = setupController(); + networkControllerGetStateMock.mockReturnValue({ + selectedNetworkClientId: 'mainnet', + networkConfigurationsByChainId: { + '0x1': { + chainId: '0x1', + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [], + }, + '0xe708': { + chainId: '0xe708', + name: 'Linea Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [], + }, + '0x2105': { + chainId: '0x2105', + name: 'Base Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [], + }, + }, + networksMetadata: {}, + }); + multichainNetworkControllerGetStateMock.mockReturnValue({ + multichainNetworkConfigurationsByChainId: { + [SolScope.Mainnet]: { + chainId: SolScope.Mainnet, + name: 'Solana Mainnet', + isEvm: false, + nativeCurrency: + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501', + }, + [BtcScope.Mainnet]: { + chainId: BtcScope.Mainnet, + name: 'Bitcoin Mainnet', + isEvm: false, + nativeCurrency: 'bip122:000000000019d6689c085ae165831e93/slip44:0', + }, + [TrxScope.Mainnet]: { + chainId: TrxScope.Mainnet, + name: 'Tron Mainnet', + isEvm: false, + nativeCurrency: 'tron:728126428/slip44:195', + }, + [XlmScope.Pubnet]: { + chainId: XlmScope.Pubnet, + name: 'Stellar Mainnet', + isEvm: false, + nativeCurrency: 'stellar:pubnet/slip44:148', + }, + }, + selectedMultichainNetworkChainId: SolScope.Mainnet, + isEvmSelected: false, + networksWithTransactionActivity: {}, + }); // Initially disable some networks controller.disableNetwork('0xe708'); // Linea @@ -1294,51 +1405,45 @@ describe('NetworkEnablementController', () => { }); it('enables all popular networks from constants', () => { - const { controller, messenger } = setupController(); - - // Mock all popular networks to be available in configurations - jest.spyOn(messenger, 'call').mockImplementation( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (actionType: string, ..._args: any[]): any => { - if (actionType === 'NetworkController:getState') { - // Create mock configurations for all popular networks - const networkConfigurationsByChainId = POPULAR_NETWORKS.reduce( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (acc: any, chainId: string) => { - acc[chainId] = { chainId, name: `Network ${chainId}` }; - return acc; - }, - {}, - ); - return { - selectedNetworkClientId: 'mainnet', - networkConfigurationsByChainId, - networksMetadata: {}, - }; - } - if (actionType === 'MultichainNetworkController:getState') { - return { - multichainNetworkConfigurationsByChainId: { - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { - chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - name: 'Solana Mainnet', - nativeCurrency: 'SOL', - }, - [BtcScope.Mainnet]: { - chainId: BtcScope.Mainnet, - name: 'Bitcoin Mainnet', - }, - }, - selectedMultichainNetworkChainId: - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - isEvmSelected: false, - networksWithTransactionActivity: {}, - }; - } - throw new Error(`Unexpected action type: ${actionType}`); + const { + controller, + multichainNetworkControllerGetStateMock, + networkControllerGetStateMock, + } = setupController(); + + multichainNetworkControllerGetStateMock.mockReturnValue({ + multichainNetworkConfigurationsByChainId: { + [SolScope.Mainnet]: { + chainId: SolScope.Mainnet, + name: 'Solana Mainnet', + isEvm: false, + nativeCurrency: + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501', + }, + [BtcScope.Mainnet]: { + chainId: BtcScope.Mainnet, + name: 'Bitcoin Mainnet', + isEvm: false, + nativeCurrency: 'bip122:000000000019d6689c085ae165831e93/slip44:0', + }, }, - ); + selectedMultichainNetworkChainId: SolScope.Mainnet, + isEvmSelected: false, + networksWithTransactionActivity: {}, + }); + networkControllerGetStateMock.mockReturnValue({ + selectedNetworkClientId: 'mainnet', + networksMetadata: {}, + networkConfigurationsByChainId: POPULAR_NETWORKS.reduce( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (acc: any, chainId: string) => { + acc[chainId] = { chainId, name: `Network ${chainId}` }; + return acc; + }, + {}, + ), + }); // The function should enable all popular networks defined in constants expect(() => controller.enableAllPopularNetworks()).not.toThrow(); @@ -1379,60 +1484,211 @@ describe('NetworkEnablementController', () => { }); }); - it('disables existing networks and enables only popular networks (exclusive behavior)', async () => { - const { controller, rootMessenger, messenger } = setupController(); + it('enables all popular networks from config registry controller', () => { + const { + controller, + networkControllerGetStateMock, + configRegistryControllerGetStateMock, + } = setupController({ + config: { + state: { + enabledNetworkMap: { + [KnownCaipNamespace.Eip155]: {}, + }, + }, + }, + }); - // Mock the network configurations to include popular networks - jest - .spyOn(messenger, 'call') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation((actionType: string, ..._args: any[]): any => { - if (actionType === 'NetworkController:getState') { - return { - selectedNetworkClientId: 'mainnet', - networkConfigurationsByChainId: { - '0x1': { - chainId: '0x1', - name: 'Ethereum Mainnet', - nativeCurrency: 'ETH', - }, - '0xe708': { - chainId: '0xe708', - name: 'Linea Mainnet', - nativeCurrency: 'ETH', - }, - '0x2105': { - chainId: '0x2105', - name: 'Base Mainnet', - nativeCurrency: 'ETH', - }, - '0x2': { chainId: '0x2', name: 'Test Network' }, // Non-popular network + networkControllerGetStateMock.mockReturnValue({ + selectedNetworkClientId: 'mainnet', + networksMetadata: {}, + networkConfigurationsByChainId: { + '0x270f': { + chainId: '0x270f', + name: 'Some Network', + nativeCurrency: 'SNET', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [], + }, + }, + }); + + configRegistryControllerGetStateMock.mockReturnValue({ + configs: { + networks: { + 'eip155:9999': createMockRegistryNetworkConfig({ + chainId: 'eip155:9999', + }), + }, + }, + version: '1', + lastFetched: Date.now(), + etag: 'mock-etag', + }); + + controller.enableAllPopularNetworks(); + + expect(controller.isNetworkEnabled('0x270f')).toBe(true); + expect( + controller.state.enabledNetworkMap[KnownCaipNamespace.Eip155]['0x270f'], + ).toBe(true); + }); + + it.each([ + { + isActive: false, + isFeatured: true, + isTestnet: false, + }, + { + isActive: true, + isFeatured: false, + isTestnet: false, + isDefault: true, + isDeletable: true, + isDeprecated: false, + priority: 1, + }, + { + isActive: true, + isFeatured: true, + isTestnet: true, + isDefault: true, + isDeletable: true, + isDeprecated: false, + priority: 1, + }, + ])( + 'disables all networks that are set as not popular in the config registry controller', + (config) => { + const { + controller, + networkControllerGetStateMock, + configRegistryControllerGetStateMock, + } = setupController({ + config: { + state: { + enabledNetworkMap: { + [KnownCaipNamespace.Eip155]: {}, }, - networksMetadata: {}, - }; - } - if (actionType === 'MultichainNetworkController:getState') { - return { - multichainNetworkConfigurationsByChainId: { - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { - chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - name: 'Solana Mainnet', - nativeCurrency: 'SOL', - }, - [BtcScope.Mainnet]: { - chainId: BtcScope.Mainnet, - name: 'Bitcoin Mainnet', + }, + }, + }); + + networkControllerGetStateMock.mockReturnValue({ + selectedNetworkClientId: 'mainnet', + networksMetadata: {}, + networkConfigurationsByChainId: { + '0x270f': { + chainId: '0x270f', + name: 'Some Network', + nativeCurrency: 'SNET', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [], + }, + }, + }); + + configRegistryControllerGetStateMock.mockReturnValue({ + configs: { + networks: { + 'eip155:9999': createMockRegistryNetworkConfig({ + chainId: 'eip155:9999', + config: { + ...config, + isDefault: true, + isDeletable: true, + isDeprecated: false, + priority: 1, }, - }, - selectedMultichainNetworkChainId: - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - isEvmSelected: false, - networksWithTransactionActivity: {}, - }; - } - throw new Error(`Unexpected action type: ${actionType}`); + }), + }, + }, + version: '1', + lastFetched: Date.now(), + etag: 'mock-etag', }); + controller.enableAllPopularNetworks(); + + expect(controller.isNetworkEnabled('0x270f')).toBe(false); + expect( + controller.state.enabledNetworkMap[KnownCaipNamespace.Eip155][ + '0x270f' + ], + ).toBeUndefined(); + }, + ); + + it('disables existing networks and enables only popular networks (exclusive behavior)', async () => { + const { + controller, + rootMessenger, + networkControllerGetStateMock, + multichainNetworkControllerGetStateMock, + } = setupController(); + networkControllerGetStateMock.mockReturnValue({ + selectedNetworkClientId: 'mainnet', + networkConfigurationsByChainId: { + '0x1': { + chainId: '0x1', + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [], + }, + '0xe708': { + chainId: '0xe708', + name: 'Linea Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [], + }, + '0x2105': { + chainId: '0x2105', + name: 'Base Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [], + }, + // Non-popular network + '0x2': { + chainId: '0x2', + name: 'Test Network', + nativeCurrency: 'TEST', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + rpcEndpoints: [], + }, + }, + networksMetadata: {}, + }); + multichainNetworkControllerGetStateMock.mockReturnValue({ + multichainNetworkConfigurationsByChainId: { + [SolScope.Mainnet]: { + chainId: SolScope.Mainnet, + name: 'Solana Mainnet', + isEvm: false as const, + nativeCurrency: + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501', + }, + [BtcScope.Mainnet]: { + chainId: BtcScope.Mainnet, + name: 'Bitcoin Mainnet', + isEvm: false as const, + nativeCurrency: 'bip122:000000000019d6689c085ae165831e93/slip44:0', + }, + }, + selectedMultichainNetworkChainId: SolScope.Mainnet, + isEvmSelected: false, + networksWithTransactionActivity: {}, + }); + // Add a non-popular network rootMessenger.publish('NetworkController:networkAdded', { chainId: '0x2', // A network not in POPULAR_NETWORKS @@ -1474,44 +1730,32 @@ describe('NetworkEnablementController', () => { }); it('enables Bitcoin mainnet when configured in MultichainNetworkController', () => { - const { controller, messenger } = setupController(); - - // Mock the network configurations to include Bitcoin - jest - .spyOn(messenger, 'call') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation((actionType: string, ..._args: unknown[]): any => { - const responses = { - 'NetworkController:getState': { - selectedNetworkClientId: 'mainnet', - networkConfigurationsByChainId: {}, - networksMetadata: {}, - }, - 'MultichainNetworkController:getState': { - multichainNetworkConfigurationsByChainId: { - [BtcScope.Mainnet]: { - chainId: BtcScope.Mainnet, - name: 'Bitcoin Mainnet', - isEvm: false as const, - nativeCurrency: - 'bip122:000000000019d6689c085ae165831e93/slip44:0' as `${string}:${string}/${string}:${string}`, + const { controller, multichainNetworkControllerGetStateMock } = + setupController({ + config: { + state: { + enabledNetworkMap: { + [KnownCaipNamespace.Bip122]: { + [BtcScope.Mainnet]: false, // Initially disabled }, }, - selectedMultichainNetworkChainId: BtcScope.Mainnet, - isEvmSelected: false, - networksWithTransactionActivity: {}, }, - }; - return responses[actionType as keyof typeof responses]; + }, }); - - // Initially disable Bitcoin to test enablement - // eslint-disable-next-line dot-notation - controller['update']((state) => { - state.enabledNetworkMap[KnownCaipNamespace.Bip122][BtcScope.Mainnet] = - false; + multichainNetworkControllerGetStateMock.mockReturnValue({ + multichainNetworkConfigurationsByChainId: { + [BtcScope.Mainnet]: { + chainId: BtcScope.Mainnet, + name: 'Bitcoin Mainnet', + isEvm: false as const, + nativeCurrency: + 'bip122:000000000019d6689c085ae165831e93/slip44:0' as `${string}:${string}/${string}:${string}`, + }, + }, + selectedMultichainNetworkChainId: BtcScope.Mainnet, + isEvmSelected: false, + networksWithTransactionActivity: {}, }); - expect(controller.isNetworkEnabled(BtcScope.Mainnet)).toBe(false); // enableAllPopularNetworks should re-enable Bitcoin when it exists in config @@ -2331,13 +2575,13 @@ describe('NetworkEnablementController', () => { }); it('excludes multichain mainnets when not in MultichainNetworkController state', () => { - const { controller } = setupController({ - multichainGetState: () => ({ - multichainNetworkConfigurationsByChainId: {}, - selectedMultichainNetworkChainId: 'eip155:1', - isEvmSelected: true, - networksWithTransactionActivity: {}, - }), + const { controller, multichainNetworkControllerGetStateMock } = + setupController({}); + multichainNetworkControllerGetStateMock.mockReturnValue({ + multichainNetworkConfigurationsByChainId: {}, + selectedMultichainNetworkChainId: SolScope.Devnet, + isEvmSelected: true, + networksWithTransactionActivity: {}, }); const result = controller.listPopularNetworks(); @@ -2407,14 +2651,15 @@ describe('NetworkEnablementController', () => { }); it('returns empty when none of the multichain mainnets are configured', () => { - const { controller } = setupController({ - multichainGetState: () => ({ - multichainNetworkConfigurationsByChainId: {}, - selectedMultichainNetworkChainId: 'eip155:1', - isEvmSelected: true, - networksWithTransactionActivity: {}, - }), + const { controller, multichainNetworkControllerGetStateMock } = + setupController(); + multichainNetworkControllerGetStateMock.mockReturnValue({ + multichainNetworkConfigurationsByChainId: {}, + selectedMultichainNetworkChainId: SolScope.Devnet, + isEvmSelected: true, + networksWithTransactionActivity: {}, }); + const result = controller.listPopularMultichainNetworks(); expect(result).toStrictEqual([]); }); diff --git a/packages/network-enablement-controller/src/NetworkEnablementController.ts b/packages/network-enablement-controller/src/NetworkEnablementController.ts index 9d3b21aed4c..fe0fce1da44 100644 --- a/packages/network-enablement-controller/src/NetworkEnablementController.ts +++ b/packages/network-enablement-controller/src/NetworkEnablementController.ts @@ -3,7 +3,8 @@ import type { ControllerGetStateAction, ControllerStateChangeEvent, } from '@metamask/base-controller'; -import { BuiltInNetworkName, ChainId } from '@metamask/controller-utils'; +import type { ConfigRegistryControllerGetStateAction } from '@metamask/config-registry-controller'; +import { BuiltInNetworkName, ChainId, toHex } from '@metamask/controller-utils'; import { BtcScope, SolScope, TrxScope, XlmScope } from '@metamask/keyring-api'; import type { Messenger } from '@metamask/messenger'; import type { MultichainNetworkControllerGetStateAction } from '@metamask/multichain-network-controller'; @@ -16,16 +17,12 @@ import type { } from '@metamask/network-controller'; import type { TransactionControllerTransactionSubmittedEvent } from '@metamask/transaction-controller'; import type { CaipChainId, CaipNamespace, Hex } from '@metamask/utils'; -import { KnownCaipNamespace } from '@metamask/utils'; +import { KnownCaipNamespace, parseCaipChainId } from '@metamask/utils'; import { POPULAR_NETWORKS } from './constants.js'; import type { NetworkEnablementControllerMethodActions } from './NetworkEnablementController-method-action-types.js'; import { Slip44Service } from './services/index.js'; -import { - deriveKeys, - isOnlyNetworkEnabledInNamespace, - isPopularNetwork, -} from './utils.js'; +import { deriveKeys, isOnlyNetworkEnabledInNamespace } from './utils.js'; const controllerName = 'NetworkEnablementController'; @@ -97,7 +94,8 @@ export type NetworkEnablementControllerGetStateAction = */ export type AllowedActions = | NetworkControllerGetStateAction - | MultichainNetworkControllerGetStateAction; + | MultichainNetworkControllerGetStateAction + | ConfigRegistryControllerGetStateAction; export type NetworkEnablementControllerActions = | NetworkEnablementControllerGetStateAction @@ -371,7 +369,7 @@ export class NetworkEnablementController extends BaseController< ); // Enable all popular EVM networks that exist in NetworkController configurations - POPULAR_NETWORKS.forEach((chainId) => { + this.#getPopularEvmChainIds().forEach((chainId) => { const { namespace, storageKey } = deriveKeys(chainId); // Check if network exists in NetworkController configurations @@ -688,7 +686,7 @@ export class NetworkEnablementController extends BaseController< ); // Count how many popular networks are enabled - const enabledPopularNetworksCount = POPULAR_NETWORKS.reduce( + const enabledPopularNetworksCount = this.#getPopularEvmChainIds().reduce( (count, chainId) => { // Only check networks that actually exist in NetworkController configurations if (!networkControllerState.networkConfigurationsByChainId[chainId]) { @@ -766,7 +764,8 @@ export class NetworkEnablementController extends BaseController< const inPopularNetworksMode = this.#isInPopularNetworksMode(); // Check if the network being added is a popular network - const isAddedNetworkPopular = isPopularNetwork(reference); + const isAddedNetworkPopular = + this.#getPopularEvmChainIds().includes(chainId); // Keep current selection only if in popular networks mode AND adding a popular network const shouldKeepCurrentSelection = @@ -794,10 +793,51 @@ export class NetworkEnablementController extends BaseController< }); } + /** + * Returns the hex chain IDs of EVM networks the config registry currently + * marks as "popular" (i.e. featured + active + non-testnet). + * + * @returns Hex chain IDs of registry-featured EVM networks. + */ + #getRegistryPopularEvmChainIds(): Hex[] { + const { + configs: { networks }, + } = this.messenger.call('ConfigRegistryController:getState'); + return Object.values(networks).reduce((popularChains, network) => { + if ( + network.config.isFeatured && + network.config.isActive && + !network.config.isTestnet && + network.chainId.startsWith('eip155:') + ) { + const hexChainId = toHex(parseCaipChainId(network.chainId).reference); + popularChains.push(hexChainId); + } + return popularChains; + }, []); + } + + /** + * Returns the set of popular EVM chain IDs using registry configs as primary source + * and the bundled `POPULAR_NETWORKS` as fallback. This ensures that the list of popular networks is always available, + * even if the registry is unavailable or doesn't have any featured networks. + * + * @returns De-duplicated hex chain IDs considered popular. + */ + #getPopularEvmChainIds(): Hex[] { + return [ + ...new Set([ + // `toHex` is used to normalize chain IDs from the bundled POPULAR_NETWORKS. + ...POPULAR_NETWORKS.map((chainId) => toHex(chainId)), + ...this.#getRegistryPopularEvmChainIds(), + ]), + ]; + } + /** * Returns popular EVM network chain IDs in hex form, restricted to networks - * that exist in NetworkController (networkConfigurationsByChainId). Source list - * is POPULAR_NETWORKS. + * that exist in NetworkController (networkConfigurationsByChainId). Source is + * the bundled `POPULAR_NETWORKS` unioned with registry-featured EVM chains. * * @returns Hex chain IDs for popular EVM networks that are configured. */ @@ -805,7 +845,7 @@ export class NetworkEnablementController extends BaseController< const networkControllerState = this.messenger.call( 'NetworkController:getState', ); - return POPULAR_NETWORKS.filter( + return this.#getPopularEvmChainIds().filter( (chainIdHex) => networkControllerState.networkConfigurationsByChainId[chainIdHex], ); diff --git a/packages/network-enablement-controller/src/utils.test.ts b/packages/network-enablement-controller/src/utils.test.ts index 40844e326b3..2cda85549ab 100644 --- a/packages/network-enablement-controller/src/utils.test.ts +++ b/packages/network-enablement-controller/src/utils.test.ts @@ -1,11 +1,7 @@ import { KnownCaipNamespace } from '@metamask/utils'; import type { NetworkEnablementControllerState } from './NetworkEnablementController.js'; -import { - deriveKeys, - isOnlyNetworkEnabledInNamespace, - isPopularNetwork, -} from './utils.js'; +import { deriveKeys, isOnlyNetworkEnabledInNamespace } from './utils.js'; describe('Utils', () => { describe('deriveKeys', () => { @@ -264,29 +260,4 @@ describe('Utils', () => { }); }); }); - - describe('isPopularNetwork', () => { - it('returns true for popular EVM networks', () => { - // Test with Ethereum mainnet (chain ID 1) - expect(isPopularNetwork('1')).toBe(true); - - // Test with Polygon mainnet (chain ID 137) - expect(isPopularNetwork('137')).toBe(true); - }); - - it('returns false for non-popular EVM networks', () => { - // Test with a custom/test network - expect(isPopularNetwork('999999')).toBe(false); - }); - - it('returns false for non-decimal references (like Bitcoin hashes)', () => { - // Test with Bitcoin block hash reference - expect(isPopularNetwork('000000000019d6689c085ae165831e93')).toBe(false); - }); - - it('returns false for invalid references', () => { - // Test with completely invalid reference - expect(isPopularNetwork('invalid-reference')).toBe(false); - }); - }); }); diff --git a/packages/network-enablement-controller/src/utils.ts b/packages/network-enablement-controller/src/utils.ts index c7534f9ad90..a8b8f8cce2d 100644 --- a/packages/network-enablement-controller/src/utils.ts +++ b/packages/network-enablement-controller/src/utils.ts @@ -8,7 +8,6 @@ import { parseCaipChainId, } from '@metamask/utils'; -import { POPULAR_NETWORKS } from './constants.js'; import type { NetworkEnablementControllerState } from './NetworkEnablementController.js'; /** @@ -85,19 +84,3 @@ export function isOnlyNetworkEnabledInNamespace( // Return false if there are zero or multiple enabled networks return false; } - -/** - * Checks if a network is considered popular based on its reference. - * - * @param reference - The network reference (typically the chain ID reference part) - * @returns True if the network is popular, false otherwise - */ -export function isPopularNetwork(reference: string): boolean { - try { - return POPULAR_NETWORKS.includes(toHex(reference)); - } catch { - // If toHex fails (e.g., for non-decimal references like Bitcoin hashes), - // the network is not popular - return false; - } -} diff --git a/packages/network-enablement-controller/tsconfig.build.json b/packages/network-enablement-controller/tsconfig.build.json index 11328a92b94..0a086401e35 100644 --- a/packages/network-enablement-controller/tsconfig.build.json +++ b/packages/network-enablement-controller/tsconfig.build.json @@ -7,6 +7,7 @@ }, "references": [ { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../config-registry-controller/tsconfig.build.json" }, { "path": "../network-controller/tsconfig.build.json" }, { "path": "../multichain-network-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, diff --git a/packages/network-enablement-controller/tsconfig.json b/packages/network-enablement-controller/tsconfig.json index e1ebf385335..c2fded4422a 100644 --- a/packages/network-enablement-controller/tsconfig.json +++ b/packages/network-enablement-controller/tsconfig.json @@ -6,6 +6,7 @@ }, "references": [ { "path": "../base-controller" }, + { "path": "../config-registry-controller" }, { "path": "../network-controller" }, { "path": "../multichain-network-controller" }, { "path": "../controller-utils" }, diff --git a/yarn.lock b/yarn.lock index 97d8dc6c6d9..7dd81f57b25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6295,7 +6295,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/config-registry-controller@workspace:packages/config-registry-controller": +"@metamask/config-registry-controller@npm:^0.4.1, @metamask/config-registry-controller@workspace:packages/config-registry-controller": version: 0.0.0-use.local resolution: "@metamask/config-registry-controller@workspace:packages/config-registry-controller" dependencies: @@ -7932,6 +7932,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" + "@metamask/config-registry-controller": "npm:^0.4.1" "@metamask/controller-utils": "npm:^12.3.0" "@metamask/keyring-api": "npm:^23.5.0" "@metamask/messenger": "npm:^2.0.0"