diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4af50ad4785..feab726ecae 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,9 +7,10 @@ /.github/ @MetaMask/core-platform ## Accounts Team -/packages/accounts-controller @MetaMask/accounts-engineers +/packages/accounts-controller @MetaMask/accounts-engineers /packages/multichain-transactions-controller @MetaMask/accounts-engineers -/packages/account-tree-controller @MetaMask/accounts-engineers +/packages/multichain-account-service @MetaMask/accounts-engineers +/packages/account-tree-controller @MetaMask/accounts-engineers ## Assets Team /packages/assets-controllers @MetaMask/metamask-assets @@ -118,6 +119,8 @@ /packages/logging-controller/CHANGELOG.md @MetaMask/confirmations @MetaMask/core-platform /packages/message-manager/package.json @MetaMask/confirmations @MetaMask/core-platform /packages/message-manager/CHANGELOG.md @MetaMask/confirmations @MetaMask/core-platform +/packages/multichain-account-service/package.json @MetaMask/accounts-engineers @MetaMask/core-platform +/packages/multichain-account-service/CHANGELOG.md @MetaMask/accounts-engineers @MetaMask/core-platform /packages/multichain-api-middleware/package.json @MetaMask/wallet-api-platform-engineers @MetaMask/core-platform /packages/multichain-api-middleware/CHANGELOG.md @MetaMask/wallet-api-platform-engineers @MetaMask/core-platform /packages/name-controller/package.json @MetaMask/confirmations @MetaMask/core-platform diff --git a/README.md b/README.md index 102537d133a..4deea97c172 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,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/messenger`](packages/messenger) +- [`@metamask/multichain-account-service`](packages/multichain-account-service) - [`@metamask/multichain-api-middleware`](packages/multichain-api-middleware) - [`@metamask/multichain-network-controller`](packages/multichain-network-controller) - [`@metamask/multichain-transactions-controller`](packages/multichain-transactions-controller) @@ -106,6 +107,7 @@ linkStyle default opacity:0.5 logging_controller(["@metamask/logging-controller"]); message_manager(["@metamask/message-manager"]); messenger(["@metamask/messenger"]); + multichain_account_service(["@metamask/multichain-account-service"]); multichain_api_middleware(["@metamask/multichain-api-middleware"]); multichain_network_controller(["@metamask/multichain-network-controller"]); multichain_transactions_controller(["@metamask/multichain-transactions-controller"]); @@ -201,6 +203,9 @@ linkStyle default opacity:0.5 logging_controller --> controller_utils; message_manager --> base_controller; message_manager --> controller_utils; + multichain_account_service --> base_controller; + multichain_account_service --> accounts_controller; + multichain_account_service --> keyring_controller; multichain_api_middleware --> chain_agnostic_permission; multichain_api_middleware --> controller_utils; multichain_api_middleware --> json_rpc_engine; diff --git a/packages/multichain-account-service/CHANGELOG.md b/packages/multichain-account-service/CHANGELOG.md new file mode 100644 index 00000000000..619de97cc8e --- /dev/null +++ b/packages/multichain-account-service/CHANGELOG.md @@ -0,0 +1,15 @@ +# 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] + +### Added + +- Add `MultichainAccountService` ([#6141](https://github.com/MetaMask/core/pull/6141)) + - This service manages multichain accounts/wallets. + +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/multichain-account-service/LICENSE b/packages/multichain-account-service/LICENSE new file mode 100644 index 00000000000..7d002dced3a --- /dev/null +++ b/packages/multichain-account-service/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2025 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/multichain-account-service/README.md b/packages/multichain-account-service/README.md new file mode 100644 index 00000000000..ee795b4005d --- /dev/null +++ b/packages/multichain-account-service/README.md @@ -0,0 +1,17 @@ +# `@metamask/multichain-account-service` + +Multichain account service. + +This service provides operations and functionalities around multichain accounts and wallets. + +## Installation + +`yarn add @metamask/multichain-account-service` + +or + +`npm install @metamask/multichain-account-service` + +## 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-account-service/jest.config.js b/packages/multichain-account-service/jest.config.js new file mode 100644 index 00000000000..ca084133399 --- /dev/null +++ b/packages/multichain-account-service/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-account-service/package.json b/packages/multichain-account-service/package.json new file mode 100644 index 00000000000..ad03fd3e785 --- /dev/null +++ b/packages/multichain-account-service/package.json @@ -0,0 +1,89 @@ +{ + "name": "@metamask/multichain-account-service", + "version": "0.0.0", + "description": "Service to manage multichain accounts", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/multichain-account-service#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-account-service", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/multichain-account-service", + "publish:preview": "yarn npm publish --tag preview", + "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/account-api": "^0.2.0", + "@metamask/base-controller": "^8.0.1", + "@metamask/keyring-api": "^19.0.0", + "@metamask/keyring-internal-api": "^7.0.0", + "@metamask/keyring-snap-client": "^6.0.0", + "@metamask/snaps-sdk": "^9.0.0", + "@metamask/snaps-utils": "^11.0.0", + "@metamask/superstruct": "^3.1.0" + }, + "devDependencies": { + "@metamask/accounts-controller": "^31.0.0", + "@metamask/auto-changelog": "^3.4.4", + "@metamask/eth-snap-keyring": "^14.0.0", + "@metamask/keyring-controller": "^22.1.0", + "@metamask/providers": "^22.1.0", + "@metamask/snaps-controllers": "^14.0.1", + "@types/jest": "^27.4.1", + "deepmerge": "^4.2.2", + "jest": "^27.5.1", + "ts-jest": "^27.1.4", + "typedoc": "^0.24.8", + "typedoc-plugin-missing-exports": "^2.0.0", + "typescript": "~5.2.2", + "webextension-polyfill": "^0.12.0" + }, + "peerDependencies": { + "@metamask/accounts-controller": "^31.0.0", + "@metamask/keyring-controller": "^22.0.0", + "@metamask/providers": "^22.0.0", + "@metamask/snaps-controllers": "^14.0.0", + "webextension-polyfill": "^0.10.0 || ^0.11.0 || ^0.12.0" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/multichain-account-service/src/MultichainAccountService.test.ts b/packages/multichain-account-service/src/MultichainAccountService.test.ts new file mode 100644 index 00000000000..e2a33a71261 --- /dev/null +++ b/packages/multichain-account-service/src/MultichainAccountService.test.ts @@ -0,0 +1,332 @@ +/* eslint-disable jsdoc/require-jsdoc */ +import type { Messenger } from '@metamask/base-controller'; +import type { KeyringAccount } from '@metamask/keyring-api'; +import { EthAccountType, SolAccountType } from '@metamask/keyring-api'; +import type { KeyringObject } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import { MultichainAccountService } from './MultichainAccountService'; +import { EvmAccountProvider } from './providers/EvmAccountProvider'; +import { SolAccountProvider } from './providers/SolAccountProvider'; +import { + getMultichainAccountServiceMessenger, + getRootMessenger, + MOCK_HARDWARE_ACCOUNT_1, + MOCK_HD_ACCOUNT_1, + MOCK_HD_ACCOUNT_2, + MOCK_HD_KEYRING_1, + MOCK_HD_KEYRING_2, + MOCK_SNAP_ACCOUNT_1, + MOCK_SNAP_ACCOUNT_2, + MockAccountBuilder, +} from './tests'; +import type { + AllowedActions, + AllowedEvents, + MultichainAccountServiceActions, + MultichainAccountServiceEvents, + MultichainAccountServiceMessenger, +} from './types'; + +// Mock providers. +jest.mock('./providers/EvmAccountProvider', () => { + return { + ...jest.requireActual('./providers/EvmAccountProvider'), + EvmAccountProvider: jest.fn(), + }; +}); +jest.mock('./providers/SolAccountProvider', () => { + return { + ...jest.requireActual('./providers/SolAccountProvider'), + SolAccountProvider: jest.fn(), + }; +}); + +type MockAccountProvider = { + getAccount: jest.Mock; + getAccounts: jest.Mock; + createAccounts: jest.Mock; + discoverAndCreateAccounts: jest.Mock; +}; +type Mocks = { + listMultichainAccounts: jest.Mock; + EvmAccountProvider: MockAccountProvider; + SolAccountProvider: MockAccountProvider; +}; + +function mockAccountProvider( + providerClass: new (messenger: MultichainAccountServiceMessenger) => Provider, + mocks: MockAccountProvider, + accounts: InternalAccount[], + type: KeyringAccount['type'], +) { + jest + .mocked(providerClass) + .mockImplementation(() => mocks as unknown as Provider); + + mocks.getAccounts.mockImplementation(() => + accounts.filter((account) => account.type === type), + ); +} + +function setup({ + messenger = getRootMessenger(), + keyrings = [MOCK_HD_KEYRING_1, MOCK_HD_KEYRING_2], + accounts, +}: { + messenger?: Messenger< + MultichainAccountServiceActions | AllowedActions, + MultichainAccountServiceEvents | AllowedEvents + >; + keyrings?: KeyringObject[]; + accounts?: InternalAccount[]; +} = {}): { + service: MultichainAccountService; + messenger: Messenger< + MultichainAccountServiceActions | AllowedActions, + MultichainAccountServiceEvents | AllowedEvents + >; + mocks: Mocks; +} { + const mocks: Mocks = { + listMultichainAccounts: jest.fn(), + EvmAccountProvider: { + getAccount: jest.fn(), + getAccounts: jest.fn(), + createAccounts: jest.fn(), + discoverAndCreateAccounts: jest.fn(), + }, + SolAccountProvider: { + getAccount: jest.fn(), + getAccounts: jest.fn(), + createAccounts: jest.fn(), + discoverAndCreateAccounts: jest.fn(), + }, + }; + + messenger.registerActionHandler('KeyringController:getState', () => ({ + isUnlocked: true, + keyrings, + })); + + if (accounts) { + mocks.listMultichainAccounts.mockImplementation(() => accounts); + + messenger.registerActionHandler( + 'AccountsController:listMultichainAccounts', + mocks.listMultichainAccounts, + ); + + mockAccountProvider( + EvmAccountProvider, + mocks.EvmAccountProvider, + accounts, + EthAccountType.Eoa, + ); + mockAccountProvider( + SolAccountProvider, + mocks.SolAccountProvider, + accounts, + SolAccountType.DataAccount, + ); + } + + const service = new MultichainAccountService({ + messenger: getMultichainAccountServiceMessenger(messenger), + }); + service.init(); + + return { service, messenger, mocks }; +} + +describe('MultichainAccountService', () => { + describe('getMultichainAccounts', () => { + it('gets multichain accounts', () => { + const { service } = setup({ + accounts: [ + // Wallet 1: + MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) + .withEntropySource(MOCK_HD_KEYRING_1.metadata.id) + .withGroupIndex(0) + .get(), + MockAccountBuilder.from(MOCK_SNAP_ACCOUNT_1) + .withEntropySource(MOCK_HD_KEYRING_1.metadata.id) + .withGroupIndex(0) + .get(), + // Wallet 2: + MockAccountBuilder.from(MOCK_HD_ACCOUNT_2) + .withEntropySource(MOCK_HD_KEYRING_2.metadata.id) + .withGroupIndex(0) + .get(), + // Not HD accounts + MOCK_SNAP_ACCOUNT_2, + MOCK_HARDWARE_ACCOUNT_1, + ], + }); + + expect( + service.getMultichainAccounts({ + entropySource: MOCK_HD_KEYRING_1.metadata.id, + }), + ).toHaveLength(1); + expect( + service.getMultichainAccounts({ + entropySource: MOCK_HD_KEYRING_2.metadata.id, + }), + ).toHaveLength(1); + }); + + it('gets multichain accounts with multiple wallets', () => { + const { service } = setup({ + accounts: [ + // Wallet 1: + MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) + .withEntropySource(MOCK_HD_KEYRING_1.metadata.id) + .withGroupIndex(0) + .get(), + MockAccountBuilder.from(MOCK_SNAP_ACCOUNT_1) + .withEntropySource(MOCK_HD_KEYRING_1.metadata.id) + .withGroupIndex(1) + .get(), + ], + }); + + const multichainAccounts = service.getMultichainAccounts({ + entropySource: MOCK_HD_KEYRING_1.metadata.id, + }); + expect(multichainAccounts).toHaveLength(2); // Group index 0 + 1. + + const internalAccounts0 = multichainAccounts[0].getAccounts(); + expect(internalAccounts0).toHaveLength(1); // Just EVM. + expect(internalAccounts0[0].type).toBe(EthAccountType.Eoa); + + const internalAccounts1 = multichainAccounts[1].getAccounts(); + expect(internalAccounts1).toHaveLength(1); // Just SOL. + expect(internalAccounts1[0].type).toBe(SolAccountType.DataAccount); + }); + + it('throws if trying to access an unknown wallet', () => { + const { service } = setup({ + keyrings: [MOCK_HD_KEYRING_1], + accounts: [ + // Wallet 1: + MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) + .withEntropySource(MOCK_HD_KEYRING_1.metadata.id) + .withGroupIndex(0) + .get(), + ], + }); + + // Wallet 2 should not exist, thus, this should throw. + expect(() => + // NOTE: We use `getMultichainAccounts` which uses `#getWallet` under the hood. + service.getMultichainAccounts({ + entropySource: MOCK_HD_KEYRING_2.metadata.id, + }), + ).toThrow('Unknown wallet, no wallet matching this entropy source'); + }); + }); + + describe('getMultichainAccount', () => { + it('gets a specific multichain account', () => { + const accounts = [ + // Wallet 1: + MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) + .withEntropySource(MOCK_HD_KEYRING_1.metadata.id) + .withGroupIndex(0) + .get(), + MockAccountBuilder.from(MOCK_HD_ACCOUNT_2) + .withEntropySource(MOCK_HD_KEYRING_1.metadata.id) + .withGroupIndex(1) + .get(), + ]; + const { service } = setup({ + accounts, + }); + + const groupIndex = 1; + const multichainAccount = service.getMultichainAccount({ + entropySource: MOCK_HD_KEYRING_1.metadata.id, + groupIndex, + }); + expect(multichainAccount.index).toBe(groupIndex); + + const internalAccounts = multichainAccount.getAccounts(); + expect(internalAccounts).toHaveLength(1); + expect(internalAccounts[0]).toStrictEqual(accounts[1]); + }); + + it('throws if trying to access an out-of-bound group index', () => { + const { service } = setup({ + accounts: [ + // Wallet 1: + MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) + .withEntropySource(MOCK_HD_KEYRING_1.metadata.id) + .withGroupIndex(0) + .get(), + ], + }); + + const groupIndex = 1; + expect(() => + service.getMultichainAccount({ + entropySource: MOCK_HD_KEYRING_1.metadata.id, + groupIndex, + }), + ).toThrow(`No multichain account for index: ${groupIndex}`); + }); + }); + + describe('on KeyringController:stateChange', () => { + it('re-sets the internal wallets if a new entropy source is being added', () => { + const keyrings = [MOCK_HD_KEYRING_1]; + const accounts = [ + // Wallet 1: + MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) + .withEntropySource(MOCK_HD_KEYRING_1.metadata.id) + .withGroupIndex(0) + .get(), + ]; + const { service, messenger, mocks } = setup({ + keyrings, + accounts, + }); + + // This wallet does not exist yet. + expect(() => + service.getMultichainAccounts({ + entropySource: MOCK_HD_KEYRING_2.metadata.id, + }), + ).toThrow('Unknown wallet, no wallet matching this entropy source'); + + // Simulate new keyring being added. + keyrings.push(MOCK_HD_KEYRING_2); + // NOTE: We also need to update the account list now, since accounts + // are being used as soon as we construct the multichain account + // wallet. + accounts.push( + // Wallet 2: + MockAccountBuilder.from(MOCK_HD_ACCOUNT_2) + .withEntropySource(MOCK_HD_KEYRING_2.metadata.id) + .withGroupIndex(0) + .get(), + ); + mocks.EvmAccountProvider.getAccounts.mockImplementation(() => accounts); + messenger.publish( + 'KeyringController:stateChange', + { + isUnlocked: true, + keyrings, + }, + [], + ); + + // We should now be able to query that wallet. + expect( + service.getMultichainAccounts({ + entropySource: MOCK_HD_KEYRING_2.metadata.id, + }), + ).toHaveLength(1); + }); + }); +}); diff --git a/packages/multichain-account-service/src/MultichainAccountService.ts b/packages/multichain-account-service/src/MultichainAccountService.ts new file mode 100644 index 00000000000..d445c895ada --- /dev/null +++ b/packages/multichain-account-service/src/MultichainAccountService.ts @@ -0,0 +1,165 @@ +import type { + MultichainAccountWalletId, + AccountProvider, +} from '@metamask/account-api'; +import { + MultichainAccountWallet, + toMultichainAccountWalletId, + type MultichainAccount, +} from '@metamask/account-api'; +import type { EntropySourceId } from '@metamask/keyring-api'; +import type { + KeyringControllerState, + KeyringObject, +} from '@metamask/keyring-controller'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import { EvmAccountProvider } from './providers/EvmAccountProvider'; +import { SolAccountProvider } from './providers/SolAccountProvider'; +import type { MultichainAccountServiceMessenger } from './types'; + +/** + * The options that {@link MultichainAccountService} takes. + */ +type MultichainAccountServiceOptions = { + messenger: MultichainAccountServiceMessenger; +}; + +/** + * Select keyrings from keyring controller state. + * + * @param state - The keyring controller state. + * @returns The keyrings. + */ +function selectKeyringControllerKeyrings(state: KeyringControllerState) { + return state.keyrings; +} + +/** + * Service to expose multichain accounts capabilities. + */ +export class MultichainAccountService { + readonly #messenger: MultichainAccountServiceMessenger; + + readonly #providers: AccountProvider[]; + + readonly #wallets: Map< + MultichainAccountWalletId, + MultichainAccountWallet + >; + + /** + * Constructs a new MultichainAccountService. + * + * @param options - The options. + * @param options.messenger - The messenger suited to this + * MultichainAccountService. + */ + constructor({ messenger }: MultichainAccountServiceOptions) { + this.#messenger = messenger; + this.#wallets = new Map(); + // TODO: Rely on keyring capabilities once the keyring API is used by all keyrings. + this.#providers = [ + new EvmAccountProvider(this.#messenger), + new SolAccountProvider(this.#messenger), + ]; + } + + /** + * Initialize the service and constructs the internal reprensentation of + * multichain accounts and wallets. + */ + init(): void { + // Gather all entropy sources first. + const state = this.#messenger.call('KeyringController:getState'); + this.#setMultichainAccountWallets(state.keyrings); + + this.#messenger.subscribe( + 'KeyringController:stateChange', + (keyrings) => { + this.#setMultichainAccountWallets(keyrings); + }, + selectKeyringControllerKeyrings, + ); + } + + #setMultichainAccountWallets(keyrings: KeyringObject[]) { + for (const keyring of keyrings) { + if (keyring.type === (KeyringTypes.hd as string)) { + // Only HD keyrings have an entropy source/SRP. + const entropySource = keyring.metadata.id; + + // Do not re-create wallets if they exists. Even if a keyrings got new accounts, this + // will be handled by the `*AccountProvider`s which are always in-sync with their + // keyrings and controllers (like the `AccountsController`). + if (!this.#wallets.has(toMultichainAccountWalletId(entropySource))) { + // This will automatically "associate" all multichain accounts for that wallet + // (based on the accounts owned by each account providers). + const wallet = new MultichainAccountWallet({ + entropySource, + providers: this.#providers, + }); + + this.#wallets.set(wallet.id, wallet); + } + } + } + } + + #getWallet( + entropySource: EntropySourceId, + ): MultichainAccountWallet { + const wallet = this.#wallets.get( + toMultichainAccountWalletId(entropySource), + ); + + if (!wallet) { + throw new Error('Unknown wallet, no wallet matching this entropy source'); + } + + return wallet; + } + + /** + * Gets a reference to the multichain account matching this entropy source and group index. + * + * @param options - Options. + * @param options.entropySource - The entropy source of the multichain account. + * @param options.groupIndex - The group index of the multichain account. + * @throws If none multichain account match this entropy source and group index. + * @returns A reference to the multichain account. + */ + getMultichainAccount({ + entropySource, + groupIndex, + }: { + entropySource: EntropySourceId; + groupIndex: number; + }): MultichainAccount { + const multichainAccount = + this.#getWallet(entropySource).getMultichainAccount(groupIndex); + + if (!multichainAccount) { + throw new Error(`No multichain account for index: ${groupIndex}`); + } + + return multichainAccount; + } + + /** + * Gets all multichain accounts for a given entropy source. + * + * @param options - Options. + * @param options.entropySource - The entropy source to query. + * @throws If no multichain accounts match this entropy source. + * @returns A list of all multichain accounts. + */ + getMultichainAccounts({ + entropySource, + }: { + entropySource: EntropySourceId; + }): MultichainAccount[] { + return this.#getWallet(entropySource).getMultichainAccounts(); + } +} diff --git a/packages/multichain-account-service/src/index.ts b/packages/multichain-account-service/src/index.ts new file mode 100644 index 00000000000..f4d1271304a --- /dev/null +++ b/packages/multichain-account-service/src/index.ts @@ -0,0 +1,6 @@ +export type { + MultichainAccountServiceActions, + MultichainAccountServiceEvents, + MultichainAccountServiceMessenger, +} from './types'; +export { MultichainAccountService } from './MultichainAccountService'; diff --git a/packages/multichain-account-service/src/providers/BaseAccountProvider.test.ts b/packages/multichain-account-service/src/providers/BaseAccountProvider.test.ts new file mode 100644 index 00000000000..d379a9514e6 --- /dev/null +++ b/packages/multichain-account-service/src/providers/BaseAccountProvider.test.ts @@ -0,0 +1,43 @@ +import { KeyringAccountEntropyTypeOption } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import type { Json } from '@metamask/utils'; + +import { isBip44Account } from './BaseAccountProvider'; +import { MOCK_HD_ACCOUNT_1 } from '../tests'; + +describe('isBip44Account', () => { + it('returns true if an account is BIP-44 compatible', () => { + expect(isBip44Account(MOCK_HD_ACCOUNT_1)).toBe(true); + }); + + it.each([ + { + tc: 'no entropy options', + options: { + // No entropy + }, + }, + { + tc: 'invalid entropy type', + options: { + entropy: { + type: KeyringAccountEntropyTypeOption.PrivateKey, + }, + }, + }, + ])( + 'returns false if an account is not BIP-44 compatible: $tc', + ({ options }) => { + const account: InternalAccount = { + ...MOCK_HD_ACCOUNT_1, + options: { + ...options, + } as unknown as Record, // To allow `undefined` values. + }; + + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + expect(isBip44Account(account)).toBe(false); + expect(consoleSpy).toHaveBeenCalled(); + }, + ); +}); diff --git a/packages/multichain-account-service/src/providers/BaseAccountProvider.ts b/packages/multichain-account-service/src/providers/BaseAccountProvider.ts new file mode 100644 index 00000000000..dacc4ceab62 --- /dev/null +++ b/packages/multichain-account-service/src/providers/BaseAccountProvider.ts @@ -0,0 +1,88 @@ +import type { AccountProvider } from '@metamask/account-api'; +import type { AccountId } from '@metamask/accounts-controller'; +import type { + KeyringAccount, + KeyringAccountEntropyMnemonicOptions, +} from '@metamask/keyring-api'; +import { KeyringAccountEntropyTypeOption } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import type { MultichainAccountServiceMessenger } from '../types'; + +export type Bip44Account = Account & { + options: { + entropy: KeyringAccountEntropyMnemonicOptions; + }; +}; + +/** + * Checks if an account is BIP-44 compatible. + * + * @param account - The account to be tested. + * @returns True if the account is BIP-44 compatible. + */ +export function isBip44Account( + account: Account, +): account is Bip44Account { + if ( + !account.options.entropy || + account.options.entropy.type !== KeyringAccountEntropyTypeOption.Mnemonic + ) { + console.warn( + "! Found an HD account with invalid entropy options: account won't be associated to its wallet.", + ); + return false; + } + + return true; +} + +export abstract class BaseAccountProvider + implements AccountProvider +{ + protected readonly messenger: MultichainAccountServiceMessenger; + + constructor(messenger: MultichainAccountServiceMessenger) { + this.messenger = messenger; + } + + #getAccounts( + filter: (account: InternalAccount) => boolean = () => true, + ): Bip44Account[] { + const accounts: Bip44Account[] = []; + + for (const account of this.messenger.call( + // NOTE: Even though the name is misleading, this only fetches all internal + // accounts, including EVM and non-EVM. We might wanna change this action + // name once we fully support multichain accounts. + 'AccountsController:listMultichainAccounts', + )) { + if ( + this.isAccountCompatible(account) && + isBip44Account(account) && + filter(account) + ) { + accounts.push(account); + } + } + + return accounts; + } + + getAccounts(): InternalAccount[] { + return this.#getAccounts(); + } + + getAccount(id: AccountId): InternalAccount { + // TODO: Maybe just use a proper find for faster lookup? + const [found] = this.#getAccounts((account) => account.id === id); + + if (!found) { + throw new Error(`Unable to find account: ${id}`); + } + + return found; + } + + abstract isAccountCompatible(account: InternalAccount): boolean; +} diff --git a/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts b/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts new file mode 100644 index 00000000000..459da3643a7 --- /dev/null +++ b/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts @@ -0,0 +1,87 @@ +import type { Messenger } from '@metamask/base-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import { EvmAccountProvider } from './EvmAccountProvider'; +import { + getMultichainAccountServiceMessenger, + getRootMessenger, + MOCK_HD_ACCOUNT_1, + MOCK_HD_ACCOUNT_2, +} from '../tests'; +import type { + AllowedActions, + AllowedEvents, + MultichainAccountServiceActions, + MultichainAccountServiceEvents, +} from '../types'; + +/** + * Sets up a EvmAccountProvider for testing. + * + * @param options - Configuration options for setup. + * @param options.messenger - An optional messenger instance to use. Defaults to a new Messenger. + * @param options.accounts - List of accounts to use. + * @returns An object containing the controller instance and the messenger. + */ +function setup({ + messenger = getRootMessenger(), + accounts = [], +}: { + messenger?: Messenger< + MultichainAccountServiceActions | AllowedActions, + MultichainAccountServiceEvents | AllowedEvents + >; + accounts?: InternalAccount[]; +} = {}): { + provider: EvmAccountProvider; + messenger: Messenger< + MultichainAccountServiceActions | AllowedActions, + MultichainAccountServiceEvents | AllowedEvents + >; +} { + messenger.registerActionHandler( + 'AccountsController:listMultichainAccounts', + () => accounts, + ); + + const provider = new EvmAccountProvider( + getMultichainAccountServiceMessenger(messenger), + ); + + return { + provider, + messenger, + }; +} + +describe('EvmAccountProvider', () => { + it('gets accounts', () => { + const accounts = [MOCK_HD_ACCOUNT_1, MOCK_HD_ACCOUNT_2]; + const { provider } = setup({ + accounts, + }); + + expect(provider.getAccounts()).toStrictEqual(accounts); + }); + + it('gets a specific account', () => { + const account = MOCK_HD_ACCOUNT_1; + const { provider } = setup({ + accounts: [account], + }); + + expect(provider.getAccount(account.id)).toStrictEqual(account); + }); + + it('throws if account does not exist', () => { + const account = MOCK_HD_ACCOUNT_1; + const { provider } = setup({ + accounts: [account], + }); + + const unknownAccount = MOCK_HD_ACCOUNT_2; + expect(() => provider.getAccount(unknownAccount.id)).toThrow( + `Unable to find account: ${unknownAccount.id}`, + ); + }); +}); diff --git a/packages/multichain-account-service/src/providers/EvmAccountProvider.ts b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts new file mode 100644 index 00000000000..d950baca9f3 --- /dev/null +++ b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts @@ -0,0 +1,14 @@ +import { EthAccountType } from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import { BaseAccountProvider } from './BaseAccountProvider'; + +export class EvmAccountProvider extends BaseAccountProvider { + isAccountCompatible(account: InternalAccount): boolean { + return ( + account.type === EthAccountType.Eoa && + account.metadata.keyring.type === (KeyringTypes.hd as string) + ); + } +} diff --git a/packages/multichain-account-service/src/providers/SolAccountProvider.test.ts b/packages/multichain-account-service/src/providers/SolAccountProvider.test.ts new file mode 100644 index 00000000000..83a13ba0ed4 --- /dev/null +++ b/packages/multichain-account-service/src/providers/SolAccountProvider.test.ts @@ -0,0 +1,87 @@ +import type { Messenger } from '@metamask/base-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import { SolAccountProvider } from './SolAccountProvider'; +import { + getMultichainAccountServiceMessenger, + getRootMessenger, + MOCK_HD_ACCOUNT_1, + MOCK_SNAP_ACCOUNT_1, +} from '../tests'; +import type { + AllowedActions, + AllowedEvents, + MultichainAccountServiceActions, + MultichainAccountServiceEvents, +} from '../types'; + +/** + * Sets up a SolAccountProvider for testing. + * + * @param options - Configuration options for setup. + * @param options.messenger - An optional messenger instance to use. Defaults to a new Messenger. + * @param options.accounts - List of accounts to use. + * @returns An object containing the controller instance and the messenger. + */ +function setup({ + messenger = getRootMessenger(), + accounts = [], +}: { + messenger?: Messenger< + MultichainAccountServiceActions | AllowedActions, + MultichainAccountServiceEvents | AllowedEvents + >; + accounts?: InternalAccount[]; +} = {}): { + provider: SolAccountProvider; + messenger: Messenger< + MultichainAccountServiceActions | AllowedActions, + MultichainAccountServiceEvents | AllowedEvents + >; +} { + messenger.registerActionHandler( + 'AccountsController:listMultichainAccounts', + () => accounts, + ); + + const provider = new SolAccountProvider( + getMultichainAccountServiceMessenger(messenger), + ); + + return { + provider, + messenger, + }; +} + +describe('SolAccountProvider', () => { + it('gets accounts', () => { + const accounts = [MOCK_SNAP_ACCOUNT_1]; + const { provider } = setup({ + accounts, + }); + + expect(provider.getAccounts()).toStrictEqual(accounts); + }); + + it('gets a specific account', () => { + const account = MOCK_SNAP_ACCOUNT_1; + const { provider } = setup({ + accounts: [account], + }); + + expect(provider.getAccount(account.id)).toStrictEqual(account); + }); + + it('throws if account does not exist', () => { + const account = MOCK_SNAP_ACCOUNT_1; + const { provider } = setup({ + accounts: [account], + }); + + const unknownAccount = MOCK_HD_ACCOUNT_1; + expect(() => provider.getAccount(unknownAccount.id)).toThrow( + `Unable to find account: ${unknownAccount.id}`, + ); + }); +}); diff --git a/packages/multichain-account-service/src/providers/SolAccountProvider.ts b/packages/multichain-account-service/src/providers/SolAccountProvider.ts new file mode 100644 index 00000000000..8d92b94f7f0 --- /dev/null +++ b/packages/multichain-account-service/src/providers/SolAccountProvider.ts @@ -0,0 +1,17 @@ +import { SolAccountType } from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; +import type { SnapId } from '@metamask/snaps-sdk'; + +import { BaseAccountProvider } from './BaseAccountProvider'; + +export class SolAccountProvider extends BaseAccountProvider { + static SOLANA_SNAP_ID = 'npm:@metamask/solana-wallet-snap' as SnapId; + + isAccountCompatible(account: InternalAccount): boolean { + return ( + account.type === SolAccountType.DataAccount && + account.metadata.keyring.type === (KeyringTypes.snap as string) + ); + } +} diff --git a/packages/multichain-account-service/src/tests/accounts.ts b/packages/multichain-account-service/src/tests/accounts.ts new file mode 100644 index 00000000000..10191238f50 --- /dev/null +++ b/packages/multichain-account-service/src/tests/accounts.ts @@ -0,0 +1,190 @@ +import type { EntropySourceId } from '@metamask/keyring-api'; +import { + EthAccountType, + EthMethod, + EthScope, + KeyringAccountEntropyTypeOption, + SolAccountType, + SolMethod, + SolScope, +} from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import { isBip44Account } from '../providers/BaseAccountProvider'; + +const ETH_EOA_METHODS = [ + EthMethod.PersonalSign, + EthMethod.Sign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV1, + EthMethod.SignTypedDataV3, + EthMethod.SignTypedDataV4, +] as const; + +const SOL_METHODS = Object.values(SolMethod); + +export const MOCK_SNAP_1 = { + id: 'local:mock-snap-id-1', + name: 'Mock Snap 1', + enabled: true, + manifest: { + proposedName: 'Mock Snap 1', + }, +}; + +export const MOCK_SNAP_2 = { + id: 'local:mock-snap-id-2', + name: 'Mock Snap 2', + enabled: true, + manifest: { + proposedName: 'Mock Snap 2', + }, +}; + +export const MOCK_ENTROPY_SOURCE_1 = 'mock-keyring-id-1'; +export const MOCK_ENTROPY_SOURCE_2 = 'mock-keyring-id-2'; + +export const MOCK_HD_KEYRING_1 = { + type: KeyringTypes.hd, + metadata: { id: MOCK_ENTROPY_SOURCE_1, name: 'HD Keyring 1' }, + accounts: ['0x123'], +}; + +export const MOCK_HD_KEYRING_2 = { + type: KeyringTypes.hd, + metadata: { id: MOCK_ENTROPY_SOURCE_2, name: 'HD Keyring 2' }, + accounts: ['0x456'], +}; + +export const MOCK_HD_ACCOUNT_1: InternalAccount = { + id: 'mock-id-1', + address: '0x123', + options: { + entropy: { + type: KeyringAccountEntropyTypeOption.Mnemonic, + id: MOCK_HD_KEYRING_1.metadata.id, + groupIndex: 0, + derivationPath: '', + }, + }, + methods: [...ETH_EOA_METHODS], + type: EthAccountType.Eoa, + scopes: [EthScope.Eoa], + metadata: { + name: 'Account 1', + keyring: { type: KeyringTypes.hd }, + importTime: 0, + lastSelected: 0, + nameLastUpdatedAt: 0, + }, +}; + +export const MOCK_HD_ACCOUNT_2: InternalAccount = { + id: 'mock-id-2', + address: '0x456', + options: { + entropy: { + type: KeyringAccountEntropyTypeOption.Mnemonic, + id: MOCK_HD_KEYRING_2.metadata.id, + groupIndex: 0, + derivationPath: '', + }, + }, + methods: [...ETH_EOA_METHODS], + type: EthAccountType.Eoa, + scopes: [EthScope.Eoa], + metadata: { + name: 'Account 2', + keyring: { type: KeyringTypes.hd }, + importTime: 0, + lastSelected: 0, + nameLastUpdatedAt: 0, + }, +}; + +export const MOCK_SNAP_ACCOUNT_1: InternalAccount = { + id: 'mock-snap-id-1', + address: 'aabbccdd', + options: { + entropy: { + type: KeyringAccountEntropyTypeOption.Mnemonic, + // NOTE: shares entropy with MOCK_HD_ACCOUNT_2 + id: MOCK_HD_KEYRING_2.metadata.id, + groupIndex: 0, + derivationPath: '', + }, + }, + methods: SOL_METHODS, + type: SolAccountType.DataAccount, + scopes: [SolScope.Mainnet], + metadata: { + name: 'Snap Account 1', + keyring: { type: KeyringTypes.snap }, + snap: MOCK_SNAP_1, + importTime: 0, + lastSelected: 0, + }, +}; + +export const MOCK_SNAP_ACCOUNT_2: InternalAccount = { + id: 'mock-snap-id-2', + address: '0x789', + options: {}, + methods: [...ETH_EOA_METHODS], + type: EthAccountType.Eoa, + scopes: [EthScope.Eoa], + metadata: { + name: 'Snap Acc 2', + keyring: { type: KeyringTypes.snap }, + snap: MOCK_SNAP_2, + importTime: 0, + lastSelected: 0, + }, +}; + +export const MOCK_HARDWARE_ACCOUNT_1: InternalAccount = { + id: 'mock-hardware-id-1', + address: '0xABC', + options: {}, + methods: [...ETH_EOA_METHODS], + type: EthAccountType.Eoa, + scopes: [EthScope.Eoa], + metadata: { + name: 'Hardware Acc 1', + keyring: { type: KeyringTypes.ledger }, + importTime: 0, + lastSelected: 0, + }, +}; + +export class MockAccountBuilder { + readonly #account: InternalAccount; + + constructor(account: InternalAccount) { + // Make a deep-copy to avoid mutating the same ref. + this.#account = JSON.parse(JSON.stringify(account)); + } + + static from(account: InternalAccount): MockAccountBuilder { + return new MockAccountBuilder(account); + } + + withEntropySource(entropySource: EntropySourceId) { + if (isBip44Account(this.#account)) { + this.#account.options.entropy.id = entropySource; + } + return this; + } + + withGroupIndex(groupIndex: number) { + if (isBip44Account(this.#account)) { + this.#account.options.entropy.groupIndex = groupIndex; + } + return this; + } + + get() { + return this.#account; + } +} diff --git a/packages/multichain-account-service/src/tests/index.ts b/packages/multichain-account-service/src/tests/index.ts new file mode 100644 index 00000000000..69176bd5f7f --- /dev/null +++ b/packages/multichain-account-service/src/tests/index.ts @@ -0,0 +1,2 @@ +export * from './accounts'; +export * from './messenger'; diff --git a/packages/multichain-account-service/src/tests/messenger.ts b/packages/multichain-account-service/src/tests/messenger.ts new file mode 100644 index 00000000000..be139d579e6 --- /dev/null +++ b/packages/multichain-account-service/src/tests/messenger.ts @@ -0,0 +1,44 @@ +import { Messenger } from '@metamask/base-controller'; + +import type { + AllowedActions, + AllowedEvents, + MultichainAccountServiceActions, + MultichainAccountServiceEvents, + MultichainAccountServiceMessenger, +} from '../types'; + +/** + * Creates a new root messenger instance for testing. + * + * @returns A new Messenger instance. + */ +export function getRootMessenger() { + return new Messenger< + MultichainAccountServiceActions | AllowedActions, + MultichainAccountServiceEvents | AllowedEvents + >(); +} + +/** + * Retrieves a restricted messenger for the MultichainAccountService. + * + * @param messenger - The root messenger instance. Defaults to a new Messenger created by getRootMessenger(). + * @returns The restricted messenger for the MultichainAccountService. + */ +export function getMultichainAccountServiceMessenger( + messenger: ReturnType, +): MultichainAccountServiceMessenger { + return messenger.getRestricted({ + name: 'MultichainAccountService', + allowedEvents: ['KeyringController:stateChange'], + allowedActions: [ + 'AccountsController:getAccount', + 'AccountsController:getAccountByAddress', + 'AccountsController:listMultichainAccounts', + 'SnapController:handleRequest', + 'KeyringController:withKeyring', + 'KeyringController:getState', + ], + }); +} diff --git a/packages/multichain-account-service/src/types.ts b/packages/multichain-account-service/src/types.ts new file mode 100644 index 00000000000..129ae0c853a --- /dev/null +++ b/packages/multichain-account-service/src/types.ts @@ -0,0 +1,53 @@ +import type { + AccountsControllerGetAccountAction, + AccountsControllerGetAccountByAddressAction, + AccountsControllerListMultichainAccountsAction, +} from '@metamask/accounts-controller'; +import type { RestrictedMessenger } from '@metamask/base-controller'; +import type { + KeyringControllerGetStateAction, + KeyringControllerStateChangeEvent, + KeyringControllerWithKeyringAction, +} from '@metamask/keyring-controller'; +import type { HandleSnapRequest as SnapControllerHandleSnapRequestAction } from '@metamask/snaps-controllers'; + +/** + * All actions that {@link MultichainAccountService} registers so that other + * modules can call them. + */ +export type MultichainAccountServiceActions = never; +/** + * All events that {@link MultichainAccountService} publishes so that other modules + * can subscribe to them. + */ +export type MultichainAccountServiceEvents = never; + +/** + * All actions registered by other modules that {@link MultichainAccountService} + * calls. + */ +export type AllowedActions = + | AccountsControllerListMultichainAccountsAction + | AccountsControllerGetAccountAction + | AccountsControllerGetAccountByAddressAction + | SnapControllerHandleSnapRequestAction + | KeyringControllerWithKeyringAction + | KeyringControllerGetStateAction; + +/** + * All events published by other modules that {@link MultichainAccountService} + * subscribes to. + */ +export type AllowedEvents = KeyringControllerStateChangeEvent; + +/** + * The messenger restricted to actions and events that + * {@link MultichainAccountService} needs to access. + */ +export type MultichainAccountServiceMessenger = RestrictedMessenger< + 'MultichainAccountService', + MultichainAccountServiceActions | AllowedActions, + MultichainAccountServiceEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; diff --git a/packages/multichain-account-service/tsconfig.build.json b/packages/multichain-account-service/tsconfig.build.json new file mode 100644 index 00000000000..c01fbe218d1 --- /dev/null +++ b/packages/multichain-account-service/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../accounts-controller/tsconfig.build.json" }, + { "path": "../keyring-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/multichain-account-service/tsconfig.json b/packages/multichain-account-service/tsconfig.json new file mode 100644 index 00000000000..c67da70b6eb --- /dev/null +++ b/packages/multichain-account-service/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { "path": "../base-controller" }, + { "path": "../accounts-controller" }, + { "path": "../keyring-controller" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/multichain-account-service/typedoc.json b/packages/multichain-account-service/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/multichain-account-service/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/teams.json b/teams.json index 7d805a7a364..444c99b3d2b 100644 --- a/teams.json +++ b/teams.json @@ -24,6 +24,7 @@ "metamask/logging-controller": "team-confirmations", "metamask/message-manager": "team-confirmations", "metamask/messenger": "team-wallet-framework", + "metamask/multichain-account-service": "team-accounts", "metamask/multichain-api-middleware": "team-wallet-api-platform", "metamask/multichain-network-controller": "team-wallet-api-platform", "metamask/name-controller": "team-confirmations", diff --git a/tsconfig.build.json b/tsconfig.build.json index 645e49bf322..4316194d801 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -28,6 +28,7 @@ { "path": "./packages/logging-controller/tsconfig.build.json" }, { "path": "./packages/message-manager/tsconfig.build.json" }, { "path": "./packages/messenger/tsconfig.build.json" }, + { "path": "./packages/multichain-account-service/tsconfig.build.json" }, { "path": "./packages/multichain-api-middleware/tsconfig.build.json" }, { "path": "./packages/multichain-network-controller/tsconfig.build.json" }, { diff --git a/tsconfig.json b/tsconfig.json index 23293be18e9..ed869d305fa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,6 +33,7 @@ { "path": "./packages/keyring-controller" }, { "path": "./packages/message-manager" }, { "path": "./packages/messenger" }, + { "path": "./packages/multichain-account-service" }, { "path": "./packages/multichain-api-middleware" }, { "path": "./packages/multichain-network-controller" }, { "path": "./packages/multichain-transactions-controller" }, diff --git a/yarn.lock b/yarn.lock index 38b8008fa4d..2e426a326e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3807,6 +3807,41 @@ __metadata: languageName: node linkType: hard +"@metamask/multichain-account-service@workspace:packages/multichain-account-service": + version: 0.0.0-use.local + resolution: "@metamask/multichain-account-service@workspace:packages/multichain-account-service" + dependencies: + "@metamask/account-api": "npm:^0.2.0" + "@metamask/accounts-controller": "npm:^31.0.0" + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^8.0.1" + "@metamask/eth-snap-keyring": "npm:^14.0.0" + "@metamask/keyring-api": "npm:^19.0.0" + "@metamask/keyring-controller": "npm:^22.1.0" + "@metamask/keyring-internal-api": "npm:^7.0.0" + "@metamask/keyring-snap-client": "npm:^6.0.0" + "@metamask/providers": "npm:^22.1.0" + "@metamask/snaps-controllers": "npm:^14.0.1" + "@metamask/snaps-sdk": "npm:^9.0.0" + "@metamask/snaps-utils": "npm:^11.0.0" + "@metamask/superstruct": "npm:^3.1.0" + "@types/jest": "npm:^27.4.1" + deepmerge: "npm:^4.2.2" + jest: "npm:^27.5.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" + webextension-polyfill: "npm:^0.12.0" + peerDependencies: + "@metamask/accounts-controller": ^31.0.0 + "@metamask/keyring-controller": ^22.0.0 + "@metamask/providers": ^22.0.0 + "@metamask/snaps-controllers": ^14.0.0 + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + languageName: unknown + linkType: soft + "@metamask/multichain-api-middleware@workspace:packages/multichain-api-middleware": version: 0.0.0-use.local resolution: "@metamask/multichain-api-middleware@workspace:packages/multichain-api-middleware"