From 47c7310446d04bfd2bf57be648ad83a47100ec1b Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Fri, 24 Jan 2025 09:56:21 -0300 Subject: [PATCH 01/69] feat add multichain-network-controller package --- .../CHANGELOG.md | 10 + .../multichain-network-controller/LICENSE | 20 ++ .../multichain-network-controller/README.md | 15 ++ .../jest.config.js | 26 +++ .../package.json | 71 +++++++ .../src/constants.ts | 45 ++++ .../src/index.ts | 25 +++ .../src/multichain-network-controller.ts | 193 ++++++++++++++++++ .../tsconfig.build.json | 12 ++ .../tsconfig.json | 25 +++ .../typedoc.json | 7 + yarn.lock | 22 ++ 12 files changed, 471 insertions(+) create mode 100644 packages/multichain-network-controller/CHANGELOG.md create mode 100644 packages/multichain-network-controller/LICENSE create mode 100644 packages/multichain-network-controller/README.md create mode 100644 packages/multichain-network-controller/jest.config.js create mode 100644 packages/multichain-network-controller/package.json create mode 100644 packages/multichain-network-controller/src/constants.ts create mode 100644 packages/multichain-network-controller/src/index.ts create mode 100644 packages/multichain-network-controller/src/multichain-network-controller.ts create mode 100644 packages/multichain-network-controller/tsconfig.build.json create mode 100644 packages/multichain-network-controller/tsconfig.json create mode 100644 packages/multichain-network-controller/typedoc.json 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..6f8bff03fc4 --- /dev/null +++ b/packages/multichain-network-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2024 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..b95e9453ec6 --- /dev/null +++ b/packages/multichain-network-controller/package.json @@ -0,0 +1,71 @@ +{ + "name": "@metamask/multichain-network-controller", + "version": "0.0.0", + "private": true, + "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" + }, + "dependencies": { + "@metamask/base-controller": "^7.1.1", + "@metamask/utils": "^11.0.1" + }, + "devDependencies": { + "@metamask/accounts-controller": "^21.0.1", + "@metamask/auto-changelog": "^3.4.4", + "@metamask/controller-utils": "^11.4.5", + "@metamask/network-controller": "^22.1.1", + "@types/jest": "^27.4.1", + "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" + }, + "engines": { + "node": "^18.18 || >=20" + } +} diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts new file mode 100644 index 00000000000..7c45a68e2f5 --- /dev/null +++ b/packages/multichain-network-controller/src/constants.ts @@ -0,0 +1,45 @@ +import { NetworkStatus } from '@metamask/network-controller'; + +import type { + MultichainNetworkConfiguration, + MultichainNetworkMetadata, +} from './multichain-network-controller'; + +export const bitcoinCaip2ChainId = 'bip122:000000000019d6689c085ae165831e93'; +export const solanaCaip2ChainId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; + +export const multichainNetworkConfigurations: Record = { + bitcoinCaip2ChainId: { + chainId: bitcoinCaip2ChainId, + + name: 'Bitcoin Mainnet', + + blockExplorerUrls: [], + + nativeCurrency: 'BTC', + + isEvm: false, + }, + solanaCaip2ChainId: { + chainId: solanaCaip2ChainId, + + name: 'Solana Mainnet', + + blockExplorerUrls: [], + + nativeCurrency: 'SOL', + + isEvm: false, + }, +}; + +export const networksMetadata: Record = { + bitcoinCaip2ChainId: { + features: [], + status: NetworkStatus.Available, + }, + solanaCaip2ChainId: { + features: [], + status: NetworkStatus.Available, + }, +}; diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts new file mode 100644 index 00000000000..a6741ec62c0 --- /dev/null +++ b/packages/multichain-network-controller/src/index.ts @@ -0,0 +1,25 @@ +export type { + GasPricesControllerActions, + GasPricesControllerEvents, + GasPricesControllerGetStateAction, + GasPricesControllerMessenger, + GasPricesControllerState, + GasPricesControllerStateChangeEvent, +} from './gas-prices-controller'; +export { + getDefaultGasPricesControllerState, + GasPricesController, +} from './gas-prices-controller'; +export type { + PetNamesControllerActions, + PetNamesControllerEvents, + PetNamesControllerGetStateAction, + PetNamesControllerMessenger, + PetNamesControllerState, + PetNamesControllerStateChangeEvent, +} from './multichain-network-controller'; +export { + getDefaultPetNamesControllerState, + PetNamesController, +} from './multichain-network-controller'; +export { GasPricesService } from './gas-prices-service/gas-prices-service'; diff --git a/packages/multichain-network-controller/src/multichain-network-controller.ts b/packages/multichain-network-controller/src/multichain-network-controller.ts new file mode 100644 index 00000000000..7389a2eb848 --- /dev/null +++ b/packages/multichain-network-controller/src/multichain-network-controller.ts @@ -0,0 +1,193 @@ +import type { AccountsControllerSetSelectedAccountAction } from '@metamask/accounts-controller'; +import { + BaseController, + type ControllerGetStateAction, + type ControllerStateChangeEvent, + type RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import type { + NetworkStatus, + NetworkControllerGetNetworkConfigurationByNetworkClientId, + NetworkControllerSetActiveNetworkAction, + NetworkControllerGetStateAction, + NetworkControllerStateChangeEvent, +} from '@metamask/network-controller'; +import type { Draft } from 'immer'; + +import { + bitcoinCaip2ChainId, + multichainNetworkConfigurations, + networksMetadata, +} from './constants'; + +const controllerName = 'MultichainNetworkController'; + +export type MultichainNetworkMetadata = { + features: string[]; + status: NetworkStatus; +}; + +export type MultichainNetworkConfiguration = { + chainId: string; // Should be Caip2 type + name: string; + nativeCurrency: string; // Should be Caip19 type + blockExplorerUrls: string[]; + defaultBlockExplorerUrlIndex?: number; + lastUpdated?: number; + isEvm?: false; +}; + +/** + * State used by the {@link MultichainNetworkController} to cache network configurations. + */ +export type MultichainNetworkControllerState = { + multichainNetworkConfigurationsByChainId: Record< + string, + MultichainNetworkConfiguration + >; + selectedMultichainNetworkChainId: string; + multichainNetworksMetadata: Record; +}; + +/** + * Default state of the {@link MultichainNetworkController}. + */ +export const defaultState: MultichainNetworkControllerState = { + multichainNetworkConfigurationsByChainId: multichainNetworkConfigurations, + selectedMultichainNetworkChainId: bitcoinCaip2ChainId, + multichainNetworksMetadata: networksMetadata, +}; + +/** + * Returns the state of the {@link MultichainNetworkController}. + */ +export type MultichainNetworkControllerGetStateAction = + ControllerGetStateAction< + typeof controllerName, + MultichainNetworkControllerState + >; + +/** + * Event emitted when the state of the {@link MultichainNetworkController} changes. + */ +export type MultichainNetworkStateControllerStateChange = + ControllerStateChangeEvent< + typeof controllerName, + MultichainNetworkControllerState + >; + +/** + * Actions exposed by the {@link MultichainNetworkController}. + */ +export type MultichainNetworkStateControllerActions = + MultichainNetworkControllerGetStateAction; + +/** + * Events emitted by {@link MultichainNetworkController}. + */ +export type MultichainNetworkControllerEvents = + MultichainNetworkStateControllerStateChange; + +/** + * Actions that this controller is allowed to call. + */ +export type AllowedActions = + | NetworkControllerGetStateAction + | NetworkControllerSetActiveNetworkAction + | AccountsControllerSetSelectedAccountAction + | NetworkControllerGetNetworkConfigurationByNetworkClientId; + +/** + * Events that this controller is allowed to subscribe. + */ +export type AllowedEvents = NetworkControllerStateChangeEvent; + +/** + * Messenger type for the MultichainNetworkController. + */ +export type MultichainNetworkControllerMessenger = + RestrictedControllerMessenger< + typeof controllerName, + MultichainNetworkStateControllerActions | AllowedActions, + MultichainNetworkControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] + >; + +/** + * {@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. + */ +const multichainNetworkControllerMetadata = { + multichainNetworkConfigurationsByChainId: { persist: true, anonymous: false }, + selectedMultichainNetworkChainId: { persist: true, anonymous: false }, + multichainNetworksMetadata: { persist: true, anonymous: false }, +}; + +/** + * The MultichainNetworkController is responsible for fetching and caching account + * balances. + */ +export class MultichainNetworkController extends BaseController< + typeof controllerName, + MultichainNetworkControllerState, + MultichainNetworkControllerMessenger +> { + constructor({ + messenger, + state, + }: { + messenger: MultichainNetworkControllerMessenger; + state: MultichainNetworkControllerState; + }) { + super({ + messenger, + name: controllerName, + metadata: multichainNetworkControllerMetadata, + state: { + ...defaultState, + ...state, + }, + }); + } + + async setActiveNetwork( + networkConfigurationId: string, + chainId?: string, + ): Promise { + console.log( + 'start setActiveNetwork in MultichainNetworkController', + networkConfigurationId, + chainId, + ); + if (chainId && Object.keys(this.state).includes(chainId)) { + console.log( + 'MultichainNetworkController: update network configuration', + networkConfigurationId, + chainId, + ); + this.update((state: Draft) => { + state.selectedMultichainNetworkChainId = chainId; + // state.nonEvmSelected = true; + }); + return; + } + + console.log( + 'MultichainNetworkController: update network configuration on NetworkController', + networkConfigurationId, + chainId, + ); + // this.update((state: Draft) => { + // state.nonEvmSelected = false; + // }); + + await this.messagingSystem.call( + 'NetworkController:setActiveNetwork', + networkConfigurationId, + ); + } +} diff --git a/packages/multichain-network-controller/tsconfig.build.json b/packages/multichain-network-controller/tsconfig.build.json new file mode 100644 index 00000000000..7211fee8918 --- /dev/null +++ b/packages/multichain-network-controller/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/types", + "rootDir": "./src" + }, + "references": [ + { "path": "../../packages/base-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..55667c1f975 --- /dev/null +++ b/packages/multichain-network-controller/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../controller-utils" }, + { "path": "../accounts-controller" }, + { "path": "../network-controller" } + ], + "include": ["../../types", "./src"], + /** + * Here we ensure that TypeScript resolves `@metamask/*` imports to the + * uncompiled source code for packages that live in this repo. + * + * NOTE: This must be synchronized with the `moduleNameMapper` option in + * `jest.config.packages.js`. + * + * NOTE 2: This is not necessary when copying this package to `packages/`. + */ + "paths": { + "@metamask/*": ["../../packages/*/src", "../*/src"] + } +} 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/yarn.lock b/yarn.lock index c78d34c1204..f5c1174bd2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3309,6 +3309,28 @@ __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/accounts-controller": "npm:^21.0.1" + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^7.1.1" + "@metamask/controller-utils": "npm:^11.4.5" + "@metamask/network-controller": "npm:^22.1.1" + "@metamask/utils": "npm:^11.0.1" + "@types/jest": "npm:^27.4.1" + 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" + languageName: unknown + linkType: soft + "@metamask/multichain@workspace:packages/multichain": version: 0.0.0-use.local resolution: "@metamask/multichain@workspace:packages/multichain" From 7b64a8cc2861be4476de8270767786b99de614e3 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Fri, 24 Jan 2025 09:58:21 -0300 Subject: [PATCH 02/69] chore: update exports --- .../src/index.ts | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts index a6741ec62c0..e2128ca49d9 100644 --- a/packages/multichain-network-controller/src/index.ts +++ b/packages/multichain-network-controller/src/index.ts @@ -1,25 +1,2 @@ -export type { - GasPricesControllerActions, - GasPricesControllerEvents, - GasPricesControllerGetStateAction, - GasPricesControllerMessenger, - GasPricesControllerState, - GasPricesControllerStateChangeEvent, -} from './gas-prices-controller'; -export { - getDefaultGasPricesControllerState, - GasPricesController, -} from './gas-prices-controller'; -export type { - PetNamesControllerActions, - PetNamesControllerEvents, - PetNamesControllerGetStateAction, - PetNamesControllerMessenger, - PetNamesControllerState, - PetNamesControllerStateChangeEvent, -} from './multichain-network-controller'; -export { - getDefaultPetNamesControllerState, - PetNamesController, -} from './multichain-network-controller'; -export { GasPricesService } from './gas-prices-service/gas-prices-service'; +export * from './multichain-network-controller'; +export * from './constants'; From 37dc7c52464fd77e497a61ebd1f5d3d67b363a4e Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Fri, 24 Jan 2025 12:38:45 -0300 Subject: [PATCH 03/69] fix: build --- .../multichain-network-controller/package.json | 3 +-- ...oller.ts => MultichainNetworkController.ts} | 0 .../src/constants.ts | 2 +- .../multichain-network-controller/src/index.ts | 18 ++++++++++++++++-- .../tsconfig.build.json | 6 ++++-- .../tsconfig.json | 1 - tsconfig.build.json | 3 ++- 7 files changed, 24 insertions(+), 9 deletions(-) rename packages/multichain-network-controller/src/{multichain-network-controller.ts => MultichainNetworkController.ts} (100%) diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index b95e9453ec6..1e50b1bed4e 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/multichain-network-controller", - "version": "0.0.0", + "version": "0.0.1", "private": true, "description": "Multichain network controller", "keywords": [ @@ -53,7 +53,6 @@ "devDependencies": { "@metamask/accounts-controller": "^21.0.1", "@metamask/auto-changelog": "^3.4.4", - "@metamask/controller-utils": "^11.4.5", "@metamask/network-controller": "^22.1.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", diff --git a/packages/multichain-network-controller/src/multichain-network-controller.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts similarity index 100% rename from packages/multichain-network-controller/src/multichain-network-controller.ts rename to packages/multichain-network-controller/src/MultichainNetworkController.ts diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index 7c45a68e2f5..b371dba2477 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -3,7 +3,7 @@ import { NetworkStatus } from '@metamask/network-controller'; import type { MultichainNetworkConfiguration, MultichainNetworkMetadata, -} from './multichain-network-controller'; +} from './MultichainNetworkController'; export const bitcoinCaip2ChainId = 'bip122:000000000019d6689c085ae165831e93'; export const solanaCaip2ChainId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts index e2128ca49d9..abb722b27ae 100644 --- a/packages/multichain-network-controller/src/index.ts +++ b/packages/multichain-network-controller/src/index.ts @@ -1,2 +1,16 @@ -export * from './multichain-network-controller'; -export * from './constants'; +export type { + MultichainNetworkConfiguration, + MultichainNetworkMetadata, + MultichainNetworkControllerState, + MultichainNetworkControllerGetStateAction, + AllowedActions, + AllowedEvents, + MultichainNetworkControllerMessenger, +} from './MultichainNetworkController'; +export { MultichainNetworkController } from './MultichainNetworkController'; +export { + bitcoinCaip2ChainId, + solanaCaip2ChainId, + multichainNetworkConfigurations, + networksMetadata, +} from './constants'; diff --git a/packages/multichain-network-controller/tsconfig.build.json b/packages/multichain-network-controller/tsconfig.build.json index 7211fee8918..7609b693a3b 100644 --- a/packages/multichain-network-controller/tsconfig.build.json +++ b/packages/multichain-network-controller/tsconfig.build.json @@ -2,11 +2,13 @@ "extends": "../../tsconfig.packages.build.json", "compilerOptions": { "baseUrl": "./", - "outDir": "./dist/types", + "outDir": "./dist", "rootDir": "./src" }, "references": [ - { "path": "../../packages/base-controller/tsconfig.build.json" } + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../network-controller/tsconfig.build.json" }, + { "path": "../accounts-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/multichain-network-controller/tsconfig.json b/packages/multichain-network-controller/tsconfig.json index 55667c1f975..d1a5db81ec3 100644 --- a/packages/multichain-network-controller/tsconfig.json +++ b/packages/multichain-network-controller/tsconfig.json @@ -5,7 +5,6 @@ }, "references": [ { "path": "../base-controller" }, - { "path": "../controller-utils" }, { "path": "../accounts-controller" }, { "path": "../network-controller" } ], diff --git a/tsconfig.build.json b/tsconfig.build.json index a5ac14a68ca..cdc9f000373 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -38,7 +38,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": [] From 454c3dbcff68e5b099d0d621531e1ea9db728c11 Mon Sep 17 00:00:00 2001 From: gantunesr <17601467+gantunesr@users.noreply.github.com> Date: Mon, 27 Jan 2025 01:34:07 -0300 Subject: [PATCH 04/69] chore: update initial state --- .../src/MultichainNetworkController.ts | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 7389a2eb848..d9bbe3f0730 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -47,15 +47,17 @@ export type MultichainNetworkControllerState = { >; selectedMultichainNetworkChainId: string; multichainNetworksMetadata: Record; + nonEvmSelected: boolean; }; /** * Default state of the {@link MultichainNetworkController}. */ export const defaultState: MultichainNetworkControllerState = { - multichainNetworkConfigurationsByChainId: multichainNetworkConfigurations, + multichainNetworkConfigurationsByChainId: {}, selectedMultichainNetworkChainId: bitcoinCaip2ChainId, - multichainNetworksMetadata: networksMetadata, + multichainNetworksMetadata: {}, + nonEvmSelected: false, }; /** @@ -125,6 +127,7 @@ const multichainNetworkControllerMetadata = { multichainNetworkConfigurationsByChainId: { persist: true, anonymous: false }, selectedMultichainNetworkChainId: { persist: true, anonymous: false }, multichainNetworksMetadata: { persist: true, anonymous: false }, + nonEvmSelected: { persist: true, anonymous: false }, }; /** @@ -154,40 +157,25 @@ export class MultichainNetworkController extends BaseController< }); } - async setActiveNetwork( - networkConfigurationId: string, - chainId?: string, - ): Promise { - console.log( - 'start setActiveNetwork in MultichainNetworkController', - networkConfigurationId, - chainId, - ); + async setActiveNetwork(clientId: string, chainId?: string): Promise { if (chainId && Object.keys(this.state).includes(chainId)) { - console.log( - 'MultichainNetworkController: update network configuration', - networkConfigurationId, - chainId, - ); this.update((state: Draft) => { state.selectedMultichainNetworkChainId = chainId; - // state.nonEvmSelected = true; + state.nonEvmSelected = true; }); return; } - console.log( - 'MultichainNetworkController: update network configuration on NetworkController', - networkConfigurationId, - chainId, - ); - // this.update((state: Draft) => { - // state.nonEvmSelected = false; - // }); + this.update((state: Draft) => { + state.nonEvmSelected = false; + }); await this.messagingSystem.call( 'NetworkController:setActiveNetwork', - networkConfigurationId, + clientId, ); + + // TO DO: Should emit event to notify that the network has changed + // so the accounts-controller can update the selected account } } From 2f4d06c31cdf189a6b7f41e77ce75642fe6750fb Mon Sep 17 00:00:00 2001 From: tommasini Date: Wed, 29 Jan 2025 00:15:57 +0000 Subject: [PATCH 05/69] add setter for update nonEvmNetwork state variable to true and setter to update nonEvmNetwork state variable to false; added unit tests for setActiveNetwork and both new setters --- .../src/MultichainNetworkController.test.ts | 154 ++++++++++++++++++ .../src/MultichainNetworkController.ts | 20 ++- 2 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 packages/multichain-network-controller/src/MultichainNetworkController.test.ts 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..f8afc5e7ceb --- /dev/null +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -0,0 +1,154 @@ +import { ControllerMessenger } from '@metamask/base-controller'; +import type { + MultichainNetworkStateControllerActions, + MultichainNetworkControllerEvents, +} from './MultichainNetworkController'; +import { MultichainNetworkController } from './MultichainNetworkController'; +import { bitcoinCaip2ChainId } from './constants'; + +const name = 'MultichainNetworkController'; + +type AllowedActions = MultichainNetworkStateControllerActions | { + type: 'NetworkController:setActiveNetwork'; + handler: (clientId: string) => void; +}; + +type AllowedEvents = MultichainNetworkControllerEvents; + +const buildMessenger = () => { + return new ControllerMessenger(); +}; + +const buildMultichainNetworkControllerMessenger = ( + messenger: ControllerMessenger, +) => { + return messenger.getRestricted({ + name, + allowedActions: ['NetworkController:setActiveNetwork'], + allowedEvents: [], + }); +}; + +describe('MultichainNetworkController', () => { + let controller: MultichainNetworkController; + let messenger: ControllerMessenger; + + beforeEach(() => { + messenger = buildMessenger(); + messenger.registerActionHandler( + 'NetworkController:setActiveNetwork', + jest.fn(), + ); + + jest.spyOn(messenger, 'call'); + + const restrictedMessenger = buildMultichainNetworkControllerMessenger(messenger); + + controller = new MultichainNetworkController({ + messenger: restrictedMessenger, + state: { + multichainNetworkConfigurationsByChainId: {}, + selectedMultichainNetworkChainId: bitcoinCaip2ChainId, + multichainNetworksMetadata: {}, + nonEvmSelected: false, + }, + }); + }); + + describe('setActiveNetwork', () => { + it('should set non-EVM network when valid chainId is provided', async () => { + const clientId = 'testClient'; + const chainId = 'bip122:000000000019d6689c085ae165831e93'; + messenger = buildMessenger(); + messenger.registerActionHandler( + 'NetworkController:setActiveNetwork', + jest.fn(), + ); + + jest.spyOn(messenger, 'call'); + + const restrictedMessenger = buildMultichainNetworkControllerMessenger(messenger); + + const multiChainController = new MultichainNetworkController({ + messenger: restrictedMessenger, + state: { + multichainNetworkConfigurationsByChainId: { + [chainId]: { + chainId, + name: 'Bitcoin', + nativeCurrency: 'BTC', + blockExplorerUrls: ['https://blockstream.info/'], + }, + }, + selectedMultichainNetworkChainId: bitcoinCaip2ChainId, + multichainNetworksMetadata: {}, + nonEvmSelected: false, + }, + }); + + await multiChainController.setActiveNetwork(clientId, chainId); + + expect(multiChainController.state.selectedMultichainNetworkChainId).toBe(chainId); + expect(multiChainController.state.nonEvmSelected).toBe(true); + expect(messenger.call).not.toHaveBeenCalled(); + }); + + it('should set EVM network when chainId is not provided', async () => { + const clientId = 'testClient'; + + await controller.setActiveNetwork(clientId); + + expect(controller.state.nonEvmSelected).toBe(false); + expect(messenger.call).toHaveBeenCalledWith( + 'NetworkController:setActiveNetwork', + clientId, + ); + }); + + it('should set EVM network when invalid chainId is provided', async () => { + const clientId = 'testClient'; + const invalidChainId = 'invalid-chain-id'; + + await controller.setActiveNetwork(clientId, invalidChainId); + + expect(controller.state.nonEvmSelected).toBe(false); + expect(messenger.call).toHaveBeenCalledWith( + 'NetworkController:setActiveNetwork', + clientId, + ); + }); + }); + + describe('setNonEvmSelected', () => { + it('should set nonEvmSelected to true', () => { + controller.setNonEvmSelected(); + expect(controller.state.nonEvmSelected).toBe(true); + }); + }); + + describe('setEvmSelected', () => { + it('should set nonEvmSelected to false', () => { + messenger = buildMessenger(); + messenger.registerActionHandler( + 'NetworkController:setActiveNetwork', + jest.fn(), + ); + + jest.spyOn(messenger, 'call'); + + const restrictedMessenger = buildMultichainNetworkControllerMessenger(messenger); + const multiChainController = new MultichainNetworkController({ + messenger: restrictedMessenger, + state: { + multichainNetworkConfigurationsByChainId: {}, + selectedMultichainNetworkChainId: bitcoinCaip2ChainId, + multichainNetworksMetadata: {}, + nonEvmSelected: true, + }, + }); + + multiChainController.setEvmSelected(); + expect(multiChainController.state.nonEvmSelected).toBe(false); + }); + }); +}); diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index d9bbe3f0730..66f51456bdd 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -158,7 +158,7 @@ export class MultichainNetworkController extends BaseController< } async setActiveNetwork(clientId: string, chainId?: string): Promise { - if (chainId && Object.keys(this.state).includes(chainId)) { + if (chainId && Object.keys(this.state.multichainNetworkConfigurationsByChainId).includes(chainId)) { this.update((state: Draft) => { state.selectedMultichainNetworkChainId = chainId; state.nonEvmSelected = true; @@ -175,7 +175,21 @@ export class MultichainNetworkController extends BaseController< clientId, ); - // TO DO: Should emit event to notify that the network has changed - // so the accounts-controller can update the selected account + } + /** + * Sets the non-EVM selected network. + */ + setNonEvmSelected() { + this.update((state: Draft) => { + state.nonEvmSelected = true; + }); + } + /** + * Sets the EVM selected network. + */ + setEvmSelected() { + this.update((state: Draft) => { + state.nonEvmSelected = false; + }); } } From 2f8058a77a0e0aba45853faf4878fc3df0653caa Mon Sep 17 00:00:00 2001 From: tommasini Date: Wed, 29 Jan 2025 00:44:21 +0000 Subject: [PATCH 06/69] added blank line between functions --- .../src/MultichainNetworkController.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 66f51456bdd..ac6124b2ec2 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -176,6 +176,7 @@ export class MultichainNetworkController extends BaseController< ); } + /** * Sets the non-EVM selected network. */ @@ -184,6 +185,7 @@ export class MultichainNetworkController extends BaseController< state.nonEvmSelected = true; }); } + /** * Sets the EVM selected network. */ From 6b7b3b60adc2b95c3744e7b7b528ea95211ed1d5 Mon Sep 17 00:00:00 2001 From: tommasini Date: Wed, 29 Jan 2025 00:56:46 +0000 Subject: [PATCH 07/69] removed unused import and re order imports, to fix lint warnings --- .../src/MultichainNetworkController.test.ts | 3 ++- .../src/MultichainNetworkController.ts | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index f8afc5e7ceb..80b968c7fc7 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -1,10 +1,11 @@ import { ControllerMessenger } from '@metamask/base-controller'; + import type { MultichainNetworkStateControllerActions, MultichainNetworkControllerEvents, } from './MultichainNetworkController'; -import { MultichainNetworkController } from './MultichainNetworkController'; import { bitcoinCaip2ChainId } from './constants'; +import { MultichainNetworkController } from './MultichainNetworkController'; const name = 'MultichainNetworkController'; diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index ac6124b2ec2..bfda573c40c 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -16,8 +16,6 @@ import type { Draft } from 'immer'; import { bitcoinCaip2ChainId, - multichainNetworkConfigurations, - networksMetadata, } from './constants'; const controllerName = 'MultichainNetworkController'; @@ -185,7 +183,7 @@ export class MultichainNetworkController extends BaseController< state.nonEvmSelected = true; }); } - + /** * Sets the EVM selected network. */ From 8a5c9d0304b0e9cd1c6cb1b28a5cd81574b396ed Mon Sep 17 00:00:00 2001 From: Cal-L Date: Wed, 29 Jan 2025 13:35:33 -0800 Subject: [PATCH 08/69] Handle network and account updates when switching networks --- .../src/MultichainNetworkController.ts | 131 +++++++++++++----- 1 file changed, 93 insertions(+), 38 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index bfda573c40c..37cf13991b0 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -1,4 +1,8 @@ -import type { AccountsControllerSetSelectedAccountAction } from '@metamask/accounts-controller'; +import type { + AccountsControllerGetSelectedAccountAction, + AccountsControllerGetSelectedMultichainAccountAction, + AccountsControllerSetSelectedAccountAction, +} from '@metamask/accounts-controller'; import { BaseController, type ControllerGetStateAction, @@ -7,16 +11,12 @@ import { } from '@metamask/base-controller'; import type { NetworkStatus, - NetworkControllerGetNetworkConfigurationByNetworkClientId, NetworkControllerSetActiveNetworkAction, NetworkControllerGetStateAction, - NetworkControllerStateChangeEvent, } from '@metamask/network-controller'; import type { Draft } from 'immer'; - -import { - bitcoinCaip2ChainId, -} from './constants'; +import { bitcoinCaip2ChainId } from './constants'; +import { CaipChainId } from '@metamask/utils'; const controllerName = 'MultichainNetworkController'; @@ -95,12 +95,8 @@ export type AllowedActions = | NetworkControllerGetStateAction | NetworkControllerSetActiveNetworkAction | AccountsControllerSetSelectedAccountAction - | NetworkControllerGetNetworkConfigurationByNetworkClientId; - -/** - * Events that this controller is allowed to subscribe. - */ -export type AllowedEvents = NetworkControllerStateChangeEvent; + | AccountsControllerGetSelectedAccountAction + | AccountsControllerGetSelectedMultichainAccountAction; /** * Messenger type for the MultichainNetworkController. @@ -109,9 +105,9 @@ export type MultichainNetworkControllerMessenger = RestrictedControllerMessenger< typeof controllerName, MultichainNetworkStateControllerActions | AllowedActions, - MultichainNetworkControllerEvents | AllowedEvents, + MultichainNetworkControllerEvents, AllowedActions['type'], - AllowedEvents['type'] + never >; /** @@ -155,41 +151,100 @@ export class MultichainNetworkController extends BaseController< }); } - async setActiveNetwork(clientId: string, chainId?: string): Promise { - if (chainId && Object.keys(this.state.multichainNetworkConfigurationsByChainId).includes(chainId)) { + /** + * Handles switching between EVM and non-EVM networks. + * + * @param evmClientId - The client ID of the EVM network to set active. + * @param nonEvmChainId - The chain ID of the non-EVM network to set active. + */ + async setActiveNetwork({ + evmClientId, + nonEvmChainId, + }: { + evmClientId?: string; + nonEvmChainId?: CaipChainId; + }): Promise { + // Throw an error if both EVM and non-EVM networks are set + if (evmClientId && nonEvmChainId) { + throw new Error('Cannot set both EVM and non-EVM networks!'); + } + + // Handle non-EVM networks + if (nonEvmChainId) { + // Prevent setting same network + if (nonEvmChainId === this.state.selectedMultichainNetworkChainId) { + // Indicate that the non-EVM network is selected + this.update((state: Draft) => { + state.nonEvmSelected = true; + }); + return; + } + + // Check if the non-EVM chain ID is supported + if ( + !Object.keys( + this.state.multichainNetworkConfigurationsByChainId, + ).includes(nonEvmChainId) + ) { + throw new Error('Non-EVM chain ID is not supported!'); + } + + // Update selected account to non evm account + const lastSelectedNonEvmAccount = await this.messagingSystem.call( + 'AccountsController:getSelectedMultichainAccount', + nonEvmChainId, + ); + + if (!lastSelectedNonEvmAccount?.id) { + throw new Error('No non-EVM account found!'); + } + + this.messagingSystem.call( + 'AccountsController:setSelectedAccount', + lastSelectedNonEvmAccount.id, + ); + this.update((state: Draft) => { - state.selectedMultichainNetworkChainId = chainId; + state.selectedMultichainNetworkChainId = nonEvmChainId; state.nonEvmSelected = true; }); + return; } - this.update((state: Draft) => { - state.nonEvmSelected = false; - }); + // Handle EVM networks + if (!evmClientId) { + throw new Error('EVM client ID is required!'); + } - await this.messagingSystem.call( - 'NetworkController:setActiveNetwork', - clientId, + // Update evm selected account + const lastSelectedEvmAccount = await this.messagingSystem.call( + 'AccountsController:getSelectedAccount', ); - } - - /** - * Sets the non-EVM selected network. - */ - setNonEvmSelected() { - this.update((state: Draft) => { - state.nonEvmSelected = true; - }); - } + this.messagingSystem.call( + 'AccountsController:setSelectedAccount', + lastSelectedEvmAccount.id, + ); - /** - * Sets the EVM selected network. - */ - setEvmSelected() { + // Indicate that the non-EVM network is not selected this.update((state: Draft) => { state.nonEvmSelected = false; }); + + // Prevent setting same network + const { selectedNetworkClientId } = await this.messagingSystem.call( + 'NetworkController:getState', + ); + + if (evmClientId === selectedNetworkClientId) { + return; + } + + // Update evm active network + this.messagingSystem.call( + 'NetworkController:setActiveNetwork', + evmClientId, + ); } } From f8980567999acc44896240b5837ef1d5ad3433a9 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Wed, 29 Jan 2025 14:41:21 -0800 Subject: [PATCH 09/69] Register message handlers --- .../src/MultichainNetworkController.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 37cf13991b0..e5d34860ad1 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -67,6 +67,11 @@ export type MultichainNetworkControllerGetStateAction = MultichainNetworkControllerState >; +export type MultichainNetworkControllerSetActiveNetworkAction = { + type: `${typeof controllerName}:setActiveNetwork`; + handler: MultichainNetworkController['setActiveNetwork']; +}; + /** * Event emitted when the state of the {@link MultichainNetworkController} changes. */ @@ -80,7 +85,8 @@ export type MultichainNetworkStateControllerStateChange = * Actions exposed by the {@link MultichainNetworkController}. */ export type MultichainNetworkStateControllerActions = - MultichainNetworkControllerGetStateAction; + | MultichainNetworkControllerGetStateAction + | MultichainNetworkControllerSetActiveNetworkAction; /** * Events emitted by {@link MultichainNetworkController}. @@ -149,6 +155,8 @@ export class MultichainNetworkController extends BaseController< ...state, }, }); + + this.#registerMessageHandlers(); } /** @@ -247,4 +255,15 @@ export class MultichainNetworkController extends BaseController< evmClientId, ); } + + /** + * Registers message handlers. + * @private + */ + #registerMessageHandlers() { + this.messagingSystem.registerActionHandler( + 'MultichainNetworkController:setActiveNetwork', + this.setActiveNetwork.bind(this), + ); + } } From edb686887e3fa0cae98c9695b44ba8397a86a11d Mon Sep 17 00:00:00 2001 From: Cal-L Date: Wed, 29 Jan 2025 21:23:07 -0800 Subject: [PATCH 10/69] Separate concerns when updating network on multichain network controller --- .../src/AccountsController.ts | 126 ++++++++++++------ .../src/MultichainNetworkController.ts | 75 +++++++---- .../src/index.ts | 1 + 3 files changed, 140 insertions(+), 62 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index 8bcc55889ea..4bf286154ad 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -25,6 +25,7 @@ import type { KeyringControllerGetAccountsAction, KeyringControllerStateChangeEvent, } from '@metamask/keyring-controller'; +import type { MultichainNetworkSetActiveNetworkEvent } from '@metamask/multichain-network-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { SnapControllerState, @@ -187,7 +188,8 @@ export type AllowedEvents = | KeyringControllerStateChangeEvent | SnapKeyringAccountAssetListUpdatedEvent | SnapKeyringAccountBalancesUpdatedEvent - | SnapKeyringAccountTransactionsUpdatedEvent; + | SnapKeyringAccountTransactionsUpdatedEvent + | MultichainNetworkSetActiveNetworkEvent; 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(); } @@ -1169,6 +1135,90 @@ export class AccountsController extends BaseController< return internalAccount ? internalAccount.metadata[metadataKey] : undefined; } + /** + * Subscribes to message events. + * @private + */ + #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:setActiveNetwork', + ({ evmClientId, nonEvmChainId }) => { + if (evmClientId && nonEvmChainId) { + throw new Error( + 'Cannot set accounts from both EVM and non-EVM networks!', + ); + } + + let accountId: string | undefined; + + if (nonEvmChainId) { + // Update selected account to non evm account + const lastSelectedNonEvmAccount = + this.getSelectedMultichainAccount(nonEvmChainId); + if (!lastSelectedNonEvmAccount?.id) { + throw new Error('No non-EVM account found!'); + } + accountId = lastSelectedNonEvmAccount?.id; + } else if (evmClientId) { + // Update selected account to evm account + const lastSelectedEvmAccount = this.getSelectedAccount(); + accountId = lastSelectedEvmAccount.id; + } + + if (!accountId) { + throw new Error( + `No account found when switching multichain network! evmClientId - ${evmClientId}, nonEvmChainId - ${nonEvmChainId}`, + ); + } + + this.update((currentState: Draft) => { + currentState.internalAccounts.accounts[ + accountId + ].metadata.lastSelected = Date.now(); + currentState.internalAccounts.selectedAccount = accountId; + }); + }, + ); + } + /** * Registers message handlers for the AccountsController. * @private diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index e5d34860ad1..0e1512f7b82 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -1,6 +1,8 @@ import type { AccountsControllerGetSelectedAccountAction, AccountsControllerGetSelectedMultichainAccountAction, + AccountsControllerSelectedAccountChangeEvent, + AccountsControllerSelectedEvmAccountChangeEvent, AccountsControllerSetSelectedAccountAction, } from '@metamask/accounts-controller'; import { @@ -81,6 +83,16 @@ export type MultichainNetworkStateControllerStateChange = MultichainNetworkControllerState >; +export type MultichainNetworkSetActiveNetworkEvent = { + type: `${typeof controllerName}:setActiveNetwork`; + payload: [ + { + evmClientId?: string; + nonEvmChainId?: CaipChainId; + }, + ]; +}; + /** * Actions exposed by the {@link MultichainNetworkController}. */ @@ -92,7 +104,8 @@ export type MultichainNetworkStateControllerActions = * Events emitted by {@link MultichainNetworkController}. */ export type MultichainNetworkControllerEvents = - MultichainNetworkStateControllerStateChange; + | MultichainNetworkStateControllerStateChange + | MultichainNetworkSetActiveNetworkEvent; /** * Actions that this controller is allowed to call. @@ -104,6 +117,13 @@ export type AllowedActions = | AccountsControllerGetSelectedAccountAction | AccountsControllerGetSelectedMultichainAccountAction; +/** + * Events that this controller is allowed to subscribe. + */ +export type AllowedEvents = + | AccountsControllerSelectedAccountChangeEvent + | AccountsControllerSelectedEvmAccountChangeEvent; + /** * Messenger type for the MultichainNetworkController. */ @@ -111,9 +131,9 @@ export type MultichainNetworkControllerMessenger = RestrictedControllerMessenger< typeof controllerName, MultichainNetworkStateControllerActions | AllowedActions, - MultichainNetworkControllerEvents, + MultichainNetworkControllerEvents | AllowedEvents, AllowedActions['type'], - never + AllowedEvents['type'] >; /** @@ -156,6 +176,7 @@ export class MultichainNetworkController extends BaseController< }, }); + this.#subscribeToMessageEvents(); this.#registerMessageHandlers(); } @@ -197,19 +218,9 @@ export class MultichainNetworkController extends BaseController< throw new Error('Non-EVM chain ID is not supported!'); } - // Update selected account to non evm account - const lastSelectedNonEvmAccount = await this.messagingSystem.call( - 'AccountsController:getSelectedMultichainAccount', - nonEvmChainId, - ); - - if (!lastSelectedNonEvmAccount?.id) { - throw new Error('No non-EVM account found!'); - } - - this.messagingSystem.call( - 'AccountsController:setSelectedAccount', - lastSelectedNonEvmAccount.id, + this.messagingSystem.publish( + 'MultichainNetworkController:setActiveNetwork', + { nonEvmChainId }, ); this.update((state: Draft) => { @@ -225,14 +236,11 @@ export class MultichainNetworkController extends BaseController< throw new Error('EVM client ID is required!'); } - // Update evm selected account - const lastSelectedEvmAccount = await this.messagingSystem.call( - 'AccountsController:getSelectedAccount', - ); - - this.messagingSystem.call( - 'AccountsController:setSelectedAccount', - lastSelectedEvmAccount.id, + this.messagingSystem.publish( + 'MultichainNetworkController:setActiveNetwork', + { + evmClientId, + }, ); // Indicate that the non-EVM network is not selected @@ -256,6 +264,25 @@ export class MultichainNetworkController extends BaseController< ); } + /** + * Subscribes to message events. + * @private + */ + #subscribeToMessageEvents() { + this.messagingSystem.subscribe( + 'AccountsController:selectedAccountChange', + async (state) => { + // Switch to non-EVM network + }, + ); + this.messagingSystem.subscribe( + 'AccountsController:selectedEvmAccountChange', + async (state) => { + // Switch to EVM network + }, + ); + } + /** * Registers message handlers. * @private diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts index abb722b27ae..bdb972aae79 100644 --- a/packages/multichain-network-controller/src/index.ts +++ b/packages/multichain-network-controller/src/index.ts @@ -3,6 +3,7 @@ export type { MultichainNetworkMetadata, MultichainNetworkControllerState, MultichainNetworkControllerGetStateAction, + MultichainNetworkSetActiveNetworkEvent, AllowedActions, AllowedEvents, MultichainNetworkControllerMessenger, From 1c3dc668fd6ae5815d3d53dd5823f58f067b397f Mon Sep 17 00:00:00 2001 From: Cal-L Date: Wed, 29 Jan 2025 21:53:28 -0800 Subject: [PATCH 11/69] Handle switching network when account changes --- .../src/MultichainNetworkController.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 0e1512f7b82..c8e80c8445e 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -19,6 +19,7 @@ import type { import type { Draft } from 'immer'; import { bitcoinCaip2ChainId } from './constants'; import { CaipChainId } from '@metamask/utils'; +import { isEvmAccountType } from '@metamask/keyring-api'; const controllerName = 'MultichainNetworkController'; @@ -269,16 +270,20 @@ export class MultichainNetworkController extends BaseController< * @private */ #subscribeToMessageEvents() { + // Handle network switch when account is changed this.messagingSystem.subscribe( 'AccountsController:selectedAccountChange', - async (state) => { - // Switch to non-EVM network - }, - ); - this.messagingSystem.subscribe( - 'AccountsController:selectedEvmAccountChange', - async (state) => { - // Switch to EVM network + async ({ type: accountType }) => { + const isNonEvmAccount = !isEvmAccountType(accountType); + + // No need to update if already on the correct network + if (isNonEvmAccount === this.state.nonEvmSelected) { + return; + } + + this.update((state: Draft) => { + state.nonEvmSelected = isNonEvmAccount; + }); }, ); } From c1050f7a425aa60e5ba125678c028ac16c691ce5 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Wed, 29 Jan 2025 21:56:54 -0800 Subject: [PATCH 12/69] Remove unused actions and events --- .../src/MultichainNetworkController.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index c8e80c8445e..cf9fbbf2b7c 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -1,10 +1,4 @@ -import type { - AccountsControllerGetSelectedAccountAction, - AccountsControllerGetSelectedMultichainAccountAction, - AccountsControllerSelectedAccountChangeEvent, - AccountsControllerSelectedEvmAccountChangeEvent, - AccountsControllerSetSelectedAccountAction, -} from '@metamask/accounts-controller'; +import type { AccountsControllerSelectedAccountChangeEvent } from '@metamask/accounts-controller'; import { BaseController, type ControllerGetStateAction, @@ -113,17 +107,12 @@ export type MultichainNetworkControllerEvents = */ export type AllowedActions = | NetworkControllerGetStateAction - | NetworkControllerSetActiveNetworkAction - | AccountsControllerSetSelectedAccountAction - | AccountsControllerGetSelectedAccountAction - | AccountsControllerGetSelectedMultichainAccountAction; + | NetworkControllerSetActiveNetworkAction; /** * Events that this controller is allowed to subscribe. */ -export type AllowedEvents = - | AccountsControllerSelectedAccountChangeEvent - | AccountsControllerSelectedEvmAccountChangeEvent; +export type AllowedEvents = AccountsControllerSelectedAccountChangeEvent; /** * Messenger type for the MultichainNetworkController. From a89bff469aaa0622108d8b1193e65c70d731429b Mon Sep 17 00:00:00 2001 From: tommasini Date: Thu, 30 Jan 2025 21:46:33 +0000 Subject: [PATCH 13/69] btc and solana chain id from keyring api, update types and added javascript documentation, and solve most of the review comments --- .../multichain-network-controller/LICENSE | 2 +- .../package.json | 9 +- .../src/MultichainNetworkController.test.ts | 14 +- .../src/MultichainNetworkController.ts | 124 +++++++++++------- .../src/constants.ts | 22 ++-- .../src/index.ts | 4 - .../tsconfig.json | 12 -- yarn.lock | 48 ++++++- 8 files changed, 142 insertions(+), 93 deletions(-) diff --git a/packages/multichain-network-controller/LICENSE b/packages/multichain-network-controller/LICENSE index 6f8bff03fc4..7d002dced3a 100644 --- a/packages/multichain-network-controller/LICENSE +++ b/packages/multichain-network-controller/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 MetaMask +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 diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index 1e50b1bed4e..e9fb31c0408 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/multichain-network-controller", - "version": "0.0.1", + "version": "0.0.0", "private": true, "description": "Multichain network controller", "keywords": [ @@ -48,12 +48,11 @@ }, "dependencies": { "@metamask/base-controller": "^7.1.1", + "@metamask/keyring-api": "^16.1.0", "@metamask/utils": "^11.0.1" }, "devDependencies": { - "@metamask/accounts-controller": "^21.0.1", "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.1.1", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "immer": "^9.0.6", @@ -64,6 +63,10 @@ "typedoc-plugin-missing-exports": "^2.0.0", "typescript": "~5.2.2" }, + "peerDependencies": { + "@metamask/accounts-controller": "^21.0.1", + "@metamask/network-controller": "^22.1.1" + }, "engines": { "node": "^18.18 || >=20" } diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index 80b968c7fc7..529468c104e 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -1,21 +1,13 @@ import { ControllerMessenger } from '@metamask/base-controller'; - import type { - MultichainNetworkStateControllerActions, - MultichainNetworkControllerEvents, + AllowedActions, + AllowedEvents, } from './MultichainNetworkController'; -import { bitcoinCaip2ChainId } from './constants'; + import { MultichainNetworkController } from './MultichainNetworkController'; const name = 'MultichainNetworkController'; -type AllowedActions = MultichainNetworkStateControllerActions | { - type: 'NetworkController:setActiveNetwork'; - handler: (clientId: string) => void; -}; - -type AllowedEvents = MultichainNetworkControllerEvents; - const buildMessenger = () => { return new ControllerMessenger(); }; diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index bfda573c40c..52c310556fe 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -1,10 +1,13 @@ import type { AccountsControllerSetSelectedAccountAction } from '@metamask/accounts-controller'; import { BaseController, + StateMetadata, type ControllerGetStateAction, type ControllerStateChangeEvent, type RestrictedControllerMessenger, } from '@metamask/base-controller'; +import { BtcScope } from '@metamask/keyring-api'; + import type { NetworkStatus, NetworkControllerGetNetworkConfigurationByNetworkClientId, @@ -12,11 +15,9 @@ import type { NetworkControllerGetStateAction, NetworkControllerStateChangeEvent, } from '@metamask/network-controller'; -import type { Draft } from 'immer'; -import { - bitcoinCaip2ChainId, -} from './constants'; + +import { CaipAssetType, CaipChainId, KnownCaipNamespace, parseCaipChainId } from '@metamask/utils'; const controllerName = 'MultichainNetworkController'; @@ -26,37 +27,70 @@ export type MultichainNetworkMetadata = { }; export type MultichainNetworkConfiguration = { - chainId: string; // Should be Caip2 type + /** + * The chain ID of the network. + */ + chainId: CaipChainId; + /** + * The name of the network. + */ name: string; - nativeCurrency: string; // Should be Caip19 type + /** + * The native asset type of the network. + */ + nativeAsset: CaipAssetType; + /** + * The block explorer URLs of the network. + */ blockExplorerUrls: string[]; + /** + * The default block explorer URL index of the network. + */ defaultBlockExplorerUrlIndex?: number; + /** + * The last updated timestamp of the network. + */ lastUpdated?: number; - isEvm?: false; + /** + * Whether the network is an EVM network or non-evm network. + */ + isEvm: boolean; }; /** * State used by the {@link MultichainNetworkController} to cache network configurations. */ export type MultichainNetworkControllerState = { + /** + * The network configurations by chain ID. + */ multichainNetworkConfigurationsByChainId: Record< string, MultichainNetworkConfiguration >; - selectedMultichainNetworkChainId: string; + /** + * The chain ID of the selected network. + */ + selectedMultichainNetworkChainId: CaipChainId; + /** + * The metadata of the networks. + */ multichainNetworksMetadata: Record; + /** + * Whether the non-EVM network is selected by the wallet. + */ nonEvmSelected: boolean; }; /** * Default state of the {@link MultichainNetworkController}. */ -export const defaultState: MultichainNetworkControllerState = { +export const getDefaultMultichainNetworkControllerState = (): MultichainNetworkControllerState => ({ multichainNetworkConfigurationsByChainId: {}, - selectedMultichainNetworkChainId: bitcoinCaip2ChainId, + selectedMultichainNetworkChainId: BtcScope.Mainnet, multichainNetworksMetadata: {}, nonEvmSelected: false, -}; +}); /** * Returns the state of the {@link MultichainNetworkController}. @@ -70,7 +104,7 @@ export type MultichainNetworkControllerGetStateAction = /** * Event emitted when the state of the {@link MultichainNetworkController} changes. */ -export type MultichainNetworkStateControllerStateChange = +export type MultichainNetworkControllerStateChange = ControllerStateChangeEvent< typeof controllerName, MultichainNetworkControllerState @@ -79,14 +113,19 @@ export type MultichainNetworkStateControllerStateChange = /** * Actions exposed by the {@link MultichainNetworkController}. */ -export type MultichainNetworkStateControllerActions = +export type MultichainNetworkControllerActions = MultichainNetworkControllerGetStateAction; /** * Events emitted by {@link MultichainNetworkController}. */ export type MultichainNetworkControllerEvents = - MultichainNetworkStateControllerStateChange; + MultichainNetworkControllerStateChange; + +export type MultichainNetworkControllerAllowedActions = MultichainNetworkControllerActions | AllowedActions; + +export type MultichainNetworkControllerAllowedEvents = MultichainNetworkControllerEvents | AllowedEvents; + /** * Actions that this controller is allowed to call. @@ -108,8 +147,8 @@ export type AllowedEvents = NetworkControllerStateChangeEvent; export type MultichainNetworkControllerMessenger = RestrictedControllerMessenger< typeof controllerName, - MultichainNetworkStateControllerActions | AllowedActions, - MultichainNetworkControllerEvents | AllowedEvents, + MultichainNetworkControllerAllowedActions, + MultichainNetworkControllerAllowedEvents, AllowedActions['type'], AllowedEvents['type'] >; @@ -122,11 +161,11 @@ export type MultichainNetworkControllerMessenger = * the `anonymous` flag. */ const multichainNetworkControllerMetadata = { - multichainNetworkConfigurationsByChainId: { persist: true, anonymous: false }, - selectedMultichainNetworkChainId: { persist: true, anonymous: false }, - multichainNetworksMetadata: { persist: true, anonymous: false }, - nonEvmSelected: { persist: true, anonymous: false }, -}; + multichainNetworkConfigurationsByChainId: { persist: true, anonymous: true }, + selectedMultichainNetworkChainId: { persist: true, anonymous: true }, + multichainNetworksMetadata: { persist: true, anonymous: true }, + nonEvmSelected: { persist: true, anonymous: true }, +} satisfies StateMetadata; /** * The MultichainNetworkController is responsible for fetching and caching account @@ -139,57 +178,44 @@ export class MultichainNetworkController extends BaseController< > { constructor({ messenger, - state, + state = {}, }: { messenger: MultichainNetworkControllerMessenger; - state: MultichainNetworkControllerState; + state?: Partial; }) { super({ messenger, name: controllerName, metadata: multichainNetworkControllerMetadata, state: { - ...defaultState, + ...getDefaultMultichainNetworkControllerState(), ...state, }, }); } - - async setActiveNetwork(clientId: string, chainId?: string): Promise { - if (chainId && Object.keys(this.state.multichainNetworkConfigurationsByChainId).includes(chainId)) { - this.update((state: Draft) => { - state.selectedMultichainNetworkChainId = chainId; + /** + * Sets the active network. + * + * @param clientId - The client ID of the evm network. + * @param caipChainId - The chain ID of the non-evm network. + */ + async setActiveNetwork(clientId: string, caipChainId?: CaipChainId): Promise { + if (caipChainId && Object.keys(this.state.multichainNetworkConfigurationsByChainId).includes(caipChainId)) { + this.update((state) => { + state.selectedMultichainNetworkChainId = caipChainId; state.nonEvmSelected = true; }); return; } - this.update((state: Draft) => { - state.nonEvmSelected = false; - }); - await this.messagingSystem.call( 'NetworkController:setActiveNetwork', clientId, ); - } - - /** - * Sets the non-EVM selected network. - */ - setNonEvmSelected() { - this.update((state: Draft) => { - state.nonEvmSelected = true; - }); - } - - /** - * Sets the EVM selected network. - */ - setEvmSelected() { - this.update((state: Draft) => { + this.update((state) => { state.nonEvmSelected = false; }); } } + diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index b371dba2477..fe7934215bd 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -4,41 +4,43 @@ import type { MultichainNetworkConfiguration, MultichainNetworkMetadata, } from './MultichainNetworkController'; +import { BtcScope, SolScope } from '@metamask/keyring-api'; -export const bitcoinCaip2ChainId = 'bip122:000000000019d6689c085ae165831e93'; -export const solanaCaip2ChainId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'; + +export const btcNativeAsset = `${BtcScope.Mainnet}/slip44:0`; +export const solNativeAsset = `${SolScope.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`; export const multichainNetworkConfigurations: Record = { - bitcoinCaip2ChainId: { - chainId: bitcoinCaip2ChainId, + [BtcScope.Mainnet] : { + chainId: BtcScope.Mainnet, name: 'Bitcoin Mainnet', blockExplorerUrls: [], - nativeCurrency: 'BTC', + nativeAsset: btcNativeAsset, isEvm: false, }, - solanaCaip2ChainId: { - chainId: solanaCaip2ChainId, + [SolScope.Mainnet]: { + chainId: SolScope.Mainnet, name: 'Solana Mainnet', blockExplorerUrls: [], - nativeCurrency: 'SOL', + nativeAsset: solNativeAsset, isEvm: false, }, }; export const networksMetadata: Record = { - bitcoinCaip2ChainId: { + [BtcScope.Mainnet]: { features: [], status: NetworkStatus.Available, }, - solanaCaip2ChainId: { + [SolScope.Mainnet]: { features: [], status: NetworkStatus.Available, }, diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts index abb722b27ae..07cd878b4f9 100644 --- a/packages/multichain-network-controller/src/index.ts +++ b/packages/multichain-network-controller/src/index.ts @@ -3,14 +3,10 @@ export type { MultichainNetworkMetadata, MultichainNetworkControllerState, MultichainNetworkControllerGetStateAction, - AllowedActions, - AllowedEvents, MultichainNetworkControllerMessenger, } from './MultichainNetworkController'; export { MultichainNetworkController } from './MultichainNetworkController'; export { - bitcoinCaip2ChainId, - solanaCaip2ChainId, multichainNetworkConfigurations, networksMetadata, } from './constants'; diff --git a/packages/multichain-network-controller/tsconfig.json b/packages/multichain-network-controller/tsconfig.json index d1a5db81ec3..1646207284c 100644 --- a/packages/multichain-network-controller/tsconfig.json +++ b/packages/multichain-network-controller/tsconfig.json @@ -9,16 +9,4 @@ { "path": "../network-controller" } ], "include": ["../../types", "./src"], - /** - * Here we ensure that TypeScript resolves `@metamask/*` imports to the - * uncompiled source code for packages that live in this repo. - * - * NOTE: This must be synchronized with the `moduleNameMapper` option in - * `jest.config.packages.js`. - * - * NOTE 2: This is not necessary when copying this package to `packages/`. - */ - "paths": { - "@metamask/*": ["../../packages/*/src", "../*/src"] - } } diff --git a/yarn.lock b/yarn.lock index 3a6b48f4de9..32c4206f985 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2300,7 +2300,7 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^21.0.1, @metamask/accounts-controller@npm:^21.0.2, @metamask/accounts-controller@workspace:packages/accounts-controller": +"@metamask/accounts-controller@npm:^21.0.2, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller" dependencies: @@ -3208,6 +3208,18 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-api@npm:^16.1.0": + version: 16.1.0 + resolution: "@metamask/keyring-api@npm:16.1.0" + dependencies: + "@metamask/keyring-utils": "npm:^2.0.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.1.0" + bech32: "npm:^2.0.0" + checksum: 10/6a3877e8e70b02728d4dc056a0eab5d961dd3089236539827ffb4194a3acdc9c71436cc3248ed1d6bf62d3dc0b6e69e2379177db6d690af1a77d4698767324fd + languageName: node + linkType: hard + "@metamask/keyring-controller@npm:^19.0.4, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" @@ -3297,6 +3309,17 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-utils@npm:^2.0.0": + version: 2.0.0 + resolution: "@metamask/keyring-utils@npm:2.0.0" + dependencies: + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.1.0" + bitcoin-address-validation: "npm:^2.2.3" + checksum: 10/f7514821fb3bd5f5be575e0d74d5cf8becbdeac35a3e13dcd9e8bf789ba34aa2072783bdc3d0ddac479b97c986bcb54d77cdccedf5945d1c33ef310790e90efb + languageName: node + linkType: hard + "@metamask/logging-controller@npm:^6.0.3, @metamask/logging-controller@workspace:packages/logging-controller": version: 0.0.0-use.local resolution: "@metamask/logging-controller@workspace:packages/logging-controller" @@ -3348,10 +3371,9 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/multichain-network-controller@workspace:packages/multichain-network-controller" dependencies: - "@metamask/accounts-controller": "npm:^21.0.1" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.1.1" - "@metamask/network-controller": "npm:^22.1.1" + "@metamask/keyring-api": "npm:^16.1.0" "@metamask/utils": "npm:^11.0.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" @@ -3362,6 +3384,9 @@ __metadata: typedoc: "npm:^0.24.8" typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" + peerDependencies: + "@metamask/accounts-controller": ^21.0.1 + "@metamask/network-controller": ^22.1.1 languageName: unknown linkType: soft @@ -4185,6 +4210,23 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^11.1.0": + version: 11.1.0 + resolution: "@metamask/utils@npm:11.1.0" + dependencies: + "@ethereumjs/tx": "npm:^4.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@noble/hashes": "npm:^1.3.1" + "@scure/base": "npm:^1.1.3" + "@types/debug": "npm:^4.1.7" + debug: "npm:^4.3.4" + pony-cause: "npm:^2.1.10" + semver: "npm:^7.5.4" + uuid: "npm:^9.0.1" + checksum: 10/756f13987881fe26adaa0a54354bc5af20cedee4dd228a736d481697dc634adb9e6e54d8f1dcc1d487b2376ab4ba8c576ecbb24beab2fb63aff721d0d5c0f5fe + languageName: node + linkType: hard + "@metamask/utils@npm:^8.2.0": version: 8.5.0 resolution: "@metamask/utils@npm:8.5.0" From dab430b120d79e0667f664bbdaca987439b527c1 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 30 Jan 2025 14:02:29 -0800 Subject: [PATCH 14/69] Remove draft type --- .../src/AccountsController.ts | 83 ++++++++++--------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index 4bf286154ad..41624aa4929 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -425,7 +425,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; @@ -473,7 +473,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 }, @@ -509,42 +509,47 @@ export class AccountsController extends BaseController< const accounts: Record = [ ...normalAccounts, ...snapAccounts, - ].reduce((internalAccountMap, internalAccount) => { - const keyringTypeName = keyringTypeToName( - internalAccount.metadata.keyring.type, - ); - const keyringAccountIndex = keyringTypes.get(keyringTypeName) ?? 0; - if (keyringAccountIndex) { - keyringTypes.set(keyringTypeName, keyringAccountIndex + 1); - } else { - keyringTypes.set(keyringTypeName, 1); - } - - const existingAccount = previousAccounts[internalAccount.id]; - - internalAccountMap[internalAccount.id] = { - ...internalAccount, + ].reduce( + (internalAccountMap, internalAccount) => { + const keyringTypeName = keyringTypeToName( + internalAccount.metadata.keyring.type, + ); + const keyringAccountIndex = keyringTypes.get(keyringTypeName) ?? 0; + if (keyringAccountIndex) { + keyringTypes.set(keyringTypeName, keyringAccountIndex + 1); + } else { + keyringTypes.set(keyringTypeName, 1); + } - metadata: { - ...internalAccount.metadata, - name: - this.#populateExistingMetadata(existingAccount?.id, 'name') ?? - `${keyringTypeName} ${keyringAccountIndex + 1}`, - importTime: - this.#populateExistingMetadata(existingAccount?.id, 'importTime') ?? - Date.now(), - lastSelected: - this.#populateExistingMetadata( - existingAccount?.id, - 'lastSelected', - ) ?? 0, - }, - }; + const existingAccount = previousAccounts[internalAccount.id]; + + internalAccountMap[internalAccount.id] = { + ...internalAccount, + + metadata: { + ...internalAccount.metadata, + name: + this.#populateExistingMetadata(existingAccount?.id, 'name') ?? + `${keyringTypeName} ${keyringAccountIndex + 1}`, + importTime: + this.#populateExistingMetadata( + existingAccount?.id, + 'importTime', + ) ?? Date.now(), + lastSelected: + this.#populateExistingMetadata( + existingAccount?.id, + 'lastSelected', + ) ?? 0, + }, + }; - return internalAccountMap; - }, {} as Record); + return internalAccountMap; + }, + {} as Record, + ); - this.update((currentState: Draft) => { + this.update((currentState) => { currentState.internalAccounts.accounts = accounts; if ( @@ -578,7 +583,7 @@ export class AccountsController extends BaseController< */ loadBackup(backup: AccountsControllerState): void { if (backup.internalAccounts) { - this.update((currentState: Draft) => { + this.update((currentState) => { currentState.internalAccounts = backup.internalAccounts; }); } @@ -825,7 +830,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( @@ -887,7 +892,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]; @@ -1209,7 +1214,7 @@ export class AccountsController extends BaseController< ); } - this.update((currentState: Draft) => { + this.update((currentState) => { currentState.internalAccounts.accounts[ accountId ].metadata.lastSelected = Date.now(); From 6994dbe2fe5b4a9dbaf2c612b5f18e2da0c7d0a2 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 30 Jan 2025 14:30:47 -0800 Subject: [PATCH 15/69] Fix account change subscription in multichain network controller --- .../package.json | 3 +- .../src/MultichainNetworkController.ts | 42 ++++++++-- .../src/utils.ts | 18 ++++ yarn.lock | 84 +++++++++++++++++++ 4 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 packages/multichain-network-controller/src/utils.ts diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index e9fb31c0408..e1424698fe9 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -49,7 +49,8 @@ "dependencies": { "@metamask/base-controller": "^7.1.1", "@metamask/keyring-api": "^16.1.0", - "@metamask/utils": "^11.0.1" + "@metamask/utils": "^11.0.1", + "@solana/addresses": "^2.0.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index ea1f1c08459..3a918c21399 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -14,6 +14,7 @@ import type { } from '@metamask/network-controller'; import { isEvmAccountType } from '@metamask/keyring-api'; import { CaipAssetType, CaipChainId } from '@metamask/utils'; +import { nonEvmNetworkChainIdByAccountAddress } from './utils'; const controllerName = 'MultichainNetworkController'; @@ -302,16 +303,47 @@ export class MultichainNetworkController extends BaseController< // Handle network switch when account is changed this.messagingSystem.subscribe( 'AccountsController:selectedAccountChange', - async ({ type: accountType }) => { - const isNonEvmAccount = !isEvmAccountType(accountType); + async ({ type: accountType, address: accountAddress }) => { + const isEvmAccount = isEvmAccountType(accountType); + + // Handle switching to EVM network + if (isEvmAccount) { + if (!this.state.nonEvmSelected) { + // No need to update if already on evm network + return; + } + + // Otherwise, switch to EVM network + const { selectedNetworkClientId } = await this.messagingSystem.call( + 'NetworkController:getState', + ); + + this.messagingSystem.call( + 'NetworkController:setActiveNetwork', + selectedNetworkClientId, + ); + + this.update((state) => { + state.nonEvmSelected = false; + }); - // No need to update if already on the correct network - if (isNonEvmAccount === this.state.nonEvmSelected) { + return; + } + + // Handle switching to non-EVM network + const nonEvmChainId = + nonEvmNetworkChainIdByAccountAddress(accountAddress); + const isSameNonEvmNetwork = + nonEvmChainId === this.state.selectedMultichainNetworkChainId; + + if (isSameNonEvmNetwork) { + // No need to update if already on the same non-EVM network return; } this.update((state) => { - state.nonEvmSelected = isNonEvmAccount; + state.selectedMultichainNetworkChainId = nonEvmChainId; + state.nonEvmSelected = true; }); }, ); diff --git a/packages/multichain-network-controller/src/utils.ts b/packages/multichain-network-controller/src/utils.ts new file mode 100644 index 00000000000..38dc429935c --- /dev/null +++ b/packages/multichain-network-controller/src/utils.ts @@ -0,0 +1,18 @@ +import { CaipChainId } from '@metamask/utils'; +import { isAddress as isSolanaAddress } from '@solana/addresses'; +import { BtcScope, SolScope } from '@metamask/keyring-api'; +/** + * 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 nonEvmNetworkChainIdByAccountAddress( + address: string, +): CaipChainId { + // 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; +} diff --git a/yarn.lock b/yarn.lock index 32c4206f985..4ba2b531701 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3375,6 +3375,7 @@ __metadata: "@metamask/base-controller": "npm:^7.1.1" "@metamask/keyring-api": "npm:^16.1.0" "@metamask/utils": "npm:^11.0.1" + "@solana/addresses": "npm:^2.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" immer: "npm:^9.0.6" @@ -4648,6 +4649,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" @@ -7015,6 +7092,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" From ebe7e291e13e36705885ae30ecabfa0d57fdf48e Mon Sep 17 00:00:00 2001 From: Cal-L Date: Thu, 30 Jan 2025 14:36:21 -0800 Subject: [PATCH 16/69] Add comment --- .../src/MultichainNetworkController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 3a918c21399..d4b6813fbf6 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -285,6 +285,7 @@ export class MultichainNetworkController extends BaseController< ); if (evmClientId === selectedNetworkClientId) { + // EVM network is already selected, no need to update NetworkController return; } From 20a1dbc16c1178e15567868a5403e27d484cb3f8 Mon Sep 17 00:00:00 2001 From: tommasini Date: Fri, 31 Jan 2025 20:46:16 +0000 Subject: [PATCH 17/69] replace nativeAsset name varialbe for nativeCurrency name variable --- .../src/MultichainNetworkController.ts | 2 +- packages/multichain-network-controller/src/constants.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 52c310556fe..1906453eab4 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -38,7 +38,7 @@ export type MultichainNetworkConfiguration = { /** * The native asset type of the network. */ - nativeAsset: CaipAssetType; + nativeCurrency: CaipAssetType; /** * The block explorer URLs of the network. */ diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index fe7934215bd..ed40524a993 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -18,7 +18,7 @@ export const multichainNetworkConfigurations: Record Date: Mon, 3 Feb 2025 16:57:28 -0800 Subject: [PATCH 18/69] git commit -m "Duplicate internal accounts mock" --- .../src/test/utils.test.ts | 77 +++++++++++++++ .../src/test/utils.ts | 96 +++++++++++++++++++ .../src/utils.ts | 6 +- 3 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 packages/multichain-network-controller/src/test/utils.test.ts create mode 100644 packages/multichain-network-controller/src/test/utils.ts diff --git a/packages/multichain-network-controller/src/test/utils.test.ts b/packages/multichain-network-controller/src/test/utils.test.ts new file mode 100644 index 00000000000..e73393461b3 --- /dev/null +++ b/packages/multichain-network-controller/src/test/utils.test.ts @@ -0,0 +1,77 @@ +import { BtcAccountType, EthAccountType } from '@metamask/keyring-api'; + +import { createMockInternalAccount } from './utils'; + +describe('createMockInternalAccount', () => { + it('create a mock internal account', () => { + const account = createMockInternalAccount(); + expect(account).toStrictEqual({ + id: expect.any(String), + address: expect.any(String), + type: expect.any(String), + options: expect.any(Object), + methods: expect.any(Array), + metadata: { + name: expect.any(String), + keyring: { type: expect.any(String) }, + importTime: expect.any(Number), + lastSelected: expect.any(Number), + snap: undefined, + }, + }); + }); + + it('create a mock internal account with custom values', () => { + const customSnap = { + id: '1', + enabled: true, + name: 'Snap 1', + }; + const account = createMockInternalAccount({ + id: '1', + address: '0x123', + type: EthAccountType.Erc4337, + name: 'Custom Account', + snap: customSnap, + }); + expect(account).toStrictEqual({ + id: '1', + address: '0x123', + type: EthAccountType.Erc4337, + options: expect.any(Object), + methods: expect.any(Array), + metadata: { + name: 'Custom Account', + keyring: { type: expect.any(String) }, + importTime: expect.any(Number), + lastSelected: expect.any(Number), + snap: customSnap, + }, + }); + }); + + it('create a non-EVM account', () => { + const account = createMockInternalAccount({ type: BtcAccountType.P2wpkh }); + expect(account).toStrictEqual({ + id: expect.any(String), + address: expect.any(String), + type: BtcAccountType.P2wpkh, + options: expect.any(Object), + methods: expect.any(Array), + metadata: { + name: expect.any(String), + keyring: { type: expect.any(String) }, + importTime: expect.any(Number), + lastSelected: expect.any(Number), + snap: undefined, + }, + }); + }); + + it('will throw if an unknown account type was passed', () => { + // @ts-expect-error testing unknown account type + expect(() => createMockInternalAccount({ type: 'unknown' })).toThrow( + 'Unknown account type: unknown', + ); + }); +}); diff --git a/packages/multichain-network-controller/src/test/utils.ts b/packages/multichain-network-controller/src/test/utils.ts new file mode 100644 index 00000000000..13586f5aad3 --- /dev/null +++ b/packages/multichain-network-controller/src/test/utils.ts @@ -0,0 +1,96 @@ +import { + BtcAccountType, + EthAccountType, + SolAccountType, + BtcMethod, + EthMethod, + SolMethod, + type KeyringAccountType, +} from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import { v4 } from 'uuid'; + +/** + * 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.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 = v4(), + 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/src/utils.ts b/packages/multichain-network-controller/src/utils.ts index 38dc429935c..5caefa207fa 100644 --- a/packages/multichain-network-controller/src/utils.ts +++ b/packages/multichain-network-controller/src/utils.ts @@ -1,6 +1,6 @@ import { CaipChainId } from '@metamask/utils'; import { isAddress as isSolanaAddress } from '@solana/addresses'; -import { BtcScope, SolScope } from '@metamask/keyring-api'; +import { BtcScopes, SolScopes } from '@metamask/keyring-api'; /** * Returns the chain id of the non-EVM network based on the account address. * @@ -12,7 +12,7 @@ export function nonEvmNetworkChainIdByAccountAddress( ): CaipChainId { // 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 SolScopes.Mainnet; } - return BtcScope.Mainnet; + return BtcScopes.Mainnet; } From 429f6ad811846f2ec98918bece089478df4e6d79 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Mon, 3 Feb 2025 16:57:54 -0800 Subject: [PATCH 19/69] Update package deps --- packages/multichain-network-controller/package.json | 3 ++- packages/multichain-network-controller/tsconfig.build.json | 2 +- packages/multichain-network-controller/tsconfig.json | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index e1424698fe9..c959a616574 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -48,12 +48,13 @@ }, "dependencies": { "@metamask/base-controller": "^7.1.1", - "@metamask/keyring-api": "^16.1.0", + "@metamask/keyring-api": "^15.0.0", "@metamask/utils": "^11.0.1", "@solana/addresses": "^2.0.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", + "@metamask/keyring-controller": "^19.0.4", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "immer": "^9.0.6", diff --git a/packages/multichain-network-controller/tsconfig.build.json b/packages/multichain-network-controller/tsconfig.build.json index 7609b693a3b..41c2d082d3d 100644 --- a/packages/multichain-network-controller/tsconfig.build.json +++ b/packages/multichain-network-controller/tsconfig.build.json @@ -8,7 +8,7 @@ "references": [ { "path": "../base-controller/tsconfig.build.json" }, { "path": "../network-controller/tsconfig.build.json" }, - { "path": "../accounts-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 index 1646207284c..faf37b65fc5 100644 --- a/packages/multichain-network-controller/tsconfig.json +++ b/packages/multichain-network-controller/tsconfig.json @@ -5,8 +5,8 @@ }, "references": [ { "path": "../base-controller" }, - { "path": "../accounts-controller" }, - { "path": "../network-controller" } + { "path": "../network-controller" }, + { "path": "../keyring-controller" } ], - "include": ["../../types", "./src"], + "include": ["../../types", "./src"] } From a1618aba952b81859a0ed08ccd5f2c56c05215dd Mon Sep 17 00:00:00 2001 From: Cal-L Date: Mon, 3 Feb 2025 16:58:15 -0800 Subject: [PATCH 20/69] Clean up MultichainNetworkController --- .../src/MultichainNetworkController.test.ts | 434 +++++++++++++----- .../src/MultichainNetworkController.ts | 58 ++- 2 files changed, 360 insertions(+), 132 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index 529468c104e..4569629125b 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -1,147 +1,365 @@ import { ControllerMessenger } from '@metamask/base-controller'; +import { + BtcScopes, + SolScopes, + EthAccountType, + BtcAccountType, + SolAccountType, + type KeyringAccountType, +} from '@metamask/keyring-api'; import type { AllowedActions, AllowedEvents, + MultichainNetworkControllerAllowedActions, + MultichainNetworkControllerAllowedEvents, } from './MultichainNetworkController'; - import { MultichainNetworkController } from './MultichainNetworkController'; +import type { + NetworkControllerGetStateAction, + NetworkControllerSetActiveNetworkAction, +} from '@metamask/network-controller'; +import { InfuraNetworkType } from '@metamask/controller-utils'; +import type { CaipChainId } from '@metamask/utils'; +import { createMockInternalAccount } from './test/utils'; +import { multichainNetworkConfigurations } from './constants'; -const name = 'MultichainNetworkController'; +const controllerName = 'MultichainNetworkController'; -const buildMessenger = () => { - return new ControllerMessenger(); -}; +/** + * Setup a test controller instance. + * + * @param args - Arguments to this function. + * @param args.state - Initial state for 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 ControllerMessenger< + MultichainNetworkControllerAllowedActions, + MultichainNetworkControllerAllowedEvents + >(); -const buildMultichainNetworkControllerMessenger = ( - messenger: ControllerMessenger, -) => { - return messenger.getRestricted({ - name, - allowedActions: ['NetworkController:setActiveNetwork'], - allowedEvents: [], - }); -}; + const publishSpy = jest.spyOn(messenger, 'publish'); -describe('MultichainNetworkController', () => { - let controller: MultichainNetworkController; - let messenger: ControllerMessenger; + // 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, + ); - beforeEach(() => { - messenger = buildMessenger(); - messenger.registerActionHandler( + const controllerMessenger = messenger.getRestricted< + typeof controllerName, + AllowedActions['type'], + AllowedEvents['type'] + >({ + name: controllerName, + allowedActions: [ 'NetworkController:setActiveNetwork', - jest.fn(), - ); - - jest.spyOn(messenger, 'call'); - - const restrictedMessenger = buildMultichainNetworkControllerMessenger(messenger); - - controller = new MultichainNetworkController({ - messenger: restrictedMessenger, - state: { - multichainNetworkConfigurationsByChainId: {}, - selectedMultichainNetworkChainId: bitcoinCaip2ChainId, - multichainNetworksMetadata: {}, - nonEvmSelected: false, - }, - }); + '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: SolScopes.Mainnet, + multichainNetworkConfigurationsByChainId: multichainNetworkConfigurations, + multichainNetworksMetadata: {}, + nonEvmSelected: false, + ...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('setActiveNetwork', () => { - it('should set non-EVM network when valid chainId is provided', async () => { - const clientId = 'testClient'; - const chainId = 'bip122:000000000019d6689c085ae165831e93'; - messenger = buildMessenger(); - messenger.registerActionHandler( - 'NetworkController:setActiveNetwork', - jest.fn(), - ); - - jest.spyOn(messenger, 'call'); - - const restrictedMessenger = buildMultichainNetworkControllerMessenger(messenger); - - const multiChainController = new MultichainNetworkController({ - messenger: restrictedMessenger, - state: { - multichainNetworkConfigurationsByChainId: { - [chainId]: { - chainId, - name: 'Bitcoin', - nativeCurrency: 'BTC', - blockExplorerUrls: ['https://blockstream.info/'], - }, - }, - selectedMultichainNetworkChainId: bitcoinCaip2ChainId, - multichainNetworksMetadata: {}, - nonEvmSelected: false, - }, - }); - - await multiChainController.setActiveNetwork(clientId, chainId); - - expect(multiChainController.state.selectedMultichainNetworkChainId).toBe(chainId); - expect(multiChainController.state.nonEvmSelected).toBe(true); - expect(messenger.call).not.toHaveBeenCalled(); + it('should throw error when both EVM and non-EVM networks are provided', async () => { + const { controller } = setupController(); + await expect( + controller.setActiveNetwork({ + evmClientId: InfuraNetworkType.mainnet, + nonEvmChainId: SolScopes.Mainnet, + }), + ).rejects.toThrow('Cannot set both EVM and non-EVM networks!'); + }); + + it('should throw error if nonEvmChainId is an empty string', async () => { + const { controller } = setupController(); + await expect( + controller.setActiveNetwork({ + nonEvmChainId: '' as CaipChainId, + }), + ).rejects.toThrow('Non-EVM chain ID is required!'); + }); + + it('should throw error if evmClientId is an empty string', async () => { + const { controller } = setupController(); + await expect( + controller.setActiveNetwork({ + evmClientId: '', + }), + ).rejects.toThrow('EVM client ID is required!'); + }); + + 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({ + nonEvmChainId: SolScopes.Mainnet, }); - it('should set EVM network when chainId is not provided', async () => { - const clientId = 'testClient'; + // Check that the Solana is now the selected network + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScopes.Mainnet, + ); + + // Check that the a non evm network is now active + expect(controller.state.nonEvmSelected).toBe(true); + + // Check that the messenger published the correct event + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainNetworkController:setActiveNetwork', + { nonEvmChainId: SolScopes.Mainnet }, + ); + }); + + it('should throw error when unsupported non-EVM chainId is provided', async () => { + const { controller } = setupController(); + const unsupportedChainId = 'non-existent-chain:0'; + + await expect( + controller.setActiveNetwork({ + nonEvmChainId: unsupportedChainId, + }), + ).rejects.toThrow('Non-EVM chain ID is not supported!'); + }); + + 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: { nonEvmSelected: true } }, + }); + + // Set active network to Bitcoin + await controller.setActiveNetwork({ + nonEvmChainId: BtcScopes.Mainnet, + }); + + // Check that the Solana is now the selected network + expect(controller.state.selectedMultichainNetworkChainId).toBe( + BtcScopes.Mainnet, + ); + + // Check that BTC network is now active + expect(controller.state.nonEvmSelected).toBe(true); + + // Check that the messenger published the correct event + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainNetworkController:setActiveNetwork', + { nonEvmChainId: BtcScopes.Mainnet }, + ); + }); - await controller.setActiveNetwork(clientId); + 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({ + evmClientId: selectedNetworkClientId, + }); + + // Check that EVM network is selected expect(controller.state.nonEvmSelected).toBe(false); - expect(messenger.call).toHaveBeenCalledWith( - 'NetworkController:setActiveNetwork', - clientId, + + // Check that the messenger published the correct event + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainNetworkController:setActiveNetwork', + { evmClientId: selectedNetworkClientId }, ); + + // Check that NetworkController:setActiveNetwork was not called + expect(mockSetActiveNetwork).not.toHaveBeenCalled(); }); - it('should set EVM network when invalid chainId is provided', async () => { - const clientId = 'testClient'; - const invalidChainId = 'invalid-chain-id'; + 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 evmClientId = 'linea'; - await controller.setActiveNetwork(clientId, invalidChainId); + await controller.setActiveNetwork({ + evmClientId, + }); + // Check that EVM network is selected expect(controller.state.nonEvmSelected).toBe(false); - expect(messenger.call).toHaveBeenCalledWith( - 'NetworkController:setActiveNetwork', - clientId, + + // Check that the messenger published the correct event + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainNetworkController:setActiveNetwork', + { evmClientId }, ); + + // Check that NetworkController:setActiveNetwork was not called + expect(mockSetActiveNetwork).toHaveBeenCalledWith(evmClientId); }); }); - describe('setNonEvmSelected', () => { - it('should set nonEvmSelected to true', () => { - controller.setNonEvmSelected(); + describe('handle AccountsController:selectedAccountChange event', () => { + it('nonEvmSelected should be false 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.nonEvmSelected).toBe(false); + + // Switching to EVM account + triggerSelectedAccountChange(EthAccountType.Eoa); + + // EVM network is still active + expect(controller.state.nonEvmSelected).toBe(false); + }); + 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: { nonEvmSelected: true } }, + getNetworkState: jest.fn().mockImplementation(() => ({ + selectedNetworkClientId: InfuraNetworkType.mainnet, + })), + }); + + // non-EVM network is currently active expect(controller.state.nonEvmSelected).toBe(true); + + // Switching to EVM account + triggerSelectedAccountChange(EthAccountType.Eoa); + + // EVM network is now active + expect(controller.state.nonEvmSelected).toBe(false); }); - }); + it('non-EVM network should be active when switching to account of same non-EVM network', async () => { + // By default, Solana is selected and active + const { controller, triggerSelectedAccountChange } = setupController({ + options: { + state: { + nonEvmSelected: true, + selectedMultichainNetworkChainId: SolScopes.Mainnet, + }, + }, + }); - describe('setEvmSelected', () => { - it('should set nonEvmSelected to false', () => { - messenger = buildMessenger(); - messenger.registerActionHandler( - 'NetworkController:setActiveNetwork', - jest.fn(), - ); - - jest.spyOn(messenger, 'call'); - - const restrictedMessenger = buildMultichainNetworkControllerMessenger(messenger); - const multiChainController = new MultichainNetworkController({ - messenger: restrictedMessenger, + // non-EVM network is currently active + expect(controller.state.nonEvmSelected).toBe(true); + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScopes.Mainnet, + ); + + // Switching to Solana account + triggerSelectedAccountChange(SolAccountType.DataAccount); + + // Solana is still the selected network + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScopes.Mainnet, + ); + expect(controller.state.nonEvmSelected).toBe(true); + }); + 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: { - multichainNetworkConfigurationsByChainId: {}, - selectedMultichainNetworkChainId: bitcoinCaip2ChainId, - multichainNetworksMetadata: {}, nonEvmSelected: true, + selectedMultichainNetworkChainId: SolScopes.Mainnet, }, - }); - - multiChainController.setEvmSelected(); - expect(multiChainController.state.nonEvmSelected).toBe(false); + }, }); + + // Solana is currently active + expect(controller.state.nonEvmSelected).toBe(true); + expect(controller.state.selectedMultichainNetworkChainId).toBe( + SolScopes.Mainnet, + ); + + // Switching to Bitcoin account + triggerSelectedAccountChange(BtcAccountType.P2wpkh); + + // Bitcoin is now the selected network + expect(controller.state.selectedMultichainNetworkChainId).toBe( + BtcScopes.Mainnet, + ); + expect(controller.state.nonEvmSelected).toBe(true); + }); }); }); diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index d7fd5da1aec..835e5115628 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -1,19 +1,18 @@ -import type { AccountsControllerSelectedAccountChangeEvent } from '@metamask/accounts-controller'; import { BaseController, - StateMetadata, + type StateMetadata, type ControllerGetStateAction, type ControllerStateChangeEvent, type RestrictedControllerMessenger, } from '@metamask/base-controller'; -import { BtcScope } from '@metamask/keyring-api'; +import { isEvmAccountType, SolScopes } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { NetworkStatus, NetworkControllerSetActiveNetworkAction, NetworkControllerGetStateAction, } from '@metamask/network-controller'; -import { isEvmAccountType } from '@metamask/keyring-api'; -import { CaipAssetType, CaipChainId } from '@metamask/utils'; +import type { CaipAssetType, CaipChainId } from '@metamask/utils'; import { nonEvmNetworkChainIdByAccountAddress } from './utils'; const controllerName = 'MultichainNetworkController'; @@ -81,11 +80,13 @@ export type MultichainNetworkControllerState = { /** * Default state of the {@link MultichainNetworkController}. + * + * @returns The default state of the {@link MultichainNetworkController}. */ export const getDefaultMultichainNetworkControllerState = (): MultichainNetworkControllerState => ({ multichainNetworkConfigurationsByChainId: {}, - selectedMultichainNetworkChainId: BtcScope.Mainnet, + selectedMultichainNetworkChainId: SolScopes.Mainnet, multichainNetworksMetadata: {}, nonEvmSelected: false, }); @@ -142,6 +143,12 @@ export type AllowedActions = | NetworkControllerGetStateAction | NetworkControllerSetActiveNetworkAction; +// Re-define event here to avoid circular dependency with AccountsController +type AccountsControllerSelectedAccountChangeEvent = { + type: `AccountsController:selectedAccountChange`; + payload: [InternalAccount]; +}; + /** * Events that this controller is allowed to subscribe. */ @@ -214,8 +221,9 @@ export class MultichainNetworkController extends BaseController< /** * Handles switching between EVM and non-EVM networks. * - * @param evmClientId - The client ID of the EVM network to set active. - * @param nonEvmChainId - The chain ID of the non-EVM network to set active. + * @param args - The arguments to set the active network. + * @param args.evmClientId - The client ID of the EVM network to set active. + * @param args.nonEvmChainId - The chain ID of the non-EVM network to set active. */ async setActiveNetwork({ evmClientId, @@ -225,18 +233,29 @@ export class MultichainNetworkController extends BaseController< nonEvmChainId?: CaipChainId; }): Promise { // Throw an error if both EVM and non-EVM networks are set - if (evmClientId && nonEvmChainId) { + if (evmClientId !== undefined && nonEvmChainId !== undefined) { throw new Error('Cannot set both EVM and non-EVM networks!'); } // Handle non-EVM networks - if (nonEvmChainId) { + if (nonEvmChainId !== undefined) { + // Handle EVM networks + if (nonEvmChainId.length === 0) { + throw new Error('Non-EVM chain ID is required!'); + } + // Prevent setting same network if (nonEvmChainId === this.state.selectedMultichainNetworkChainId) { // Indicate that the non-EVM network is selected this.update((state) => { state.nonEvmSelected = true; }); + + // Notify listeners that setActiveNetwork was called + this.messagingSystem.publish( + 'MultichainNetworkController:setActiveNetwork', + { nonEvmChainId }, + ); return; } @@ -249,6 +268,7 @@ export class MultichainNetworkController extends BaseController< throw new Error('Non-EVM chain ID is not supported!'); } + // Notify listeners that setActiveNetwork was called this.messagingSystem.publish( 'MultichainNetworkController:setActiveNetwork', { nonEvmChainId }, @@ -267,6 +287,7 @@ export class MultichainNetworkController extends BaseController< throw new Error('EVM client ID is required!'); } + // Notify listeners that setActiveNetwork was called this.messagingSystem.publish( 'MultichainNetworkController:setActiveNetwork', { @@ -280,7 +301,7 @@ export class MultichainNetworkController extends BaseController< }); // Prevent setting same network - const { selectedNetworkClientId } = await this.messagingSystem.call( + const { selectedNetworkClientId } = this.messagingSystem.call( 'NetworkController:getState', ); @@ -290,7 +311,7 @@ export class MultichainNetworkController extends BaseController< } // Update evm active network - this.messagingSystem.call( + await this.messagingSystem.call( 'NetworkController:setActiveNetwork', evmClientId, ); @@ -314,20 +335,10 @@ export class MultichainNetworkController extends BaseController< return; } - // Otherwise, switch to EVM network - const { selectedNetworkClientId } = await this.messagingSystem.call( - 'NetworkController:getState', - ); - - this.messagingSystem.call( - 'NetworkController:setActiveNetwork', - selectedNetworkClientId, - ); - + // Make EVM network active this.update((state) => { state.nonEvmSelected = false; }); - return; } @@ -352,7 +363,6 @@ export class MultichainNetworkController extends BaseController< /** * Registers message handlers. - * @private */ #registerMessageHandlers() { this.messagingSystem.registerActionHandler( From 7f1ecd9cf30904f08027e770a9bdb29283200d1f Mon Sep 17 00:00:00 2001 From: Cal-L Date: Mon, 3 Feb 2025 16:59:16 -0800 Subject: [PATCH 21/69] Update constants --- .../src/constants.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index ed40524a993..a11f124bdd7 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -4,15 +4,17 @@ import type { MultichainNetworkConfiguration, MultichainNetworkMetadata, } from './MultichainNetworkController'; -import { BtcScope, SolScope } from '@metamask/keyring-api'; +import { BtcScopes, SolScopes } from '@metamask/keyring-api'; +export const btcNativeAsset = `${BtcScopes.Mainnet}/slip44:0`; +export const solNativeAsset = `${SolScopes.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`; -export const btcNativeAsset = `${BtcScope.Mainnet}/slip44:0`; -export const solNativeAsset = `${SolScope.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`; - -export const multichainNetworkConfigurations: Record = { - [BtcScope.Mainnet] : { - chainId: BtcScope.Mainnet, +export const multichainNetworkConfigurations: Record< + string, + MultichainNetworkConfiguration +> = { + [BtcScopes.Mainnet]: { + chainId: BtcScopes.Mainnet, name: 'Bitcoin Mainnet', @@ -22,8 +24,8 @@ export const multichainNetworkConfigurations: Record = { - [BtcScope.Mainnet]: { + [BtcScopes.Mainnet]: { features: [], status: NetworkStatus.Available, }, - [SolScope.Mainnet]: { + [SolScopes.Mainnet]: { features: [], status: NetworkStatus.Available, }, From ba0d1ea903995eb5035054d73198705da9b25262 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Mon, 3 Feb 2025 16:59:39 -0800 Subject: [PATCH 22/69] Update branch coverage threshold --- packages/multichain-network-controller/jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain-network-controller/jest.config.js b/packages/multichain-network-controller/jest.config.js index ca084133399..9392b3b2347 100644 --- a/packages/multichain-network-controller/jest.config.js +++ b/packages/multichain-network-controller/jest.config.js @@ -17,7 +17,7 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 100, + branches: 96.29, functions: 100, lines: 100, statements: 100, From f0d9e53680d6c24225bdf82b22ad444d334eb7f8 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Mon, 3 Feb 2025 16:59:50 -0800 Subject: [PATCH 23/69] Update yarn lock --- yarn.lock | 43 ++----------------------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4ba2b531701..b829e0faf97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3208,18 +3208,6 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^16.1.0": - version: 16.1.0 - resolution: "@metamask/keyring-api@npm:16.1.0" - dependencies: - "@metamask/keyring-utils": "npm:^2.0.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.1.0" - bech32: "npm:^2.0.0" - checksum: 10/6a3877e8e70b02728d4dc056a0eab5d961dd3089236539827ffb4194a3acdc9c71436cc3248ed1d6bf62d3dc0b6e69e2379177db6d690af1a77d4698767324fd - languageName: node - linkType: hard - "@metamask/keyring-controller@npm:^19.0.4, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" @@ -3309,17 +3297,6 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-utils@npm:^2.0.0": - version: 2.0.0 - resolution: "@metamask/keyring-utils@npm:2.0.0" - dependencies: - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.1.0" - bitcoin-address-validation: "npm:^2.2.3" - checksum: 10/f7514821fb3bd5f5be575e0d74d5cf8becbdeac35a3e13dcd9e8bf789ba34aa2072783bdc3d0ddac479b97c986bcb54d77cdccedf5945d1c33ef310790e90efb - languageName: node - linkType: hard - "@metamask/logging-controller@npm:^6.0.3, @metamask/logging-controller@workspace:packages/logging-controller": version: 0.0.0-use.local resolution: "@metamask/logging-controller@workspace:packages/logging-controller" @@ -3373,7 +3350,8 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.1.1" - "@metamask/keyring-api": "npm:^16.1.0" + "@metamask/keyring-api": "npm:^15.0.0" + "@metamask/keyring-controller": "npm:^19.0.4" "@metamask/utils": "npm:^11.0.1" "@solana/addresses": "npm:^2.0.0" "@types/jest": "npm:^27.4.1" @@ -4211,23 +4189,6 @@ __metadata: languageName: node linkType: hard -"@metamask/utils@npm:^11.1.0": - version: 11.1.0 - resolution: "@metamask/utils@npm:11.1.0" - dependencies: - "@ethereumjs/tx": "npm:^4.2.0" - "@metamask/superstruct": "npm:^3.1.0" - "@noble/hashes": "npm:^1.3.1" - "@scure/base": "npm:^1.1.3" - "@types/debug": "npm:^4.1.7" - debug: "npm:^4.3.4" - pony-cause: "npm:^2.1.10" - semver: "npm:^7.5.4" - uuid: "npm:^9.0.1" - checksum: 10/756f13987881fe26adaa0a54354bc5af20cedee4dd228a736d481697dc634adb9e6e54d8f1dcc1d487b2376ab4ba8c576ecbb24beab2fb63aff721d0d5c0f5fe - languageName: node - linkType: hard - "@metamask/utils@npm:^8.2.0": version: 8.5.0 resolution: "@metamask/utils@npm:8.5.0" From 81b318e5abe6da0f0f1198cda622e7345cd0c525 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Mon, 3 Feb 2025 17:00:38 -0800 Subject: [PATCH 24/69] Handle multichain network change on accounts controller --- .../src/AccountsController.ts | 100 +++++++++++------- .../accounts-controller/src/tests/mocks.ts | 8 +- 2 files changed, 64 insertions(+), 44 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index 41624aa4929..e5ad498afa2 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -25,7 +25,6 @@ import type { KeyringControllerGetAccountsAction, KeyringControllerStateChangeEvent, } from '@metamask/keyring-controller'; -import type { MultichainNetworkSetActiveNetworkEvent } from '@metamask/multichain-network-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { SnapControllerState, @@ -40,8 +39,6 @@ import { isCaipChainId, parseCaipChainId, } from '@metamask/utils'; -import type { Draft } from 'immer'; - import { getUUIDFromAddressOfNormalAccount, isNormalKeyringType, @@ -183,6 +180,17 @@ export type AccountsControllerAccountAssetListUpdatedEvent = { payload: SnapKeyringAccountAssetListUpdatedEvent['payload']; }; +// Re-define event here to avoid circular dependency with MultichainNetworkController +type MultichainNetworkSetActiveNetworkEvent = { + type: `MultichainNetworkController:setActiveNetwork`; + payload: [ + { + evmClientId?: string; + nonEvmChainId?: CaipChainId; + }, + ]; +}; + export type AllowedEvents = | SnapStateChange | KeyringControllerStateChangeEvent @@ -1122,6 +1130,55 @@ export class AccountsController extends BaseController< return accountsState; } + /** + * Handles the change in multichain network by updating the selected account. + * + * @param args - The arguments to handle the multichain network change. + * @param args.evmClientId - The ID of the EVM client. + * @param args.nonEvmChainId - The CAIP2 of the non-EVM chain. + */ + #handleOnMultichainNetworkChange({ + evmClientId, + nonEvmChainId, + }: { + evmClientId?: string; + nonEvmChainId?: CaipChainId; + }) { + if (evmClientId && nonEvmChainId) { + throw new Error( + 'Cannot set accounts from both EVM and non-EVM networks!', + ); + } + + let accountId: string | undefined; + + if (nonEvmChainId) { + // Update selected account to non evm account + const lastSelectedNonEvmAccount = + this.getSelectedMultichainAccount(nonEvmChainId); + if (!lastSelectedNonEvmAccount?.id) { + throw new Error('No non-EVM account found!'); + } + accountId = lastSelectedNonEvmAccount?.id; + } else if (evmClientId) { + // Update selected account to evm account + const lastSelectedEvmAccount = this.getSelectedAccount(); + accountId = lastSelectedEvmAccount.id; + } + + if (!accountId) { + throw new Error( + `No account found when switching multichain network! evmClientId - ${evmClientId}, nonEvmChainId - ${nonEvmChainId}`, + ); + } + + 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. * @param accountId - The ID of the account. @@ -1185,42 +1242,7 @@ export class AccountsController extends BaseController< // Handle account change when multichain network is changed this.messagingSystem.subscribe( 'MultichainNetworkController:setActiveNetwork', - ({ evmClientId, nonEvmChainId }) => { - if (evmClientId && nonEvmChainId) { - throw new Error( - 'Cannot set accounts from both EVM and non-EVM networks!', - ); - } - - let accountId: string | undefined; - - if (nonEvmChainId) { - // Update selected account to non evm account - const lastSelectedNonEvmAccount = - this.getSelectedMultichainAccount(nonEvmChainId); - if (!lastSelectedNonEvmAccount?.id) { - throw new Error('No non-EVM account found!'); - } - accountId = lastSelectedNonEvmAccount?.id; - } else if (evmClientId) { - // Update selected account to evm account - const lastSelectedEvmAccount = this.getSelectedAccount(); - accountId = lastSelectedEvmAccount.id; - } - - if (!accountId) { - throw new Error( - `No account found when switching multichain network! evmClientId - ${evmClientId}, nonEvmChainId - ${nonEvmChainId}`, - ); - } - - this.update((currentState) => { - currentState.internalAccounts.accounts[ - accountId - ].metadata.lastSelected = Date.now(); - currentState.internalAccounts.selectedAccount = accountId; - }); - }, + this.#handleOnMultichainNetworkChange, ); } diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts index c5224ab0be4..25dbf779769 100644 --- a/packages/accounts-controller/src/tests/mocks.ts +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -5,10 +5,8 @@ import { EthMethod, } 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 { KeyringAccountType } from '@metamask/keyring-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?: { From 85fcbb5ca4f76e71174963e398b303e8362b7972 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 09:54:30 -0800 Subject: [PATCH 25/69] Increase unit test coverage on util --- .../src/test/utils.test.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/multichain-network-controller/src/test/utils.test.ts b/packages/multichain-network-controller/src/test/utils.test.ts index e73393461b3..fa72c0f99b2 100644 --- a/packages/multichain-network-controller/src/test/utils.test.ts +++ b/packages/multichain-network-controller/src/test/utils.test.ts @@ -1,4 +1,8 @@ -import { BtcAccountType, EthAccountType } from '@metamask/keyring-api'; +import { + BtcAccountType, + EthAccountType, + SolAccountType, +} from '@metamask/keyring-api'; import { createMockInternalAccount } from './utils'; @@ -50,7 +54,7 @@ describe('createMockInternalAccount', () => { }); }); - it('create a non-EVM account', () => { + it('create a BTC account', () => { const account = createMockInternalAccount({ type: BtcAccountType.P2wpkh }); expect(account).toStrictEqual({ id: expect.any(String), @@ -68,6 +72,26 @@ describe('createMockInternalAccount', () => { }); }); + it('create a Solana account', () => { + const account = createMockInternalAccount({ + type: SolAccountType.DataAccount, + }); + expect(account).toStrictEqual({ + id: expect.any(String), + address: expect.any(String), + type: SolAccountType.DataAccount, + options: expect.any(Object), + methods: expect.any(Array), + metadata: { + name: expect.any(String), + keyring: { type: expect.any(String) }, + importTime: expect.any(Number), + lastSelected: expect.any(Number), + snap: undefined, + }, + }); + }); + it('will throw if an unknown account type was passed', () => { // @ts-expect-error testing unknown account type expect(() => createMockInternalAccount({ type: 'unknown' })).toThrow( From a3b670bd21648aff4bfebec12387ca9da334559f Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 09:55:03 -0800 Subject: [PATCH 26/69] Increase test coverage for MultichainNetworkController --- .../src/MultichainNetworkController.test.ts | 14 +++- .../src/MultichainNetworkController.ts | 73 ++++++++++--------- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index 4569629125b..98d8d6206bd 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -13,7 +13,10 @@ import type { MultichainNetworkControllerAllowedActions, MultichainNetworkControllerAllowedEvents, } from './MultichainNetworkController'; -import { MultichainNetworkController } from './MultichainNetworkController'; +import { + getDefaultMultichainNetworkControllerState, + MultichainNetworkController, +} from './MultichainNetworkController'; import type { NetworkControllerGetStateAction, NetworkControllerSetActiveNetworkAction, @@ -135,6 +138,15 @@ function setupController({ } describe('MultichainNetworkController', () => { + describe('constructor', () => { + it('should set default state', () => { + const { controller } = setupController(); + expect(controller.state).toStrictEqual( + getDefaultMultichainNetworkControllerState(), + ); + }); + }); + describe('setActiveNetwork', () => { it('should throw error when both EVM and non-EVM networks are provided', async () => { const { controller } = setupController(); diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 835e5115628..d3fdcf403e7 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -317,6 +317,45 @@ export class MultichainNetworkController extends BaseController< ); } + /** + * Handles switching between EVM and non-EVM networks when an account is changed + * + * @param account - The account that was changed + */ + #handleSelectedAccountChange = (account: InternalAccount) => { + const { type: accountType, address: accountAddress } = account; + const isEvmAccount = isEvmAccountType(accountType); + + // Handle switching to EVM network + if (isEvmAccount) { + if (!this.state.nonEvmSelected) { + // No need to update if already on evm network + return; + } + + // Make EVM network active + this.update((state) => { + state.nonEvmSelected = false; + }); + return; + } + + // Handle switching to non-EVM network + const nonEvmChainId = nonEvmNetworkChainIdByAccountAddress(accountAddress); + const isSameNonEvmNetwork = + nonEvmChainId === this.state.selectedMultichainNetworkChainId; + + if (isSameNonEvmNetwork) { + // No need to update if already on the same non-EVM network + return; + } + + this.update((state) => { + state.selectedMultichainNetworkChainId = nonEvmChainId; + state.nonEvmSelected = true; + }); + }; + /** * Subscribes to message events. * @private @@ -325,39 +364,7 @@ export class MultichainNetworkController extends BaseController< // Handle network switch when account is changed this.messagingSystem.subscribe( 'AccountsController:selectedAccountChange', - async ({ type: accountType, address: accountAddress }) => { - const isEvmAccount = isEvmAccountType(accountType); - - // Handle switching to EVM network - if (isEvmAccount) { - if (!this.state.nonEvmSelected) { - // No need to update if already on evm network - return; - } - - // Make EVM network active - this.update((state) => { - state.nonEvmSelected = false; - }); - return; - } - - // Handle switching to non-EVM network - const nonEvmChainId = - nonEvmNetworkChainIdByAccountAddress(accountAddress); - const isSameNonEvmNetwork = - nonEvmChainId === this.state.selectedMultichainNetworkChainId; - - if (isSameNonEvmNetwork) { - // No need to update if already on the same non-EVM network - return; - } - - this.update((state) => { - state.selectedMultichainNetworkChainId = nonEvmChainId; - state.nonEvmSelected = true; - }); - }, + this.#handleSelectedAccountChange.bind(this), ); } From 22f05116c16723f26ab328a9f77174d084a077a2 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 09:56:17 -0800 Subject: [PATCH 27/69] Increase test coverage for accounts controller --- .../src/AccountsController.test.ts | 197 +++++++++++++++--- .../src/AccountsController.ts | 27 ++- 2 files changed, 181 insertions(+), 43 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 008a18a983d..a417811e245 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -11,18 +11,28 @@ import { EthMethod, EthScopes, BtcScopes, + SolScopes, } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { InternalAccount, InternalAccountType, } from '@metamask/keyring-internal-api'; +import { InfuraNetworkType } from '@metamask/controller-utils'; import type { SnapControllerState } from '@metamask/snaps-controllers'; import { SnapStatus } from '@metamask/snaps-utils'; -import type { CaipChainId } from '@metamask/utils'; +import { type CaipChainId } from '@metamask/utils'; import * as uuid from 'uuid'; import type { V4Options } from 'uuid'; +// Mock logger before importing AccountsController +var mockLogger = jest.fn(); +jest.mock('@metamask/utils', () => ({ + ...jest.requireActual('@metamask/utils'), + createModuleLogger: () => jest.fn(), + createProjectLogger: () => mockLogger, +})); + import type { AccountsControllerActions, AccountsControllerEvents, @@ -307,6 +317,7 @@ function buildAccountsControllerMessenger(messenger = buildMessenger()) { 'SnapKeyring:accountAssetListUpdated', 'SnapKeyring:accountBalancesUpdated', 'SnapKeyring:accountTransactionsUpdated', + 'MultichainNetworkController:setActiveNetwork', ], allowedActions: [ 'KeyringController:getAccounts', @@ -339,6 +350,10 @@ function setupAccountsController({ AccountsControllerActions | AllowedActions, AccountsControllerEvents | AllowedEvents >; + triggerMultichainNetworkChange: (args: { + evmClientId?: string; + nonEvmChainId?: CaipChainId; + }) => void; } { const accountsControllerMessenger = buildAccountsControllerMessenger(messenger); @@ -347,10 +362,47 @@ function setupAccountsController({ messenger: accountsControllerMessenger, state: { ...defaultState, ...initialState }, }); - return { accountsController, messenger }; + + const triggerMultichainNetworkChange = ({ + evmClientId, + nonEvmChainId, + }: { + evmClientId?: string; + nonEvmChainId?: CaipChainId; + }) => { + messenger.publish('MultichainNetworkController:setActiveNetwork', { + evmClientId, + nonEvmChainId, + }); + }; + + 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 +1566,116 @@ describe('AccountsController', () => { }); }); + describe('handle MultichainNetworkController:setActiveNetwork event', () => { + it('should log warning if both evmClientId and nonEvmChainId are provided', () => { + const messenger = buildMessenger(); + const { accountsController, triggerMultichainNetworkChange } = + setupAccountsController({ + initialState: { + internalAccounts: { + accounts: { [mockAccount.id]: mockAccount }, + selectedAccount: mockAccount.id, + }, + }, + messenger, + }); + + // Publish multichain network change event + triggerMultichainNetworkChange({ + evmClientId: InfuraNetworkType.mainnet, + nonEvmChainId: SolScopes.Mainnet, + }); + + // Warning is logged + expect(mockLogger).toHaveBeenCalledWith( + `Cannot set accounts from both EVM and non-EVM networks! evmClientId - ${InfuraNetworkType.mainnet}, nonEvmChainId - ${SolScopes.Mainnet}`, + ); + + // Selected account is not changed + expect(accountsController.state.internalAccounts.selectedAccount).toBe( + mockAccount.id, + ); + }); + + it('should log warning if no non-EVM account is found', () => { + const messenger = buildMessenger(); + const { triggerMultichainNetworkChange } = setupAccountsController({ + initialState: { + internalAccounts: { + // BTC account does not exist + accounts: { [mockAccount.id]: mockAccount }, + selectedAccount: mockAccount.id, + }, + }, + messenger, + }); + + // Triggered from network switch to Bitcoin mainnet + triggerMultichainNetworkChange({ + nonEvmChainId: BtcScopes.Mainnet, + }); + + expect(mockLogger).toHaveBeenCalledWith( + `No non-EVM account found for non-EVM chain ID - ${BtcScopes.Mainnet}!`, + ); + }); + + 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({ + nonEvmChainId: BtcScopes.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: { + [mockNewerEvmAccount.id]: mockNewerEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, + }, + selectedAccount: mockBtcAccount.id, + }, + }, + messenger, + }); + + // Triggered from network switch to Bitcoin mainnet + triggerMultichainNetworkChange({ + evmClientId: EthScopes.Mainnet, + }); + + // ETH mainnet account is now selected + expect(accountsController.state.internalAccounts.selectedAccount).toBe( + mockNewerEvmAccount.id, + ); + }); + }); + describe('updateAccounts', () => { const mockAddress1 = '0x123'; const mockAddress2 = '0x456'; @@ -2144,29 +2306,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, @@ -2177,7 +2316,7 @@ describe('AccountsController', () => { expected: mockOlderEvmAccount, }, { - lastSelectedAccount: mockNonEvmAccount, + lastSelectedAccount: mockBtcAccount, expected: mockNewerEvmAccount, }, ])( @@ -2189,7 +2328,7 @@ describe('AccountsController', () => { accounts: { [mockOlderEvmAccount.id]: mockOlderEvmAccount, [mockNewerEvmAccount.id]: mockNewerEvmAccount, - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, }, selectedAccount: lastSelectedAccount.id, }, @@ -2205,9 +2344,9 @@ describe('AccountsController', () => { initialState: { internalAccounts: { accounts: { - [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 e5ad498afa2..c8a72b325a4 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -38,6 +38,7 @@ import { type Json, isCaipChainId, parseCaipChainId, + createProjectLogger, } from '@metamask/utils'; import { getUUIDFromAddressOfNormalAccount, @@ -45,6 +46,8 @@ import { keyringTypeToName, } from './utils'; +export const logger = createProjectLogger('accounts-controller'); + const controllerName = 'AccountsController'; export type AccountId = string; @@ -488,7 +491,7 @@ export class AccountsController extends BaseController< }; // Do not remove this comment - This error is flaky: Comment out or restore the `ts-expect-error` directive below as needed. // See: https://github.com/MetaMask/utils/issues/168 - // // @ts-expect-error Known issue - `Json` causes recursive error in immer `Draft`/`WritableDraft` types + // @ts-expect-error Known issue - `Json` causes recursive error in immer `Draft`/`WritableDraft` types currentState.internalAccounts.accounts[accountId] = internalAccount; if (metadata.name) { @@ -1137,7 +1140,7 @@ export class AccountsController extends BaseController< * @param args.evmClientId - The ID of the EVM client. * @param args.nonEvmChainId - The CAIP2 of the non-EVM chain. */ - #handleOnMultichainNetworkChange({ + #handleMultichainNetworkChange({ evmClientId, nonEvmChainId, }: { @@ -1145,19 +1148,21 @@ export class AccountsController extends BaseController< nonEvmChainId?: CaipChainId; }) { if (evmClientId && nonEvmChainId) { - throw new Error( - 'Cannot set accounts from both EVM and non-EVM networks!', - ); + const errorMessage = `Cannot set accounts from both EVM and non-EVM networks! evmClientId - ${evmClientId}, nonEvmChainId - ${nonEvmChainId}`; + logger(errorMessage); + return; } - let accountId: string | undefined; + let accountId: string; if (nonEvmChainId) { // Update selected account to non evm account const lastSelectedNonEvmAccount = this.getSelectedMultichainAccount(nonEvmChainId); if (!lastSelectedNonEvmAccount?.id) { - throw new Error('No non-EVM account found!'); + const errorMessage = `No non-EVM account found for non-EVM chain ID - ${nonEvmChainId}!`; + logger(errorMessage); + return; } accountId = lastSelectedNonEvmAccount?.id; } else if (evmClientId) { @@ -1166,12 +1171,6 @@ export class AccountsController extends BaseController< accountId = lastSelectedEvmAccount.id; } - if (!accountId) { - throw new Error( - `No account found when switching multichain network! evmClientId - ${evmClientId}, nonEvmChainId - ${nonEvmChainId}`, - ); - } - this.update((currentState) => { currentState.internalAccounts.accounts[accountId].metadata.lastSelected = Date.now(); @@ -1242,7 +1241,7 @@ export class AccountsController extends BaseController< // Handle account change when multichain network is changed this.messagingSystem.subscribe( 'MultichainNetworkController:setActiveNetwork', - this.#handleOnMultichainNetworkChange, + this.#handleMultichainNetworkChange.bind(this), ); } From 51ed3fafe327b50175ef4c05e09acce0c3403c59 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 10:33:01 -0800 Subject: [PATCH 28/69] Simplify tests --- .../src/AccountsController.test.ts | 64 +------------------ .../src/AccountsController.ts | 16 +---- .../accounts-controller/src/tests/mocks.ts | 2 +- packages/accounts-controller/src/utils.ts | 3 + 4 files changed, 6 insertions(+), 79 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index a417811e245..d34a9b3ea00 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -1,4 +1,5 @@ import { ControllerMessenger } from '@metamask/base-controller'; +import { InfuraNetworkType } from '@metamask/controller-utils'; import type { AccountAssetListUpdatedEventPayload, AccountBalancesUpdatedEventPayload, @@ -18,21 +19,11 @@ import type { InternalAccount, InternalAccountType, } from '@metamask/keyring-internal-api'; -import { InfuraNetworkType } from '@metamask/controller-utils'; import type { SnapControllerState } from '@metamask/snaps-controllers'; import { SnapStatus } from '@metamask/snaps-utils'; import { type CaipChainId } from '@metamask/utils'; import * as uuid from 'uuid'; import type { V4Options } from 'uuid'; - -// Mock logger before importing AccountsController -var mockLogger = jest.fn(); -jest.mock('@metamask/utils', () => ({ - ...jest.requireActual('@metamask/utils'), - createModuleLogger: () => jest.fn(), - createProjectLogger: () => mockLogger, -})); - import type { AccountsControllerActions, AccountsControllerEvents, @@ -1567,59 +1558,6 @@ describe('AccountsController', () => { }); describe('handle MultichainNetworkController:setActiveNetwork event', () => { - it('should log warning if both evmClientId and nonEvmChainId are provided', () => { - const messenger = buildMessenger(); - const { accountsController, triggerMultichainNetworkChange } = - setupAccountsController({ - initialState: { - internalAccounts: { - accounts: { [mockAccount.id]: mockAccount }, - selectedAccount: mockAccount.id, - }, - }, - messenger, - }); - - // Publish multichain network change event - triggerMultichainNetworkChange({ - evmClientId: InfuraNetworkType.mainnet, - nonEvmChainId: SolScopes.Mainnet, - }); - - // Warning is logged - expect(mockLogger).toHaveBeenCalledWith( - `Cannot set accounts from both EVM and non-EVM networks! evmClientId - ${InfuraNetworkType.mainnet}, nonEvmChainId - ${SolScopes.Mainnet}`, - ); - - // Selected account is not changed - expect(accountsController.state.internalAccounts.selectedAccount).toBe( - mockAccount.id, - ); - }); - - it('should log warning if no non-EVM account is found', () => { - const messenger = buildMessenger(); - const { triggerMultichainNetworkChange } = setupAccountsController({ - initialState: { - internalAccounts: { - // BTC account does not exist - accounts: { [mockAccount.id]: mockAccount }, - selectedAccount: mockAccount.id, - }, - }, - messenger, - }); - - // Triggered from network switch to Bitcoin mainnet - triggerMultichainNetworkChange({ - nonEvmChainId: BtcScopes.Mainnet, - }); - - expect(mockLogger).toHaveBeenCalledWith( - `No non-EVM account found for non-EVM chain ID - ${BtcScopes.Mainnet}!`, - ); - }); - it('should update selected account to non-EVM account when switching to non-EVM network', () => { const messenger = buildMessenger(); const { accountsController, triggerMultichainNetworkChange } = diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index c8a72b325a4..8dc68155177 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -38,7 +38,6 @@ import { type Json, isCaipChainId, parseCaipChainId, - createProjectLogger, } from '@metamask/utils'; import { getUUIDFromAddressOfNormalAccount, @@ -46,8 +45,6 @@ import { keyringTypeToName, } from './utils'; -export const logger = createProjectLogger('accounts-controller'); - const controllerName = 'AccountsController'; export type AccountId = string; @@ -1147,24 +1144,13 @@ export class AccountsController extends BaseController< evmClientId?: string; nonEvmChainId?: CaipChainId; }) { - if (evmClientId && nonEvmChainId) { - const errorMessage = `Cannot set accounts from both EVM and non-EVM networks! evmClientId - ${evmClientId}, nonEvmChainId - ${nonEvmChainId}`; - logger(errorMessage); - return; - } - let accountId: string; if (nonEvmChainId) { // Update selected account to non evm account const lastSelectedNonEvmAccount = this.getSelectedMultichainAccount(nonEvmChainId); - if (!lastSelectedNonEvmAccount?.id) { - const errorMessage = `No non-EVM account found for non-EVM chain ID - ${nonEvmChainId}!`; - logger(errorMessage); - return; - } - accountId = lastSelectedNonEvmAccount?.id; + accountId = lastSelectedNonEvmAccount!.id; } else if (evmClientId) { // Update selected account to evm account const lastSelectedEvmAccount = this.getSelectedAccount(); diff --git a/packages/accounts-controller/src/tests/mocks.ts b/packages/accounts-controller/src/tests/mocks.ts index 25dbf779769..ab7e55eca81 100644 --- a/packages/accounts-controller/src/tests/mocks.ts +++ b/packages/accounts-controller/src/tests/mocks.ts @@ -4,9 +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 } from '@metamask/keyring-internal-api'; -import { KeyringAccountType } from '@metamask/keyring-api'; import { v4 } from 'uuid'; export const createMockInternalAccount = ({ diff --git a/packages/accounts-controller/src/utils.ts b/packages/accounts-controller/src/utils.ts index d3cb5aede23..fe4b490249a 100644 --- a/packages/accounts-controller/src/utils.ts +++ b/packages/accounts-controller/src/utils.ts @@ -47,6 +47,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. */ @@ -62,6 +63,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. */ @@ -71,6 +73,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. */ From 1fef6c20dae1013880cdcc51b8f98a685bee502c Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 10:33:43 -0800 Subject: [PATCH 29/69] Remove unused import --- packages/accounts-controller/src/AccountsController.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index d34a9b3ea00..c7d21f0202c 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -12,7 +12,6 @@ import { EthMethod, EthScopes, BtcScopes, - SolScopes, } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { From b5dc0eb3151618c6a205f102a8be85790e934976 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 10:55:50 -0800 Subject: [PATCH 30/69] Fix all lint issues --- .../src/AccountsController.test.ts | 41 ++++--------------- .../src/AccountsController.ts | 18 ++++---- .../package.json | 1 + .../src/MultichainNetworkController.test.ts | 19 ++++----- .../src/MultichainNetworkController.ts | 4 +- .../src/constants.ts | 2 +- .../src/test/utils.ts | 5 ++- .../src/utils.ts | 4 +- yarn.lock | 1 + 9 files changed, 40 insertions(+), 55 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index c7d21f0202c..d7e5a9a2952 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -1,5 +1,4 @@ import { ControllerMessenger } from '@metamask/base-controller'; -import { InfuraNetworkType } from '@metamask/controller-utils'; import type { AccountAssetListUpdatedEventPayload, AccountBalancesUpdatedEventPayload, @@ -23,6 +22,7 @@ import { SnapStatus } from '@metamask/snaps-utils'; import { type CaipChainId } from '@metamask/utils'; import * as uuid from 'uuid'; import type { V4Options } from 'uuid'; + import type { AccountsControllerActions, AccountsControllerEvents, @@ -2310,29 +2310,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, @@ -2341,18 +2318,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", @@ -2363,7 +2340,7 @@ describe('AccountsController', () => { accounts: { [mockOlderEvmAccount.id]: mockOlderEvmAccount, [mockNewerEvmAccount.id]: mockNewerEvmAccount, - [mockNonEvmAccount.id]: mockNonEvmAccount, + [mockBtcAccount.id]: mockBtcAccount, }, selectedAccount: selectedAccount.id, }, @@ -2388,9 +2365,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 8dc68155177..b2711d9b3d6 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -39,6 +39,7 @@ import { isCaipChainId, parseCaipChainId, } from '@metamask/utils'; + import { getUUIDFromAddressOfNormalAccount, isNormalKeyringType, @@ -416,6 +417,7 @@ export class AccountsController extends BaseController< /** * Returns the account with the specified address. * ! This method will only return the first account that matches the address + * * @param address - The address of the account to retrieve. * @returns The account with the specified address, or undefined if not found. */ @@ -599,6 +601,7 @@ export class AccountsController extends BaseController< /** * Generates an internal account for a non-Snap account. + * * @param address - The address of the account. * @param type - The type of the account. * @returns The generated internal account. @@ -918,6 +921,7 @@ export class AccountsController extends BaseController< /** * Returns the list of accounts for a given keyring type. + * * @param keyringType - The type of keyring. * @param accounts - Accounts to filter by keyring type. * @returns The list of accounts associcated with this keyring type. @@ -964,6 +968,7 @@ export class AccountsController extends BaseController< /** * Returns the next account number for a given keyring type. + * * @param keyringType - The type of keyring. * @param accounts - Existing accounts to check for the next available account number. * @returns An object containing the account prefix and index to use. @@ -1001,8 +1006,6 @@ export class AccountsController extends BaseController< const index = Math.max( keyringAccounts.length + 1, - // ESLint is confused; this is a number. - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands lastDefaultIndexUsedForKeyringType + 1, ); @@ -1011,7 +1014,7 @@ export class AccountsController extends BaseController< /** * Checks if an account is compatible with a given chain namespace. - * @private + * * @param account - The account to check compatibility for. * @param chainId - The CAIP2 to check compatibility with. * @returns Returns true if the account is compatible with the chain namespace, otherwise false. @@ -1040,6 +1043,7 @@ export class AccountsController extends BaseController< * Handles the addition of a new account to the controller. * If the account is not a Snap Keyring account, generates an internal account for it and adds it to the controller. * If the account is a Snap Keyring account, retrieves the account from the keyring and adds it to the controller. + * * @param accountsState - AccountsController accounts state that is to be mutated. * @param account - The address and keyring type object of the new account. * @returns The updated AccountsController accounts state. @@ -1112,6 +1116,7 @@ export class AccountsController extends BaseController< /** * Handles the removal of an account from the internal accounts list. + * * @param accountsState - AccountsController accounts state that is to be mutated. * @param accountId - The ID of the account to be removed. * @returns The updated AccountsController state. @@ -1150,7 +1155,8 @@ export class AccountsController extends BaseController< // Update selected account to non evm account const lastSelectedNonEvmAccount = this.getSelectedMultichainAccount(nonEvmChainId); - accountId = lastSelectedNonEvmAccount!.id; + // @ts-expect-error - This should never be undefined, otherwise it's a bug that should be handled + accountId = lastSelectedNonEvmAccount.id; } else if (evmClientId) { // Update selected account to evm account const lastSelectedEvmAccount = this.getSelectedAccount(); @@ -1166,13 +1172,13 @@ export class AccountsController extends BaseController< /** * Retrieves the value of a specific metadata key for an existing account. + * * @param accountId - The ID of the account. * @param metadataKey - The key of the metadata to retrieve. * @param account - The account object to retrieve the metadata key from. * @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, @@ -1184,7 +1190,6 @@ export class AccountsController extends BaseController< /** * Subscribes to message events. - * @private */ #subscribeToMessageEvents() { this.messagingSystem.subscribe( @@ -1233,7 +1238,6 @@ export class AccountsController extends BaseController< /** * Registers message handlers for the AccountsController. - * @private */ #registerMessageHandlers() { this.messagingSystem.registerActionHandler( diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index c959a616574..7397511306e 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -56,6 +56,7 @@ "@metamask/auto-changelog": "^3.4.4", "@metamask/keyring-controller": "^19.0.4", "@types/jest": "^27.4.1", + "@types/uuid": "^8.3.0", "deepmerge": "^4.2.2", "immer": "^9.0.6", "jest": "^27.5.1", diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index 98d8d6206bd..407f1826d77 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -1,4 +1,5 @@ import { ControllerMessenger } from '@metamask/base-controller'; +import { InfuraNetworkType } from '@metamask/controller-utils'; import { BtcScopes, SolScopes, @@ -7,24 +8,22 @@ import { SolAccountType, type KeyringAccountType, } from '@metamask/keyring-api'; +import type { + NetworkControllerGetStateAction, + NetworkControllerSetActiveNetworkAction, +} from '@metamask/network-controller'; +import type { CaipChainId } from '@metamask/utils'; + +import { multichainNetworkConfigurations } from './constants'; import type { AllowedActions, AllowedEvents, MultichainNetworkControllerAllowedActions, MultichainNetworkControllerAllowedEvents, -} from './MultichainNetworkController'; -import { getDefaultMultichainNetworkControllerState, MultichainNetworkController, } from './MultichainNetworkController'; -import type { - NetworkControllerGetStateAction, - NetworkControllerSetActiveNetworkAction, -} from '@metamask/network-controller'; -import { InfuraNetworkType } from '@metamask/controller-utils'; -import type { CaipChainId } from '@metamask/utils'; import { createMockInternalAccount } from './test/utils'; -import { multichainNetworkConfigurations } from './constants'; const controllerName = 'MultichainNetworkController'; @@ -32,7 +31,7 @@ const controllerName = 'MultichainNetworkController'; * Setup a test controller instance. * * @param args - Arguments to this function. - * @param args.state - Initial state for controller. + * @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. diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index d3fdcf403e7..24fdd9f2924 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -13,6 +13,7 @@ import type { NetworkControllerGetStateAction, } from '@metamask/network-controller'; import type { CaipAssetType, CaipChainId } from '@metamask/utils'; + import { nonEvmNetworkChainIdByAccountAddress } from './utils'; const controllerName = 'MultichainNetworkController'; @@ -322,7 +323,7 @@ export class MultichainNetworkController extends BaseController< * * @param account - The account that was changed */ - #handleSelectedAccountChange = (account: InternalAccount) => { + readonly #handleSelectedAccountChange = (account: InternalAccount) => { const { type: accountType, address: accountAddress } = account; const isEvmAccount = isEvmAccountType(accountType); @@ -358,7 +359,6 @@ export class MultichainNetworkController extends BaseController< /** * Subscribes to message events. - * @private */ #subscribeToMessageEvents() { // Handle network switch when account is changed diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index a11f124bdd7..c6ac8837a3a 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -1,10 +1,10 @@ +import { BtcScopes, SolScopes } from '@metamask/keyring-api'; import { NetworkStatus } from '@metamask/network-controller'; import type { MultichainNetworkConfiguration, MultichainNetworkMetadata, } from './MultichainNetworkController'; -import { BtcScopes, SolScopes } from '@metamask/keyring-api'; export const btcNativeAsset = `${BtcScopes.Mainnet}/slip44:0`; export const solNativeAsset = `${SolScopes.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`; diff --git a/packages/multichain-network-controller/src/test/utils.ts b/packages/multichain-network-controller/src/test/utils.ts index 13586f5aad3..93036ff4a2b 100644 --- a/packages/multichain-network-controller/src/test/utils.ts +++ b/packages/multichain-network-controller/src/test/utils.ts @@ -7,8 +7,8 @@ import { SolMethod, type KeyringAccountType, } from '@metamask/keyring-api'; -import type { InternalAccount } from '@metamask/keyring-internal-api'; import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import { v4 } from 'uuid'; /** @@ -22,6 +22,9 @@ import { v4 } from 'uuid'; * @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. diff --git a/packages/multichain-network-controller/src/utils.ts b/packages/multichain-network-controller/src/utils.ts index 5caefa207fa..19d24813463 100644 --- a/packages/multichain-network-controller/src/utils.ts +++ b/packages/multichain-network-controller/src/utils.ts @@ -1,6 +1,6 @@ -import { CaipChainId } from '@metamask/utils'; -import { isAddress as isSolanaAddress } from '@solana/addresses'; import { BtcScopes, SolScopes } from '@metamask/keyring-api'; +import type { CaipChainId } from '@metamask/utils'; +import { isAddress as isSolanaAddress } from '@solana/addresses'; /** * Returns the chain id of the non-EVM network based on the account address. * diff --git a/yarn.lock b/yarn.lock index b829e0faf97..56872b0376f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3355,6 +3355,7 @@ __metadata: "@metamask/utils": "npm:^11.0.1" "@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" From 2a5132613dacdf3439a6053e0d5e10221562135e Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 11:12:20 -0800 Subject: [PATCH 31/69] Fix unit tests --- .../accounts-controller/src/AccountsController.ts | 8 ++++---- .../src/MultichainNetworkController.test.ts | 14 ++++++++------ .../src/MultichainNetworkController.ts | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index b2711d9b3d6..65ab9e1f007 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -1142,13 +1142,13 @@ export class AccountsController extends BaseController< * @param args.evmClientId - The ID of the EVM client. * @param args.nonEvmChainId - The CAIP2 of the non-EVM chain. */ - #handleMultichainNetworkChange({ + readonly #handleMultichainNetworkChange = ({ evmClientId, nonEvmChainId, }: { evmClientId?: string; nonEvmChainId?: CaipChainId; - }) { + }) => { let accountId: string; if (nonEvmChainId) { @@ -1168,7 +1168,7 @@ export class AccountsController extends BaseController< Date.now(); currentState.internalAccounts.selectedAccount = accountId; }); - } + }; /** * Retrieves the value of a specific metadata key for an existing account. @@ -1232,7 +1232,7 @@ export class AccountsController extends BaseController< // Handle account change when multichain network is changed this.messagingSystem.subscribe( 'MultichainNetworkController:setActiveNetwork', - this.#handleMultichainNetworkChange.bind(this), + this.#handleMultichainNetworkChange, ); } diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index 407f1826d77..e88cfaefddb 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -15,11 +15,11 @@ import type { import type { CaipChainId } from '@metamask/utils'; import { multichainNetworkConfigurations } from './constants'; -import type { - AllowedActions, - AllowedEvents, - MultichainNetworkControllerAllowedActions, - MultichainNetworkControllerAllowedEvents, +import { + type AllowedActions, + type AllowedEvents, + type MultichainNetworkControllerAllowedActions, + type MultichainNetworkControllerAllowedEvents, getDefaultMultichainNetworkControllerState, MultichainNetworkController, } from './MultichainNetworkController'; @@ -139,7 +139,9 @@ function setupController({ describe('MultichainNetworkController', () => { describe('constructor', () => { it('should set default state', () => { - const { controller } = setupController(); + const { controller } = setupController({ + options: { state: getDefaultMultichainNetworkControllerState() }, + }); expect(controller.state).toStrictEqual( getDefaultMultichainNetworkControllerState(), ); diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 24fdd9f2924..3781ec31c24 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -364,7 +364,7 @@ export class MultichainNetworkController extends BaseController< // Handle network switch when account is changed this.messagingSystem.subscribe( 'AccountsController:selectedAccountChange', - this.#handleSelectedAccountChange.bind(this), + this.#handleSelectedAccountChange, ); } From b0d8baf394615f4fed8b9dcc6f37d23ac96d0f20 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 11:34:57 -0800 Subject: [PATCH 32/69] Remove empty object --- .../src/MultichainNetworkController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 3781ec31c24..8d730e17306 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -200,10 +200,10 @@ export class MultichainNetworkController extends BaseController< > { constructor({ messenger, - state = {}, + state, }: { messenger: MultichainNetworkControllerMessenger; - state?: Partial; + state: Partial; }) { super({ messenger, From a2dbe43499e1dd04a66e02fb1d599dd03ca49a88 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 11:46:20 -0800 Subject: [PATCH 33/69] Remove ts expect for build to pass --- packages/accounts-controller/src/AccountsController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index 65ab9e1f007..e795047a621 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -490,7 +490,7 @@ export class AccountsController extends BaseController< }; // Do not remove this comment - This error is flaky: Comment out or restore the `ts-expect-error` directive below as needed. // See: https://github.com/MetaMask/utils/issues/168 - // @ts-expect-error Known issue - `Json` causes recursive error in immer `Draft`/`WritableDraft` types + // // @ts-expect-error Known issue - `Json` causes recursive error in immer `Draft`/`WritableDraft` types currentState.internalAccounts.accounts[accountId] = internalAccount; if (metadata.name) { From 107d1d544106314286c88cb8ebcc14aca2164dff Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 13:02:20 -0800 Subject: [PATCH 34/69] Add missing condition when switching accounts --- .../src/MultichainNetworkController.test.ts | 11 +++++++---- .../src/MultichainNetworkController.ts | 3 +++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index e88cfaefddb..52e136618e3 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -304,6 +304,7 @@ describe('MultichainNetworkController', () => { // EVM network is still active expect(controller.state.nonEvmSelected).toBe(false); }); + 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({ @@ -322,19 +323,20 @@ describe('MultichainNetworkController', () => { // EVM network is now active expect(controller.state.nonEvmSelected).toBe(false); }); - it('non-EVM network should be active when switching to account of same non-EVM network', async () => { + 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: { - nonEvmSelected: true, + nonEvmSelected: false, selectedMultichainNetworkChainId: SolScopes.Mainnet, }, }, }); - // non-EVM network is currently active - expect(controller.state.nonEvmSelected).toBe(true); + // EVM network is currently active + expect(controller.state.nonEvmSelected).toBe(false); + expect(controller.state.selectedMultichainNetworkChainId).toBe( SolScopes.Mainnet, ); @@ -348,6 +350,7 @@ describe('MultichainNetworkController', () => { ); expect(controller.state.nonEvmSelected).toBe(true); }); + 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({ diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 8d730e17306..37b90c9b1bc 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -348,6 +348,9 @@ export class MultichainNetworkController extends BaseController< if (isSameNonEvmNetwork) { // No need to update if already on the same non-EVM network + this.update((state) => { + state.nonEvmSelected = true; + }); return; } From 2c65ccd0eab108a6a8c49bfdbda8acb7133f8c1b Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 13:13:14 -0800 Subject: [PATCH 35/69] Update package versions --- packages/multichain-network-controller/package.json | 4 ++-- packages/multichain-network-controller/src/index.ts | 2 ++ yarn.lock | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index 7397511306e..3902d8bc6e5 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -48,13 +48,13 @@ }, "dependencies": { "@metamask/base-controller": "^7.1.1", - "@metamask/keyring-api": "^15.0.0", + "@metamask/keyring-api": "^16.1.0", "@metamask/utils": "^11.0.1", "@solana/addresses": "^2.0.0" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^19.0.4", + "@metamask/keyring-controller": "^19.0.5", "@types/jest": "^27.4.1", "@types/uuid": "^8.3.0", "deepmerge": "^4.2.2", diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts index 1f191fa003c..770e3abef67 100644 --- a/packages/multichain-network-controller/src/index.ts +++ b/packages/multichain-network-controller/src/index.ts @@ -5,6 +5,8 @@ export type { MultichainNetworkControllerGetStateAction, MultichainNetworkSetActiveNetworkEvent, MultichainNetworkControllerMessenger, + MultichainNetworkControllerAllowedActions, + MultichainNetworkControllerAllowedEvents, } from './MultichainNetworkController'; export { MultichainNetworkController } from './MultichainNetworkController'; export { multichainNetworkConfigurations, networksMetadata } from './constants'; diff --git a/yarn.lock b/yarn.lock index 5f236754aaf..0e0dae1517a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3375,8 +3375,8 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^7.1.1" - "@metamask/keyring-api": "npm:^15.0.0" - "@metamask/keyring-controller": "npm:^19.0.4" + "@metamask/keyring-api": "npm:^16.1.0" + "@metamask/keyring-controller": "npm:^19.0.5" "@metamask/utils": "npm:^11.0.1" "@solana/addresses": "npm:^2.0.0" "@types/jest": "npm:^27.4.1" From a4fbabc6b9aa8f91c64e2196ebf7ac636d23a61e Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 13:28:06 -0800 Subject: [PATCH 36/69] Keep consistent accounts controller version --- packages/multichain-network-controller/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index 3902d8bc6e5..2b531f0d3b3 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -67,7 +67,7 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/accounts-controller": "^21.0.1", + "@metamask/accounts-controller": "^22.0.0", "@metamask/network-controller": "^22.1.1" }, "engines": { diff --git a/yarn.lock b/yarn.lock index 0e0dae1517a..6c777080e98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3390,7 +3390,7 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" peerDependencies: - "@metamask/accounts-controller": ^21.0.1 + "@metamask/accounts-controller": ^22.0.0 "@metamask/network-controller": ^22.1.1 languageName: unknown linkType: soft From 3c653e66101c981a59b7baa7ac0d04b169108ee8 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 13:46:31 -0800 Subject: [PATCH 37/69] Make utils package consistent --- packages/multichain-network-controller/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index 2b531f0d3b3..589c8efe6e9 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -49,7 +49,7 @@ "dependencies": { "@metamask/base-controller": "^7.1.1", "@metamask/keyring-api": "^16.1.0", - "@metamask/utils": "^11.0.1", + "@metamask/utils": "^11.1.0", "@solana/addresses": "^2.0.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 6c777080e98..7ce5bce27d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3377,7 +3377,7 @@ __metadata: "@metamask/base-controller": "npm:^7.1.1" "@metamask/keyring-api": "npm:^16.1.0" "@metamask/keyring-controller": "npm:^19.0.5" - "@metamask/utils": "npm:^11.0.1" + "@metamask/utils": "npm:^11.1.0" "@solana/addresses": "npm:^2.0.0" "@types/jest": "npm:^27.4.1" "@types/uuid": "npm:^8.3.0" From a9f942f9dcd770c20c47a1cf13f0816de4fb55a7 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 13:50:40 -0800 Subject: [PATCH 38/69] Update imports from keyring-api --- .../src/MultichainNetworkController.test.ts | 32 +++++++++---------- .../src/MultichainNetworkController.ts | 4 +-- .../src/constants.ts | 18 +++++------ .../src/utils.ts | 6 ++-- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index 52e136618e3..c4e19aeddff 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -1,8 +1,8 @@ import { ControllerMessenger } from '@metamask/base-controller'; import { InfuraNetworkType } from '@metamask/controller-utils'; import { - BtcScopes, - SolScopes, + BtcScope, + SolScope, EthAccountType, BtcAccountType, SolAccountType, @@ -100,7 +100,7 @@ function setupController({ const controller = new MultichainNetworkController({ messenger: options.messenger || controllerMessenger, state: { - selectedMultichainNetworkChainId: SolScopes.Mainnet, + selectedMultichainNetworkChainId: SolScope.Mainnet, multichainNetworkConfigurationsByChainId: multichainNetworkConfigurations, multichainNetworksMetadata: {}, nonEvmSelected: false, @@ -154,7 +154,7 @@ describe('MultichainNetworkController', () => { await expect( controller.setActiveNetwork({ evmClientId: InfuraNetworkType.mainnet, - nonEvmChainId: SolScopes.Mainnet, + nonEvmChainId: SolScope.Mainnet, }), ).rejects.toThrow('Cannot set both EVM and non-EVM networks!'); }); @@ -183,12 +183,12 @@ describe('MultichainNetworkController', () => { // Set active network to Solana await controller.setActiveNetwork({ - nonEvmChainId: SolScopes.Mainnet, + nonEvmChainId: SolScope.Mainnet, }); // Check that the Solana is now the selected network expect(controller.state.selectedMultichainNetworkChainId).toBe( - SolScopes.Mainnet, + SolScope.Mainnet, ); // Check that the a non evm network is now active @@ -197,7 +197,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( 'MultichainNetworkController:setActiveNetwork', - { nonEvmChainId: SolScopes.Mainnet }, + { nonEvmChainId: SolScope.Mainnet }, ); }); @@ -220,12 +220,12 @@ describe('MultichainNetworkController', () => { // Set active network to Bitcoin await controller.setActiveNetwork({ - nonEvmChainId: BtcScopes.Mainnet, + nonEvmChainId: BtcScope.Mainnet, }); // Check that the Solana is now the selected network expect(controller.state.selectedMultichainNetworkChainId).toBe( - BtcScopes.Mainnet, + BtcScope.Mainnet, ); // Check that BTC network is now active @@ -234,7 +234,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( 'MultichainNetworkController:setActiveNetwork', - { nonEvmChainId: BtcScopes.Mainnet }, + { nonEvmChainId: BtcScope.Mainnet }, ); }); @@ -329,7 +329,7 @@ describe('MultichainNetworkController', () => { options: { state: { nonEvmSelected: false, - selectedMultichainNetworkChainId: SolScopes.Mainnet, + selectedMultichainNetworkChainId: SolScope.Mainnet, }, }, }); @@ -338,7 +338,7 @@ describe('MultichainNetworkController', () => { expect(controller.state.nonEvmSelected).toBe(false); expect(controller.state.selectedMultichainNetworkChainId).toBe( - SolScopes.Mainnet, + SolScope.Mainnet, ); // Switching to Solana account @@ -346,7 +346,7 @@ describe('MultichainNetworkController', () => { // Solana is still the selected network expect(controller.state.selectedMultichainNetworkChainId).toBe( - SolScopes.Mainnet, + SolScope.Mainnet, ); expect(controller.state.nonEvmSelected).toBe(true); }); @@ -357,7 +357,7 @@ describe('MultichainNetworkController', () => { options: { state: { nonEvmSelected: true, - selectedMultichainNetworkChainId: SolScopes.Mainnet, + selectedMultichainNetworkChainId: SolScope.Mainnet, }, }, }); @@ -365,7 +365,7 @@ describe('MultichainNetworkController', () => { // Solana is currently active expect(controller.state.nonEvmSelected).toBe(true); expect(controller.state.selectedMultichainNetworkChainId).toBe( - SolScopes.Mainnet, + SolScope.Mainnet, ); // Switching to Bitcoin account @@ -373,7 +373,7 @@ describe('MultichainNetworkController', () => { // Bitcoin is now the selected network expect(controller.state.selectedMultichainNetworkChainId).toBe( - BtcScopes.Mainnet, + BtcScope.Mainnet, ); expect(controller.state.nonEvmSelected).toBe(true); }); diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 37b90c9b1bc..260729258e3 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -5,7 +5,7 @@ import { type ControllerStateChangeEvent, type RestrictedControllerMessenger, } from '@metamask/base-controller'; -import { isEvmAccountType, SolScopes } from '@metamask/keyring-api'; +import { isEvmAccountType, SolScope } from '@metamask/keyring-api'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { NetworkStatus, @@ -87,7 +87,7 @@ export type MultichainNetworkControllerState = { export const getDefaultMultichainNetworkControllerState = (): MultichainNetworkControllerState => ({ multichainNetworkConfigurationsByChainId: {}, - selectedMultichainNetworkChainId: SolScopes.Mainnet, + selectedMultichainNetworkChainId: SolScope.Mainnet, multichainNetworksMetadata: {}, nonEvmSelected: false, }); diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index c6ac8837a3a..d250b114928 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -1,4 +1,4 @@ -import { BtcScopes, SolScopes } from '@metamask/keyring-api'; +import { BtcScope, SolScope } from '@metamask/keyring-api'; import { NetworkStatus } from '@metamask/network-controller'; import type { @@ -6,15 +6,15 @@ import type { MultichainNetworkMetadata, } from './MultichainNetworkController'; -export const btcNativeAsset = `${BtcScopes.Mainnet}/slip44:0`; -export const solNativeAsset = `${SolScopes.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`; +export const btcNativeAsset = `${BtcScope.Mainnet}/slip44:0`; +export const solNativeAsset = `${SolScope.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`; export const multichainNetworkConfigurations: Record< string, MultichainNetworkConfiguration > = { - [BtcScopes.Mainnet]: { - chainId: BtcScopes.Mainnet, + [BtcScope.Mainnet]: { + chainId: BtcScope.Mainnet, name: 'Bitcoin Mainnet', @@ -24,8 +24,8 @@ export const multichainNetworkConfigurations: Record< isEvm: false, }, - [SolScopes.Mainnet]: { - chainId: SolScopes.Mainnet, + [SolScope.Mainnet]: { + chainId: SolScope.Mainnet, name: 'Solana Mainnet', @@ -38,11 +38,11 @@ export const multichainNetworkConfigurations: Record< }; export const networksMetadata: Record = { - [BtcScopes.Mainnet]: { + [BtcScope.Mainnet]: { features: [], status: NetworkStatus.Available, }, - [SolScopes.Mainnet]: { + [SolScope.Mainnet]: { features: [], status: NetworkStatus.Available, }, diff --git a/packages/multichain-network-controller/src/utils.ts b/packages/multichain-network-controller/src/utils.ts index 19d24813463..e8295b40cc2 100644 --- a/packages/multichain-network-controller/src/utils.ts +++ b/packages/multichain-network-controller/src/utils.ts @@ -1,4 +1,4 @@ -import { BtcScopes, SolScopes } from '@metamask/keyring-api'; +import { BtcScope, SolScope } from '@metamask/keyring-api'; import type { CaipChainId } from '@metamask/utils'; import { isAddress as isSolanaAddress } from '@solana/addresses'; /** @@ -12,7 +12,7 @@ export function nonEvmNetworkChainIdByAccountAddress( ): CaipChainId { // This condition is not the most robust. Once we support more networks, we will need to update this logic. if (isSolanaAddress(address)) { - return SolScopes.Mainnet; + return SolScope.Mainnet; } - return BtcScopes.Mainnet; + return BtcScope.Mainnet; } From 1a1abeb91f2262105ab4fba98cec18efa2eaa907 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 13:53:20 -0800 Subject: [PATCH 39/69] Fix import --- packages/accounts-controller/src/AccountsController.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index e7f41112f78..eb136dbd8d7 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -1576,7 +1576,7 @@ describe('AccountsController', () => { // Triggered from network switch to Bitcoin mainnet triggerMultichainNetworkChange({ - nonEvmChainId: BtcScopes.Mainnet, + nonEvmChainId: BtcScope.Mainnet, }); // BTC account is now selected @@ -1603,7 +1603,7 @@ describe('AccountsController', () => { // Triggered from network switch to Bitcoin mainnet triggerMultichainNetworkChange({ - evmClientId: EthScopes.Mainnet, + evmClientId: EthScope.Mainnet, }); // ETH mainnet account is now selected From dcca50279be05a4fddc58c2ac6c5ed5262c2e7f4 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 4 Feb 2025 14:03:06 -0800 Subject: [PATCH 40/69] Remove lint warnings for utils --- eslint-warning-thresholds.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index fe01d02cf41..18e03fd6d51 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 }, From f86d786256d231fd9e02c1363267d56b68f0aa51 Mon Sep 17 00:00:00 2001 From: Gustavo Antunes <17601467+gantunesr@users.noreply.github.com> Date: Wed, 5 Feb 2025 08:06:44 -0300 Subject: [PATCH 41/69] chore: update network types (#5277) --- .../src/MultichainNetworkController.ts | 49 +++++++++++++------ .../src/constants.ts | 10 +++- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 260729258e3..d05ead35087 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -23,37 +23,56 @@ export type MultichainNetworkMetadata = { status: NetworkStatus; }; -export type MultichainNetworkConfiguration = { +export type CommonNetworkConfiguration = { + /** + * EVM network flag. + */ + isEvm: boolean; + + /** + * The block explorers of the network. + */ + blockExplorers: { + urls: string[]; + defaultIndex: number; + }; + /** * The chain ID of the network. */ chainId: CaipChainId; + /** * The name of the network. */ name: string; + /** * The native asset type of the network. */ nativeCurrency: CaipAssetType; +}; + +export type NonEvmNetworkConfiguration = CommonNetworkConfiguration & { + isEvm: false; +}; + +export type EvmNetworkConfiguration = CommonNetworkConfiguration & { + isEvm: true; + /** - * The block explorer URLs of the network. - */ - blockExplorerUrls: string[]; - /** - * The default block explorer URL index of the network. - */ - defaultBlockExplorerUrlIndex?: number; - /** - * The last updated timestamp of the network. - */ - lastUpdated?: number; - /** - * Whether the network is an EVM network or non-evm network. + * The RPC endpoints of the network. */ - isEvm: boolean; + rpcEndpoints: { + urls: string[]; + defaultIndex: number; + }; }; +export type MultichainNetworkConfiguration = + | EvmNetworkConfiguration + | NonEvmNetworkConfiguration; + /** * State used by the {@link MultichainNetworkController} to cache network configurations. */ diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index d250b114928..50f87ace88f 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -18,7 +18,10 @@ export const multichainNetworkConfigurations: Record< name: 'Bitcoin Mainnet', - blockExplorerUrls: [], + blockExplorers: { + urls: [], + defaultIndex: 0, + }, nativeCurrency: btcNativeAsset, @@ -29,7 +32,10 @@ export const multichainNetworkConfigurations: Record< name: 'Solana Mainnet', - blockExplorerUrls: [], + blockExplorers: { + urls: [], + defaultIndex: 0, + }, nativeCurrency: solNativeAsset, From c58256ddab2b090c2feeff4ae138208b9ee3b696 Mon Sep 17 00:00:00 2001 From: Gustavo Antunes <17601467+gantunesr@users.noreply.github.com> Date: Wed, 5 Feb 2025 17:20:08 -0300 Subject: [PATCH 42/69] chore: add block explorer URLs (#5288) --- packages/multichain-network-controller/src/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index 50f87ace88f..df8a602d191 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -19,7 +19,7 @@ export const multichainNetworkConfigurations: Record< name: 'Bitcoin Mainnet', blockExplorers: { - urls: [], + urls: ['https://blockstream.info'], defaultIndex: 0, }, @@ -33,7 +33,7 @@ export const multichainNetworkConfigurations: Record< name: 'Solana Mainnet', blockExplorers: { - urls: [], + urls: ['https://explorer.solana.com'], defaultIndex: 0, }, From 38ec763dc54fbdcac4feeadde6c8546dde7c11c2 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Fri, 7 Feb 2025 13:32:38 -0800 Subject: [PATCH 43/69] Run update-readme-content script to update README graph --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 9645d28513e..b53081a89ba 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,10 @@ linkStyle default opacity:0.5 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 +140,13 @@ 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_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 +193,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; From f733a4567b13bae241056317fedb6d932b817f66 Mon Sep 17 00:00:00 2001 From: Cal Leung Date: Fri, 7 Feb 2025 13:33:20 -0800 Subject: [PATCH 44/69] Update packages/multichain-network-controller/tsconfig.json Co-authored-by: Elliot Winkler --- packages/multichain-network-controller/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain-network-controller/tsconfig.json b/packages/multichain-network-controller/tsconfig.json index faf37b65fc5..e5ff777b642 100644 --- a/packages/multichain-network-controller/tsconfig.json +++ b/packages/multichain-network-controller/tsconfig.json @@ -8,5 +8,5 @@ { "path": "../network-controller" }, { "path": "../keyring-controller" } ], - "include": ["../../types", "./src"] + "include": ["../../types", "./src", "./tests"] } From 8017a64408c2d677f15eb94be2f40c8e7d93ce78 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Fri, 7 Feb 2025 14:03:41 -0800 Subject: [PATCH 45/69] Update event to onNetworkChange --- .../src/AccountsController.test.ts | 6 +++--- .../accounts-controller/src/AccountsController.ts | 4 ++-- .../src/MultichainNetworkController.test.ts | 8 ++++---- .../src/MultichainNetworkController.ts | 12 ++++++------ packages/multichain-network-controller/src/index.ts | 12 +----------- 5 files changed, 16 insertions(+), 26 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index eb136dbd8d7..e1b0a3f8376 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -307,7 +307,7 @@ function buildAccountsControllerMessenger(messenger = buildMessenger()) { 'SnapKeyring:accountAssetListUpdated', 'SnapKeyring:accountBalancesUpdated', 'SnapKeyring:accountTransactionsUpdated', - 'MultichainNetworkController:setActiveNetwork', + 'MultichainNetworkController:onNetworkChange', ], allowedActions: [ 'KeyringController:getAccounts', @@ -360,7 +360,7 @@ function setupAccountsController({ evmClientId?: string; nonEvmChainId?: CaipChainId; }) => { - messenger.publish('MultichainNetworkController:setActiveNetwork', { + messenger.publish('MultichainNetworkController:onNetworkChange', { evmClientId, nonEvmChainId, }); @@ -1556,7 +1556,7 @@ describe('AccountsController', () => { }); }); - describe('handle MultichainNetworkController:setActiveNetwork event', () => { + describe('handle MultichainNetworkController:onNetworkChange event', () => { it('should update selected account to non-EVM account when switching to non-EVM network', () => { const messenger = buildMessenger(); const { accountsController, triggerMultichainNetworkChange } = diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index be5ba038d3e..f5aab8034c1 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -183,7 +183,7 @@ export type AccountsControllerAccountAssetListUpdatedEvent = { // Re-define event here to avoid circular dependency with MultichainNetworkController type MultichainNetworkSetActiveNetworkEvent = { - type: `MultichainNetworkController:setActiveNetwork`; + type: `MultichainNetworkController:onNetworkChange`; payload: [ { evmClientId?: string; @@ -1231,7 +1231,7 @@ export class AccountsController extends BaseController< // Handle account change when multichain network is changed this.messagingSystem.subscribe( - 'MultichainNetworkController:setActiveNetwork', + 'MultichainNetworkController:onNetworkChange', this.#handleMultichainNetworkChange, ); } diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index c4e19aeddff..cf998c3f8f9 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -196,7 +196,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( - 'MultichainNetworkController:setActiveNetwork', + 'MultichainNetworkController:onNetworkChange', { nonEvmChainId: SolScope.Mainnet }, ); }); @@ -233,7 +233,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( - 'MultichainNetworkController:setActiveNetwork', + 'MultichainNetworkController:onNetworkChange', { nonEvmChainId: BtcScope.Mainnet }, ); }); @@ -256,7 +256,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( - 'MultichainNetworkController:setActiveNetwork', + 'MultichainNetworkController:onNetworkChange', { evmClientId: selectedNetworkClientId }, ); @@ -281,7 +281,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( - 'MultichainNetworkController:setActiveNetwork', + 'MultichainNetworkController:onNetworkChange', { evmClientId }, ); diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index d05ead35087..a283b7cc082 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -133,8 +133,8 @@ export type MultichainNetworkControllerStateChange = ControllerStateChangeEvent< MultichainNetworkControllerState >; -export type MultichainNetworkSetActiveNetworkEvent = { - type: `${typeof controllerName}:setActiveNetwork`; +export type MultichainNetworkOnNetworkChangeEvent = { + type: `${typeof controllerName}:onNetworkChange`; payload: [ { evmClientId?: string; @@ -154,7 +154,7 @@ export type MultichainNetworkControllerActions = * Events emitted by {@link MultichainNetworkController}. */ export type MultichainNetworkControllerEvents = - MultichainNetworkSetActiveNetworkEvent; + MultichainNetworkOnNetworkChangeEvent; /** * Actions that this controller is allowed to call. @@ -273,7 +273,7 @@ export class MultichainNetworkController extends BaseController< // Notify listeners that setActiveNetwork was called this.messagingSystem.publish( - 'MultichainNetworkController:setActiveNetwork', + 'MultichainNetworkController:onNetworkChange', { nonEvmChainId }, ); return; @@ -290,7 +290,7 @@ export class MultichainNetworkController extends BaseController< // Notify listeners that setActiveNetwork was called this.messagingSystem.publish( - 'MultichainNetworkController:setActiveNetwork', + 'MultichainNetworkController:onNetworkChange', { nonEvmChainId }, ); @@ -309,7 +309,7 @@ export class MultichainNetworkController extends BaseController< // Notify listeners that setActiveNetwork was called this.messagingSystem.publish( - 'MultichainNetworkController:setActiveNetwork', + 'MultichainNetworkController:onNetworkChange', { evmClientId, }, diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts index 770e3abef67..e1ce5e20607 100644 --- a/packages/multichain-network-controller/src/index.ts +++ b/packages/multichain-network-controller/src/index.ts @@ -1,12 +1,2 @@ -export type { - MultichainNetworkConfiguration, - MultichainNetworkMetadata, - MultichainNetworkControllerState, - MultichainNetworkControllerGetStateAction, - MultichainNetworkSetActiveNetworkEvent, - MultichainNetworkControllerMessenger, - MultichainNetworkControllerAllowedActions, - MultichainNetworkControllerAllowedEvents, -} from './MultichainNetworkController'; -export { MultichainNetworkController } from './MultichainNetworkController'; +export * from './MultichainNetworkController'; export { multichainNetworkConfigurations, networksMetadata } from './constants'; From 4745dac8d8bb52561688f29563611357dbb1f9f1 Mon Sep 17 00:00:00 2001 From: Cal Leung Date: Fri, 7 Feb 2025 14:05:08 -0800 Subject: [PATCH 46/69] Update packages/multichain-network-controller/src/MultichainNetworkController.ts Co-authored-by: Elliot Winkler --- .../src/MultichainNetworkController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index a283b7cc082..b990ec9a482 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -81,7 +81,7 @@ export type MultichainNetworkControllerState = { * The network configurations by chain ID. */ multichainNetworkConfigurationsByChainId: Record< - string, + CaipChainId, MultichainNetworkConfiguration >; /** From 83288252c6acd60b38f64a139685f5e519773e64 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Fri, 7 Feb 2025 16:31:58 -0800 Subject: [PATCH 47/69] Change nonEvmSelected to isEvmSelected --- .../src/MultichainNetworkController.test.ts | 36 +++++++++---------- .../src/MultichainNetworkController.ts | 22 ++++++------ 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index cf998c3f8f9..b98a8b7d2d2 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -103,7 +103,7 @@ function setupController({ selectedMultichainNetworkChainId: SolScope.Mainnet, multichainNetworkConfigurationsByChainId: multichainNetworkConfigurations, multichainNetworksMetadata: {}, - nonEvmSelected: false, + isEvmSelected: true, ...options.state, }, }); @@ -192,7 +192,7 @@ describe('MultichainNetworkController', () => { ); // Check that the a non evm network is now active - expect(controller.state.nonEvmSelected).toBe(true); + expect(controller.state.isEvmSelected).toBe(false); // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( @@ -215,7 +215,7 @@ describe('MultichainNetworkController', () => { 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: { nonEvmSelected: true } }, + options: { state: { isEvmSelected: false } }, }); // Set active network to Bitcoin @@ -229,7 +229,7 @@ describe('MultichainNetworkController', () => { ); // Check that BTC network is now active - expect(controller.state.nonEvmSelected).toBe(true); + expect(controller.state.isEvmSelected).toBe(false); // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( @@ -252,7 +252,7 @@ describe('MultichainNetworkController', () => { }); // Check that EVM network is selected - expect(controller.state.nonEvmSelected).toBe(false); + expect(controller.state.isEvmSelected).toBe(true); // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( @@ -277,7 +277,7 @@ describe('MultichainNetworkController', () => { }); // Check that EVM network is selected - expect(controller.state.nonEvmSelected).toBe(false); + expect(controller.state.isEvmSelected).toBe(true); // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( @@ -291,51 +291,51 @@ describe('MultichainNetworkController', () => { }); describe('handle AccountsController:selectedAccountChange event', () => { - it('nonEvmSelected should be false when both switching to EVM account and EVM network is already active', async () => { + 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.nonEvmSelected).toBe(false); + expect(controller.state.isEvmSelected).toBe(true); // Switching to EVM account triggerSelectedAccountChange(EthAccountType.Eoa); // EVM network is still active - expect(controller.state.nonEvmSelected).toBe(false); + 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: { nonEvmSelected: true } }, + options: { state: { isEvmSelected: false } }, getNetworkState: jest.fn().mockImplementation(() => ({ selectedNetworkClientId: InfuraNetworkType.mainnet, })), }); // non-EVM network is currently active - expect(controller.state.nonEvmSelected).toBe(true); + expect(controller.state.isEvmSelected).toBe(false); // Switching to EVM account triggerSelectedAccountChange(EthAccountType.Eoa); // EVM network is now active - expect(controller.state.nonEvmSelected).toBe(false); + 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: { - nonEvmSelected: false, + isEvmSelected: true, selectedMultichainNetworkChainId: SolScope.Mainnet, }, }, }); // EVM network is currently active - expect(controller.state.nonEvmSelected).toBe(false); + expect(controller.state.isEvmSelected).toBe(true); expect(controller.state.selectedMultichainNetworkChainId).toBe( SolScope.Mainnet, @@ -348,7 +348,7 @@ describe('MultichainNetworkController', () => { expect(controller.state.selectedMultichainNetworkChainId).toBe( SolScope.Mainnet, ); - expect(controller.state.nonEvmSelected).toBe(true); + expect(controller.state.isEvmSelected).toBe(false); }); it('non-EVM network should change when switching to account on different non-EVM network', async () => { @@ -356,14 +356,14 @@ describe('MultichainNetworkController', () => { const { controller, triggerSelectedAccountChange } = setupController({ options: { state: { - nonEvmSelected: true, + isEvmSelected: false, selectedMultichainNetworkChainId: SolScope.Mainnet, }, }, }); // Solana is currently active - expect(controller.state.nonEvmSelected).toBe(true); + expect(controller.state.isEvmSelected).toBe(false); expect(controller.state.selectedMultichainNetworkChainId).toBe( SolScope.Mainnet, ); @@ -375,7 +375,7 @@ describe('MultichainNetworkController', () => { expect(controller.state.selectedMultichainNetworkChainId).toBe( BtcScope.Mainnet, ); - expect(controller.state.nonEvmSelected).toBe(true); + expect(controller.state.isEvmSelected).toBe(false); }); }); }); diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index b990ec9a482..612785ad1b6 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -93,9 +93,9 @@ export type MultichainNetworkControllerState = { */ multichainNetworksMetadata: Record; /** - * Whether the non-EVM network is selected by the wallet. + * Whether EVM or non-EVM network is selected */ - nonEvmSelected: boolean; + isEvmSelected: boolean; }; /** @@ -108,7 +108,7 @@ export const getDefaultMultichainNetworkControllerState = multichainNetworkConfigurationsByChainId: {}, selectedMultichainNetworkChainId: SolScope.Mainnet, multichainNetworksMetadata: {}, - nonEvmSelected: false, + isEvmSelected: true, }); /** @@ -205,7 +205,7 @@ const multichainNetworkControllerMetadata = { multichainNetworkConfigurationsByChainId: { persist: true, anonymous: true }, selectedMultichainNetworkChainId: { persist: true, anonymous: true }, multichainNetworksMetadata: { persist: true, anonymous: true }, - nonEvmSelected: { persist: true, anonymous: true }, + isEvmSelected: { persist: true, anonymous: true }, } satisfies StateMetadata; /** @@ -268,7 +268,7 @@ export class MultichainNetworkController extends BaseController< if (nonEvmChainId === this.state.selectedMultichainNetworkChainId) { // Indicate that the non-EVM network is selected this.update((state) => { - state.nonEvmSelected = true; + state.isEvmSelected = false; }); // Notify listeners that setActiveNetwork was called @@ -296,7 +296,7 @@ export class MultichainNetworkController extends BaseController< this.update((state) => { state.selectedMultichainNetworkChainId = nonEvmChainId; - state.nonEvmSelected = true; + state.isEvmSelected = false; }); return; @@ -317,7 +317,7 @@ export class MultichainNetworkController extends BaseController< // Indicate that the non-EVM network is not selected this.update((state) => { - state.nonEvmSelected = false; + state.isEvmSelected = true; }); // Prevent setting same network @@ -348,14 +348,14 @@ export class MultichainNetworkController extends BaseController< // Handle switching to EVM network if (isEvmAccount) { - if (!this.state.nonEvmSelected) { + if (this.state.isEvmSelected) { // No need to update if already on evm network return; } // Make EVM network active this.update((state) => { - state.nonEvmSelected = false; + state.isEvmSelected = true; }); return; } @@ -368,14 +368,14 @@ export class MultichainNetworkController extends BaseController< if (isSameNonEvmNetwork) { // No need to update if already on the same non-EVM network this.update((state) => { - state.nonEvmSelected = true; + state.isEvmSelected = false; }); return; } this.update((state) => { state.selectedMultichainNetworkChainId = nonEvmChainId; - state.nonEvmSelected = true; + state.isEvmSelected = false; }); }; From ff3296fe3f1a7df1d37437e68c1cffcc80ecf207 Mon Sep 17 00:00:00 2001 From: Cal Leung Date: Fri, 7 Feb 2025 16:35:17 -0800 Subject: [PATCH 48/69] Update packages/multichain-network-controller/src/constants.ts Co-authored-by: Elliot Winkler --- packages/multichain-network-controller/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index df8a602d191..e74d0b08768 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -43,7 +43,7 @@ export const multichainNetworkConfigurations: Record< }, }; -export const networksMetadata: Record = { +export const NETWORKS_METADATA: Record = { [BtcScope.Mainnet]: { features: [], status: NetworkStatus.Available, From 51b4348ca41d9b65b75bcd5e755021f34bfb5d33 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Fri, 7 Feb 2025 16:44:40 -0800 Subject: [PATCH 49/69] Update event name for MultichainNetworkController --- .../src/AccountsController.test.ts | 6 +++--- .../accounts-controller/src/AccountsController.ts | 8 ++++---- .../src/MultichainNetworkController.test.ts | 8 ++++---- .../src/MultichainNetworkController.ts | 12 ++++++------ 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index e1b0a3f8376..6c7fc6cd145 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -307,7 +307,7 @@ function buildAccountsControllerMessenger(messenger = buildMessenger()) { 'SnapKeyring:accountAssetListUpdated', 'SnapKeyring:accountBalancesUpdated', 'SnapKeyring:accountTransactionsUpdated', - 'MultichainNetworkController:onNetworkChange', + 'MultichainNetworkController:networkDidChange', ], allowedActions: [ 'KeyringController:getAccounts', @@ -360,7 +360,7 @@ function setupAccountsController({ evmClientId?: string; nonEvmChainId?: CaipChainId; }) => { - messenger.publish('MultichainNetworkController:onNetworkChange', { + messenger.publish('MultichainNetworkController:networkDidChange', { evmClientId, nonEvmChainId, }); @@ -1556,7 +1556,7 @@ describe('AccountsController', () => { }); }); - describe('handle MultichainNetworkController:onNetworkChange event', () => { + 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 } = diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index f5aab8034c1..13b77a3a33b 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -182,8 +182,8 @@ export type AccountsControllerAccountAssetListUpdatedEvent = { }; // Re-define event here to avoid circular dependency with MultichainNetworkController -type MultichainNetworkSetActiveNetworkEvent = { - type: `MultichainNetworkController:onNetworkChange`; +type MultichainNetworkControllerNetworkDidChangeEvent = { + type: `MultichainNetworkController:networkDidChange`; payload: [ { evmClientId?: string; @@ -198,7 +198,7 @@ export type AllowedEvents = | SnapKeyringAccountAssetListUpdatedEvent | SnapKeyringAccountBalancesUpdatedEvent | SnapKeyringAccountTransactionsUpdatedEvent - | MultichainNetworkSetActiveNetworkEvent; + | MultichainNetworkControllerNetworkDidChangeEvent; export type AccountsControllerEvents = | AccountsControllerChangeEvent @@ -1231,7 +1231,7 @@ export class AccountsController extends BaseController< // Handle account change when multichain network is changed this.messagingSystem.subscribe( - 'MultichainNetworkController:onNetworkChange', + 'MultichainNetworkController:networkDidChange', this.#handleMultichainNetworkChange, ); } diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index b98a8b7d2d2..c9b895a0176 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -196,7 +196,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( - 'MultichainNetworkController:onNetworkChange', + 'MultichainNetworkController:networkDidChange', { nonEvmChainId: SolScope.Mainnet }, ); }); @@ -233,7 +233,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( - 'MultichainNetworkController:onNetworkChange', + 'MultichainNetworkController:networkDidChange', { nonEvmChainId: BtcScope.Mainnet }, ); }); @@ -256,7 +256,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( - 'MultichainNetworkController:onNetworkChange', + 'MultichainNetworkController:networkDidChange', { evmClientId: selectedNetworkClientId }, ); @@ -281,7 +281,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( - 'MultichainNetworkController:onNetworkChange', + 'MultichainNetworkController:networkDidChange', { evmClientId }, ); diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 612785ad1b6..9fe82b36e1f 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -133,8 +133,8 @@ export type MultichainNetworkControllerStateChange = ControllerStateChangeEvent< MultichainNetworkControllerState >; -export type MultichainNetworkOnNetworkChangeEvent = { - type: `${typeof controllerName}:onNetworkChange`; +export type MultichainNetworkControllerNetworkDidChangeEvent = { + type: `${typeof controllerName}:networkDidChange`; payload: [ { evmClientId?: string; @@ -154,7 +154,7 @@ export type MultichainNetworkControllerActions = * Events emitted by {@link MultichainNetworkController}. */ export type MultichainNetworkControllerEvents = - MultichainNetworkOnNetworkChangeEvent; + MultichainNetworkControllerNetworkDidChangeEvent; /** * Actions that this controller is allowed to call. @@ -273,7 +273,7 @@ export class MultichainNetworkController extends BaseController< // Notify listeners that setActiveNetwork was called this.messagingSystem.publish( - 'MultichainNetworkController:onNetworkChange', + 'MultichainNetworkController:networkDidChange', { nonEvmChainId }, ); return; @@ -290,7 +290,7 @@ export class MultichainNetworkController extends BaseController< // Notify listeners that setActiveNetwork was called this.messagingSystem.publish( - 'MultichainNetworkController:onNetworkChange', + 'MultichainNetworkController:networkDidChange', { nonEvmChainId }, ); @@ -309,7 +309,7 @@ export class MultichainNetworkController extends BaseController< // Notify listeners that setActiveNetwork was called this.messagingSystem.publish( - 'MultichainNetworkController:onNetworkChange', + 'MultichainNetworkController:networkDidChange', { evmClientId, }, From 6902a4f9053e4f9897514d0826cbc184617605f2 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Fri, 7 Feb 2025 16:48:30 -0800 Subject: [PATCH 50/69] Update function name --- .../src/MultichainNetworkController.ts | 4 ++-- packages/multichain-network-controller/src/utils.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 9fe82b36e1f..ff59c20feeb 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -14,7 +14,7 @@ import type { } from '@metamask/network-controller'; import type { CaipAssetType, CaipChainId } from '@metamask/utils'; -import { nonEvmNetworkChainIdByAccountAddress } from './utils'; +import { getChainIdForNonEvmAddress } from './utils'; const controllerName = 'MultichainNetworkController'; @@ -361,7 +361,7 @@ export class MultichainNetworkController extends BaseController< } // Handle switching to non-EVM network - const nonEvmChainId = nonEvmNetworkChainIdByAccountAddress(accountAddress); + const nonEvmChainId = getChainIdForNonEvmAddress(accountAddress); const isSameNonEvmNetwork = nonEvmChainId === this.state.selectedMultichainNetworkChainId; diff --git a/packages/multichain-network-controller/src/utils.ts b/packages/multichain-network-controller/src/utils.ts index e8295b40cc2..b58073c3447 100644 --- a/packages/multichain-network-controller/src/utils.ts +++ b/packages/multichain-network-controller/src/utils.ts @@ -7,9 +7,7 @@ import { isAddress as isSolanaAddress } from '@solana/addresses'; * @param address - The address to check. * @returns The caip chain id of the non-EVM network. */ -export function nonEvmNetworkChainIdByAccountAddress( - address: string, -): CaipChainId { +export function getChainIdForNonEvmAddress(address: string): CaipChainId { // 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; From bbb7ab1742e713e21e5b51098003996cbf12e05f Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 11 Feb 2025 10:50:46 -0800 Subject: [PATCH 51/69] Begin addressing comments in PR --- .../src/MultichainNetworkController.test.ts | 7 +- .../src/constants.ts | 4 +- .../src/index.ts | 2 +- .../src/test/utils.test.ts | 101 ------------------ .../{src => }/test/utils.ts | 0 .../tsconfig.json | 2 +- 6 files changed, 8 insertions(+), 108 deletions(-) delete mode 100644 packages/multichain-network-controller/src/test/utils.test.ts rename packages/multichain-network-controller/{src => }/test/utils.ts (100%) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index c9b895a0176..35de861883f 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -14,7 +14,7 @@ import type { } from '@metamask/network-controller'; import type { CaipChainId } from '@metamask/utils'; -import { multichainNetworkConfigurations } from './constants'; +import { MULTICHAIN_NETWORK_CONFIGURATIONS } from './constants'; import { type AllowedActions, type AllowedEvents, @@ -23,7 +23,7 @@ import { getDefaultMultichainNetworkControllerState, MultichainNetworkController, } from './MultichainNetworkController'; -import { createMockInternalAccount } from './test/utils'; +import { createMockInternalAccount } from '../test/utils'; const controllerName = 'MultichainNetworkController'; @@ -101,7 +101,8 @@ function setupController({ messenger: options.messenger || controllerMessenger, state: { selectedMultichainNetworkChainId: SolScope.Mainnet, - multichainNetworkConfigurationsByChainId: multichainNetworkConfigurations, + multichainNetworkConfigurationsByChainId: + MULTICHAIN_NETWORK_CONFIGURATIONS, multichainNetworksMetadata: {}, isEvmSelected: true, ...options.state, diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index df8a602d191..f5a0f3d7def 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -9,7 +9,7 @@ import type { export const btcNativeAsset = `${BtcScope.Mainnet}/slip44:0`; export const solNativeAsset = `${SolScope.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`; -export const multichainNetworkConfigurations: Record< +export const MULTICHAIN_NETWORK_CONFIGURATIONS: Record< string, MultichainNetworkConfiguration > = { @@ -43,7 +43,7 @@ export const multichainNetworkConfigurations: Record< }, }; -export const networksMetadata: Record = { +export const NETWORKS_METADATA: Record = { [BtcScope.Mainnet]: { features: [], status: NetworkStatus.Available, diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts index e1ce5e20607..79145dc7240 100644 --- a/packages/multichain-network-controller/src/index.ts +++ b/packages/multichain-network-controller/src/index.ts @@ -1,2 +1,2 @@ export * from './MultichainNetworkController'; -export { multichainNetworkConfigurations, networksMetadata } from './constants'; +export * from './constants'; diff --git a/packages/multichain-network-controller/src/test/utils.test.ts b/packages/multichain-network-controller/src/test/utils.test.ts deleted file mode 100644 index fa72c0f99b2..00000000000 --- a/packages/multichain-network-controller/src/test/utils.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { - BtcAccountType, - EthAccountType, - SolAccountType, -} from '@metamask/keyring-api'; - -import { createMockInternalAccount } from './utils'; - -describe('createMockInternalAccount', () => { - it('create a mock internal account', () => { - const account = createMockInternalAccount(); - expect(account).toStrictEqual({ - id: expect.any(String), - address: expect.any(String), - type: expect.any(String), - options: expect.any(Object), - methods: expect.any(Array), - metadata: { - name: expect.any(String), - keyring: { type: expect.any(String) }, - importTime: expect.any(Number), - lastSelected: expect.any(Number), - snap: undefined, - }, - }); - }); - - it('create a mock internal account with custom values', () => { - const customSnap = { - id: '1', - enabled: true, - name: 'Snap 1', - }; - const account = createMockInternalAccount({ - id: '1', - address: '0x123', - type: EthAccountType.Erc4337, - name: 'Custom Account', - snap: customSnap, - }); - expect(account).toStrictEqual({ - id: '1', - address: '0x123', - type: EthAccountType.Erc4337, - options: expect.any(Object), - methods: expect.any(Array), - metadata: { - name: 'Custom Account', - keyring: { type: expect.any(String) }, - importTime: expect.any(Number), - lastSelected: expect.any(Number), - snap: customSnap, - }, - }); - }); - - it('create a BTC account', () => { - const account = createMockInternalAccount({ type: BtcAccountType.P2wpkh }); - expect(account).toStrictEqual({ - id: expect.any(String), - address: expect.any(String), - type: BtcAccountType.P2wpkh, - options: expect.any(Object), - methods: expect.any(Array), - metadata: { - name: expect.any(String), - keyring: { type: expect.any(String) }, - importTime: expect.any(Number), - lastSelected: expect.any(Number), - snap: undefined, - }, - }); - }); - - it('create a Solana account', () => { - const account = createMockInternalAccount({ - type: SolAccountType.DataAccount, - }); - expect(account).toStrictEqual({ - id: expect.any(String), - address: expect.any(String), - type: SolAccountType.DataAccount, - options: expect.any(Object), - methods: expect.any(Array), - metadata: { - name: expect.any(String), - keyring: { type: expect.any(String) }, - importTime: expect.any(Number), - lastSelected: expect.any(Number), - snap: undefined, - }, - }); - }); - - it('will throw if an unknown account type was passed', () => { - // @ts-expect-error testing unknown account type - expect(() => createMockInternalAccount({ type: 'unknown' })).toThrow( - 'Unknown account type: unknown', - ); - }); -}); diff --git a/packages/multichain-network-controller/src/test/utils.ts b/packages/multichain-network-controller/test/utils.ts similarity index 100% rename from packages/multichain-network-controller/src/test/utils.ts rename to packages/multichain-network-controller/test/utils.ts diff --git a/packages/multichain-network-controller/tsconfig.json b/packages/multichain-network-controller/tsconfig.json index e5ff777b642..9a11d6c7f89 100644 --- a/packages/multichain-network-controller/tsconfig.json +++ b/packages/multichain-network-controller/tsconfig.json @@ -8,5 +8,5 @@ { "path": "../network-controller" }, { "path": "../keyring-controller" } ], - "include": ["../../types", "./src", "./tests"] + "include": ["../../types", "./src", "./tests", "test"] } From d2185e9e7dd073182741158876a25c70ccc47d5a Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 11 Feb 2025 11:08:47 -0800 Subject: [PATCH 52/69] Change evmClientId to evmNetworkClientId --- .../src/AccountsController.test.ts | 10 +++++----- .../src/AccountsController.ts | 10 +++++----- .../src/MultichainNetworkController.test.ts | 18 +++++++++--------- .../src/MultichainNetworkController.ts | 18 +++++++++--------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 6c7fc6cd145..7f071a17479 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -341,7 +341,7 @@ function setupAccountsController({ AccountsControllerEvents | AllowedEvents >; triggerMultichainNetworkChange: (args: { - evmClientId?: string; + evmNetworkClientId?: string; nonEvmChainId?: CaipChainId; }) => void; } { @@ -354,14 +354,14 @@ function setupAccountsController({ }); const triggerMultichainNetworkChange = ({ - evmClientId, + evmNetworkClientId, nonEvmChainId, }: { - evmClientId?: string; + evmNetworkClientId?: string; nonEvmChainId?: CaipChainId; }) => { messenger.publish('MultichainNetworkController:networkDidChange', { - evmClientId, + evmNetworkClientId, nonEvmChainId, }); }; @@ -1603,7 +1603,7 @@ describe('AccountsController', () => { // Triggered from network switch to Bitcoin mainnet triggerMultichainNetworkChange({ - evmClientId: EthScope.Mainnet, + evmNetworkClientId: EthScope.Mainnet, }); // ETH mainnet account is now selected diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index 13b77a3a33b..bcd6c06a2ec 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -186,7 +186,7 @@ type MultichainNetworkControllerNetworkDidChangeEvent = { type: `MultichainNetworkController:networkDidChange`; payload: [ { - evmClientId?: string; + evmNetworkClientId?: string; nonEvmChainId?: CaipChainId; }, ]; @@ -1139,14 +1139,14 @@ export class AccountsController extends BaseController< * Handles the change in multichain network by updating the selected account. * * @param args - The arguments to handle the multichain network change. - * @param args.evmClientId - The ID of the EVM client. + * @param args.evmNetworkClientId - The ID of the EVM client. * @param args.nonEvmChainId - The CAIP2 of the non-EVM chain. */ readonly #handleMultichainNetworkChange = ({ - evmClientId, + evmNetworkClientId, nonEvmChainId, }: { - evmClientId?: string; + evmNetworkClientId?: string; nonEvmChainId?: CaipChainId; }) => { let accountId: string; @@ -1157,7 +1157,7 @@ export class AccountsController extends BaseController< this.getSelectedMultichainAccount(nonEvmChainId); // @ts-expect-error - This should never be undefined, otherwise it's a bug that should be handled accountId = lastSelectedNonEvmAccount.id; - } else if (evmClientId) { + } else if (evmNetworkClientId) { // Update selected account to evm account const lastSelectedEvmAccount = this.getSelectedAccount(); accountId = lastSelectedEvmAccount.id; diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index 35de861883f..658e4fdd358 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -154,7 +154,7 @@ describe('MultichainNetworkController', () => { const { controller } = setupController(); await expect( controller.setActiveNetwork({ - evmClientId: InfuraNetworkType.mainnet, + evmNetworkClientId: InfuraNetworkType.mainnet, nonEvmChainId: SolScope.Mainnet, }), ).rejects.toThrow('Cannot set both EVM and non-EVM networks!'); @@ -169,11 +169,11 @@ describe('MultichainNetworkController', () => { ).rejects.toThrow('Non-EVM chain ID is required!'); }); - it('should throw error if evmClientId is an empty string', async () => { + it('should throw error if evmNetworkClientId is an empty string', async () => { const { controller } = setupController(); await expect( controller.setActiveNetwork({ - evmClientId: '', + evmNetworkClientId: '', }), ).rejects.toThrow('EVM client ID is required!'); }); @@ -249,7 +249,7 @@ describe('MultichainNetworkController', () => { }); await controller.setActiveNetwork({ - evmClientId: selectedNetworkClientId, + evmNetworkClientId: selectedNetworkClientId, }); // Check that EVM network is selected @@ -258,7 +258,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( 'MultichainNetworkController:networkDidChange', - { evmClientId: selectedNetworkClientId }, + { evmNetworkClientId: selectedNetworkClientId }, ); // Check that NetworkController:setActiveNetwork was not called @@ -271,10 +271,10 @@ describe('MultichainNetworkController', () => { selectedNetworkClientId: InfuraNetworkType.mainnet, })), }); - const evmClientId = 'linea'; + const evmNetworkClientId = 'linea'; await controller.setActiveNetwork({ - evmClientId, + evmNetworkClientId, }); // Check that EVM network is selected @@ -283,11 +283,11 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( 'MultichainNetworkController:networkDidChange', - { evmClientId }, + { evmNetworkClientId }, ); // Check that NetworkController:setActiveNetwork was not called - expect(mockSetActiveNetwork).toHaveBeenCalledWith(evmClientId); + expect(mockSetActiveNetwork).toHaveBeenCalledWith(evmNetworkClientId); }); }); diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index ff59c20feeb..56cee860d08 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -137,7 +137,7 @@ export type MultichainNetworkControllerNetworkDidChangeEvent = { type: `${typeof controllerName}:networkDidChange`; payload: [ { - evmClientId?: string; + evmNetworkClientId?: string; nonEvmChainId?: CaipChainId; }, ]; @@ -242,18 +242,18 @@ export class MultichainNetworkController extends BaseController< * Handles switching between EVM and non-EVM networks. * * @param args - The arguments to set the active network. - * @param args.evmClientId - The client ID of the EVM network to set active. + * @param args.evmNetworkClientId - The client ID of the EVM network to set active. * @param args.nonEvmChainId - The chain ID of the non-EVM network to set active. */ async setActiveNetwork({ - evmClientId, + evmNetworkClientId, nonEvmChainId, }: { - evmClientId?: string; + evmNetworkClientId?: string; nonEvmChainId?: CaipChainId; }): Promise { // Throw an error if both EVM and non-EVM networks are set - if (evmClientId !== undefined && nonEvmChainId !== undefined) { + if (evmNetworkClientId !== undefined && nonEvmChainId !== undefined) { throw new Error('Cannot set both EVM and non-EVM networks!'); } @@ -303,7 +303,7 @@ export class MultichainNetworkController extends BaseController< } // Handle EVM networks - if (!evmClientId) { + if (!evmNetworkClientId) { throw new Error('EVM client ID is required!'); } @@ -311,7 +311,7 @@ export class MultichainNetworkController extends BaseController< this.messagingSystem.publish( 'MultichainNetworkController:networkDidChange', { - evmClientId, + evmNetworkClientId, }, ); @@ -325,7 +325,7 @@ export class MultichainNetworkController extends BaseController< 'NetworkController:getState', ); - if (evmClientId === selectedNetworkClientId) { + if (evmNetworkClientId === selectedNetworkClientId) { // EVM network is already selected, no need to update NetworkController return; } @@ -333,7 +333,7 @@ export class MultichainNetworkController extends BaseController< // Update evm active network await this.messagingSystem.call( 'NetworkController:setActiveNetwork', - evmClientId, + evmNetworkClientId, ); } From c683df35efea413c30ff8265efabc30ff13b5325 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 11 Feb 2025 12:28:39 -0800 Subject: [PATCH 53/69] use overload for setActiveNetwork --- .../src/MultichainNetworkController.test.ts | 66 ++------ .../src/MultichainNetworkController.ts | 157 +++++++++--------- .../src/constants.ts | 18 +- 3 files changed, 98 insertions(+), 143 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index 658e4fdd358..1cdff0cd2af 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -14,7 +14,7 @@ import type { } from '@metamask/network-controller'; import type { CaipChainId } from '@metamask/utils'; -import { MULTICHAIN_NETWORK_CONFIGURATIONS } from './constants'; +import { AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS } from './constants'; import { type AllowedActions, type AllowedEvents, @@ -102,8 +102,7 @@ function setupController({ state: { selectedMultichainNetworkChainId: SolScope.Mainnet, multichainNetworkConfigurationsByChainId: - MULTICHAIN_NETWORK_CONFIGURATIONS, - multichainNetworksMetadata: {}, + AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS, isEvmSelected: true, ...options.state, }, @@ -150,42 +149,12 @@ describe('MultichainNetworkController', () => { }); describe('setActiveNetwork', () => { - it('should throw error when both EVM and non-EVM networks are provided', async () => { - const { controller } = setupController(); - await expect( - controller.setActiveNetwork({ - evmNetworkClientId: InfuraNetworkType.mainnet, - nonEvmChainId: SolScope.Mainnet, - }), - ).rejects.toThrow('Cannot set both EVM and non-EVM networks!'); - }); - - it('should throw error if nonEvmChainId is an empty string', async () => { - const { controller } = setupController(); - await expect( - controller.setActiveNetwork({ - nonEvmChainId: '' as CaipChainId, - }), - ).rejects.toThrow('Non-EVM chain ID is required!'); - }); - - it('should throw error if evmNetworkClientId is an empty string', async () => { - const { controller } = setupController(); - await expect( - controller.setActiveNetwork({ - evmNetworkClientId: '', - }), - ).rejects.toThrow('EVM client ID is required!'); - }); - 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({ - nonEvmChainId: SolScope.Mainnet, - }); + await controller.setActiveNetwork(SolScope.Mainnet); // Check that the Solana is now the selected network expect(controller.state.selectedMultichainNetworkChainId).toBe( @@ -203,13 +172,20 @@ describe('MultichainNetworkController', () => { }); it('should throw error when unsupported non-EVM chainId is provided', async () => { - const { controller } = setupController(); - const unsupportedChainId = 'non-existent-chain:0'; + // Only support Solana + const { controller } = setupController({ + options: { + state: { + multichainNetworkConfigurationsByChainId: { + ...AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS[SolScope.Mainnet], + }, + }, + }, + }); + // Switch to Bitcoin, which for testing purposes is not supported await expect( - controller.setActiveNetwork({ - nonEvmChainId: unsupportedChainId, - }), + controller.setActiveNetwork(BtcScope.Mainnet), ).rejects.toThrow('Non-EVM chain ID is not supported!'); }); @@ -220,9 +196,7 @@ describe('MultichainNetworkController', () => { }); // Set active network to Bitcoin - await controller.setActiveNetwork({ - nonEvmChainId: BtcScope.Mainnet, - }); + await controller.setActiveNetwork(BtcScope.Mainnet); // Check that the Solana is now the selected network expect(controller.state.selectedMultichainNetworkChainId).toBe( @@ -248,9 +222,7 @@ describe('MultichainNetworkController', () => { })), }); - await controller.setActiveNetwork({ - evmNetworkClientId: selectedNetworkClientId, - }); + await controller.setActiveNetwork(selectedNetworkClientId); // Check that EVM network is selected expect(controller.state.isEvmSelected).toBe(true); @@ -273,9 +245,7 @@ describe('MultichainNetworkController', () => { }); const evmNetworkClientId = 'linea'; - await controller.setActiveNetwork({ - evmNetworkClientId, - }); + await controller.setActiveNetwork(evmNetworkClientId); // Check that EVM network is selected expect(controller.state.isEvmSelected).toBe(true); diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 56cee860d08..d4741fcd801 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -11,10 +11,16 @@ import type { NetworkStatus, NetworkControllerSetActiveNetworkAction, NetworkControllerGetStateAction, + NetworkClientId, } from '@metamask/network-controller'; -import type { CaipAssetType, CaipChainId } from '@metamask/utils'; +import { + isCaipChainId, + type CaipAssetType, + type CaipChainId, +} from '@metamask/utils'; import { getChainIdForNonEvmAddress } from './utils'; +import { AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS } from './constants'; const controllerName = 'MultichainNetworkController'; @@ -28,7 +34,6 @@ export type CommonNetworkConfiguration = { * EVM network flag. */ isEvm: boolean; - /** * The block explorers of the network. */ @@ -36,17 +41,14 @@ export type CommonNetworkConfiguration = { urls: string[]; defaultIndex: number; }; - /** * The chain ID of the network. */ chainId: CaipChainId; - /** * The name of the network. */ name: string; - /** * The native asset type of the network. */ @@ -88,10 +90,6 @@ export type MultichainNetworkControllerState = { * The chain ID of the selected network. */ selectedMultichainNetworkChainId: CaipChainId; - /** - * The metadata of the networks. - */ - multichainNetworksMetadata: Record; /** * Whether EVM or non-EVM network is selected */ @@ -105,9 +103,9 @@ export type MultichainNetworkControllerState = { */ export const getDefaultMultichainNetworkControllerState = (): MultichainNetworkControllerState => ({ - multichainNetworkConfigurationsByChainId: {}, + multichainNetworkConfigurationsByChainId: + AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS, selectedMultichainNetworkChainId: SolScope.Mainnet, - multichainNetworksMetadata: {}, isEvmSelected: true, }); @@ -204,7 +202,6 @@ export type MultichainNetworkControllerMessenger = const multichainNetworkControllerMetadata = { multichainNetworkConfigurationsByChainId: { persist: true, anonymous: true }, selectedMultichainNetworkChainId: { persist: true, anonymous: true }, - multichainNetworksMetadata: { persist: true, anonymous: true }, isEvmSelected: { persist: true, anonymous: true }, } satisfies StateMetadata; @@ -239,102 +236,98 @@ export class MultichainNetworkController extends BaseController< } /** - * Handles switching between EVM and non-EVM networks. + * Sets the active EVM network. * - * @param args - The arguments to set the active network. - * @param args.evmNetworkClientId - The client ID of the EVM network to set active. - * @param args.nonEvmChainId - The chain ID of the non-EVM network to set active. + * @param id - The client ID of the EVM network to set active. */ - async setActiveNetwork({ - evmNetworkClientId, - nonEvmChainId, - }: { - evmNetworkClientId?: string; - nonEvmChainId?: CaipChainId; - }): Promise { - // Throw an error if both EVM and non-EVM networks are set - if (evmNetworkClientId !== undefined && nonEvmChainId !== undefined) { - throw new Error('Cannot set both EVM and non-EVM networks!'); - } + async #setActiveEvmNetwork(id: NetworkClientId): Promise { + // Notify listeners that setActiveNetwork was called + this.messagingSystem.publish( + 'MultichainNetworkController:networkDidChange', + { + evmNetworkClientId: id, + }, + ); - // Handle non-EVM networks - if (nonEvmChainId !== undefined) { - // Handle EVM networks - if (nonEvmChainId.length === 0) { - throw new Error('Non-EVM chain ID is required!'); - } + // Indicate that the non-EVM network is not selected + this.update((state) => { + state.isEvmSelected = true; + }); - // Prevent setting same network - if (nonEvmChainId === this.state.selectedMultichainNetworkChainId) { - // 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', - { nonEvmChainId }, - ); - return; - } + // Prevent setting same network + const { selectedNetworkClientId } = this.messagingSystem.call( + 'NetworkController:getState', + ); - // Check if the non-EVM chain ID is supported - if ( - !Object.keys( - this.state.multichainNetworkConfigurationsByChainId, - ).includes(nonEvmChainId) - ) { - throw new Error('Non-EVM chain ID is not supported!'); - } + if (id === selectedNetworkClientId) { + // EVM network is already selected, no need to update NetworkController + return; + } - // Notify listeners that setActiveNetwork was called - this.messagingSystem.publish( - 'MultichainNetworkController:networkDidChange', - { nonEvmChainId }, - ); + // 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: CaipChainId): void { + // Prevent setting same network + if (id === this.state.selectedMultichainNetworkChainId) { + // Indicate that the non-EVM network is selected this.update((state) => { - state.selectedMultichainNetworkChainId = nonEvmChainId; state.isEvmSelected = false; }); + // Notify listeners that setActiveNetwork was called + this.messagingSystem.publish( + 'MultichainNetworkController:networkDidChange', + { nonEvmChainId: id }, + ); return; } - // Handle EVM networks - if (!evmNetworkClientId) { - throw new Error('EVM client ID is required!'); + // Check if the non-EVM chain ID is supported + if ( + !Object.keys( + this.state.multichainNetworkConfigurationsByChainId, + ).includes(id) + ) { + throw new Error('Non-EVM chain ID is not supported!'); } // Notify listeners that setActiveNetwork was called this.messagingSystem.publish( 'MultichainNetworkController:networkDidChange', - { - evmNetworkClientId, - }, + { nonEvmChainId: id }, ); - // Indicate that the non-EVM network is not selected this.update((state) => { - state.isEvmSelected = true; + state.selectedMultichainNetworkChainId = id; + state.isEvmSelected = false; }); + } - // Prevent setting same network - const { selectedNetworkClientId } = this.messagingSystem.call( - 'NetworkController:getState', - ); + /** + * Switches to a non-EVM network. + * + * @param nonEvmChainId - The chain ID of the non-EVM network to set active. + */ + async setActiveNetwork(nonEvmChainId: CaipChainId): Promise; - if (evmNetworkClientId === selectedNetworkClientId) { - // EVM network is already selected, no need to update NetworkController - return; - } + /** + * Switches to an EVM network. + * + * @param evmNetworkClientId - The client ID of the EVM network to set active. + */ + async setActiveNetwork(evmNetworkClientId: NetworkClientId): Promise; - // Update evm active network - await this.messagingSystem.call( - 'NetworkController:setActiveNetwork', - evmNetworkClientId, - ); + async setActiveNetwork(id: CaipChainId | NetworkClientId): Promise { + isCaipChainId(id) + ? this.#setActiveNonEvmNetwork(id) + : await this.#setActiveEvmNetwork(id); } /** diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index f5a0f3d7def..ca7567f63f3 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -6,39 +6,31 @@ import type { MultichainNetworkMetadata, } from './MultichainNetworkController'; -export const btcNativeAsset = `${BtcScope.Mainnet}/slip44:0`; -export const solNativeAsset = `${SolScope.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`; +export const BTC_NATIVE_ASSET = `${BtcScope.Mainnet}/slip44:0`; +export const SOL_NATIVE_ASSET = `${SolScope.Mainnet}/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`; -export const MULTICHAIN_NETWORK_CONFIGURATIONS: Record< +export const AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS: Record< string, MultichainNetworkConfiguration > = { [BtcScope.Mainnet]: { chainId: BtcScope.Mainnet, - name: 'Bitcoin Mainnet', - blockExplorers: { urls: ['https://blockstream.info'], defaultIndex: 0, }, - - nativeCurrency: btcNativeAsset, - + nativeCurrency: BTC_NATIVE_ASSET, isEvm: false, }, [SolScope.Mainnet]: { chainId: SolScope.Mainnet, - name: 'Solana Mainnet', - blockExplorers: { urls: ['https://explorer.solana.com'], defaultIndex: 0, }, - - nativeCurrency: solNativeAsset, - + nativeCurrency: SOL_NATIVE_ASSET, isEvm: false, }, }; From dc8b5793170acfcb2f2abf64cfb0ffb385161f0b Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 11 Feb 2025 17:24:50 -0800 Subject: [PATCH 54/69] Update root package json and README --- README.md | 4 ++++ tsconfig.json | 1 + yarn.lock | 1 + 3 files changed, 6 insertions(+) diff --git a/README.md b/README.md index b53081a89ba..b7ebbec2805 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"]); @@ -143,6 +145,8 @@ linkStyle default opacity:0.5 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; 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 7ce5bce27d5..c3e4d22a15f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2330,6 +2330,7 @@ __metadata: webextension-polyfill: "npm:^0.12.0" peerDependencies: "@metamask/keyring-controller": ^19.0.0 + "@metamask/network-controller": ^22.1.1 "@metamask/providers": ^18.1.0 "@metamask/snaps-controllers": ^9.19.0 webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 From 60c52ee57fc41aa5baac76e2cd52a104f569b554 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 11 Feb 2025 17:25:08 -0800 Subject: [PATCH 55/69] Clean up multichain network controller --- .../package.json | 1 - .../src/MultichainNetworkController.test.ts | 65 ++-- .../src/MultichainNetworkController.ts | 279 +++--------------- .../src/constants.ts | 51 +++- .../src/index.ts | 2 + .../src/types.ts | 176 +++++++++++ .../src/utils.test.ts | 45 +++ .../src/utils.ts | 23 +- 8 files changed, 372 insertions(+), 270 deletions(-) create mode 100644 packages/multichain-network-controller/src/types.ts create mode 100644 packages/multichain-network-controller/src/utils.test.ts diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index 589c8efe6e9..44b98736aca 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -1,7 +1,6 @@ { "name": "@metamask/multichain-network-controller", "version": "0.0.0", - "private": true, "description": "Multichain network controller", "keywords": [ "MetaMask", diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index 1cdff0cd2af..747294f551f 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -7,22 +7,25 @@ import { BtcAccountType, SolAccountType, type KeyringAccountType, + CaipChainId, } from '@metamask/keyring-api'; import type { NetworkControllerGetStateAction, NetworkControllerSetActiveNetworkAction, } from '@metamask/network-controller'; -import type { CaipChainId } from '@metamask/utils'; -import { AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS } from './constants'; +import { + AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS, + DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE, +} from './constants'; import { type AllowedActions, type AllowedEvents, type MultichainNetworkControllerAllowedActions, type MultichainNetworkControllerAllowedEvents, - getDefaultMultichainNetworkControllerState, - MultichainNetworkController, -} from './MultichainNetworkController'; + type MultichainNetworkControllerState, +} from './types'; +import { MultichainNetworkController } from './MultichainNetworkController'; import { createMockInternalAccount } from '../test/utils'; const controllerName = 'MultichainNetworkController'; @@ -101,8 +104,8 @@ function setupController({ messenger: options.messenger || controllerMessenger, state: { selectedMultichainNetworkChainId: SolScope.Mainnet, - multichainNetworkConfigurationsByChainId: - AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS, + // multichainNetworkConfigurationsByChainId: + // DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE, isEvmSelected: true, ...options.state, }, @@ -140,10 +143,10 @@ describe('MultichainNetworkController', () => { describe('constructor', () => { it('should set default state', () => { const { controller } = setupController({ - options: { state: getDefaultMultichainNetworkControllerState() }, + options: { state: DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE }, }); expect(controller.state).toStrictEqual( - getDefaultMultichainNetworkControllerState(), + DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE, ); }); }); @@ -167,26 +170,36 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( 'MultichainNetworkController:networkDidChange', - { nonEvmChainId: SolScope.Mainnet }, + SolScope.Mainnet, ); }); it('should throw error when unsupported non-EVM chainId is provided', async () => { - // Only support Solana - const { controller } = setupController({ - options: { - state: { - multichainNetworkConfigurationsByChainId: { - ...AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS[SolScope.Mainnet], - }, - }, - }, - }); + const { controller } = setupController(); + const unsupportedChainId = 'eip155:1' as CaipChainId; - // Switch to Bitcoin, which for testing purposes is not supported await expect( - controller.setActiveNetwork(BtcScope.Mainnet), - ).rejects.toThrow('Non-EVM chain ID is not supported!'); + 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 () => { @@ -209,7 +222,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( 'MultichainNetworkController:networkDidChange', - { nonEvmChainId: BtcScope.Mainnet }, + BtcScope.Mainnet, ); }); @@ -230,7 +243,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( 'MultichainNetworkController:networkDidChange', - { evmNetworkClientId: selectedNetworkClientId }, + selectedNetworkClientId, ); // Check that NetworkController:setActiveNetwork was not called @@ -253,7 +266,7 @@ describe('MultichainNetworkController', () => { // Check that the messenger published the correct event expect(publishSpy).toHaveBeenCalledWith( 'MultichainNetworkController:networkDidChange', - { evmNetworkClientId }, + evmNetworkClientId, ); // Check that NetworkController:setActiveNetwork was not called diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index d4741fcd801..95e7d302e4d 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -1,216 +1,30 @@ -import { - BaseController, - type StateMetadata, - type ControllerGetStateAction, - type ControllerStateChangeEvent, - type RestrictedControllerMessenger, -} from '@metamask/base-controller'; -import { isEvmAccountType, SolScope } from '@metamask/keyring-api'; +import { BaseController } from '@metamask/base-controller'; +import { isEvmAccountType } from '@metamask/keyring-api'; import type { InternalAccount } from '@metamask/keyring-internal-api'; -import type { - NetworkStatus, - NetworkControllerSetActiveNetworkAction, - NetworkControllerGetStateAction, - NetworkClientId, -} from '@metamask/network-controller'; -import { - isCaipChainId, - type CaipAssetType, - type CaipChainId, -} from '@metamask/utils'; - -import { getChainIdForNonEvmAddress } from './utils'; -import { AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS } from './constants'; - -const controllerName = 'MultichainNetworkController'; - -export type MultichainNetworkMetadata = { - features: string[]; - status: NetworkStatus; -}; - -export type CommonNetworkConfiguration = { - /** - * EVM network flag. - */ - isEvm: boolean; - /** - * The block explorers of the network. - */ - blockExplorers: { - urls: string[]; - defaultIndex: number; - }; - /** - * The chain ID of the network. - */ - chainId: CaipChainId; - /** - * The name of the network. - */ - name: string; - /** - * The native asset type of the network. - */ - nativeCurrency: CaipAssetType; -}; - -export type NonEvmNetworkConfiguration = CommonNetworkConfiguration & { - isEvm: false; -}; - -export type EvmNetworkConfiguration = CommonNetworkConfiguration & { - isEvm: true; - - /** - * The RPC endpoints of the network. - */ - rpcEndpoints: { - urls: string[]; - defaultIndex: 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: CaipChainId; - /** - * Whether EVM or non-EVM network is selected - */ - isEvmSelected: boolean; -}; - -/** - * 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, - }); - -/** - * Returns the state of the {@link MultichainNetworkController}. - */ -export type MultichainNetworkControllerGetStateAction = - ControllerGetStateAction< - typeof controllerName, - MultichainNetworkControllerState - >; +import type { NetworkClientId } from '@metamask/network-controller'; -export type MultichainNetworkControllerSetActiveNetworkAction = { - type: `${typeof controllerName}:setActiveNetwork`; - handler: MultichainNetworkController['setActiveNetwork']; -}; - -/** - * Event emitted when the state of the {@link MultichainNetworkController} changes. - */ -export type MultichainNetworkControllerStateChange = ControllerStateChangeEvent< - typeof controllerName, - MultichainNetworkControllerState ->; - -export type MultichainNetworkControllerNetworkDidChangeEvent = { - type: `${typeof controllerName}:networkDidChange`; - payload: [ - { - evmNetworkClientId?: string; - nonEvmChainId?: CaipChainId; - }, - ]; -}; - -/** - * 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 -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 = - RestrictedControllerMessenger< - typeof controllerName, - MultichainNetworkControllerAllowedActions, - MultichainNetworkControllerAllowedEvents, - AllowedActions['type'], - AllowedEvents['type'] - >; - -/** - * {@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. - */ -const multichainNetworkControllerMetadata = { - multichainNetworkConfigurationsByChainId: { persist: true, anonymous: true }, - selectedMultichainNetworkChainId: { persist: true, anonymous: true }, - isEvmSelected: { persist: true, anonymous: true }, -} satisfies StateMetadata; +import { + checkIfSupportedCaipChainId, + getChainIdForNonEvmAddress, +} from './utils'; +import { + MULTICHAIN_NETWORK_CONTROLLER_NAME, + type MultichainNetworkControllerState, + type MultichainNetworkControllerMessenger, + type SupportedCaipChainId, +} from './types'; +import { + MULTICHAIN_NETWORK_CONTROLLER_METADATA, + DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE, +} from './constants'; +import { isCaipChainId } from '@metamask/utils'; /** * The MultichainNetworkController is responsible for fetching and caching account * balances. */ export class MultichainNetworkController extends BaseController< - typeof controllerName, + typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, MultichainNetworkControllerState, MultichainNetworkControllerMessenger > { @@ -219,14 +33,17 @@ export class MultichainNetworkController extends BaseController< state, }: { messenger: MultichainNetworkControllerMessenger; - state: Partial; + state?: Omit< + Partial, + 'multichainNetworkConfigurationsByChainId' + >; }) { super({ messenger, - name: controllerName, - metadata: multichainNetworkControllerMetadata, + name: MULTICHAIN_NETWORK_CONTROLLER_NAME, + metadata: MULTICHAIN_NETWORK_CONTROLLER_METADATA, state: { - ...getDefaultMultichainNetworkControllerState(), + ...DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE, ...state, }, }); @@ -244,9 +61,7 @@ export class MultichainNetworkController extends BaseController< // Notify listeners that setActiveNetwork was called this.messagingSystem.publish( 'MultichainNetworkController:networkDidChange', - { - evmNetworkClientId: id, - }, + id, ); // Indicate that the non-EVM network is not selected @@ -273,9 +88,13 @@ export class MultichainNetworkController extends BaseController< * * @param id - The chain ID of the non-EVM network to set active. */ - #setActiveNonEvmNetwork(id: CaipChainId): void { - // Prevent setting same network + #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; @@ -284,24 +103,14 @@ export class MultichainNetworkController extends BaseController< // Notify listeners that setActiveNetwork was called this.messagingSystem.publish( 'MultichainNetworkController:networkDidChange', - { nonEvmChainId: id }, + id, ); - return; - } - - // Check if the non-EVM chain ID is supported - if ( - !Object.keys( - this.state.multichainNetworkConfigurationsByChainId, - ).includes(id) - ) { - throw new Error('Non-EVM chain ID is not supported!'); } // Notify listeners that setActiveNetwork was called this.messagingSystem.publish( 'MultichainNetworkController:networkDidChange', - { nonEvmChainId: id }, + id, ); this.update((state) => { @@ -315,7 +124,7 @@ export class MultichainNetworkController extends BaseController< * * @param nonEvmChainId - The chain ID of the non-EVM network to set active. */ - async setActiveNetwork(nonEvmChainId: CaipChainId): Promise; + async setActiveNetwork(nonEvmChainId: SupportedCaipChainId): Promise; /** * Switches to an EVM network. @@ -324,10 +133,18 @@ export class MultichainNetworkController extends BaseController< */ async setActiveNetwork(evmNetworkClientId: NetworkClientId): Promise; - async setActiveNetwork(id: CaipChainId | NetworkClientId): Promise { - isCaipChainId(id) - ? this.#setActiveNonEvmNetwork(id) - : await this.#setActiveEvmNetwork(id); + async setActiveNetwork( + id: SupportedCaipChainId | NetworkClientId, + ): Promise { + if (isCaipChainId(id)) { + const isSupportedCaipChainId = checkIfSupportedCaipChainId(id); + if (!isSupportedCaipChainId) { + throw new Error(`Unsupported Caip chain ID: ${id}`); + } + this.#setActiveNonEvmNetwork(id); + } else { + this.#setActiveEvmNetwork(id); + } } /** diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index ca7567f63f3..18200228517 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -1,40 +1,45 @@ import { BtcScope, SolScope } from '@metamask/keyring-api'; import { NetworkStatus } from '@metamask/network-controller'; +import { type StateMetadata } from '@metamask/base-controller'; import type { MultichainNetworkConfiguration, + MultichainNetworkControllerState, MultichainNetworkMetadata, -} from './MultichainNetworkController'; + 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< - string, + SupportedCaipChainId, MultichainNetworkConfiguration > = { [BtcScope.Mainnet]: { chainId: BtcScope.Mainnet, name: 'Bitcoin Mainnet', - blockExplorers: { - urls: ['https://blockstream.info'], - defaultIndex: 0, - }, + blockExplorerUrls: ['https://blockstream.info'], + defaultBlockExplorerUrlIndex: 0, nativeCurrency: BTC_NATIVE_ASSET, isEvm: false, }, [SolScope.Mainnet]: { chainId: SolScope.Mainnet, name: 'Solana Mainnet', - blockExplorers: { - urls: ['https://explorer.solana.com'], - defaultIndex: 0, - }, + blockExplorerUrls: ['https://explorer.solana.com'], + defaultBlockExplorerUrlIndex: 0, nativeCurrency: SOL_NATIVE_ASSET, isEvm: false, }, }; +/** + * Metadata for the supported networks. + */ export const NETWORKS_METADATA: Record = { [BtcScope.Mainnet]: { features: [], @@ -45,3 +50,29 @@ export const NETWORKS_METADATA: Record = { status: NetworkStatus.Available, }, }; + +/** + * Default state of the {@link MultichainNetworkController}. + * + * @returns The default state of the {@link MultichainNetworkController}. + */ +export const DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE: 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 index 79145dc7240..e2329c6e68b 100644 --- a/packages/multichain-network-controller/src/index.ts +++ b/packages/multichain-network-controller/src/index.ts @@ -1,2 +1,4 @@ export * from './MultichainNetworkController'; export * from './constants'; +export * from './types'; +export { isSupportedCaipChainId } 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..167a8c7d821 --- /dev/null +++ b/packages/multichain-network-controller/src/types.ts @@ -0,0 +1,176 @@ +import { + type ControllerGetStateAction, + type ControllerStateChangeEvent, + type RestrictedControllerMessenger, +} from '@metamask/base-controller'; +import { BtcScope, 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 block explorers of the network. + */ + blockExplorerUrls: string[]; + /** + * The index of the default block explorer URL. + */ + defaultBlockExplorerUrlIndex: number; + /** + * The chain ID of the network. + */ + chainId: SupportedCaipChainId; + /** + * The name of the network. + */ + name: string; + /** + * The native asset type of the network. + */ + nativeCurrency: CaipAssetType; +}; + +export type NonEvmNetworkConfiguration = CommonNetworkConfiguration & { + isEvm: false; +}; + +// 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 & { +// isEvm: true; +// /** +// * The RPC endpoints of the network. +// */ +// rpcEndpoints: string[]; +// /** +// * The index of the default RPC endpoint. +// */ +// defaultRpcEndpointIndex: 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< + SupportedCaipChainId, + 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 + >; + +type SetActiveNetworkMethod = { + (id: SupportedCaipChainId): Promise; + (id: 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 = + RestrictedControllerMessenger< + 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..856ca69f82b --- /dev/null +++ b/packages/multichain-network-controller/src/utils.test.ts @@ -0,0 +1,45 @@ +import { BtcScope, CaipChainId, SolScope } from '@metamask/keyring-api'; + +import { + getChainIdForNonEvmAddress, + checkIfSupportedCaipChainId, +} from './utils'; +import { SupportedCaipChainId } from './types'; + +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); + }); + + it('provides type guard functionality', () => { + const chainId = SolScope.Mainnet; + if (checkIfSupportedCaipChainId(chainId)) { + // TypeScript should recognize chainId as SupportedCaipChainId + const supportedChainId: SupportedCaipChainId = chainId; + expect(supportedChainId).toBe(chainId); + } + }); + }); +}); diff --git a/packages/multichain-network-controller/src/utils.ts b/packages/multichain-network-controller/src/utils.ts index b58073c3447..869571afe75 100644 --- a/packages/multichain-network-controller/src/utils.ts +++ b/packages/multichain-network-controller/src/utils.ts @@ -1,16 +1,35 @@ import { BtcScope, SolScope } from '@metamask/keyring-api'; -import type { CaipChainId } from '@metamask/utils'; import { isAddress as isSolanaAddress } from '@solana/addresses'; + +import { SupportedCaipChainId } from './types'; +import { CaipChainId, isCaipChainId } from '@metamask/utils'; +import { AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS } from './constants'; + /** * 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): CaipChainId { +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); +} From 525678880190ada92f196589b60ecdd97209d945 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 11 Feb 2025 18:08:58 -0800 Subject: [PATCH 56/69] Remove unused imports --- .../src/MultichainNetworkController.test.ts | 13 ++++--------- .../src/MultichainNetworkController.ts | 8 ++++---- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index 747294f551f..a9ad3c7a48e 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -14,22 +14,17 @@ import type { NetworkControllerSetActiveNetworkAction, } from '@metamask/network-controller'; -import { - AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS, - DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE, -} from './constants'; +import { DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE } from './constants'; import { type AllowedActions, type AllowedEvents, type MultichainNetworkControllerAllowedActions, type MultichainNetworkControllerAllowedEvents, - type MultichainNetworkControllerState, + MULTICHAIN_NETWORK_CONTROLLER_NAME, } from './types'; import { MultichainNetworkController } from './MultichainNetworkController'; import { createMockInternalAccount } from '../test/utils'; -const controllerName = 'MultichainNetworkController'; - /** * Setup a test controller instance. * @@ -87,11 +82,11 @@ function setupController({ ); const controllerMessenger = messenger.getRestricted< - typeof controllerName, + typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, AllowedActions['type'], AllowedEvents['type'] >({ - name: controllerName, + name: MULTICHAIN_NETWORK_CONTROLLER_NAME, allowedActions: [ 'NetworkController:setActiveNetwork', 'NetworkController:getState', diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 95e7d302e4d..6a9ed4a6d38 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -122,16 +122,16 @@ export class MultichainNetworkController extends BaseController< /** * Switches to a non-EVM network. * - * @param nonEvmChainId - The chain ID of the non-EVM network to set active. + * @param id - The chain ID of the non-EVM network to set active. */ - async setActiveNetwork(nonEvmChainId: SupportedCaipChainId): Promise; + async setActiveNetwork(id: SupportedCaipChainId): Promise; /** * Switches to an EVM network. * - * @param evmNetworkClientId - The client ID of the EVM network to set active. + * @param id - The client ID of the EVM network to set active. */ - async setActiveNetwork(evmNetworkClientId: NetworkClientId): Promise; + async setActiveNetwork(id: NetworkClientId): Promise; async setActiveNetwork( id: SupportedCaipChainId | NetworkClientId, From 2596c18ed7d30e424f95fcb021a87a9437987a4d Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 11 Feb 2025 19:17:51 -0800 Subject: [PATCH 57/69] Fix lint errors on MultichainNetworkController --- .../package.json | 7 +++- .../src/MultichainNetworkController.test.ts | 6 ++-- .../src/MultichainNetworkController.ts | 34 +++++++------------ .../src/constants.ts | 2 +- .../src/index.ts | 2 +- .../src/types.ts | 9 +++-- .../src/utils.test.ts | 12 +------ .../src/utils.ts | 4 +-- .../{test => tests}/utils.ts | 3 +- .../tsconfig.json | 2 +- 10 files changed, 33 insertions(+), 48 deletions(-) rename packages/multichain-network-controller/{test => tests}/utils.ts (98%) diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index 44b98736aca..d84916cd591 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -43,7 +43,8 @@ "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" + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", + "publish:preview": "yarn npm publish --tag preview" }, "dependencies": { "@metamask/base-controller": "^7.1.1", @@ -71,5 +72,9 @@ }, "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 index a9ad3c7a48e..f1749ce24ce 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -7,7 +7,7 @@ import { BtcAccountType, SolAccountType, type KeyringAccountType, - CaipChainId, + type CaipChainId, } from '@metamask/keyring-api'; import type { NetworkControllerGetStateAction, @@ -15,6 +15,7 @@ import type { } from '@metamask/network-controller'; import { DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE } from './constants'; +import { MultichainNetworkController } from './MultichainNetworkController'; import { type AllowedActions, type AllowedEvents, @@ -22,8 +23,7 @@ import { type MultichainNetworkControllerAllowedEvents, MULTICHAIN_NETWORK_CONTROLLER_NAME, } from './types'; -import { MultichainNetworkController } from './MultichainNetworkController'; -import { createMockInternalAccount } from '../test/utils'; +import { createMockInternalAccount } from '../tests/utils'; /** * Setup a test controller instance. diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 6a9ed4a6d38..5f3c437c8a7 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -2,11 +2,12 @@ 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 { - checkIfSupportedCaipChainId, - getChainIdForNonEvmAddress, -} from './utils'; + MULTICHAIN_NETWORK_CONTROLLER_METADATA, + DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE, +} from './constants'; import { MULTICHAIN_NETWORK_CONTROLLER_NAME, type MultichainNetworkControllerState, @@ -14,10 +15,9 @@ import { type SupportedCaipChainId, } from './types'; import { - MULTICHAIN_NETWORK_CONTROLLER_METADATA, - DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE, -} from './constants'; -import { isCaipChainId } from '@metamask/utils'; + checkIfSupportedCaipChainId, + getChainIdForNonEvmAddress, +} from './utils'; /** * The MultichainNetworkController is responsible for fetching and caching account @@ -120,19 +120,11 @@ export class MultichainNetworkController extends BaseController< } /** - * Switches to a non-EVM network. + * Sets the active network. * - * @param id - The chain ID of the non-EVM network to set active. + * @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): Promise; - - /** - * Switches to an EVM network. - * - * @param id - The client ID of the EVM network to set active. - */ - async setActiveNetwork(id: NetworkClientId): Promise; - async setActiveNetwork( id: SupportedCaipChainId | NetworkClientId, ): Promise { @@ -141,10 +133,10 @@ export class MultichainNetworkController extends BaseController< if (!isSupportedCaipChainId) { throw new Error(`Unsupported Caip chain ID: ${id}`); } - this.#setActiveNonEvmNetwork(id); - } else { - this.#setActiveEvmNetwork(id); + return this.#setActiveNonEvmNetwork(id); } + + return await this.#setActiveEvmNetwork(id); } /** diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index 18200228517..1f5f363201b 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -1,6 +1,6 @@ +import { type StateMetadata } from '@metamask/base-controller'; import { BtcScope, SolScope } from '@metamask/keyring-api'; import { NetworkStatus } from '@metamask/network-controller'; -import { type StateMetadata } from '@metamask/base-controller'; import type { MultichainNetworkConfiguration, diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts index e2329c6e68b..937bbbd6000 100644 --- a/packages/multichain-network-controller/src/index.ts +++ b/packages/multichain-network-controller/src/index.ts @@ -1,4 +1,4 @@ export * from './MultichainNetworkController'; export * from './constants'; export * from './types'; -export { isSupportedCaipChainId } from './utils'; +export { checkIfSupportedCaipChainId } from './utils'; diff --git a/packages/multichain-network-controller/src/types.ts b/packages/multichain-network-controller/src/types.ts index 167a8c7d821..fb83a6e8790 100644 --- a/packages/multichain-network-controller/src/types.ts +++ b/packages/multichain-network-controller/src/types.ts @@ -3,7 +3,7 @@ import { type ControllerStateChangeEvent, type RestrictedControllerMessenger, } from '@metamask/base-controller'; -import { BtcScope, SolScope } from '@metamask/keyring-api'; +import type { BtcScope, SolScope } from '@metamask/keyring-api'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { NetworkStatus, @@ -101,10 +101,9 @@ export type MultichainNetworkControllerGetStateAction = MultichainNetworkControllerState >; -type SetActiveNetworkMethod = { - (id: SupportedCaipChainId): Promise; - (id: NetworkClientId): Promise; -}; +export type SetActiveNetworkMethod = ( + id: SupportedCaipChainId | NetworkClientId, +) => Promise; export type MultichainNetworkControllerSetActiveNetworkAction = { type: `${typeof MULTICHAIN_NETWORK_CONTROLLER_NAME}:setActiveNetwork`; diff --git a/packages/multichain-network-controller/src/utils.test.ts b/packages/multichain-network-controller/src/utils.test.ts index 856ca69f82b..ec6fe58bb55 100644 --- a/packages/multichain-network-controller/src/utils.test.ts +++ b/packages/multichain-network-controller/src/utils.test.ts @@ -1,10 +1,9 @@ -import { BtcScope, CaipChainId, SolScope } from '@metamask/keyring-api'; +import { BtcScope, SolScope, type CaipChainId } from '@metamask/keyring-api'; import { getChainIdForNonEvmAddress, checkIfSupportedCaipChainId, } from './utils'; -import { SupportedCaipChainId } from './types'; describe('utils', () => { describe('getChainIdForNonEvmAddress', () => { @@ -32,14 +31,5 @@ describe('utils', () => { it('returns false for unsupported CAIP chain IDs', () => { expect(checkIfSupportedCaipChainId('eip155:1')).toBe(false); }); - - it('provides type guard functionality', () => { - const chainId = SolScope.Mainnet; - if (checkIfSupportedCaipChainId(chainId)) { - // TypeScript should recognize chainId as SupportedCaipChainId - const supportedChainId: SupportedCaipChainId = chainId; - expect(supportedChainId).toBe(chainId); - } - }); }); }); diff --git a/packages/multichain-network-controller/src/utils.ts b/packages/multichain-network-controller/src/utils.ts index 869571afe75..d5de67fc80b 100644 --- a/packages/multichain-network-controller/src/utils.ts +++ b/packages/multichain-network-controller/src/utils.ts @@ -1,9 +1,9 @@ import { BtcScope, SolScope } from '@metamask/keyring-api'; +import type { CaipChainId } from '@metamask/utils'; import { isAddress as isSolanaAddress } from '@solana/addresses'; -import { SupportedCaipChainId } from './types'; -import { CaipChainId, isCaipChainId } from '@metamask/utils'; import { AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS } from './constants'; +import type { SupportedCaipChainId } from './types'; /** * Returns the chain id of the non-EVM network based on the account address. diff --git a/packages/multichain-network-controller/test/utils.ts b/packages/multichain-network-controller/tests/utils.ts similarity index 98% rename from packages/multichain-network-controller/test/utils.ts rename to packages/multichain-network-controller/tests/utils.ts index 93036ff4a2b..141f6f29f9e 100644 --- a/packages/multichain-network-controller/test/utils.ts +++ b/packages/multichain-network-controller/tests/utils.ts @@ -9,7 +9,6 @@ import { } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; -import { v4 } from 'uuid'; /** * Creates a mock internal account. This is a duplicated function from the accounts-controller package @@ -30,7 +29,7 @@ import { v4 } from 'uuid'; * @returns A mock internal account. */ export const createMockInternalAccount = ({ - id = v4(), + id = 'dummy-id', address = '0x2990079bcdee240329a520d2444386fc119da21a', type = EthAccountType.Eoa, name = 'Account 1', diff --git a/packages/multichain-network-controller/tsconfig.json b/packages/multichain-network-controller/tsconfig.json index 9a11d6c7f89..952a231baf0 100644 --- a/packages/multichain-network-controller/tsconfig.json +++ b/packages/multichain-network-controller/tsconfig.json @@ -8,5 +8,5 @@ { "path": "../network-controller" }, { "path": "../keyring-controller" } ], - "include": ["../../types", "./src", "./tests", "test"] + "include": ["../../types", "./src", "./tests", "tests"] } From 2e5eed32f22c191ea4feb029ea9085a5890206ed Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 11 Feb 2025 19:18:29 -0800 Subject: [PATCH 58/69] Fix lint errors on accounts controller files --- packages/accounts-controller/package.json | 1 + .../src/AccountsController.test.ts | 37 +++------- .../src/AccountsController.ts | 74 ++++++++----------- packages/accounts-controller/src/types.ts | 10 +++ .../accounts-controller/tsconfig.build.json | 3 +- packages/accounts-controller/tsconfig.json | 5 +- 6 files changed, 57 insertions(+), 73 deletions(-) create mode 100644 packages/accounts-controller/src/types.ts diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index ac1f0c72404..05c23bcfe25 100644 --- a/packages/accounts-controller/package.json +++ b/packages/accounts-controller/package.json @@ -76,6 +76,7 @@ }, "peerDependencies": { "@metamask/keyring-controller": "^19.0.0", + "@metamask/network-controller": "^22.1.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 7f071a17479..d872bc071c6 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,9 +18,10 @@ 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'; +import type { CaipChainId } from '@metamask/utils'; import * as uuid from 'uuid'; import type { V4Options } from 'uuid'; @@ -31,11 +33,11 @@ import type { AllowedEvents, } from './AccountsController'; import { AccountsController, EMPTY_ACCOUNT } from './AccountsController'; -import { createMockInternalAccount } from './tests/mocks'; import { getUUIDOptionsFromAddressOfNormalAccount, keyringTypeToName, } from './utils'; +import { createMockInternalAccount } from './tests/mocks'; jest.mock('uuid'); const mockUUID = jest.spyOn(uuid, 'v4'); @@ -340,10 +342,7 @@ function setupAccountsController({ AccountsControllerActions | AllowedActions, AccountsControllerEvents | AllowedEvents >; - triggerMultichainNetworkChange: (args: { - evmNetworkClientId?: string; - nonEvmChainId?: CaipChainId; - }) => void; + triggerMultichainNetworkChange: (id: NetworkClientId | CaipChainId) => void; } { const accountsControllerMessenger = buildAccountsControllerMessenger(messenger); @@ -353,18 +352,8 @@ function setupAccountsController({ state: { ...defaultState, ...initialState }, }); - const triggerMultichainNetworkChange = ({ - evmNetworkClientId, - nonEvmChainId, - }: { - evmNetworkClientId?: string; - nonEvmChainId?: CaipChainId; - }) => { - messenger.publish('MultichainNetworkController:networkDidChange', { - evmNetworkClientId, - nonEvmChainId, - }); - }; + const triggerMultichainNetworkChange = (id: NetworkClientId | CaipChainId) => + messenger.publish('MultichainNetworkController:networkDidChange', id); return { accountsController, messenger, triggerMultichainNetworkChange }; } @@ -1575,9 +1564,7 @@ describe('AccountsController', () => { }); // Triggered from network switch to Bitcoin mainnet - triggerMultichainNetworkChange({ - nonEvmChainId: BtcScope.Mainnet, - }); + triggerMultichainNetworkChange(BtcScope.Mainnet); // BTC account is now selected expect(accountsController.state.internalAccounts.selectedAccount).toBe( @@ -1592,7 +1579,7 @@ describe('AccountsController', () => { initialState: { internalAccounts: { accounts: { - [mockNewerEvmAccount.id]: mockNewerEvmAccount, + [mockOlderEvmAccount.id]: mockOlderEvmAccount, [mockBtcAccount.id]: mockBtcAccount, }, selectedAccount: mockBtcAccount.id, @@ -1602,13 +1589,11 @@ describe('AccountsController', () => { }); // Triggered from network switch to Bitcoin mainnet - triggerMultichainNetworkChange({ - evmNetworkClientId: EthScope.Mainnet, - }); + triggerMultichainNetworkChange(InfuraNetworkType.mainnet); // ETH mainnet account is now selected expect(accountsController.state.internalAccounts.selectedAccount).toBe( - mockNewerEvmAccount.id, + mockOlderEvmAccount.id, ); }); }); diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index bcd6c06a2ec..4c582b03ab1 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -1,45 +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 { MultichainNetworkControllerNetworkDidChangeEvent } from './types'; import { getUUIDFromAddressOfNormalAccount, isNormalKeyringType, @@ -181,17 +183,6 @@ export type AccountsControllerAccountAssetListUpdatedEvent = { payload: SnapKeyringAccountAssetListUpdatedEvent['payload']; }; -// Re-define event here to avoid circular dependency with MultichainNetworkController -type MultichainNetworkControllerNetworkDidChangeEvent = { - type: `MultichainNetworkController:networkDidChange`; - payload: [ - { - evmNetworkClientId?: string; - nonEvmChainId?: CaipChainId; - }, - ]; -}; - export type AllowedEvents = | SnapStateChange | KeyringControllerStateChangeEvent @@ -1138,26 +1129,21 @@ export class AccountsController extends BaseController< /** * Handles the change in multichain network by updating the selected account. * - * @param args - The arguments to handle the multichain network change. - * @param args.evmNetworkClientId - The ID of the EVM client. - * @param args.nonEvmChainId - The CAIP2 of the non-EVM chain. + * @param id - The EVM client ID or non-EVM chain ID that changed. */ - readonly #handleMultichainNetworkChange = ({ - evmNetworkClientId, - nonEvmChainId, - }: { - evmNetworkClientId?: string; - nonEvmChainId?: CaipChainId; - }) => { + readonly #handleMultichainNetworkChange = ( + id: NetworkClientId | CaipChainId, + ) => { let accountId: string; - if (nonEvmChainId) { + // 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(nonEvmChainId); + 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 if (evmNetworkClientId) { + } else { // Update selected account to evm account const lastSelectedEvmAccount = this.getSelectedAccount(); accountId = lastSelectedEvmAccount.id; 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/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"] } From 2892d25ec905c38174575d2300a66957a726f4d1 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 11 Feb 2025 19:18:39 -0800 Subject: [PATCH 59/69] Update codeowners --- teams.json | 1 + 1 file changed, 1 insertion(+) 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", From a0bcef584f176efb8300c40775e8092d2899ee4d Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 11 Feb 2025 19:21:16 -0800 Subject: [PATCH 60/69] Merge with main --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 7baf27442d0..12805bda943 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3300,7 +3300,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^19.0.7, @metamask/keyring-controller@workspace:packages/keyring-controller": +"@metamask/keyring-controller@npm:^19.0.5, @metamask/keyring-controller@npm:^19.0.7, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" dependencies: From fdfe88653976092c1fc533d60fd49a85616f46e1 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 11 Feb 2025 19:39:44 -0800 Subject: [PATCH 61/69] Update based off latest main --- .../src/AccountsController.test.ts | 2 +- .../multichain-network-controller/package.json | 8 ++++---- .../src/MultichainNetworkController.test.ts | 4 ++-- .../multichain-network-controller/src/types.ts | 17 ++++++++--------- yarn.lock | 10 +++++----- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index d872bc071c6..6eda1b8c031 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -33,11 +33,11 @@ import type { AllowedEvents, } from './AccountsController'; import { AccountsController, EMPTY_ACCOUNT } from './AccountsController'; +import { createMockInternalAccount } from './tests/mocks'; import { getUUIDOptionsFromAddressOfNormalAccount, keyringTypeToName, } from './utils'; -import { createMockInternalAccount } from './tests/mocks'; jest.mock('uuid'); const mockUUID = jest.spyOn(uuid, 'v4'); diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index d84916cd591..c23af14ca9f 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -47,14 +47,14 @@ "publish:preview": "yarn npm publish --tag preview" }, "dependencies": { - "@metamask/base-controller": "^7.1.1", - "@metamask/keyring-api": "^16.1.0", + "@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.0.5", + "@metamask/keyring-controller": "^19.0.7", "@types/jest": "^27.4.1", "@types/uuid": "^8.3.0", "deepmerge": "^4.2.2", @@ -67,7 +67,7 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/accounts-controller": "^22.0.0", + "@metamask/accounts-controller": "^23.0.0", "@metamask/network-controller": "^22.1.1" }, "engines": { diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index f1749ce24ce..eedd27541aa 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -1,4 +1,4 @@ -import { ControllerMessenger } from '@metamask/base-controller'; +import { Messenger } from '@metamask/base-controller'; import { InfuraNetworkType } from '@metamask/controller-utils'; import { BtcScope, @@ -51,7 +51,7 @@ function setupController({ Parameters >; } = {}) { - const messenger = new ControllerMessenger< + const messenger = new Messenger< MultichainNetworkControllerAllowedActions, MultichainNetworkControllerAllowedEvents >(); diff --git a/packages/multichain-network-controller/src/types.ts b/packages/multichain-network-controller/src/types.ts index fb83a6e8790..afa4e31ba24 100644 --- a/packages/multichain-network-controller/src/types.ts +++ b/packages/multichain-network-controller/src/types.ts @@ -1,7 +1,7 @@ import { type ControllerGetStateAction, type ControllerStateChangeEvent, - type RestrictedControllerMessenger, + type RestrictedMessenger, } from '@metamask/base-controller'; import type { BtcScope, SolScope } from '@metamask/keyring-api'; import type { InternalAccount } from '@metamask/keyring-internal-api'; @@ -165,11 +165,10 @@ export type MultichainNetworkControllerAllowedEvents = /** * Messenger type for the MultichainNetworkController. */ -export type MultichainNetworkControllerMessenger = - RestrictedControllerMessenger< - typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, - MultichainNetworkControllerAllowedActions, - MultichainNetworkControllerAllowedEvents, - AllowedActions['type'], - AllowedEvents['type'] - >; +export type MultichainNetworkControllerMessenger = RestrictedMessenger< + typeof MULTICHAIN_NETWORK_CONTROLLER_NAME, + MultichainNetworkControllerAllowedActions, + MultichainNetworkControllerAllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; diff --git a/yarn.lock b/yarn.lock index 12805bda943..b05163a3188 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3300,7 +3300,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^19.0.5, @metamask/keyring-controller@npm:^19.0.7, @metamask/keyring-controller@workspace:packages/keyring-controller": +"@metamask/keyring-controller@npm:^19.0.7, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" dependencies: @@ -3457,9 +3457,9 @@ __metadata: resolution: "@metamask/multichain-network-controller@workspace:packages/multichain-network-controller" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/base-controller": "npm:^7.1.1" - "@metamask/keyring-api": "npm:^16.1.0" - "@metamask/keyring-controller": "npm:^19.0.5" + "@metamask/base-controller": "npm:^8.0.0" + "@metamask/keyring-api": "npm:^17.0.0" + "@metamask/keyring-controller": "npm:^19.0.7" "@metamask/utils": "npm:^11.1.0" "@solana/addresses": "npm:^2.0.0" "@types/jest": "npm:^27.4.1" @@ -3473,7 +3473,7 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" peerDependencies: - "@metamask/accounts-controller": ^22.0.0 + "@metamask/accounts-controller": ^23.0.0 "@metamask/network-controller": ^22.1.1 languageName: unknown linkType: soft From f36c02e7df308e9fd3d8871af9f8a68b5f9b87d0 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Tue, 11 Feb 2025 19:40:22 -0800 Subject: [PATCH 62/69] Run yarn update-readme-content --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b7ebbec2805..b969a2cd98e 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ 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; From 4623c124b99b0f3eb90d340d528e471d9a5efa11 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Wed, 12 Feb 2025 10:02:52 -0800 Subject: [PATCH 63/69] Address comments in PR --- README.md | 1 + packages/accounts-controller/package.json | 5 +++-- .../jest.config.js | 2 +- .../src/MultichainNetworkController.test.ts | 8 +++----- .../src/MultichainNetworkController.ts | 4 ++-- .../src/constants.ts | 6 +++--- .../src/index.ts | 20 ++++++++++++++++--- .../tsconfig.json | 2 +- yarn.lock | 5 +++-- 9 files changed, 34 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index b969a2cd98e..6ffab5f882b 100644 --- a/README.md +++ b/README.md @@ -107,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; diff --git a/packages/accounts-controller/package.json b/packages/accounts-controller/package.json index 5144cba0f1c..cd54cd0b149 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.0.7", + "@metamask/network-controller": "^22.2.1", "@metamask/providers": "^18.1.1", "@metamask/snaps-controllers": "^9.19.0", "@types/jest": "^27.4.1", @@ -75,8 +76,8 @@ "webextension-polyfill": "^0.12.0" }, "peerDependencies": { - "@metamask/keyring-controller": "^19.0.0", - "@metamask/network-controller": "^22.1.1", + "@metamask/keyring-controller": "^19.0.7", + "@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/multichain-network-controller/jest.config.js b/packages/multichain-network-controller/jest.config.js index 9392b3b2347..ca084133399 100644 --- a/packages/multichain-network-controller/jest.config.js +++ b/packages/multichain-network-controller/jest.config.js @@ -17,7 +17,7 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 96.29, + branches: 100, functions: 100, lines: 100, statements: 100, diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts index eedd27541aa..f5dc5beedf4 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.test.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.test.ts @@ -14,7 +14,7 @@ import type { NetworkControllerSetActiveNetworkAction, } from '@metamask/network-controller'; -import { DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE } from './constants'; +import { getDefaultMultichainNetworkControllerState } from './constants'; import { MultichainNetworkController } from './MultichainNetworkController'; import { type AllowedActions, @@ -99,8 +99,6 @@ function setupController({ messenger: options.messenger || controllerMessenger, state: { selectedMultichainNetworkChainId: SolScope.Mainnet, - // multichainNetworkConfigurationsByChainId: - // DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE, isEvmSelected: true, ...options.state, }, @@ -138,10 +136,10 @@ describe('MultichainNetworkController', () => { describe('constructor', () => { it('should set default state', () => { const { controller } = setupController({ - options: { state: DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE }, + options: { state: getDefaultMultichainNetworkControllerState() }, }); expect(controller.state).toStrictEqual( - DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE, + getDefaultMultichainNetworkControllerState(), ); }); }); diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 5f3c437c8a7..4858ee8266e 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -6,7 +6,7 @@ import { isCaipChainId } from '@metamask/utils'; import { MULTICHAIN_NETWORK_CONTROLLER_METADATA, - DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE, + getDefaultMultichainNetworkControllerState, } from './constants'; import { MULTICHAIN_NETWORK_CONTROLLER_NAME, @@ -43,7 +43,7 @@ export class MultichainNetworkController extends BaseController< name: MULTICHAIN_NETWORK_CONTROLLER_NAME, metadata: MULTICHAIN_NETWORK_CONTROLLER_METADATA, state: { - ...DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE, + ...getDefaultMultichainNetworkControllerState(), ...state, }, }); diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index 1f5f363201b..4fc1112dc9a 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -56,13 +56,13 @@ export const NETWORKS_METADATA: Record = { * * @returns The default state of the {@link MultichainNetworkController}. */ -export const DEFAULT_MULTICHAIN_NETWORK_CONTROLLER_STATE: MultichainNetworkControllerState = - { +export const getDefaultMultichainNetworkControllerState = + (): MultichainNetworkControllerState => ({ multichainNetworkConfigurationsByChainId: AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS, selectedMultichainNetworkChainId: SolScope.Mainnet, isEvmSelected: true, - }; + }); /** * {@link MultichainNetworkController}'s metadata. diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts index 937bbbd6000..7fa09dc2caa 100644 --- a/packages/multichain-network-controller/src/index.ts +++ b/packages/multichain-network-controller/src/index.ts @@ -1,4 +1,18 @@ -export * from './MultichainNetworkController'; -export * from './constants'; -export * from './types'; +export { MultichainNetworkController } from './MultichainNetworkController'; +export { getDefaultMultichainNetworkControllerState } from './constants'; +export type { + MultichainNetworkMetadata, + SupportedCaipChainId, + CommonNetworkConfiguration, + NonEvmNetworkConfiguration, + MultichainNetworkConfiguration, + MultichainNetworkControllerState, + MultichainNetworkControllerGetStateAction, + MultichainNetworkControllerSetActiveNetworkAction, + MultichainNetworkControllerStateChange, + MultichainNetworkControllerNetworkDidChangeEvent, + MultichainNetworkControllerActions, + MultichainNetworkControllerEvents, + MultichainNetworkControllerMessenger, +} from './types'; export { checkIfSupportedCaipChainId } from './utils'; diff --git a/packages/multichain-network-controller/tsconfig.json b/packages/multichain-network-controller/tsconfig.json index 952a231baf0..e5ff777b642 100644 --- a/packages/multichain-network-controller/tsconfig.json +++ b/packages/multichain-network-controller/tsconfig.json @@ -8,5 +8,5 @@ { "path": "../network-controller" }, { "path": "../keyring-controller" } ], - "include": ["../../types", "./src", "./tests", "tests"] + "include": ["../../types", "./src", "./tests"] } diff --git a/yarn.lock b/yarn.lock index b05163a3188..ead30e6a9d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2351,6 +2351,7 @@ __metadata: "@metamask/keyring-api": "npm:^17.0.0" "@metamask/keyring-controller": "npm:^19.0.7" "@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,8 +2370,8 @@ __metadata: uuid: "npm:^8.3.2" webextension-polyfill: "npm:^0.12.0" peerDependencies: - "@metamask/keyring-controller": ^19.0.0 - "@metamask/network-controller": ^22.1.1 + "@metamask/keyring-controller": ^19.0.7 + "@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 From 7e3d3017a3768a44195522ca8db036e95d1f390e Mon Sep 17 00:00:00 2001 From: Cal-L Date: Wed, 12 Feb 2025 10:04:45 -0800 Subject: [PATCH 64/69] update yarn lock --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0295831b640..b5fd2fcb739 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2370,7 +2370,7 @@ __metadata: uuid: "npm:^8.3.2" webextension-polyfill: "npm:^0.12.0" peerDependencies: - "@metamask/keyring-controller": ^19.0.7 + "@metamask/keyring-controller": ^19.1.0 "@metamask/network-controller": ^22.2.1 "@metamask/providers": ^18.1.0 "@metamask/snaps-controllers": ^9.19.0 @@ -3301,7 +3301,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^19.1.0, @metamask/keyring-controller@workspace:packages/keyring-controller": +"@metamask/keyring-controller@npm:^19.0.7, @metamask/keyring-controller@npm:^19.1.0, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" dependencies: From 75a7f305c5e9162837aa76ab979347b4ad1d6f95 Mon Sep 17 00:00:00 2001 From: Gustavo Antunes <17601467+gantunesr@users.noreply.github.com> Date: Wed, 12 Feb 2025 15:44:56 -0300 Subject: [PATCH 65/69] chore: add util methods to update network configuration (#5315) Co-authored-by: Charly Chevalier --- .../src/constants.ts | 4 - .../src/index.ts | 7 +- .../src/types.ts | 60 +++++++------- .../src/utils.test.ts | 79 +++++++++++++++++++ .../src/utils.ts | 62 ++++++++++++++- 5 files changed, 177 insertions(+), 35 deletions(-) diff --git a/packages/multichain-network-controller/src/constants.ts b/packages/multichain-network-controller/src/constants.ts index 4fc1112dc9a..0f84e75f9b1 100644 --- a/packages/multichain-network-controller/src/constants.ts +++ b/packages/multichain-network-controller/src/constants.ts @@ -22,16 +22,12 @@ export const AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS: Record< [BtcScope.Mainnet]: { chainId: BtcScope.Mainnet, name: 'Bitcoin Mainnet', - blockExplorerUrls: ['https://blockstream.info'], - defaultBlockExplorerUrlIndex: 0, nativeCurrency: BTC_NATIVE_ASSET, isEvm: false, }, [SolScope.Mainnet]: { chainId: SolScope.Mainnet, name: 'Solana Mainnet', - blockExplorerUrls: ['https://explorer.solana.com'], - defaultBlockExplorerUrlIndex: 0, nativeCurrency: SOL_NATIVE_ASSET, isEvm: false, }, diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts index 7fa09dc2caa..8dfc3d0ec1d 100644 --- a/packages/multichain-network-controller/src/index.ts +++ b/packages/multichain-network-controller/src/index.ts @@ -15,4 +15,9 @@ export type { MultichainNetworkControllerEvents, MultichainNetworkControllerMessenger, } from './types'; -export { checkIfSupportedCaipChainId } from './utils'; +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 index afa4e31ba24..5eb1215da2a 100644 --- a/packages/multichain-network-controller/src/types.ts +++ b/packages/multichain-network-controller/src/types.ts @@ -3,7 +3,7 @@ import { type ControllerStateChangeEvent, type RestrictedMessenger, } from '@metamask/base-controller'; -import type { BtcScope, SolScope } from '@metamask/keyring-api'; +import type { BtcScope, CaipChainId, SolScope } from '@metamask/keyring-api'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { NetworkStatus, @@ -27,49 +27,53 @@ export type CommonNetworkConfiguration = { * EVM network flag. */ isEvm: boolean; - /** - * The block explorers of the network. - */ - blockExplorerUrls: string[]; - /** - * The index of the default block explorer URL. - */ - defaultBlockExplorerUrlIndex: number; /** * The chain ID of the network. */ - chainId: SupportedCaipChainId; + chainId: CaipChainId; /** * The name of the network. */ name: string; - /** - * The native asset type of the network. - */ - nativeCurrency: CaipAssetType; }; 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 & { -// isEvm: true; -// /** -// * The RPC endpoints of the network. -// */ -// rpcEndpoints: string[]; -// /** -// * The index of the default RPC endpoint. -// */ -// defaultRpcEndpointIndex: number; -// }; +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; + | EvmNetworkConfiguration + | NonEvmNetworkConfiguration; /** * State used by the {@link MultichainNetworkController} to cache network configurations. @@ -79,7 +83,7 @@ export type MultichainNetworkControllerState = { * The network configurations by chain ID. */ multichainNetworkConfigurationsByChainId: Record< - SupportedCaipChainId, + CaipChainId, MultichainNetworkConfiguration >; /** diff --git a/packages/multichain-network-controller/src/utils.test.ts b/packages/multichain-network-controller/src/utils.test.ts index ec6fe58bb55..dbf6e5e5322 100644 --- a/packages/multichain-network-controller/src/utils.test.ts +++ b/packages/multichain-network-controller/src/utils.test.ts @@ -1,8 +1,12 @@ 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', () => { @@ -32,4 +36,79 @@ describe('utils', () => { 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 index d5de67fc80b..d6a00d7160e 100644 --- a/packages/multichain-network-controller/src/utils.ts +++ b/packages/multichain-network-controller/src/utils.ts @@ -1,9 +1,19 @@ import { BtcScope, SolScope } from '@metamask/keyring-api'; -import type { CaipChainId } from '@metamask/utils'; +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 } from './types'; +import type { + SupportedCaipChainId, + MultichainNetworkConfiguration, +} from './types'; /** * Returns the chain id of the non-EVM network based on the account address. @@ -33,3 +43,51 @@ export function checkIfSupportedCaipChainId( // 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), + }), + {}, + ); From 53bb53d5cae4dd5a858bfdcbf052a5c9ff3e9f2b Mon Sep 17 00:00:00 2001 From: Cal-L Date: Wed, 12 Feb 2025 10:48:55 -0800 Subject: [PATCH 66/69] Fix lint error --- packages/multichain-network-controller/package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/multichain-network-controller/package.json b/packages/multichain-network-controller/package.json index c23af14ca9f..b22fb7a372e 100644 --- a/packages/multichain-network-controller/package.json +++ b/packages/multichain-network-controller/package.json @@ -54,7 +54,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/keyring-controller": "^19.0.7", + "@metamask/keyring-controller": "^19.1.0", "@types/jest": "^27.4.1", "@types/uuid": "^8.3.0", "deepmerge": "^4.2.2", diff --git a/yarn.lock b/yarn.lock index b5fd2fcb739..57dcd4b6f07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3301,7 +3301,7 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-controller@npm:^19.0.7, @metamask/keyring-controller@npm:^19.1.0, @metamask/keyring-controller@workspace:packages/keyring-controller": +"@metamask/keyring-controller@npm:^19.1.0, @metamask/keyring-controller@workspace:packages/keyring-controller": version: 0.0.0-use.local resolution: "@metamask/keyring-controller@workspace:packages/keyring-controller" dependencies: @@ -3460,7 +3460,7 @@ __metadata: "@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.0.7" + "@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" From 50ba21ee23953089e5b7ea1c91a81796f9ae1147 Mon Sep 17 00:00:00 2001 From: Cal Leung Date: Wed, 12 Feb 2025 10:52:41 -0800 Subject: [PATCH 67/69] Update packages/multichain-network-controller/src/index.ts Co-authored-by: Elliot Winkler --- packages/multichain-network-controller/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts index 8dfc3d0ec1d..3df0c3580df 100644 --- a/packages/multichain-network-controller/src/index.ts +++ b/packages/multichain-network-controller/src/index.ts @@ -5,6 +5,7 @@ export type { SupportedCaipChainId, CommonNetworkConfiguration, NonEvmNetworkConfiguration, + EvmNetworkConfiguration MultichainNetworkConfiguration, MultichainNetworkControllerState, MultichainNetworkControllerGetStateAction, From 517713ca2ac4f47868139c8b0cf1f0ab3df8e0f0 Mon Sep 17 00:00:00 2001 From: Cal-L Date: Wed, 12 Feb 2025 11:07:30 -0800 Subject: [PATCH 68/69] Fix lint issue --- packages/multichain-network-controller/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain-network-controller/src/index.ts b/packages/multichain-network-controller/src/index.ts index 3df0c3580df..eaf8accddf0 100644 --- a/packages/multichain-network-controller/src/index.ts +++ b/packages/multichain-network-controller/src/index.ts @@ -5,7 +5,7 @@ export type { SupportedCaipChainId, CommonNetworkConfiguration, NonEvmNetworkConfiguration, - EvmNetworkConfiguration + EvmNetworkConfiguration, MultichainNetworkConfiguration, MultichainNetworkControllerState, MultichainNetworkControllerGetStateAction, From d91df82f4f346c4c9670ff1d8b15dbffe0f8cccc Mon Sep 17 00:00:00 2001 From: Cal Leung Date: Wed, 12 Feb 2025 11:09:14 -0800 Subject: [PATCH 69/69] Update packages/multichain-network-controller/src/MultichainNetworkController.ts Co-authored-by: Elliot Winkler --- .../src/MultichainNetworkController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain-network-controller/src/MultichainNetworkController.ts b/packages/multichain-network-controller/src/MultichainNetworkController.ts index 4858ee8266e..572dfa6d12c 100644 --- a/packages/multichain-network-controller/src/MultichainNetworkController.ts +++ b/packages/multichain-network-controller/src/MultichainNetworkController.ts @@ -131,7 +131,7 @@ export class MultichainNetworkController extends BaseController< if (isCaipChainId(id)) { const isSupportedCaipChainId = checkIfSupportedCaipChainId(id); if (!isSupportedCaipChainId) { - throw new Error(`Unsupported Caip chain ID: ${id}`); + throw new Error(`Unsupported Caip chain ID: ${String(id)}`); } return this.#setActiveNonEvmNetwork(id); }