diff --git a/README.md b/README.md index 9645d28513e..6ffab5f882b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Each package in this repository has its own README where you can find installati - [`@metamask/logging-controller`](packages/logging-controller) - [`@metamask/message-manager`](packages/message-manager) - [`@metamask/multichain`](packages/multichain) +- [`@metamask/multichain-network-controller`](packages/multichain-network-controller) - [`@metamask/multichain-transactions-controller`](packages/multichain-transactions-controller) - [`@metamask/name-controller`](packages/name-controller) - [`@metamask/network-controller`](packages/network-controller) @@ -85,6 +86,7 @@ linkStyle default opacity:0.5 logging_controller(["@metamask/logging-controller"]); message_manager(["@metamask/message-manager"]); multichain(["@metamask/multichain"]); + multichain_network_controller(["@metamask/multichain-network-controller"]); multichain_transactions_controller(["@metamask/multichain-transactions-controller"]); name_controller(["@metamask/name-controller"]); network_controller(["@metamask/network-controller"]); @@ -105,6 +107,7 @@ linkStyle default opacity:0.5 user_operation_controller(["@metamask/user-operation-controller"]); accounts_controller --> base_controller; accounts_controller --> keyring_controller; + accounts_controller --> network_controller; address_book_controller --> base_controller; address_book_controller --> controller_utils; announcement_controller --> base_controller; @@ -116,10 +119,15 @@ linkStyle default opacity:0.5 assets_controllers --> approval_controller; assets_controllers --> keyring_controller; assets_controllers --> network_controller; + assets_controllers --> permission_controller; assets_controllers --> preferences_controller; base_controller --> json_rpc_engine; composable_controller --> base_controller; composable_controller --> json_rpc_engine; + earn_controller --> base_controller; + earn_controller --> controller_utils; + earn_controller --> accounts_controller; + earn_controller --> network_controller; ens_controller --> base_controller; ens_controller --> controller_utils; ens_controller --> network_controller; @@ -136,8 +144,15 @@ linkStyle default opacity:0.5 message_manager --> base_controller; message_manager --> controller_utils; multichain --> controller_utils; + multichain --> json_rpc_engine; multichain --> network_controller; multichain --> permission_controller; + multichain_network_controller --> base_controller; + multichain_network_controller --> keyring_controller; + multichain_transactions_controller --> base_controller; + multichain_transactions_controller --> polling_controller; + multichain_transactions_controller --> accounts_controller; + multichain_transactions_controller --> keyring_controller; name_controller --> base_controller; name_controller --> controller_utils; network_controller --> base_controller; @@ -184,6 +199,7 @@ linkStyle default opacity:0.5 signature_controller --> keyring_controller; signature_controller --> logging_controller; signature_controller --> network_controller; + token_search_discovery_controller --> base_controller; transaction_controller --> base_controller; transaction_controller --> controller_utils; transaction_controller --> accounts_controller; diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index e777270690c..4b387a4f112 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -16,9 +16,6 @@ "packages/accounts-controller/src/AccountsController.test.ts": { "import-x/namespace": 1 }, - "packages/accounts-controller/src/utils.ts": { - "jsdoc/tag-lines": 3 - }, "packages/address-book-controller/src/AddressBookController.ts": { "jsdoc/check-tag-names": 13 }, diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 2f140161fbb..9260adcd998 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -63,6 +63,7 @@ "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^19.1.0", + "@metamask/network-controller": "^22.2.1", "@metamask/providers": "^18.1.1", "@metamask/snaps-controllers": "^9.19.0", "@types/jest": "^27.4.1", @@ -75,7 +76,8 @@ "webextension-polyfill": "^0.12.0" }, "peerDependencies": { - "@metamask/keyring-controller": "^19.0.0", + "@metamask/keyring-controller": "^19.1.0", + "@metamask/network-controller": "^22.2.1", "@metamask/providers": "^18.1.0", "@metamask/snaps-controllers": "^9.19.0", "webextension-polyfill": "^0.10.0 || ^0.11.0 || ^0.12.0" diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 9df36e855a1..ad80a09febf 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -1,4 +1,5 @@ import { Messenger } from '@metamask/base-controller'; +import { InfuraNetworkType } from '@metamask/controller-utils'; import type { AccountAssetListUpdatedEventPayload, AccountBalancesUpdatedEventPayload, @@ -17,6 +18,7 @@ import type { InternalAccount, InternalAccountType, } from '@metamask/keyring-internal-api'; +import type { NetworkClientId } from '@metamask/network-controller'; import type { SnapControllerState } from '@metamask/snaps-controllers'; import { SnapStatus } from '@metamask/snaps-utils'; import type { CaipChainId } from '@metamask/utils'; @@ -307,6 +309,7 @@ function buildAccountsControllerMessenger(messenger = buildMessenger()) { 'SnapKeyring:accountAssetListUpdated', 'SnapKeyring:accountBalancesUpdated', 'SnapKeyring:accountTransactionsUpdated', + 'MultichainNetworkController:networkDidChange', ], allowedActions: [ 'KeyringController:getAccounts', @@ -339,6 +342,7 @@ function setupAccountsController({ AccountsControllerActions | AllowedActions, AccountsControllerEvents | AllowedEvents >; + triggerMultichainNetworkChange: (id: NetworkClientId | CaipChainId) => void; } { const accountsControllerMessenger = buildAccountsControllerMessenger(messenger); @@ -347,10 +351,37 @@ function setupAccountsController({ messenger: accountsControllerMessenger, state: { ...defaultState, ...initialState }, }); - return { accountsController, messenger }; + + const triggerMultichainNetworkChange = (id: NetworkClientId | CaipChainId) => + messenger.publish('MultichainNetworkController:networkDidChange', id); + + return { accountsController, messenger, triggerMultichainNetworkChange }; } describe('AccountsController', () => { + const mockBtcAccount = createExpectedInternalAccount({ + id: 'mock-non-evm', + name: 'non-evm', + address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', + keyringType: KeyringTypes.snap, + type: BtcAccountType.P2wpkh, + }); + + const mockOlderEvmAccount = createExpectedInternalAccount({ + id: 'mock-id-1', + name: 'mock account 1', + address: 'mock-address-1', + keyringType: KeyringTypes.hd, + lastSelected: 11111, + }); + const mockNewerEvmAccount = createExpectedInternalAccount({ + id: 'mock-id-2', + name: 'mock account 2', + address: 'mock-address-2', + keyringType: KeyringTypes.hd, + lastSelected: 22222, + }); + describe('onSnapStateChange', () => { it('be used enable an account if the Snap is enabled and not blocked', async () => { const messenger = buildMessenger(); @@ -1514,6 +1545,59 @@ describe('AccountsController', () => { }); }); + describe('handle MultichainNetworkController:networkDidChange event', () => { + it('should update selected account to non-EVM account when switching to non-EVM network', () => { + const messenger = buildMessenger(); + const { accountsController, triggerMultichainNetworkChange } = + setupAccountsController({ + initialState: { + internalAccounts: { + accounts: { + [mockOlderEvmAccount.id]: mockOlderEvmAccount, + [mockNewerEvmAccount.id]: mockNewerEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, + }, + selectedAccount: mockNewerEvmAccount.id, + }, + }, + messenger, + }); + + // Triggered from network switch to Bitcoin mainnet + triggerMultichainNetworkChange(BtcScope.Mainnet); + + // BTC account is now selected + expect(accountsController.state.internalAccounts.selectedAccount).toBe( + mockBtcAccount.id, + ); + }); + + it('should update selected account to EVM account when switching to EVM network', () => { + const messenger = buildMessenger(); + const { accountsController, triggerMultichainNetworkChange } = + setupAccountsController({ + initialState: { + internalAccounts: { + accounts: { + [mockOlderEvmAccount.id]: mockOlderEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, + }, + selectedAccount: mockBtcAccount.id, + }, + }, + messenger, + }); + + // Triggered from network switch to Bitcoin mainnet + triggerMultichainNetworkChange(InfuraNetworkType.mainnet); + + // ETH mainnet account is now selected + expect(accountsController.state.internalAccounts.selectedAccount).toBe( + mockOlderEvmAccount.id, + ); + }); + }); + describe('updateAccounts', () => { const mockAddress1 = '0x123'; const mockAddress2 = '0x456'; @@ -2145,29 +2229,6 @@ describe('AccountsController', () => { }); describe('getSelectedAccount', () => { - const mockNonEvmAccount = createExpectedInternalAccount({ - id: 'mock-non-evm', - name: 'non-evm', - address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', - keyringType: KeyringTypes.snap, - type: BtcAccountType.P2wpkh, - }); - - const mockOlderEvmAccount = createExpectedInternalAccount({ - id: 'mock-id-1', - name: 'mock account 1', - address: 'mock-address-1', - keyringType: KeyringTypes.hd, - lastSelected: 11111, - }); - const mockNewerEvmAccount = createExpectedInternalAccount({ - id: 'mock-id-2', - name: 'mock account 2', - address: 'mock-address-2', - keyringType: KeyringTypes.hd, - lastSelected: 22222, - }); - it.each([ { lastSelectedAccount: mockNewerEvmAccount, @@ -2178,7 +2239,7 @@ describe('AccountsController', () => { expected: mockOlderEvmAccount, }, { - lastSelectedAccount: mockNonEvmAccount, + lastSelectedAccount: mockBtcAccount, expected: mockNewerEvmAccount, }, ])( @@ -2190,7 +2251,7 @@ describe('AccountsController', () => { accounts: { [mockOlderEvmAccount.id]: mockOlderEvmAccount, [mockNewerEvmAccount.id]: mockNewerEvmAccount, - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, }, selectedAccount: lastSelectedAccount.id, }, @@ -2206,9 +2267,9 @@ describe('AccountsController', () => { initialState: { internalAccounts: { accounts: { - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, }, - selectedAccount: mockNonEvmAccount.id, + selectedAccount: mockBtcAccount.id, }, }, }); @@ -2235,29 +2296,6 @@ describe('AccountsController', () => { }); describe('getSelectedMultichainAccount', () => { - const mockNonEvmAccount = createExpectedInternalAccount({ - id: 'mock-non-evm', - name: 'non-evm', - address: 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6', - keyringType: KeyringTypes.snap, - type: BtcAccountType.P2wpkh, - }); - - const mockOlderEvmAccount = createExpectedInternalAccount({ - id: 'mock-id-1', - name: 'mock account 1', - address: 'mock-address-1', - keyringType: KeyringTypes.hd, - lastSelected: 11111, - }); - const mockNewerEvmAccount = createExpectedInternalAccount({ - id: 'mock-id-2', - name: 'mock account 2', - address: 'mock-address-2', - keyringType: KeyringTypes.hd, - lastSelected: 22222, - }); - it.each([ { chainId: undefined, @@ -2266,18 +2304,18 @@ describe('AccountsController', () => { }, { chainId: undefined, - selectedAccount: mockNonEvmAccount, - expected: mockNonEvmAccount, + selectedAccount: mockBtcAccount, + expected: mockBtcAccount, }, { chainId: 'eip155:1', - selectedAccount: mockNonEvmAccount, + selectedAccount: mockBtcAccount, expected: mockNewerEvmAccount, }, { chainId: 'bip122:000000000019d6689c085ae165831e93', - selectedAccount: mockNonEvmAccount, - expected: mockNonEvmAccount, + selectedAccount: mockBtcAccount, + expected: mockBtcAccount, }, ])( "chainId $chainId with selectedAccount '$selectedAccount.id' should return $expected.id", @@ -2288,7 +2326,7 @@ describe('AccountsController', () => { accounts: { [mockOlderEvmAccount.id]: mockOlderEvmAccount, [mockNewerEvmAccount.id]: mockNewerEvmAccount, - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, }, selectedAccount: selectedAccount.id, }, @@ -2313,9 +2351,9 @@ describe('AccountsController', () => { accounts: { [mockOlderEvmAccount.id]: mockOlderEvmAccount, [mockNewerEvmAccount.id]: mockNewerEvmAccount, - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, }, - selectedAccount: mockNonEvmAccount.id, + selectedAccount: mockBtcAccount.id, }, }, }); diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index f81f77565b1..4c582b03ab1 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -1,46 +1,47 @@ -import type { - ControllerGetStateAction, - ControllerStateChangeEvent, - ExtractEventPayload, - RestrictedMessenger, +import { + type ControllerGetStateAction, + type ControllerStateChangeEvent, + type ExtractEventPayload, + type RestrictedMessenger, + BaseController, } from '@metamask/base-controller'; -import { BaseController } from '@metamask/base-controller'; -import type { - SnapKeyringAccountAssetListUpdatedEvent, - SnapKeyringAccountBalancesUpdatedEvent, - SnapKeyringAccountTransactionsUpdatedEvent, +import { + type SnapKeyringAccountAssetListUpdatedEvent, + type SnapKeyringAccountBalancesUpdatedEvent, + type SnapKeyringAccountTransactionsUpdatedEvent, + SnapKeyring, } from '@metamask/eth-snap-keyring'; -import { SnapKeyring } from '@metamask/eth-snap-keyring'; import { EthAccountType, EthMethod, EthScope, isEvmAccountType, } from '@metamask/keyring-api'; -import { KeyringTypes } from '@metamask/keyring-controller'; -import type { - KeyringControllerState, - KeyringControllerGetKeyringForAccountAction, - KeyringControllerGetKeyringsByTypeAction, - KeyringControllerGetAccountsAction, - KeyringControllerStateChangeEvent, +import { + type KeyringControllerState, + type KeyringControllerGetKeyringForAccountAction, + type KeyringControllerGetKeyringsByTypeAction, + type KeyringControllerGetAccountsAction, + type KeyringControllerStateChangeEvent, + KeyringTypes, } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; +import type { NetworkClientId } from '@metamask/network-controller'; import type { SnapControllerState, SnapStateChange, } from '@metamask/snaps-controllers'; import type { SnapId } from '@metamask/snaps-sdk'; import type { Snap } from '@metamask/snaps-utils'; -import type { CaipChainId } from '@metamask/utils'; import { type Keyring, type Json, + type CaipChainId, isCaipChainId, parseCaipChainId, } from '@metamask/utils'; -import type { Draft } from 'immer'; +import type { MultichainNetworkControllerNetworkDidChangeEvent } from './types'; import { getUUIDFromAddressOfNormalAccount, isNormalKeyringType, @@ -187,7 +188,8 @@ export type AllowedEvents = | KeyringControllerStateChangeEvent | SnapKeyringAccountAssetListUpdatedEvent | SnapKeyringAccountBalancesUpdatedEvent - | SnapKeyringAccountTransactionsUpdatedEvent; + | SnapKeyringAccountTransactionsUpdatedEvent + | MultichainNetworkControllerNetworkDidChangeEvent; export type AccountsControllerEvents = | AccountsControllerChangeEvent @@ -280,43 +282,7 @@ export class AccountsController extends BaseController< }, }); - this.messagingSystem.subscribe( - 'SnapController:stateChange', - (snapStateState) => this.#handleOnSnapStateChange(snapStateState), - ); - - this.messagingSystem.subscribe( - 'KeyringController:stateChange', - (keyringState) => this.#handleOnKeyringStateChange(keyringState), - ); - - this.messagingSystem.subscribe( - 'SnapKeyring:accountAssetListUpdated', - (snapAccountEvent) => - this.#handleOnSnapKeyringAccountEvent( - 'AccountsController:accountAssetListUpdated', - snapAccountEvent, - ), - ); - - this.messagingSystem.subscribe( - 'SnapKeyring:accountBalancesUpdated', - (snapAccountEvent) => - this.#handleOnSnapKeyringAccountEvent( - 'AccountsController:accountBalancesUpdated', - snapAccountEvent, - ), - ); - - this.messagingSystem.subscribe( - 'SnapKeyring:accountTransactionsUpdated', - (snapAccountEvent) => - this.#handleOnSnapKeyringAccountEvent( - 'AccountsController:accountTransactionsUpdated', - snapAccountEvent, - ), - ); - + this.#subscribeToMessageEvents(); this.#registerMessageHandlers(); } @@ -460,7 +426,7 @@ export class AccountsController extends BaseController< setSelectedAccount(accountId: string): void { const account = this.getAccountExpect(accountId); - this.update((currentState: Draft) => { + this.update((currentState) => { currentState.internalAccounts.accounts[account.id].metadata.lastSelected = Date.now(); currentState.internalAccounts.selectedAccount = account.id; @@ -508,7 +474,7 @@ export class AccountsController extends BaseController< throw new Error('Account name already exists'); } - this.update((currentState: Draft) => { + this.update((currentState) => { const internalAccount = { ...account, metadata: { ...account.metadata, ...metadata }, @@ -584,7 +550,7 @@ export class AccountsController extends BaseController< {} as Record, ); - this.update((currentState: Draft) => { + this.update((currentState) => { currentState.internalAccounts.accounts = accounts; if ( @@ -618,7 +584,7 @@ export class AccountsController extends BaseController< */ loadBackup(backup: AccountsControllerState): void { if (backup.internalAccounts) { - this.update((currentState: Draft) => { + this.update((currentState) => { currentState.internalAccounts = backup.internalAccounts; }); } @@ -866,7 +832,7 @@ export class AccountsController extends BaseController< } } - this.update((currentState: Draft) => { + this.update((currentState) => { if (deletedAccounts.length > 0) { for (const account of deletedAccounts) { currentState.internalAccounts.accounts = this.#handleAccountRemoved( @@ -928,7 +894,7 @@ export class AccountsController extends BaseController< (account) => account.metadata.snap, ); - this.update((currentState: Draft) => { + this.update((currentState) => { accounts.forEach((account) => { const currentAccount = currentState.internalAccounts.accounts[account.id]; @@ -1160,6 +1126,36 @@ export class AccountsController extends BaseController< return accountsState; } + /** + * Handles the change in multichain network by updating the selected account. + * + * @param id - The EVM client ID or non-EVM chain ID that changed. + */ + readonly #handleMultichainNetworkChange = ( + id: NetworkClientId | CaipChainId, + ) => { + let accountId: string; + + // We only support non-EVM Caip chain IDs at the moment. Ex Solana and Bitcoin + // MultichainNetworkController will handle throwing an error if the Caip chain ID is not supported + if (isCaipChainId(id)) { + // Update selected account to non evm account + const lastSelectedNonEvmAccount = this.getSelectedMultichainAccount(id); + // @ts-expect-error - This should never be undefined, otherwise it's a bug that should be handled + accountId = lastSelectedNonEvmAccount.id; + } else { + // Update selected account to evm account + const lastSelectedEvmAccount = this.getSelectedAccount(); + accountId = lastSelectedEvmAccount.id; + } + + this.update((currentState) => { + currentState.internalAccounts.accounts[accountId].metadata.lastSelected = + Date.now(); + currentState.internalAccounts.selectedAccount = accountId; + }); + }; + /** * Retrieves the value of a specific metadata key for an existing account. * @@ -1169,7 +1165,6 @@ export class AccountsController extends BaseController< * @returns The value of the specified metadata key, or undefined if the account or metadata key does not exist. */ // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention #populateExistingMetadata( accountId: string, metadataKey: T, @@ -1179,9 +1174,56 @@ export class AccountsController extends BaseController< return internalAccount ? internalAccount.metadata[metadataKey] : undefined; } + /** + * Subscribes to message events. + */ + #subscribeToMessageEvents() { + this.messagingSystem.subscribe( + 'SnapController:stateChange', + (snapStateState) => this.#handleOnSnapStateChange(snapStateState), + ); + + this.messagingSystem.subscribe( + 'KeyringController:stateChange', + (keyringState) => this.#handleOnKeyringStateChange(keyringState), + ); + + this.messagingSystem.subscribe( + 'SnapKeyring:accountAssetListUpdated', + (snapAccountEvent) => + this.#handleOnSnapKeyringAccountEvent( + 'AccountsController:accountAssetListUpdated', + snapAccountEvent, + ), + ); + + this.messagingSystem.subscribe( + 'SnapKeyring:accountBalancesUpdated', + (snapAccountEvent) => + this.#handleOnSnapKeyringAccountEvent( + 'AccountsController:accountBalancesUpdated', + snapAccountEvent, + ), + ); + + this.messagingSystem.subscribe( + 'SnapKeyring:accountTransactionsUpdated', + (snapAccountEvent) => + this.#handleOnSnapKeyringAccountEvent( + 'AccountsController:accountTransactionsUpdated', + snapAccountEvent, + ), + ); + + // Handle account change when multichain network is changed + this.messagingSystem.subscribe( + 'MultichainNetworkController:networkDidChange', + this.#handleMultichainNetworkChange, + ); + } + /** * Registers message handlers for the AccountsController. - * */ #registerMessageHandlers() { this.messagingSystem.registerActionHandler( diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts index c5224ab0be4..ab7e55eca81 100644 --- a/packages/accounts-controller/src/tests/mocks.ts +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -4,11 +4,9 @@ import { BtcMethod, EthMethod, } from '@metamask/keyring-api'; +import type { KeyringAccountType } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; -import type { - InternalAccount, - InternalAccountType, -} from '@metamask/keyring-internal-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import { v4 } from 'uuid'; export const createMockInternalAccount = ({ @@ -23,7 +21,7 @@ export const createMockInternalAccount = ({ }: { id?: string; address?: string; - type?: InternalAccountType; + type?: KeyringAccountType; name?: string; keyringType?: KeyringTypes; snap?: { diff --git a/packages/accounts-controller/src/types.ts b/packages/accounts-controller/src/types.ts new file mode 100644 index 00000000000..1ee9421ec42 --- /dev/null +++ b/packages/accounts-controller/src/types.ts @@ -0,0 +1,10 @@ +// This file contains duplicate code from MultichainNetworkController.ts to avoid circular dependencies +// It should be refactored to avoid duplication + +import type { CaipChainId } from '@metamask/keyring-api'; +import type { NetworkClientId } from '@metamask/network-controller'; + +export type MultichainNetworkControllerNetworkDidChangeEvent = { + type: `MultichainNetworkController:networkDidChange`; + payload: [NetworkClientId | CaipChainId]; +}; diff --git a/packages/accounts-controller/src/utils.ts b/packages/accounts-controller/src/utils.ts index 0f8e47e1aeb..3562df9b566 100644 --- a/packages/accounts-controller/src/utils.ts +++ b/packages/accounts-controller/src/utils.ts @@ -50,6 +50,7 @@ export function keyringTypeToName(keyringType: string): string { /** * Generates a UUID v4 options from a given Ethereum address. + * * @param address - The Ethereum address to generate the UUID from. * @returns The UUID v4 options. */ @@ -65,6 +66,7 @@ export function getUUIDOptionsFromAddressOfNormalAccount( /** * Generates a UUID from a given Ethereum address. + * * @param address - The Ethereum address to generate the UUID from. * @returns The generated UUID. */ @@ -74,6 +76,7 @@ export function getUUIDFromAddressOfNormalAccount(address: string): string { /** * Check if a keyring type is considered a "normal" keyring. + * * @param keyringType - The account's keyring type. * @returns True if the keyring type is considered a "normal" keyring, false otherwise. */ diff --git a/packages/accounts-controller/tsconfig.build.json b/packages/accounts-controller/tsconfig.build.json index b4fbdd4821c..2ccd968d36d 100644 --- a/packages/accounts-controller/tsconfig.build.json +++ b/packages/accounts-controller/tsconfig.build.json @@ -10,7 +10,8 @@ { "path": "../base-controller/tsconfig.build.json" }, - { "path": "../keyring-controller/tsconfig.build.json" } + { "path": "../keyring-controller/tsconfig.build.json" }, + { "path": "../network-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/accounts-controller/tsconfig.json b/packages/accounts-controller/tsconfig.json index 7263c934b6b..12cd20ecb5c 100644 --- a/packages/accounts-controller/tsconfig.json +++ b/packages/accounts-controller/tsconfig.json @@ -9,7 +9,8 @@ }, { "path": "../keyring-controller" - } + }, + { "path": "../network-controller" } ], - "include": ["../../types", "./src"] + "include": ["../../types", "./src", "src/tests"] } diff --git a/packages/multichain-network-controller/CHANGELOG.md b/packages/multichain-network-controller/CHANGELOG.md new file mode 100644 index 00000000000..b518709c7b8 --- /dev/null +++ b/packages/multichain-network-controller/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/multichain-network-controller/LICENSE b/packages/multichain-network-controller/LICENSE new file mode 100644 index 00000000000..7d002dced3a --- /dev/null +++ b/packages/multichain-network-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2025 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/multichain-network-controller/README.md b/packages/multichain-network-controller/README.md new file mode 100644 index 00000000000..6bdb2c13233 --- /dev/null +++ b/packages/multichain-network-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/multichain-network-controller` + +... + +## Installation + +`yarn add @metamask/multichain-network-controller` + +or + +`npm install @metamask/multichain-network-controller` + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/packages/multichain-network-controller/jest.config.js b/packages/multichain-network-controller/jest.config.js new file mode 100644 index 00000000000..ca084133399 --- /dev/null +++ b/packages/multichain-network-controller/jest.config.js @@ -0,0 +1,26 @@ +/* + * For a detailed explanation regarding each configuration property and type check, visit: + * https://jestjs.io/docs/configuration + */ + +const merge = require('deepmerge'); +const path = require('path'); + +const baseConfig = require('../../jest.config.packages'); + +const displayName = path.basename(__dirname); + +module.exports = merge(baseConfig, { + // The display name when running multiple projects + displayName, + + // An object that configures minimum threshold enforcement for coverage results + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}); diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json new file mode 100644 index 00000000000..b22fb7a372e --- /dev/null +++ b/packages/multichain-network-controller/package.json @@ -0,0 +1,80 @@ +{ + "name": "@metamask/multichain-network-controller", + "version": "0.0.0", + "description": "Multichain network controller", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/multichain-network-controller#readme", + "bugs": { + "url": "https://github.com/MetaMask/core/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/core.git" + }, + "license": "MIT", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", + "build:docs": "typedoc", + "changelog:update": "../../scripts/update-changelog.sh @metamask/multichain-network-controller", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/multichain-network-controller", + "since-latest-release": "../../scripts/since-latest-release.sh", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", + "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", + "publish:preview": "yarn npm publish --tag preview" + }, + "dependencies": { + "@metamask/base-controller": "^8.0.0", + "@metamask/keyring-api": "^17.0.0", + "@metamask/utils": "^11.1.0", + "@solana/addresses": "^2.0.0" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.4.4", + "@metamask/keyring-controller": "^19.1.0", + "@types/jest": "^27.4.1", + "@types/uuid": "^8.3.0", + "deepmerge": "^4.2.2", + "immer": "^9.0.6", + "jest": "^27.5.1", + "nock": "^13.3.1", + "ts-jest": "^27.1.4", + "typedoc": "^0.24.8", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~5.2.2" + }, + "peerDependencies": { + "@metamask/accounts-controller": "^23.0.0", + "@metamask/network-controller": "^22.1.1" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts new file mode 100644 index 00000000000..f5dc5beedf4 --- /dev/null +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -0,0 +1,358 @@ +import { Messenger } from '@metamask/base-controller'; +import { InfuraNetworkType } from '@metamask/controller-utils'; +import { + BtcScope, + SolScope, + EthAccountType, + BtcAccountType, + SolAccountType, + type KeyringAccountType, + type CaipChainId, +} from '@metamask/keyring-api'; +import type { + NetworkControllerGetStateAction, + NetworkControllerSetActiveNetworkAction, +} from '@metamask/network-controller'; + +import { getDefaultMultichainNetworkControllerState } from './constants'; +import { MultichainNetworkController } from './MultichainNetworkController'; +import { + type AllowedActions, + type AllowedEvents, + type MultichainNetworkControllerAllowedActions, + type MultichainNetworkControllerAllowedEvents, + MULTICHAIN_NETWORK_CONTROLLER_NAME, +} from './types'; +import { createMockInternalAccount } from '../tests/utils'; + +/** + * Setup a test controller instance. + * + * @param args - Arguments to this function. + * @param args.options - The constructor options for the controller. + * @param args.getNetworkState - Mock for NetworkController:getState action. + * @param args.setActiveNetwork - Mock for NetworkController:setActiveNetwork action. + * @returns A collection of test controllers and mocks. + */ +function setupController({ + options = {}, + getNetworkState, + setActiveNetwork, +}: { + options?: Partial< + ConstructorParameters[0] + >; + getNetworkState?: jest.Mock< + ReturnType, + Parameters + >; + setActiveNetwork?: jest.Mock< + ReturnType, + Parameters + >; +} = {}) { + const messenger = new Messenger< + MultichainNetworkControllerAllowedActions, + MultichainNetworkControllerAllowedEvents + >(); + + const publishSpy = jest.spyOn(messenger, 'publish'); + + // Register action handlers + const mockGetNetworkState = + getNetworkState ?? + jest.fn< + ReturnType, + Parameters + >(); + messenger.registerActionHandler( + 'NetworkController:getState', + mockGetNetworkState, + ); + + const mockSetActiveNetwork = + setActiveNetwork ?? + jest.fn< + ReturnType, + Parameters + >(); + messenger.registerActionHandler( + 'NetworkController:setActiveNetwork', + mockSetActiveNetwork, + ); + + const controllerMessenger = messenger.getRestricted< + typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, + AllowedActions['type'], + AllowedEvents['type'] + >({ + name: MULTICHAIN_NETWORK_CONTROLLER_NAME, + allowedActions: [ + 'NetworkController:setActiveNetwork', + 'NetworkController:getState', + ], + allowedEvents: ['AccountsController:selectedAccountChange'], + }); + + // Default state to use Solana network with EVM as active network + const controller = new MultichainNetworkController({ + messenger: options.messenger || controllerMessenger, + state: { + selectedMultichainNetworkChainId: SolScope.Mainnet, + isEvmSelected: true, + ...options.state, + }, + }); + + const triggerSelectedAccountChange = (accountType: KeyringAccountType) => { + const mockAccountAddressByAccountType: Record = + { + [EthAccountType.Eoa]: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', + [EthAccountType.Erc4337]: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', + [SolAccountType.DataAccount]: + 'So11111111111111111111111111111111111111112', + [BtcAccountType.P2wpkh]: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', + }; + const mockAccountAddress = mockAccountAddressByAccountType[accountType]; + + const mockAccount = createMockInternalAccount({ + type: accountType, + address: mockAccountAddress, + }); + messenger.publish('AccountsController:selectedAccountChange', mockAccount); + }; + + return { + messenger, + controller, + mockGetNetworkState, + mockSetActiveNetwork, + publishSpy, + triggerSelectedAccountChange, + }; +} + +describe('MultichainNetworkController', () => { + describe('constructor', () => { + it('should set default state', () => { + const { controller } = setupController({ + options: { state: getDefaultMultichainNetworkControllerState() }, + }); + expect(controller.state).toStrictEqual( + getDefaultMultichainNetworkControllerState(), + ); + }); + }); + + describe('setActiveNetwork', () => { + it('should set non-EVM network when same non-EVM chain ID is active', async () => { + // By default, Solana is selected but is NOT active (aka EVM network is active) + const { controller, publishSpy } = setupController(); + + // Set active network to Solana + await controller.setActiveNetwork(SolScope.Mainnet); + + // Check that the Solana is now the selected network + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScope.Mainnet, + ); + + // Check that the a non evm network is now active + expect(controller.state.isEvmSelected).toBe(false); + + // Check that the messenger published the correct event + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainNetworkController:networkDidChange', + SolScope.Mainnet, + ); + }); + + it('should throw error when unsupported non-EVM chainId is provided', async () => { + const { controller } = setupController(); + const unsupportedChainId = 'eip155:1' as CaipChainId; + + await expect( + controller.setActiveNetwork(unsupportedChainId), + ).rejects.toThrow(`Unsupported Caip chain ID: ${unsupportedChainId}`); + }); + + it('should do nothing when same non-EVM chain ID is set and active', async () => { + // By default, Solana is selected and active + const { controller, publishSpy } = setupController({ + options: { state: { isEvmSelected: false } }, + }); + + // Set active network to Solana + await controller.setActiveNetwork(SolScope.Mainnet); + + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScope.Mainnet, + ); + + expect(controller.state.isEvmSelected).toBe(false); + + // Check that the messenger published the correct event + expect(publishSpy).not.toHaveBeenCalled(); + }); + + it('should set non-EVM network when different non-EVM chain ID is active', async () => { + // By default, Solana is selected but is NOT active (aka EVM network is active) + const { controller, publishSpy } = setupController({ + options: { state: { isEvmSelected: false } }, + }); + + // Set active network to Bitcoin + await controller.setActiveNetwork(BtcScope.Mainnet); + + // Check that the Solana is now the selected network + expect(controller.state.selectedMultichainNetworkChainId).toBe( + BtcScope.Mainnet, + ); + + // Check that BTC network is now active + expect(controller.state.isEvmSelected).toBe(false); + + // Check that the messenger published the correct event + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainNetworkController:networkDidChange', + BtcScope.Mainnet, + ); + }); + + it('should set EVM network and call NetworkController:setActiveNetwork when same EVM network is selected', async () => { + const selectedNetworkClientId = InfuraNetworkType.mainnet; + + const { controller, mockSetActiveNetwork, publishSpy } = setupController({ + getNetworkState: jest.fn().mockImplementation(() => ({ + selectedNetworkClientId, + })), + }); + + await controller.setActiveNetwork(selectedNetworkClientId); + + // Check that EVM network is selected + expect(controller.state.isEvmSelected).toBe(true); + + // Check that the messenger published the correct event + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainNetworkController:networkDidChange', + selectedNetworkClientId, + ); + + // Check that NetworkController:setActiveNetwork was not called + expect(mockSetActiveNetwork).not.toHaveBeenCalled(); + }); + + it('should set EVM network and call NetworkController:setActiveNetwork when different EVM network is selected', async () => { + const { controller, mockSetActiveNetwork, publishSpy } = setupController({ + getNetworkState: jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: InfuraNetworkType.mainnet, + })), + }); + const evmNetworkClientId = 'linea'; + + await controller.setActiveNetwork(evmNetworkClientId); + + // Check that EVM network is selected + expect(controller.state.isEvmSelected).toBe(true); + + // Check that the messenger published the correct event + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainNetworkController:networkDidChange', + evmNetworkClientId, + ); + + // Check that NetworkController:setActiveNetwork was not called + expect(mockSetActiveNetwork).toHaveBeenCalledWith(evmNetworkClientId); + }); + }); + + describe('handle AccountsController:selectedAccountChange event', () => { + it('isEvmSelected should be true when both switching to EVM account and EVM network is already active', async () => { + // By default, Solana is selected but EVM network is active + const { controller, triggerSelectedAccountChange } = setupController(); + + // EVM network is currently active + expect(controller.state.isEvmSelected).toBe(true); + + // Switching to EVM account + triggerSelectedAccountChange(EthAccountType.Eoa); + + // EVM network is still active + expect(controller.state.isEvmSelected).toBe(true); + }); + + it('should switch to EVM network if non-EVM network is previously active', async () => { + // By default, Solana is selected and active + const { controller, triggerSelectedAccountChange } = setupController({ + options: { state: { isEvmSelected: false } }, + getNetworkState: jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: InfuraNetworkType.mainnet, + })), + }); + + // non-EVM network is currently active + expect(controller.state.isEvmSelected).toBe(false); + + // Switching to EVM account + triggerSelectedAccountChange(EthAccountType.Eoa); + + // EVM network is now active + expect(controller.state.isEvmSelected).toBe(true); + }); + it('non-EVM network should be active when switching to account of same selected non-EVM network', async () => { + // By default, Solana is selected and active + const { controller, triggerSelectedAccountChange } = setupController({ + options: { + state: { + isEvmSelected: true, + selectedMultichainNetworkChainId: SolScope.Mainnet, + }, + }, + }); + + // EVM network is currently active + expect(controller.state.isEvmSelected).toBe(true); + + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScope.Mainnet, + ); + + // Switching to Solana account + triggerSelectedAccountChange(SolAccountType.DataAccount); + + // Solana is still the selected network + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScope.Mainnet, + ); + expect(controller.state.isEvmSelected).toBe(false); + }); + + it('non-EVM network should change when switching to account on different non-EVM network', async () => { + // By default, Solana is selected and active + const { controller, triggerSelectedAccountChange } = setupController({ + options: { + state: { + isEvmSelected: false, + selectedMultichainNetworkChainId: SolScope.Mainnet, + }, + }, + }); + + // Solana is currently active + expect(controller.state.isEvmSelected).toBe(false); + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScope.Mainnet, + ); + + // Switching to Bitcoin account + triggerSelectedAccountChange(BtcAccountType.P2wpkh); + + // Bitcoin is now the selected network + expect(controller.state.selectedMultichainNetworkChainId).toBe( + BtcScope.Mainnet, + ); + expect(controller.state.isEvmSelected).toBe(false); + }); + }); +}); diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts new file mode 100644 index 00000000000..572dfa6d12c --- /dev/null +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -0,0 +1,204 @@ +import { BaseController } from '@metamask/base-controller'; +import { isEvmAccountType } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import type { NetworkClientId } from '@metamask/network-controller'; +import { isCaipChainId } from '@metamask/utils'; + +import { + MULTICHAIN_NETWORK_CONTROLLER_METADATA, + getDefaultMultichainNetworkControllerState, +} from './constants'; +import { + MULTICHAIN_NETWORK_CONTROLLER_NAME, + type MultichainNetworkControllerState, + type MultichainNetworkControllerMessenger, + type SupportedCaipChainId, +} from './types'; +import { + checkIfSupportedCaipChainId, + getChainIdForNonEvmAddress, +} from './utils'; + +/** + * The MultichainNetworkController is responsible for fetching and caching account + * balances. + */ +export class MultichainNetworkController extends BaseController< + typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, + MultichainNetworkControllerState, + MultichainNetworkControllerMessenger +> { + constructor({ + messenger, + state, + }: { + messenger: MultichainNetworkControllerMessenger; + state?: Omit< + Partial, + 'multichainNetworkConfigurationsByChainId' + >; + }) { + super({ + messenger, + name: MULTICHAIN_NETWORK_CONTROLLER_NAME, + metadata: MULTICHAIN_NETWORK_CONTROLLER_METADATA, + state: { + ...getDefaultMultichainNetworkControllerState(), + ...state, + }, + }); + + this.#subscribeToMessageEvents(); + this.#registerMessageHandlers(); + } + + /** + * Sets the active EVM network. + * + * @param id - The client ID of the EVM network to set active. + */ + async #setActiveEvmNetwork(id: NetworkClientId): Promise { + // Notify listeners that setActiveNetwork was called + this.messagingSystem.publish( + 'MultichainNetworkController:networkDidChange', + id, + ); + + // Indicate that the non-EVM network is not selected + this.update((state) => { + state.isEvmSelected = true; + }); + + // Prevent setting same network + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); + + if (id === selectedNetworkClientId) { + // EVM network is already selected, no need to update NetworkController + return; + } + + // Update evm active network + await this.messagingSystem.call('NetworkController:setActiveNetwork', id); + } + + /** + * Sets the active non-EVM network. + * + * @param id - The chain ID of the non-EVM network to set active. + */ + #setActiveNonEvmNetwork(id: SupportedCaipChainId): void { + if (id === this.state.selectedMultichainNetworkChainId) { + if (!this.state.isEvmSelected) { + // Same non-EVM network is already selected, no need to update + return; + } + + // Indicate that the non-EVM network is selected + this.update((state) => { + state.isEvmSelected = false; + }); + + // Notify listeners that setActiveNetwork was called + this.messagingSystem.publish( + 'MultichainNetworkController:networkDidChange', + id, + ); + } + + // Notify listeners that setActiveNetwork was called + this.messagingSystem.publish( + 'MultichainNetworkController:networkDidChange', + id, + ); + + this.update((state) => { + state.selectedMultichainNetworkChainId = id; + state.isEvmSelected = false; + }); + } + + /** + * Sets the active network. + * + * @param id - The non-EVM Caip chain ID or EVM client ID of the network to set active. + * @returns - A promise that resolves when the network is set active. + */ + async setActiveNetwork( + id: SupportedCaipChainId | NetworkClientId, + ): Promise { + if (isCaipChainId(id)) { + const isSupportedCaipChainId = checkIfSupportedCaipChainId(id); + if (!isSupportedCaipChainId) { + throw new Error(`Unsupported Caip chain ID: ${String(id)}`); + } + return this.#setActiveNonEvmNetwork(id); + } + + return await this.#setActiveEvmNetwork(id); + } + + /** + * Handles switching between EVM and non-EVM networks when an account is changed + * + * @param account - The account that was changed + */ + readonly #handleSelectedAccountChange = (account: InternalAccount) => { + const { type: accountType, address: accountAddress } = account; + const isEvmAccount = isEvmAccountType(accountType); + + // Handle switching to EVM network + if (isEvmAccount) { + if (this.state.isEvmSelected) { + // No need to update if already on evm network + return; + } + + // Make EVM network active + this.update((state) => { + state.isEvmSelected = true; + }); + return; + } + + // Handle switching to non-EVM network + const nonEvmChainId = getChainIdForNonEvmAddress(accountAddress); + const isSameNonEvmNetwork = + nonEvmChainId === this.state.selectedMultichainNetworkChainId; + + if (isSameNonEvmNetwork) { + // No need to update if already on the same non-EVM network + this.update((state) => { + state.isEvmSelected = false; + }); + return; + } + + this.update((state) => { + state.selectedMultichainNetworkChainId = nonEvmChainId; + state.isEvmSelected = false; + }); + }; + + /** + * Subscribes to message events. + */ + #subscribeToMessageEvents() { + // Handle network switch when account is changed + this.messagingSystem.subscribe( + 'AccountsController:selectedAccountChange', + this.#handleSelectedAccountChange, + ); + } + + /** + * Registers message handlers. + */ + #registerMessageHandlers() { + this.messagingSystem.registerActionHandler( + 'MultichainNetworkController:setActiveNetwork', + this.setActiveNetwork.bind(this), + ); + } +} diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts new file mode 100644 index 00000000000..0f84e75f9b1 --- /dev/null +++ b/packages/multichain-network-controller/src/constants.ts @@ -0,0 +1,74 @@ +import { type StateMetadata } from '@metamask/base-controller'; +import { BtcScope, SolScope } from '@metamask/keyring-api'; +import { NetworkStatus } from '@metamask/network-controller'; + +import type { + MultichainNetworkConfiguration, + MultichainNetworkControllerState, + MultichainNetworkMetadata, + SupportedCaipChainId, +} from './types'; + +export const BTC_NATIVE_ASSET = `${BtcScope.Mainnet}/slip44:0`; +export const SOL_NATIVE_ASSET = `${SolScope.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`; + +/** + * Supported networks by the MultichainNetworkController + */ +export const AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS: Record< + SupportedCaipChainId, + MultichainNetworkConfiguration +> = { + [BtcScope.Mainnet]: { + chainId: BtcScope.Mainnet, + name: 'Bitcoin Mainnet', + nativeCurrency: BTC_NATIVE_ASSET, + isEvm: false, + }, + [SolScope.Mainnet]: { + chainId: SolScope.Mainnet, + name: 'Solana Mainnet', + nativeCurrency: SOL_NATIVE_ASSET, + isEvm: false, + }, +}; + +/** + * Metadata for the supported networks. + */ +export const NETWORKS_METADATA: Record = { + [BtcScope.Mainnet]: { + features: [], + status: NetworkStatus.Available, + }, + [SolScope.Mainnet]: { + features: [], + status: NetworkStatus.Available, + }, +}; + +/** + * Default state of the {@link MultichainNetworkController}. + * + * @returns The default state of the {@link MultichainNetworkController}. + */ +export const getDefaultMultichainNetworkControllerState = + (): MultichainNetworkControllerState => ({ + multichainNetworkConfigurationsByChainId: + AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS, + selectedMultichainNetworkChainId: SolScope.Mainnet, + isEvmSelected: true, + }); + +/** + * {@link MultichainNetworkController}'s metadata. + * + * This allows us to choose if fields of the state should be persisted or not + * using the `persist` flag; and if they can be sent to Sentry or not, using + * the `anonymous` flag. + */ +export const MULTICHAIN_NETWORK_CONTROLLER_METADATA = { + multichainNetworkConfigurationsByChainId: { persist: true, anonymous: true }, + selectedMultichainNetworkChainId: { persist: true, anonymous: true }, + isEvmSelected: { persist: true, anonymous: true }, +} satisfies StateMetadata; diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts new file mode 100644 index 00000000000..eaf8accddf0 --- /dev/null +++ b/packages/multichain-network-controller/src/index.ts @@ -0,0 +1,24 @@ +export { MultichainNetworkController } from './MultichainNetworkController'; +export { getDefaultMultichainNetworkControllerState } from './constants'; +export type { + MultichainNetworkMetadata, + SupportedCaipChainId, + CommonNetworkConfiguration, + NonEvmNetworkConfiguration, + EvmNetworkConfiguration, + MultichainNetworkConfiguration, + MultichainNetworkControllerState, + MultichainNetworkControllerGetStateAction, + MultichainNetworkControllerSetActiveNetworkAction, + MultichainNetworkControllerStateChange, + MultichainNetworkControllerNetworkDidChangeEvent, + MultichainNetworkControllerActions, + MultichainNetworkControllerEvents, + MultichainNetworkControllerMessenger, +} from './types'; +export { + checkIfSupportedCaipChainId, + toMultichainNetworkConfiguration, + toMultichainNetworkConfigurationsByChainId, + toEvmCaipChainId, +} from './utils'; diff --git a/packages/multichain-network-controller/src/types.ts b/packages/multichain-network-controller/src/types.ts new file mode 100644 index 00000000000..5eb1215da2a --- /dev/null +++ b/packages/multichain-network-controller/src/types.ts @@ -0,0 +1,178 @@ +import { + type ControllerGetStateAction, + type ControllerStateChangeEvent, + type RestrictedMessenger, +} from '@metamask/base-controller'; +import type { BtcScope, CaipChainId, SolScope } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import type { + NetworkStatus, + NetworkControllerSetActiveNetworkAction, + NetworkControllerGetStateAction, + NetworkClientId, +} from '@metamask/network-controller'; +import { type CaipAssetType } from '@metamask/utils'; + +export const MULTICHAIN_NETWORK_CONTROLLER_NAME = 'MultichainNetworkController'; + +export type MultichainNetworkMetadata = { + features: string[]; + status: NetworkStatus; +}; + +export type SupportedCaipChainId = SolScope.Mainnet | BtcScope.Mainnet; + +export type CommonNetworkConfiguration = { + /** + * EVM network flag. + */ + isEvm: boolean; + /** + * The chain ID of the network. + */ + chainId: CaipChainId; + /** + * The name of the network. + */ + name: string; +}; + +export type NonEvmNetworkConfiguration = CommonNetworkConfiguration & { + /** + * EVM network flag. + */ + isEvm: false; + /** + * The native asset type of the network. + */ + nativeCurrency: CaipAssetType; +}; + +// TODO: The controller only supports non-EVM network configurations at the moment +// Once we support Caip chain IDs for EVM networks, we can re-enable EVM network configurations +export type EvmNetworkConfiguration = CommonNetworkConfiguration & { + /** + * EVM network flag. + */ + isEvm: true; + /** + * The native asset type of the network. + * For EVM, this is the network ticker since there is no standard between + * tickers and Caip IDs. + */ + nativeCurrency: string; + /** + * The block explorers of the network. + */ + blockExplorerUrls: string[]; + /** + * The index of the default block explorer URL. + */ + defaultBlockExplorerUrlIndex: number; +}; + +export type MultichainNetworkConfiguration = + | EvmNetworkConfiguration + | NonEvmNetworkConfiguration; + +/** + * State used by the {@link MultichainNetworkController} to cache network configurations. + */ +export type MultichainNetworkControllerState = { + /** + * The network configurations by chain ID. + */ + multichainNetworkConfigurationsByChainId: Record< + CaipChainId, + MultichainNetworkConfiguration + >; + /** + * The chain ID of the selected network. + */ + selectedMultichainNetworkChainId: SupportedCaipChainId; + /** + * Whether EVM or non-EVM network is selected + */ + isEvmSelected: boolean; +}; + +/** + * Returns the state of the {@link MultichainNetworkController}. + */ +export type MultichainNetworkControllerGetStateAction = + ControllerGetStateAction< + typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, + MultichainNetworkControllerState + >; + +export type SetActiveNetworkMethod = ( + id: SupportedCaipChainId | NetworkClientId, +) => Promise; + +export type MultichainNetworkControllerSetActiveNetworkAction = { + type: `${typeof MULTICHAIN_NETWORK_CONTROLLER_NAME}:setActiveNetwork`; + handler: SetActiveNetworkMethod; +}; + +/** + * Event emitted when the state of the {@link MultichainNetworkController} changes. + */ +export type MultichainNetworkControllerStateChange = ControllerStateChangeEvent< + typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, + MultichainNetworkControllerState +>; + +export type MultichainNetworkControllerNetworkDidChangeEvent = { + type: `${typeof MULTICHAIN_NETWORK_CONTROLLER_NAME}:networkDidChange`; + payload: [NetworkClientId | SupportedCaipChainId]; +}; + +/** + * Actions exposed by the {@link MultichainNetworkController}. + */ +export type MultichainNetworkControllerActions = + | MultichainNetworkControllerGetStateAction + | MultichainNetworkControllerSetActiveNetworkAction; + +/** + * Events emitted by {@link MultichainNetworkController}. + */ +export type MultichainNetworkControllerEvents = + MultichainNetworkControllerNetworkDidChangeEvent; + +/** + * Actions that this controller is allowed to call. + */ +export type AllowedActions = + | NetworkControllerGetStateAction + | NetworkControllerSetActiveNetworkAction; + +// Re-define event here to avoid circular dependency with AccountsController +export type AccountsControllerSelectedAccountChangeEvent = { + type: `AccountsController:selectedAccountChange`; + payload: [InternalAccount]; +}; + +/** + * Events that this controller is allowed to subscribe. + */ +export type AllowedEvents = AccountsControllerSelectedAccountChangeEvent; + +export type MultichainNetworkControllerAllowedActions = + | MultichainNetworkControllerActions + | AllowedActions; + +export type MultichainNetworkControllerAllowedEvents = + | MultichainNetworkControllerEvents + | AllowedEvents; + +/** + * Messenger type for the MultichainNetworkController. + */ +export type MultichainNetworkControllerMessenger = RestrictedMessenger< + typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, + MultichainNetworkControllerAllowedActions, + MultichainNetworkControllerAllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; diff --git a/packages/multichain-network-controller/src/utils.test.ts b/packages/multichain-network-controller/src/utils.test.ts new file mode 100644 index 00000000000..dbf6e5e5322 --- /dev/null +++ b/packages/multichain-network-controller/src/utils.test.ts @@ -0,0 +1,114 @@ +import { BtcScope, SolScope, type CaipChainId } from '@metamask/keyring-api'; +import { type NetworkConfiguration } from '@metamask/network-controller'; + +import { + toEvmCaipChainId, + getChainIdForNonEvmAddress, + checkIfSupportedCaipChainId, + toMultichainNetworkConfiguration, + toMultichainNetworkConfigurationsByChainId, +} from './utils'; + +describe('utils', () => { + describe('getChainIdForNonEvmAddress', () => { + it('returns Solana chain ID for Solana addresses', () => { + const solanaAddress = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; + expect(getChainIdForNonEvmAddress(solanaAddress)).toBe(SolScope.Mainnet); + }); + + it('returns Bitcoin chain ID for non-Solana addresses', () => { + const bitcoinAddress = 'bc1qzqc2aqlw8nwa0a05ehjkk7dgt8308ac7kzw9a6'; + expect(getChainIdForNonEvmAddress(bitcoinAddress)).toBe(BtcScope.Mainnet); + }); + }); + + describe('checkIfSupportedCaipChainId', () => { + it('returns true for supported CAIP chain IDs', () => { + expect(checkIfSupportedCaipChainId(SolScope.Mainnet)).toBe(true); + expect(checkIfSupportedCaipChainId(BtcScope.Mainnet)).toBe(true); + }); + + it('returns false for non-CAIP IDs', () => { + expect(checkIfSupportedCaipChainId('mainnet' as CaipChainId)).toBe(false); + }); + + it('returns false for unsupported CAIP chain IDs', () => { + expect(checkIfSupportedCaipChainId('eip155:1')).toBe(false); + }); + }); + + describe('toMultichainNetworkConfiguration', () => { + it('updates the network configuration for a single EVM network', () => { + const network: NetworkConfiguration = { + chainId: '0x1', + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + rpcEndpoints: [], + defaultRpcEndpointIndex: 0, + }; + expect(toMultichainNetworkConfiguration(network)).toStrictEqual({ + chainId: 'eip155:1', + isEvm: true, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + }); + }); + }); + + describe('toMultichainNetworkConfigurationsByChainId', () => { + it('updates the network configurations for multiple EVM networks', () => { + const networks: Record = { + '0x1': { + chainId: '0x1', + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + rpcEndpoints: [], + defaultRpcEndpointIndex: 0, + }, + '0xe708': { + chainId: '0xe708', + name: 'Linea', + nativeCurrency: 'ETH', + blockExplorerUrls: ['https://lineascan.build'], + defaultBlockExplorerUrlIndex: 0, + rpcEndpoints: [], + defaultRpcEndpointIndex: 0, + }, + }; + expect( + toMultichainNetworkConfigurationsByChainId(networks), + ).toStrictEqual({ + 'eip155:1': { + chainId: 'eip155:1', + isEvm: true, + name: 'Ethereum Mainnet', + nativeCurrency: 'ETH', + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + }, + 'eip155:59144': { + chainId: 'eip155:59144', + isEvm: true, + name: 'Linea', + nativeCurrency: 'ETH', + blockExplorerUrls: ['https://lineascan.build'], + defaultBlockExplorerUrlIndex: 0, + }, + }); + }); + }); + + describe('toEvmCaipChainId', () => { + it('converts a hex chain ID to a CAIP chain ID', () => { + expect(toEvmCaipChainId('0x1')).toBe('eip155:1'); + expect(toEvmCaipChainId('0xe708')).toBe('eip155:59144'); + expect(toEvmCaipChainId('0x539')).toBe('eip155:1337'); + }); + }); +}); diff --git a/packages/multichain-network-controller/src/utils.ts b/packages/multichain-network-controller/src/utils.ts new file mode 100644 index 00000000000..d6a00d7160e --- /dev/null +++ b/packages/multichain-network-controller/src/utils.ts @@ -0,0 +1,93 @@ +import { BtcScope, SolScope } from '@metamask/keyring-api'; +import type { NetworkConfiguration } from '@metamask/network-controller'; +import { + type Hex, + type CaipChainId, + KnownCaipNamespace, + toCaipChainId, + hexToNumber, +} from '@metamask/utils'; +import { isAddress as isSolanaAddress } from '@solana/addresses'; + +import { AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS } from './constants'; +import type { + SupportedCaipChainId, + MultichainNetworkConfiguration, +} from './types'; + +/** + * Returns the chain id of the non-EVM network based on the account address. + * + * @param address - The address to check. + * @returns The caip chain id of the non-EVM network. + */ +export function getChainIdForNonEvmAddress( + address: string, +): SupportedCaipChainId { + // This condition is not the most robust. Once we support more networks, we will need to update this logic. + if (isSolanaAddress(address)) { + return SolScope.Mainnet; + } + return BtcScope.Mainnet; +} + +/** + * Checks if the Caip chain ID is supported. + * + * @param id - The Caip chain IDto check. + * @returns Whether the chain ID is supported. + */ +export function checkIfSupportedCaipChainId( + id: CaipChainId, +): id is SupportedCaipChainId { + // Check if the chain id is supported + return Object.keys(AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS).includes(id); +} + +/** + * Converts a hex chain ID to a Caip chain ID. + * + * @param chainId - The hex chain ID to convert. + * @returns The Caip chain ID. + */ +export const toEvmCaipChainId = (chainId: Hex): CaipChainId => + toCaipChainId(KnownCaipNamespace.Eip155, hexToNumber(chainId).toString()); + +/** + * Updates a network configuration to the format used by the MultichainNetworkController. + * This method is exclusive for EVM networks with hex identifiers from the NetworkController. + * + * @param network - The network configuration to update. + * @returns The updated network configuration. + */ +export const toMultichainNetworkConfiguration = ( + network: NetworkConfiguration, +): MultichainNetworkConfiguration => { + return { + chainId: toEvmCaipChainId(network.chainId), + isEvm: true, + name: network.name, + nativeCurrency: network.nativeCurrency, + blockExplorerUrls: network.blockExplorerUrls, + defaultBlockExplorerUrlIndex: network.defaultBlockExplorerUrlIndex || 0, + }; +}; + +/** + * Updates a record of network configurations to the format used by the MultichainNetworkController. + * This method is exclusive for EVM networks with hex identifiers from the NetworkController. + * + * @param networkConfigurationsByChainId - The network configurations to update. + * @returns The updated network configurations. + */ +export const toMultichainNetworkConfigurationsByChainId = ( + networkConfigurationsByChainId: Record, +): Record => + Object.entries(networkConfigurationsByChainId).reduce( + (acc, [, network]) => ({ + ...acc, + [toEvmCaipChainId(network.chainId)]: + toMultichainNetworkConfiguration(network), + }), + {}, + ); diff --git a/packages/multichain-network-controller/tests/utils.ts b/packages/multichain-network-controller/tests/utils.ts new file mode 100644 index 00000000000..141f6f29f9e --- /dev/null +++ b/packages/multichain-network-controller/tests/utils.ts @@ -0,0 +1,98 @@ +import { + BtcAccountType, + EthAccountType, + SolAccountType, + BtcMethod, + EthMethod, + SolMethod, + type KeyringAccountType, +} from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +/** + * Creates a mock internal account. This is a duplicated function from the accounts-controller package + * This exists here to prevent circular dependencies with the accounts-controller package + * + * @param args - Arguments to this function. + * @param args.id - The ID of the account. + * @param args.address - The address of the account. + * @param args.type - The type of the account. + * @param args.name - The name of the account. + * @param args.keyringType - The keyring type of the account. + * @param args.snap - The snap of the account. + * @param args.snap.id - The ID of the snap. + * @param args.snap.enabled - Whether the snap is enabled. + * @param args.snap.name - The name of the snap. + * @param args.importTime - The import time of the account. + * @param args.lastSelected - The last selected time of the account. + * @returns A mock internal account. + */ +export const createMockInternalAccount = ({ + id = 'dummy-id', + address = '0x2990079bcdee240329a520d2444386fc119da21a', + type = EthAccountType.Eoa, + name = 'Account 1', + keyringType = KeyringTypes.hd, + snap, + importTime = Date.now(), + lastSelected = Date.now(), +}: { + id?: string; + address?: string; + type?: KeyringAccountType; + name?: string; + keyringType?: KeyringTypes; + snap?: { + id: string; + enabled: boolean; + name: string; + }; + importTime?: number; + lastSelected?: number; +} = {}): InternalAccount => { + let methods; + + switch (type) { + case EthAccountType.Eoa: + methods = [ + EthMethod.PersonalSign, + EthMethod.Sign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV1, + EthMethod.SignTypedDataV3, + EthMethod.SignTypedDataV4, + ]; + break; + case EthAccountType.Erc4337: + methods = [ + EthMethod.PatchUserOperation, + EthMethod.PrepareUserOperation, + EthMethod.SignUserOperation, + ]; + break; + case BtcAccountType.P2wpkh: + methods = [BtcMethod.SendBitcoin]; + break; + case SolAccountType.DataAccount: + methods = [SolMethod.SendAndConfirmTransaction]; + break; + default: + throw new Error(`Unknown account type: ${type as string}`); + } + + return { + id, + address, + options: {}, + methods, + type, + metadata: { + name, + keyring: { type: keyringType }, + importTime, + lastSelected, + snap, + }, + } as InternalAccount; +}; diff --git a/packages/multichain-network-controller/tsconfig.build.json b/packages/multichain-network-controller/tsconfig.build.json new file mode 100644 index 00000000000..41c2d082d3d --- /dev/null +++ b/packages/multichain-network-controller/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../network-controller/tsconfig.build.json" }, + { "path": "../keyring-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/multichain-network-controller/tsconfig.json b/packages/multichain-network-controller/tsconfig.json new file mode 100644 index 00000000000..e5ff777b642 --- /dev/null +++ b/packages/multichain-network-controller/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../network-controller" }, + { "path": "../keyring-controller" } + ], + "include": ["../../types", "./src", "./tests"] +} diff --git a/packages/multichain-network-controller/typedoc.json b/packages/multichain-network-controller/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/multichain-network-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/teams.json b/teams.json index 7060b959eaf..eabdb25af76 100644 --- a/teams.json +++ b/teams.json @@ -17,6 +17,7 @@ "metamask/logging-controller": "team-confirmations", "metamask/message-manager": "team-confirmations", "metamask/multichain": "team-wallet-api-platform", + "metamask/multichain-network-controller": "team-wallet-api-platform", "metamask/name-controller": "team-confirmations", "metamask/network-controller": "team-wallet-framework,team-assets", "metamask/notification-controller": "team-snaps-platform", diff --git a/tsconfig.build.json b/tsconfig.build.json index eea2f56d062..a091abb09e7 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -42,7 +42,8 @@ "path": "./packages/token-search-discovery-controller/tsconfig.build.json" }, { "path": "./packages/transaction-controller/tsconfig.build.json" }, - { "path": "./packages/user-operation-controller/tsconfig.build.json" } + { "path": "./packages/user-operation-controller/tsconfig.build.json" }, + { "path": "./packages/multichain-network-controller/tsconfig.build.json" } ], "files": [], "include": [] diff --git a/tsconfig.json b/tsconfig.json index c9b7f0715ec..489ba07d2a9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,7 @@ { "path": "./packages/message-manager" }, { "path": "./packages/multichain" }, { "path": "./packages/multichain-transactions-controller" }, + { "path": "./packages/multichain-network-controller" }, { "path": "./packages/name-controller" }, { "path": "./packages/network-controller" }, { "path": "./packages/notification-services-controller" }, diff --git a/yarn.lock b/yarn.lock index 774f0b6fe8d..57dcd4b6f07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2351,6 +2351,7 @@ __metadata: "@metamask/keyring-api": "npm:^17.0.0" "@metamask/keyring-controller": "npm:^19.1.0" "@metamask/keyring-internal-api": "npm:^4.0.1" + "@metamask/network-controller": "npm:^22.2.1" "@metamask/providers": "npm:^18.1.1" "@metamask/snaps-controllers": "npm:^9.19.0" "@metamask/snaps-sdk": "npm:^6.17.1" @@ -2369,7 +2370,8 @@ __metadata: uuid: "npm:^8.3.2" webextension-polyfill: "npm:^0.12.0" peerDependencies: - "@metamask/keyring-controller": ^19.0.0 + "@metamask/keyring-controller": ^19.1.0 + "@metamask/network-controller": ^22.2.1 "@metamask/providers": ^18.1.0 "@metamask/snaps-controllers": ^9.19.0 webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 @@ -3451,6 +3453,32 @@ __metadata: languageName: node linkType: hard +"@metamask/multichain-network-controller@workspace:packages/multichain-network-controller": + version: 0.0.0-use.local + resolution: "@metamask/multichain-network-controller@workspace:packages/multichain-network-controller" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^8.0.0" + "@metamask/keyring-api": "npm:^17.0.0" + "@metamask/keyring-controller": "npm:^19.1.0" + "@metamask/utils": "npm:^11.1.0" + "@solana/addresses": "npm:^2.0.0" + "@types/jest": "npm:^27.4.1" + "@types/uuid": "npm:^8.3.0" + deepmerge: "npm:^4.2.2" + immer: "npm:^9.0.6" + jest: "npm:^27.5.1" + nock: "npm:^13.3.1" + ts-jest: "npm:^27.1.4" + typedoc: "npm:^0.24.8" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.2.2" + peerDependencies: + "@metamask/accounts-controller": ^23.0.0 + "@metamask/network-controller": ^22.1.1 + languageName: unknown + linkType: soft + "@metamask/multichain-transactions-controller@workspace:packages/multichain-transactions-controller": version: 0.0.0-use.local resolution: "@metamask/multichain-transactions-controller@workspace:packages/multichain-transactions-controller" @@ -4689,6 +4717,82 @@ __metadata: languageName: node linkType: hard +"@solana/addresses@npm:^2.0.0": + version: 2.0.0 + resolution: "@solana/addresses@npm:2.0.0" + dependencies: + "@solana/assertions": "npm:2.0.0" + "@solana/codecs-core": "npm:2.0.0" + "@solana/codecs-strings": "npm:2.0.0" + "@solana/errors": "npm:2.0.0" + peerDependencies: + typescript: ">=5" + checksum: 10/f99d09c72046c73858aa8b7bc323e634a60b1023a4d280036bc94489e431075c7f29d2889e8787e33a04cfdecbe77cd8ca26c31ded73f735dc98e49c3151cc17 + languageName: node + linkType: hard + +"@solana/assertions@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/assertions@npm:2.0.0" + dependencies: + "@solana/errors": "npm:2.0.0" + peerDependencies: + typescript: ">=5" + checksum: 10/c1af37ae1bd79b1657395d9315ac261dabc9908a64af6ed80e3b7e5140909cd8c8c757f0c41fff084e26fbb4d32f091c89c092a8c1ed5e6f4565dfe7426c0979 + languageName: node + linkType: hard + +"@solana/codecs-core@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/codecs-core@npm:2.0.0" + dependencies: + "@solana/errors": "npm:2.0.0" + peerDependencies: + typescript: ">=5" + checksum: 10/e58a72e67bee3e5da60201eecda345c604b49138d5298e39b8e7d4d57a4dee47be3b0ecc8fc3429a2a60a42c952eaf860d43d3df1eb2b1d857e35368eca9c820 + languageName: node + linkType: hard + +"@solana/codecs-numbers@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/codecs-numbers@npm:2.0.0" + dependencies: + "@solana/codecs-core": "npm:2.0.0" + "@solana/errors": "npm:2.0.0" + peerDependencies: + typescript: ">=5" + checksum: 10/500144d549ea0292c2f672300610df9054339a31cb6a4e61b29623308ef3b14f15eb587ee6139cf3334d2e0f29db1da053522da244b12184bb8fbdb097b7102b + languageName: node + linkType: hard + +"@solana/codecs-strings@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/codecs-strings@npm:2.0.0" + dependencies: + "@solana/codecs-core": "npm:2.0.0" + "@solana/codecs-numbers": "npm:2.0.0" + "@solana/errors": "npm:2.0.0" + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: ">=5" + checksum: 10/4380136e2603c2cee12a28438817beb34b0fe45da222b8c38342c5b3680f02086ec7868cde0bb7b4e5dd459af5988613af1d97230c6a193db3be1c45122aba39 + languageName: node + linkType: hard + +"@solana/errors@npm:2.0.0": + version: 2.0.0 + resolution: "@solana/errors@npm:2.0.0" + dependencies: + chalk: "npm:^5.3.0" + commander: "npm:^12.1.0" + peerDependencies: + typescript: ">=5" + bin: + errors: bin/cli.mjs + checksum: 10/4191f96cad47c64266ec501ae1911a6245fd02b2f68a2c53c3dabbc63eb7c5462f170a765b584348b195da2387e7ca02096d792c67352c2c30a4f3a3cc7e4270 + languageName: node + linkType: hard + "@spruceid/siwe-parser@npm:2.1.0": version: 2.1.0 resolution: "@spruceid/siwe-parser@npm:2.1.0" @@ -7066,6 +7170,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^12.1.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 10/cdaeb672d979816853a4eed7f1310a9319e8b976172485c2a6b437ed0db0a389a44cfb222bfbde772781efa9f215bdd1b936f80d6b249485b465c6cb906e1f93 + languageName: node + linkType: hard + "commander@npm:^9.0.0": version: 9.5.0 resolution: "commander@npm:9.5.0"