diff --git a/packages/network-enablement-controller/CHANGELOG.md b/packages/network-enablement-controller/CHANGELOG.md index 1a3d85a8774..92d0d45d293 100644 --- a/packages/network-enablement-controller/CHANGELOG.md +++ b/packages/network-enablement-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** Use new `Messenger` from `@metamask/messenger` ([#6540](https://github.com/MetaMask/core/pull/6540)) + - Previously, `NetworkEnablementController` accepted a `RestrictedMessenger` instance from `@metamask/base-controller`. + ## [0.5.0] ### Added diff --git a/packages/network-enablement-controller/package.json b/packages/network-enablement-controller/package.json index 2b4fd8f0d59..ba397b16b11 100644 --- a/packages/network-enablement-controller/package.json +++ b/packages/network-enablement-controller/package.json @@ -63,6 +63,7 @@ "dependencies": { "@metamask/base-controller": "^8.3.0", "@metamask/controller-utils": "^11.12.0", + "@metamask/messenger": "^0.2.0", "@metamask/utils": "^11.4.2", "reselect": "^5.1.1" }, diff --git a/packages/network-enablement-controller/src/NetworkEnablementController.test.ts b/packages/network-enablement-controller/src/NetworkEnablementController.test.ts index 722af899985..97a2ecffc02 100644 --- a/packages/network-enablement-controller/src/NetworkEnablementController.test.ts +++ b/packages/network-enablement-controller/src/NetworkEnablementController.test.ts @@ -1,5 +1,11 @@ -import { Messenger } from '@metamask/base-controller'; import { BuiltInNetworkName, ChainId } from '@metamask/controller-utils'; +import { + Messenger, + MOCK_ANY_NAMESPACE, + type MessengerActions, + type MessengerEvents, + type MockAnyNamespace, +} from '@metamask/messenger'; import { RpcEndpointType } from '@metamask/network-controller'; import { TransactionStatus, @@ -14,60 +20,96 @@ import { useFakeTimers } from 'sinon'; import { POPULAR_NETWORKS } from './constants'; import { NetworkEnablementController } from './NetworkEnablementController'; -import type { - NetworkEnablementControllerActions, - NetworkEnablementControllerEvents, - AllowedEvents, - AllowedActions, - NetworkEnablementControllerMessenger, -} from './NetworkEnablementController'; +import type { NetworkEnablementControllerMessenger } from './NetworkEnablementController'; import { BtcScope, SolScope } from './types'; import { advanceTime } from '../../../tests/helpers'; +const controllerName = 'NetworkEnablementController'; + +type AllNetworkEnablementControllerActions = + MessengerActions; + +type AllNetworkEnablementControllerEvents = + MessengerEvents; + +type RootMessenger = Messenger< + MockAnyNamespace, + AllNetworkEnablementControllerActions, + AllNetworkEnablementControllerEvents +>; + +/** + * Creates and returns a root messenger for testing + * + * @returns A messenger instance + */ +function getRootMessenger(): RootMessenger { + return new Messenger({ + namespace: MOCK_ANY_NAMESPACE, + }); +} + const setupController = ({ config, + mockGetNetworkState = jest.fn().mockImplementation(() => ({ + networkConfigurationsByChainId: { + '0x1': { + defaultRpcEndpointIndex: 0, + rpcEndpoints: [{}], + }, + }, + })), + mockGetMultichainNetworkState = jest.fn().mockImplementation(() => ({ + multichainNetworkConfigurationsByChainId: { + 'eip155:1': { + chainId: 'eip155:1', + name: 'Ethereum Mainnet', + }, + }, + })), }: { config?: Partial< ConstructorParameters[0] >; -} = {}) => { - const messenger = new Messenger< - NetworkEnablementControllerActions | AllowedActions, - NetworkEnablementControllerEvents | AllowedEvents - >(); - - const networkEnablementControllerMessenger: NetworkEnablementControllerMessenger = - messenger.getRestricted({ - name: 'NetworkEnablementController', - allowedActions: [ - 'NetworkController:getState', - 'MultichainNetworkController:getState', - ], - allowedEvents: [ - 'NetworkController:networkAdded', - 'NetworkController:networkRemoved', - 'TransactionController:transactionSubmitted', - ], - }); + mockGetNetworkState?: jest.Mock; + mockGetMultichainNetworkState?: jest.Mock; +} = {}): { + controller: NetworkEnablementController; + rootMessenger: RootMessenger; +} => { + const rootMessenger = getRootMessenger(); + + const networkEnablementControllerMessenger = new Messenger< + typeof controllerName, + AllNetworkEnablementControllerActions, + AllNetworkEnablementControllerEvents, + RootMessenger + >({ + namespace: controllerName, + parent: rootMessenger, + }); + + rootMessenger.delegate({ + messenger: networkEnablementControllerMessenger, + actions: [ + 'NetworkController:getState', + 'MultichainNetworkController:getState', + ], + events: [ + 'NetworkController:networkAdded', + 'NetworkController:networkRemoved', + 'TransactionController:transactionSubmitted', + ], + }); - messenger.registerActionHandler( + rootMessenger.registerActionHandler( 'NetworkController:getState', - jest.fn().mockImplementation(() => ({ - networkConfigurationsByChainId: { - '0x1': { - defaultRpcEndpointIndex: 0, - rpcEndpoints: [{}], - }, - '0xe708': { - defaultRpcEndpointIndex: 0, - rpcEndpoints: [{}], - }, - '0x2105': { - defaultRpcEndpointIndex: 0, - rpcEndpoints: [{}], - }, - }, - })), + mockGetNetworkState, + ); + + rootMessenger.registerActionHandler( + 'MultichainNetworkController:getState', + mockGetMultichainNetworkState, ); const controller = new NetworkEnablementController({ @@ -77,20 +119,10 @@ const setupController = ({ return { controller, - messenger, + rootMessenger, }; }; -// Helper function to setup controller with default state (no init needed) -const setupInitializedController = ( - config?: Partial< - ConstructorParameters[0] - >, -) => { - const setup = setupController({ config }); - return setup; -}; - describe('NetworkEnablementController', () => { let clock: sinon.SinonFakeTimers; @@ -127,10 +159,10 @@ describe('NetworkEnablementController', () => { }); it('subscribes to NetworkController:networkAdded', async () => { - const { controller, messenger } = setupInitializedController(); + const { controller, rootMessenger } = setupController(); // Publish an update with avax network added - messenger.publish('NetworkController:networkAdded', { + rootMessenger.publish('NetworkController:networkAdded', { chainId: '0xa86a', blockExplorerUrls: [], defaultRpcEndpointIndex: 0, @@ -170,10 +202,10 @@ describe('NetworkEnablementController', () => { }); it('subscribes to NetworkController:networkRemoved', async () => { - const { controller, messenger } = setupInitializedController(); + const { controller, rootMessenger } = setupController(); // Publish an update with linea network removed - messenger.publish('NetworkController:networkRemoved', { + rootMessenger.publish('NetworkController:networkRemoved', { chainId: '0xe708', // Linea Mainnet blockExplorerUrls: [], defaultRpcEndpointIndex: 0, @@ -211,12 +243,12 @@ describe('NetworkEnablementController', () => { }); it('handles TransactionController:transactionSubmitted with missing chainId gracefully', async () => { - const { controller, messenger } = setupInitializedController(); + const { controller, rootMessenger } = setupController(); const initialState = { ...controller.state }; // Publish a transaction submitted event without chainId - messenger.publish('TransactionController:transactionSubmitted', { + rootMessenger.publish('TransactionController:transactionSubmitted', { transactionMeta: { networkClientId: 'test-network', id: 'test-tx-id', @@ -238,13 +270,13 @@ describe('NetworkEnablementController', () => { }); it('handles TransactionController:transactionSubmitted with malformed structure gracefully', async () => { - const { controller, messenger } = setupInitializedController(); + const { controller, rootMessenger } = setupController(); const initialState = { ...controller.state }; // Publish a transaction submitted event with malformed structure // @ts-expect-error - Testing runtime safety for malformed payload - messenger.publish('TransactionController:transactionSubmitted', { + rootMessenger.publish('TransactionController:transactionSubmitted', { // Missing transactionMeta entirely }); @@ -255,12 +287,12 @@ describe('NetworkEnablementController', () => { }); it('handles TransactionController:transactionSubmitted with null/undefined transactionMeta gracefully', async () => { - const { controller, messenger } = setupInitializedController(); + const { controller, rootMessenger } = setupController(); const initialState = { ...controller.state }; // Test with null transactionMeta - messenger.publish('TransactionController:transactionSubmitted', { + rootMessenger.publish('TransactionController:transactionSubmitted', { // @ts-expect-error - Testing runtime safety for null transactionMeta transactionMeta: null, }); @@ -271,7 +303,7 @@ describe('NetworkEnablementController', () => { expect(controller.state).toStrictEqual(initialState); // Test with undefined transactionMeta - messenger.publish('TransactionController:transactionSubmitted', { + rootMessenger.publish('TransactionController:transactionSubmitted', { // @ts-expect-error - Testing runtime safety for undefined transactionMeta transactionMeta: undefined, }); @@ -283,14 +315,14 @@ describe('NetworkEnablementController', () => { }); it('does fallback to ethereum when removing the last enabled network', async () => { - const { controller, messenger } = setupInitializedController(); + const { controller, rootMessenger } = setupController(); // disable all networks except linea controller.disableNetwork('0x1'); // Ethereum Mainnet controller.disableNetwork('0x2105'); // Base Mainnet // Publish an update with linea network removed - messenger.publish('NetworkController:networkRemoved', { + rootMessenger.publish('NetworkController:networkRemoved', { chainId: '0xe708', // Linea Mainnet blockExplorerUrls: [], defaultRpcEndpointIndex: 0, @@ -329,47 +361,38 @@ describe('NetworkEnablementController', () => { describe('init', () => { it('initializes network enablement state from controller configurations', () => { - const { controller } = setupController(); - - jest - // eslint-disable-next-line dot-notation - .spyOn(controller['messagingSystem'], 'call') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation((actionType: string, ..._args: any[]): any => { - // eslint-disable-next-line jest/no-conditional-in-test - if (actionType === 'NetworkController:getState') { - return { - selectedNetworkClientId: 'mainnet', - networkConfigurationsByChainId: { - '0x1': { chainId: '0x1', name: 'Ethereum Mainnet' }, - '0xe708': { chainId: '0xe708', name: 'Linea Mainnet' }, - '0x2105': { chainId: '0x2105', name: 'Base Mainnet' }, - }, - networksMetadata: {}, - }; - } - // eslint-disable-next-line jest/no-conditional-in-test - if (actionType === 'MultichainNetworkController:getState') { - return { - multichainNetworkConfigurationsByChainId: { - 'eip155:1': { chainId: 'eip155:1', name: 'Ethereum Mainnet' }, - 'eip155:59144': { - chainId: 'eip155:59144', - name: 'Linea Mainnet', - }, - 'eip155:8453': { chainId: 'eip155:8453', name: 'Base Mainnet' }, - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { - chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - name: 'Solana Mainnet', - }, - }, - selectedMultichainNetworkChainId: 'eip155:1', - isEvmSelected: true, - networksWithTransactionActivity: {}, - }; - } - throw new Error(`Unexpected action type: ${actionType}`); - }); + const mockGetNetworkState = jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: 'mainnet', + networkConfigurationsByChainId: { + '0x1': { chainId: '0x1', name: 'Ethereum Mainnet' }, + '0xe708': { chainId: '0xe708', name: 'Linea Mainnet' }, + '0x2105': { chainId: '0x2105', name: 'Base Mainnet' }, + }, + networksMetadata: {}, + })); + const mockGetMultichainNetworkState = jest + .fn() + .mockImplementation(() => ({ + multichainNetworkConfigurationsByChainId: { + 'eip155:1': { chainId: 'eip155:1', name: 'Ethereum Mainnet' }, + 'eip155:59144': { + chainId: 'eip155:59144', + name: 'Linea Mainnet', + }, + 'eip155:8453': { chainId: 'eip155:8453', name: 'Base Mainnet' }, + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + name: 'Solana Mainnet', + }, + }, + selectedMultichainNetworkChainId: 'eip155:1', + isEvmSelected: true, + networksWithTransactionActivity: {}, + })); + const { controller } = setupController({ + mockGetNetworkState, + mockGetMultichainNetworkState, + }); // Initialize from configurations controller.init(); @@ -400,7 +423,32 @@ describe('NetworkEnablementController', () => { it('only enables popular networks that exist in NetworkController configurations', () => { // Create a separate controller setup for this test to avoid handler conflicts - const { controller, messenger } = setupController({ + const mockGetNetworkState = jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: 'mainnet', + networkConfigurationsByChainId: { + '0x1': { chainId: '0x1', name: 'Ethereum Mainnet' }, + '0xe708': { chainId: '0xe708', name: 'Linea Mainnet' }, + // Missing other popular networks + }, + networksMetadata: {}, + })); + const mockGetMultichainNetworkState = jest + .fn() + .mockImplementation(() => ({ + multichainNetworkConfigurationsByChainId: { + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + name: 'Solana Mainnet', + }, + }, + selectedMultichainNetworkChainId: + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + isEvmSelected: false, + networksWithTransactionActivity: {}, + })); + const { controller } = setupController({ + mockGetNetworkState, + mockGetMultichainNetworkState, config: { state: { enabledNetworkMap: { @@ -411,40 +459,6 @@ describe('NetworkEnablementController', () => { }, }); - jest.spyOn(messenger, 'call').mockImplementation( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (actionType: string, ..._args: any[]): any => { - // eslint-disable-next-line jest/no-conditional-in-test - if (actionType === 'NetworkController:getState') { - return { - selectedNetworkClientId: 'mainnet', - networkConfigurationsByChainId: { - '0x1': { chainId: '0x1', name: 'Ethereum Mainnet' }, - '0xe708': { chainId: '0xe708', name: 'Linea Mainnet' }, - // Missing other popular networks - }, - networksMetadata: {}, - }; - } - // eslint-disable-next-line jest/no-conditional-in-test - if (actionType === 'MultichainNetworkController:getState') { - return { - multichainNetworkConfigurationsByChainId: { - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { - chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - name: 'Solana Mainnet', - }, - }, - selectedMultichainNetworkChainId: - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - isEvmSelected: false, - networksWithTransactionActivity: {}, - }; - } - throw new Error(`Unexpected action type: ${actionType}`); - }, - ); - // Initialize from configurations controller.init(); @@ -464,35 +478,27 @@ describe('NetworkEnablementController', () => { }); it('handles missing MultichainNetworkController gracefully', () => { - const { controller, messenger } = setupController(); - - jest - .spyOn(messenger, 'call') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation((actionType: string, ..._args: any[]): any => { - // eslint-disable-next-line jest/no-conditional-in-test - if (actionType === 'NetworkController:getState') { - return { - selectedNetworkClientId: 'mainnet', - networkConfigurationsByChainId: { - '0x1': { chainId: '0x1', name: 'Ethereum Mainnet' }, - '0xe708': { chainId: '0xe708', name: 'Linea Mainnet' }, - '0x2105': { chainId: '0x2105', name: 'Base Mainnet' }, - }, - networksMetadata: {}, - }; - } - // eslint-disable-next-line jest/no-conditional-in-test - if (actionType === 'MultichainNetworkController:getState') { - return { - multichainNetworkConfigurationsByChainId: {}, - selectedMultichainNetworkChainId: 'eip155:1', - isEvmSelected: true, - networksWithTransactionActivity: {}, - }; - } - throw new Error(`Unexpected action type: ${actionType}`); - }); + const mockGetNetworkState = jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: 'mainnet', + networkConfigurationsByChainId: { + '0x1': { chainId: '0x1', name: 'Ethereum Mainnet' }, + '0xe708': { chainId: '0xe708', name: 'Linea Mainnet' }, + '0x2105': { chainId: '0x2105', name: 'Base Mainnet' }, + }, + networksMetadata: {}, + })); + const mockGetMultichainNetworkState = jest + .fn() + .mockImplementation(() => ({ + multichainNetworkConfigurationsByChainId: {}, + selectedMultichainNetworkChainId: 'eip155:1', + isEvmSelected: true, + networksWithTransactionActivity: {}, + })); + const { controller } = setupController({ + mockGetNetworkState, + mockGetMultichainNetworkState, + }); // Should not throw expect(() => controller.init()).not.toThrow(); @@ -504,45 +510,36 @@ describe('NetworkEnablementController', () => { }); it('creates namespace buckets for all configured networks', () => { - const { controller } = setupController(); - - jest - // eslint-disable-next-line dot-notation - .spyOn(controller['messagingSystem'], 'call') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation((actionType: string, ..._args: any[]): any => { - // eslint-disable-next-line jest/no-conditional-in-test - if (actionType === 'NetworkController:getState') { - return { - selectedNetworkClientId: 'mainnet', - networkConfigurationsByChainId: { - '0x1': { chainId: '0x1', name: 'Ethereum' }, - '0x89': { chainId: '0x89', name: 'Polygon' }, - }, - networksMetadata: {}, - }; - } - // eslint-disable-next-line jest/no-conditional-in-test - if (actionType === 'MultichainNetworkController:getState') { - return { - multichainNetworkConfigurationsByChainId: { - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { - chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - name: 'Solana', - }, - 'bip122:000000000019d6689c085ae165831e93': { - chainId: 'bip122:000000000019d6689c085ae165831e93', - name: 'Bitcoin', - }, - }, - selectedMultichainNetworkChainId: - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - isEvmSelected: false, - networksWithTransactionActivity: {}, - }; - } - throw new Error(`Unexpected action type: ${actionType}`); - }); + const mockGetNetworkState = jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: 'mainnet', + networkConfigurationsByChainId: { + '0x1': { chainId: '0x1', name: 'Ethereum' }, + '0x89': { chainId: '0x89', name: 'Polygon' }, + }, + networksMetadata: {}, + })); + const mockGetMultichainNetworkState = jest + .fn() + .mockImplementation(() => ({ + multichainNetworkConfigurationsByChainId: { + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + name: 'Solana', + }, + 'bip122:000000000019d6689c085ae165831e93': { + chainId: 'bip122:000000000019d6689c085ae165831e93', + name: 'Bitcoin', + }, + }, + selectedMultichainNetworkChainId: + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + isEvmSelected: false, + networksWithTransactionActivity: {}, + })); + const { controller } = setupController({ + mockGetNetworkState, + mockGetMultichainNetworkState, + }); controller.init(); @@ -559,7 +556,40 @@ describe('NetworkEnablementController', () => { }); it('creates new namespace buckets for networks that do not exist', () => { - const { controller } = setupController(); + const mockGetNetworkState = jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: 'mainnet', + networkConfigurationsByChainId: { + '0x1': { + chainId: '0x1' as Hex, + name: 'Ethereum', + blockExplorerUrls: [], + defaultRpcEndpointIndex: 0, + nativeCurrency: 'ETH', + rpcEndpoints: [], + }, + }, + networksMetadata: {}, + })); + const mockGetMultichainNetworkState = jest + .fn() + .mockImplementation(() => ({ + multichainNetworkConfigurationsByChainId: { + 'cosmos:cosmoshub-4': { + chainId: 'cosmos:cosmoshub-4' as CaipChainId, + name: 'Cosmos Hub', + isEvm: false as const, + nativeCurrency: + 'cosmos:cosmoshub-4/slip44:118' as `${string}:${string}/${string}:${string}`, + }, + }, + selectedMultichainNetworkChainId: 'cosmos:cosmoshub-4' as CaipChainId, + isEvmSelected: false, + networksWithTransactionActivity: {}, + })); + const { controller } = setupController({ + mockGetNetworkState, + mockGetMultichainNetworkState, + }); // Start with empty state to test namespace bucket creation // eslint-disable-next-line dot-notation @@ -567,45 +597,6 @@ describe('NetworkEnablementController', () => { state.enabledNetworkMap = {}; }); - jest - // eslint-disable-next-line dot-notation - .spyOn(controller['messagingSystem'], 'call') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation((actionType: string, ..._args: unknown[]): any => { - const responses = { - 'NetworkController:getState': { - selectedNetworkClientId: 'mainnet', - networkConfigurationsByChainId: { - '0x1': { - chainId: '0x1' as Hex, - name: 'Ethereum', - blockExplorerUrls: [], - defaultRpcEndpointIndex: 0, - nativeCurrency: 'ETH', - rpcEndpoints: [], - }, - }, - networksMetadata: {}, - }, - 'MultichainNetworkController:getState': { - multichainNetworkConfigurationsByChainId: { - 'cosmos:cosmoshub-4': { - chainId: 'cosmos:cosmoshub-4' as CaipChainId, - name: 'Cosmos Hub', - isEvm: false as const, - nativeCurrency: - 'cosmos:cosmoshub-4/slip44:118' as `${string}:${string}/${string}:${string}`, - }, - }, - selectedMultichainNetworkChainId: - 'cosmos:cosmoshub-4' as CaipChainId, - isEvmSelected: false, - networksWithTransactionActivity: {}, - }, - }; - return responses[actionType as keyof typeof responses]; - }); - controller.init(); // Should have created namespace buckets for both EIP-155 and Cosmos @@ -618,43 +609,33 @@ describe('NetworkEnablementController', () => { describe('enableAllPopularNetworks', () => { it('enables all popular networks that exist in controller configurations and Solana mainnet', () => { - const { controller } = setupInitializedController(); - - // Mock the network configurations - jest - // eslint-disable-next-line dot-notation - .spyOn(controller['messagingSystem'], 'call') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation((actionType: string, ..._args: any[]): any => { - // eslint-disable-next-line jest/no-conditional-in-test - if (actionType === 'NetworkController:getState') { - return { - selectedNetworkClientId: 'mainnet', - networkConfigurationsByChainId: { - '0x1': { chainId: '0x1', name: 'Ethereum Mainnet' }, - '0xe708': { chainId: '0xe708', name: 'Linea Mainnet' }, - '0x2105': { chainId: '0x2105', name: 'Base Mainnet' }, - }, - networksMetadata: {}, - }; - } - // eslint-disable-next-line jest/no-conditional-in-test - if (actionType === 'MultichainNetworkController:getState') { - return { - multichainNetworkConfigurationsByChainId: { - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { - chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - name: 'Solana Mainnet', - }, - }, - selectedMultichainNetworkChainId: - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - isEvmSelected: false, - networksWithTransactionActivity: {}, - }; - } - throw new Error(`Unexpected action type: ${actionType}`); - }); + const mockGetNetworkState = jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: 'mainnet', + networkConfigurationsByChainId: { + '0x1': { chainId: '0x1', name: 'Ethereum Mainnet' }, + '0xe708': { chainId: '0xe708', name: 'Linea Mainnet' }, + '0x2105': { chainId: '0x2105', name: 'Base Mainnet' }, + }, + networksMetadata: {}, + })); + const mockGetMultichainNetworkState = jest + .fn() + .mockImplementation(() => ({ + multichainNetworkConfigurationsByChainId: { + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + name: 'Solana Mainnet', + }, + }, + selectedMultichainNetworkChainId: + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + isEvmSelected: false, + networksWithTransactionActivity: {}, + })); + const { controller } = setupController({ + mockGetNetworkState, + mockGetMultichainNetworkState, + }); // Initially disable some networks controller.disableNetwork('0xe708'); // Linea @@ -705,47 +686,48 @@ 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 => { - // eslint-disable-next-line jest/no-conditional-in-test - 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: {}, - }; - } - // eslint-disable-next-line jest/no-conditional-in-test - if (actionType === 'MultichainNetworkController:getState') { - return { - multichainNetworkConfigurationsByChainId: { - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { - chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - name: 'Solana Mainnet', - }, - }, - selectedMultichainNetworkChainId: - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - isEvmSelected: false, - networksWithTransactionActivity: {}, - }; - } - throw new Error(`Unexpected action type: ${actionType}`); - }, - ); + const mockGetNetworkState = jest.fn().mockImplementation(() => { + // Create mock configurations for all popular networks + const networkConfigurationsByChainId = POPULAR_NETWORKS.reduce( + ( + acc: Record< + string, + { + chainId: string; + name: string; + } + >, + chainId: string, + ) => { + acc[chainId] = { chainId, name: `Network ${chainId}` }; + return acc; + }, + {}, + ); + return { + selectedNetworkClientId: 'mainnet', + networkConfigurationsByChainId, + networksMetadata: {}, + }; + }); + const mockGetMultichainNetworkState = jest + .fn() + .mockImplementation(() => ({ + multichainNetworkConfigurationsByChainId: { + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + name: 'Solana Mainnet', + }, + }, + selectedMultichainNetworkChainId: + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + isEvmSelected: false, + networksWithTransactionActivity: {}, + })); + const { controller } = setupController({ + mockGetNetworkState, + mockGetMultichainNetworkState, + }); // The function should enable all popular networks defined in constants expect(() => controller.enableAllPopularNetworks()).not.toThrow(); @@ -778,47 +760,37 @@ describe('NetworkEnablementController', () => { }); it('does not disable any existing networks', async () => { - const { controller, messenger } = setupInitializedController(); - - // Mock the network configurations to include popular networks - jest - // eslint-disable-next-line dot-notation - .spyOn(controller['messagingSystem'], 'call') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation((actionType: string, ..._args: any[]): any => { - // eslint-disable-next-line jest/no-conditional-in-test - if (actionType === 'NetworkController:getState') { - return { - selectedNetworkClientId: 'mainnet', - networkConfigurationsByChainId: { - '0x1': { chainId: '0x1', name: 'Ethereum Mainnet' }, - '0xe708': { chainId: '0xe708', name: 'Linea Mainnet' }, - '0x2105': { chainId: '0x2105', name: 'Base Mainnet' }, - '0x2': { chainId: '0x2', name: 'Test Network' }, // Non-popular network - }, - networksMetadata: {}, - }; - } - // eslint-disable-next-line jest/no-conditional-in-test - if (actionType === 'MultichainNetworkController:getState') { - return { - multichainNetworkConfigurationsByChainId: { - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { - chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - name: 'Solana Mainnet', - }, - }, - selectedMultichainNetworkChainId: - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - isEvmSelected: false, - networksWithTransactionActivity: {}, - }; - } - throw new Error(`Unexpected action type: ${actionType}`); - }); + const mockGetNetworkState = jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: 'mainnet', + networkConfigurationsByChainId: { + '0x1': { chainId: '0x1', name: 'Ethereum Mainnet' }, + '0xe708': { chainId: '0xe708', name: 'Linea Mainnet' }, + '0x2105': { chainId: '0x2105', name: 'Base Mainnet' }, + '0x2': { chainId: '0x2', name: 'Test Network' }, // Non-popular network + }, + networksMetadata: {}, + })); + const mockGetMultichainNetworkState = jest + .fn() + .mockImplementation(() => ({ + multichainNetworkConfigurationsByChainId: { + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': { + chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + name: 'Solana Mainnet', + }, + }, + selectedMultichainNetworkChainId: + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + isEvmSelected: false, + networksWithTransactionActivity: {}, + })); + const { controller, rootMessenger } = setupController({ + mockGetNetworkState, + mockGetMultichainNetworkState, + }); // Add a non-popular network - messenger.publish('NetworkController:networkAdded', { + rootMessenger.publish('NetworkController:networkAdded', { chainId: '0x2', // A network not in POPULAR_NETWORKS blockExplorerUrls: [], defaultRpcEndpointIndex: 0, @@ -857,37 +829,31 @@ describe('NetworkEnablementController', () => { }); it('enables Bitcoin mainnet when configured in MultichainNetworkController', () => { - const { controller } = setupController(); - - // Mock the network configurations to include Bitcoin - jest - // eslint-disable-next-line dot-notation - .spyOn(controller['messagingSystem'], 'call') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation((actionType: string, ..._args: unknown[]): any => { - const responses = { - 'NetworkController:getState': { - selectedNetworkClientId: 'mainnet', - networkConfigurationsByChainId: {}, - networksMetadata: {}, + const mockGetNetworkState = jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: 'mainnet', + networkConfigurationsByChainId: {}, + networksMetadata: {}, + })); + const mockGetMultichainNetworkState = jest + .fn() + .mockImplementation(() => ({ + multichainNetworkConfigurationsByChainId: { + [BtcScope.Mainnet]: { + chainId: BtcScope.Mainnet, + name: 'Bitcoin Mainnet', + isEvm: false as const, + nativeCurrency: + 'bip122:000000000019d6689c085ae165831e93/slip44:0' as `${string}:${string}/${string}:${string}`, }, - '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}`, - }, - }, - selectedMultichainNetworkChainId: BtcScope.Mainnet, - isEvmSelected: false, - networksWithTransactionActivity: {}, - }, - }; - return responses[actionType as keyof typeof responses]; - }); + }, + selectedMultichainNetworkChainId: BtcScope.Mainnet, + isEvmSelected: false, + networksWithTransactionActivity: {}, + })); + const { controller } = setupController({ + mockGetNetworkState, + mockGetMultichainNetworkState, + }); // Initially disable Bitcoin to test enablement // eslint-disable-next-line dot-notation @@ -907,7 +873,7 @@ describe('NetworkEnablementController', () => { describe('enableNetwork', () => { it('enables a network and clears all others in the same namespace', () => { - const { controller } = setupInitializedController(); + const { controller } = setupController(); // Disable a popular network (Ethereum Mainnet) controller.disableNetwork('0x1'); @@ -957,10 +923,10 @@ describe('NetworkEnablementController', () => { }); it('enables any network and clears all others (exclusive behavior)', async () => { - const { controller, messenger } = setupInitializedController(); + const { controller, rootMessenger } = setupController(); // Add a non-popular network - messenger.publish('NetworkController:networkAdded', { + rootMessenger.publish('NetworkController:networkAdded', { chainId: '0x2', blockExplorerUrls: [], defaultRpcEndpointIndex: 0, @@ -1101,10 +1067,10 @@ describe('NetworkEnablementController', () => { }); it('handle no namespace bucket', async () => { - const { controller, messenger } = setupController(); + const { controller, rootMessenger } = setupController(); // add new network with no namespace bucket - messenger.publish('NetworkController:networkAdded', { + rootMessenger.publish('NetworkController:networkAdded', { // @ts-expect-error Intentionally passing an invalid chain ID chainId: 'bip122:000000000019d6689c085ae165831e93', blockExplorerUrls: [], @@ -1146,7 +1112,7 @@ describe('NetworkEnablementController', () => { describe('disableNetwork', () => { it('disables an EVM network using hex chain ID', () => { - const { controller } = setupInitializedController(); + const { controller } = setupController(); // Disable a network (but not the last one) controller.disableNetwork('0xe708'); // Linea Mainnet @@ -1182,7 +1148,7 @@ describe('NetworkEnablementController', () => { }); it('disables the last active network for an EVM namespace', () => { - const { controller } = setupInitializedController(); + const { controller } = setupController(); // disable all networks except one controller.disableNetwork('0xe708'); // Linea Mainnet @@ -1231,7 +1197,7 @@ describe('NetworkEnablementController', () => { describe('isNetworkEnabled', () => { it('returns true for enabled networks using hex chain ID', () => { - const { controller } = setupInitializedController(); + const { controller } = setupController(); // Test default enabled networks expect(controller.isNetworkEnabled('0x1')).toBe(true); // Ethereum Mainnet @@ -1240,7 +1206,7 @@ describe('NetworkEnablementController', () => { }); it('returns false for disabled networks using hex chain ID', () => { - const { controller } = setupInitializedController(); + const { controller } = setupController(); // Disable a network and test controller.disableNetwork('0xe708'); // Linea Mainnet (not the last one) @@ -1252,7 +1218,7 @@ describe('NetworkEnablementController', () => { }); it('returns true for enabled networks using CAIP chain ID', () => { - const { controller } = setupInitializedController(); + const { controller } = setupController(); // Test EVM networks with CAIP format expect(controller.isNetworkEnabled('eip155:1')).toBe(true); // Ethereum Mainnet @@ -1266,7 +1232,7 @@ describe('NetworkEnablementController', () => { }); it('returns false for disabled networks using CAIP chain ID', () => { - const { controller } = setupInitializedController(); + const { controller } = setupController(); // Disable a network using hex and test with CAIP controller.disableNetwork('0xe708'); // Linea Mainnet (not the last one) @@ -1301,7 +1267,7 @@ describe('NetworkEnablementController', () => { }); it('works correctly after enabling/disabling networks', () => { - const { controller } = setupInitializedController(); + const { controller } = setupController(); // Initially enabled expect(controller.isNetworkEnabled('0xe708')).toBe(true); @@ -1316,7 +1282,7 @@ describe('NetworkEnablementController', () => { }); it('maintains consistency between hex and CAIP formats for same network', () => { - const { controller } = setupInitializedController(); + const { controller } = setupController(); // Both formats should return the same result for the same network expect(controller.isNetworkEnabled('0x1')).toBe( @@ -1338,13 +1304,13 @@ describe('NetworkEnablementController', () => { }); it('works with dynamically added networks', async () => { - const { controller, messenger } = setupController(); + const { controller, rootMessenger } = setupController(); // Initially, Avalanche network should not be enabled (doesn't exist) expect(controller.isNetworkEnabled('0xa86a')).toBe(false); // Add Avalanche network - messenger.publish('NetworkController:networkAdded', { + rootMessenger.publish('NetworkController:networkAdded', { chainId: '0xa86a', blockExplorerUrls: [], defaultRpcEndpointIndex: 0, @@ -1367,7 +1333,7 @@ describe('NetworkEnablementController', () => { }); it('handles networks across different namespaces independently', async () => { - const { controller, messenger } = setupController(); + const { controller, rootMessenger } = setupController(); // EVM networks should not affect Solana network status expect( @@ -1384,7 +1350,7 @@ describe('NetworkEnablementController', () => { ).toBe(true); // Add a Bitcoin network - messenger.publish('NetworkController:networkAdded', { + rootMessenger.publish('NetworkController:networkAdded', { // @ts-expect-error Intentionally testing with Bitcoin network chainId: 'bip122:000000000019d6689c085ae165831e93', blockExplorerUrls: [], @@ -1498,10 +1464,10 @@ describe('NetworkEnablementController', () => { }); it('handles Bitcoin network addition dynamically', async () => { - const { controller, messenger } = setupController(); + const { controller, rootMessenger } = setupController(); // Add Bitcoin testnet dynamically - messenger.publish('NetworkController:networkAdded', { + rootMessenger.publish('NetworkController:networkAdded', { // @ts-expect-error Testing with Bitcoin network chainId: BtcScope.Testnet, blockExplorerUrls: [], diff --git a/packages/network-enablement-controller/src/NetworkEnablementController.ts b/packages/network-enablement-controller/src/NetworkEnablementController.ts index d4d2804d7da..f1bb95a997e 100644 --- a/packages/network-enablement-controller/src/NetworkEnablementController.ts +++ b/packages/network-enablement-controller/src/NetworkEnablementController.ts @@ -1,10 +1,10 @@ -import { BaseController } from '@metamask/base-controller'; +import { BaseController } from '@metamask/base-controller/next'; import type { ControllerGetStateAction, ControllerStateChangeEvent, - RestrictedMessenger, -} from '@metamask/base-controller'; +} from '@metamask/base-controller/next'; import { BuiltInNetworkName, ChainId } from '@metamask/controller-utils'; +import type { Messenger } from '@metamask/messenger'; import type { MultichainNetworkControllerGetStateAction } from '@metamask/multichain-network-controller'; import type { NetworkControllerGetStateAction, @@ -94,12 +94,10 @@ export type AllowedEvents = | NetworkControllerStateChangeEvent | TransactionControllerTransactionSubmittedEvent; -export type NetworkEnablementControllerMessenger = RestrictedMessenger< +export type NetworkEnablementControllerMessenger = Messenger< typeof controllerName, NetworkEnablementControllerActions | AllowedActions, - NetworkEnablementControllerEvents | AllowedEvents, - AllowedActions['type'], - AllowedEvents['type'] + NetworkEnablementControllerEvents | AllowedEvents >; /** @@ -231,10 +229,10 @@ export class NetworkEnablementController extends BaseController< enableAllPopularNetworks(): void { this.update((s) => { // Get current network configurations to check if networks exist - const networkControllerState = this.messagingSystem.call( + const networkControllerState = this.messenger.call( 'NetworkController:getState', ); - const multichainState = this.messagingSystem.call( + const multichainState = this.messenger.call( 'MultichainNetworkController:getState', ); @@ -296,12 +294,12 @@ export class NetworkEnablementController extends BaseController< init(): void { this.update((s) => { // Get network configurations from NetworkController (EVM networks) - const networkControllerState = this.messagingSystem.call( + const networkControllerState = this.messenger.call( 'NetworkController:getState', ); // Get network configurations from MultichainNetworkController (all networks) - const multichainState = this.messagingSystem.call( + const multichainState = this.messenger.call( 'MultichainNetworkController:getState', ); diff --git a/packages/network-enablement-controller/tsconfig.build.json b/packages/network-enablement-controller/tsconfig.build.json index a4d958a3017..11328a92b94 100644 --- a/packages/network-enablement-controller/tsconfig.build.json +++ b/packages/network-enablement-controller/tsconfig.build.json @@ -10,7 +10,8 @@ { "path": "../network-controller/tsconfig.build.json" }, { "path": "../multichain-network-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, - { "path": "../transaction-controller/tsconfig.build.json" } + { "path": "../transaction-controller/tsconfig.build.json" }, + { "path": "../messenger/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/network-enablement-controller/tsconfig.json b/packages/network-enablement-controller/tsconfig.json index 557e433b745..e1ebf385335 100644 --- a/packages/network-enablement-controller/tsconfig.json +++ b/packages/network-enablement-controller/tsconfig.json @@ -9,7 +9,8 @@ { "path": "../network-controller" }, { "path": "../multichain-network-controller" }, { "path": "../controller-utils" }, - { "path": "../transaction-controller" } + { "path": "../transaction-controller" }, + { "path": "../messenger" } ], "include": ["../../types", "./src"] } diff --git a/yarn.lock b/yarn.lock index 0cf2a57b624..3fbc4f07449 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4020,6 +4020,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^8.3.0" "@metamask/controller-utils": "npm:^11.12.0" + "@metamask/messenger": "npm:^0.2.0" "@metamask/multichain-network-controller": "npm:^0.12.0" "@metamask/network-controller": "npm:^24.1.0" "@metamask/transaction-controller": "npm:^60.2.0"