diff --git a/README.md b/README.md index eebd43dd694..3048da163f9 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Each package in this repository has its own README where you can find installati +- [`@metamask/account-group-controller`](packages/account-group-controller) - [`@metamask/accounts-controller`](packages/accounts-controller) - [`@metamask/address-book-controller`](packages/address-book-controller) - [`@metamask/announcement-controller`](packages/announcement-controller) @@ -74,6 +75,7 @@ Each package in this repository has its own README where you can find installati %%{ init: { 'flowchart': { 'curve': 'bumpX' } } }%% graph LR; linkStyle default opacity:0.5 + account_group_controller(["@metamask/account-group-controller"]); accounts_controller(["@metamask/accounts-controller"]); address_book_controller(["@metamask/address-book-controller"]); announcement_controller(["@metamask/announcement-controller"]); @@ -119,6 +121,9 @@ linkStyle default opacity:0.5 token_search_discovery_controller(["@metamask/token-search-discovery-controller"]); transaction_controller(["@metamask/transaction-controller"]); user_operation_controller(["@metamask/user-operation-controller"]); + account_group_controller --> base_controller; + account_group_controller --> accounts_controller; + account_group_controller --> keyring_controller; accounts_controller --> base_controller; accounts_controller --> keyring_controller; accounts_controller --> network_controller; diff --git a/packages/account-group-controller/CHANGELOG.md b/packages/account-group-controller/CHANGELOG.md new file mode 100644 index 00000000000..b518709c7b8 --- /dev/null +++ b/packages/account-group-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/account-group-controller/LICENSE b/packages/account-group-controller/LICENSE new file mode 100644 index 00000000000..7d002dced3a --- /dev/null +++ b/packages/account-group-controller/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2025 MetaMask + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/account-group-controller/README.md b/packages/account-group-controller/README.md new file mode 100644 index 00000000000..68e4f08fc36 --- /dev/null +++ b/packages/account-group-controller/README.md @@ -0,0 +1,15 @@ +# `@metamask/account-group-controller` + +example + +## Installation + +`yarn add @metamask/account-group-controller` + +or + +`npm install @metamask/account-group-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/account-group-controller/jest.config.js b/packages/account-group-controller/jest.config.js new file mode 100644 index 00000000000..ca084133399 --- /dev/null +++ b/packages/account-group-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/account-group-controller/package.json b/packages/account-group-controller/package.json new file mode 100644 index 00000000000..6077164189d --- /dev/null +++ b/packages/account-group-controller/package.json @@ -0,0 +1,74 @@ +{ + "name": "@metamask/account-group-controller", + "version": "0.0.0", + "description": "Controller to group account together based on some pre-defined rules", + "keywords": [ + "MetaMask", + "Ethereum" + ], + "homepage": "https://github.com/MetaMask/core/tree/main/packages/account-group-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:validate": "../../scripts/validate-changelog.sh @metamask/account-group-controller", + "publish:preview": "yarn npm publish --tag preview", + "test": "jest --reporters=jest-silent-reporter", + "test:clean": "jest --clearCache", + "test:verbose": "jest --verbose", + "test:watch": "jest --watch" + }, + "dependencies": { + "@metamask/base-controller": "^8.0.1" + }, + "devDependencies": { + "@metamask/accounts-controller": "^29.0.0", + "@metamask/auto-changelog": "^3.4.4", + "@metamask/keyring-api": "^17.4.0", + "@metamask/keyring-controller": "^22.0.0", + "@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" + }, + "peerDependencies": { + "@metamask/accounts-controller": "^29.0.0", + "@metamask/keyring-controller": "^22.0.0" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/account-group-controller/src/AccountGroupController.test.ts b/packages/account-group-controller/src/AccountGroupController.test.ts new file mode 100644 index 00000000000..831344f9ea0 --- /dev/null +++ b/packages/account-group-controller/src/AccountGroupController.test.ts @@ -0,0 +1,359 @@ +import { Messenger } from '@metamask/base-controller'; +import { + EthAccountType, + EthMethod, + EthScope, + SolAccountType, + SolScope, +} from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +import { + AccountGroupController, + DEFAULT_SUB_GROUP, + getDefaultAccountGroupControllerState, + type AccountGroupControllerActions, + type AccountGroupControllerEvents, + type AccountGroupControllerState, + type AllowedActions, + type AllowedEvents, +} from './AccountGroupController'; +import type { + AccountGroupControllerMessenger, + AccountGroupId, +} from './AccountGroupController'; + +const ETH_EOA_METHODS = [ + EthMethod.PersonalSign, + EthMethod.Sign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV1, + EthMethod.SignTypedDataV3, + EthMethod.SignTypedDataV4, +] as const; + +/** + * Creates a new root messenger instance for testing. + * + * @returns A new Messenger instance. + */ +function getRootMessenger() { + return new Messenger< + AccountGroupControllerActions | AllowedActions, + AccountGroupControllerEvents | AllowedEvents + >(); +} + +/** + * Retrieves a restricted messenger for the AccountGroupController. + * + * @param messenger - The root messenger instance. Defaults to a new Messenger created by getRootMessenger(). + * @returns The restricted messenger for the AccountGroupController. + */ +function getAccountGroupControllerMessenger( + messenger = getRootMessenger(), +): AccountGroupControllerMessenger { + return messenger.getRestricted({ + name: 'AccountGroupController', + allowedEvents: [ + 'AccountsController:accountAdded', + 'AccountsController:accountRemoved', + ], + allowedActions: ['AccountsController:listMultichainAccounts'], + }); +} + +/** + * Sets up the AccountGroupController for testing. + * + * @param options - Configuration options for setup. + * @param options.state - Partial initial state for the controller. Defaults to empty object. + * @param options.messenger - An optional messenger instance to use. Defaults to a new Messenger. + * @returns An object containing the controller instance and the messenger. + */ +function setup({ + state = {}, + messenger = getRootMessenger(), +}: { + state?: Partial; + messenger?: Messenger< + AccountGroupControllerActions | AllowedActions, + AccountGroupControllerEvents | AllowedEvents + >; +}): { + controller: AccountGroupController; + messenger: Messenger< + AccountGroupControllerActions | AllowedActions, + AccountGroupControllerEvents | AllowedEvents + >; +} { + const controller = new AccountGroupController({ + messenger: getAccountGroupControllerMessenger(messenger), + state: { ...getDefaultAccountGroupControllerState(), ...state }, + }); + + return { controller, messenger }; +} + +const MOCK_HD_ACCOUNT_1: InternalAccount = { + id: 'mock-id-1', + address: '0x123', + options: { + entropySource: 'mock-keyring-id-1', + }, + methods: [...ETH_EOA_METHODS], + type: EthAccountType.Eoa, + scopes: [EthScope.Eoa], + metadata: { + name: 'Account 1', + keyring: { type: KeyringTypes.hd }, + importTime: 1691565967600, + lastSelected: 1691565967656, + nameLastUpdatedAt: 1691565967656, + }, +}; + +const MOCK_HD_ACCOUNT_2: InternalAccount = { + id: 'mock-id-2', + address: '0x456', + options: { + entropySource: 'mock-keyring-id-2', + }, + methods: [...ETH_EOA_METHODS], + type: EthAccountType.Eoa, + scopes: [EthScope.Eoa], + metadata: { + name: 'Account 1', + keyring: { type: KeyringTypes.hd }, + importTime: 1691565967600, + lastSelected: 1691565967656, + nameLastUpdatedAt: 1691565967656, + }, +}; + +const MOCK_SNAP_ACCOUNT_1: InternalAccount = { + id: 'mock-snap-id-1', + address: 'aabbccdd', + options: { + entropySource: 'mock-keyring-id-2', + }, + methods: [...ETH_EOA_METHODS], + type: SolAccountType.DataAccount, + scopes: [SolScope.Mainnet, SolScope.Devnet, SolScope.Testnet], + metadata: { + name: '', + keyring: { type: KeyringTypes.snap }, + snap: { + enabled: true, + id: 'mock-snap-id-1', + name: 'snap-name-1', + }, + importTime: 1691565967600, + lastSelected: 1955565967656, + }, +}; + +const MOCK_SNAP_ACCOUNT_2: InternalAccount = { + id: 'mock-snap-id-2', + address: '0x789', + options: { + // Not an Snap HD account, so no `entropySource`. + }, + methods: [...ETH_EOA_METHODS], + type: EthAccountType.Eoa, + scopes: [EthScope.Eoa], + metadata: { + name: '', + keyring: { type: KeyringTypes.snap }, + snap: { + enabled: true, + id: 'mock-snap-id-2', + name: 'snap-name-2', + }, + importTime: 1691565967600, + lastSelected: 1955565967656, + }, +}; + +const MOCK_HARDWARE_ACCOUNT_1: InternalAccount = { + id: 'mock-hardware-id-1', + address: '0x123', + options: {}, + methods: [...ETH_EOA_METHODS], + type: EthAccountType.Eoa, + scopes: [EthScope.Eoa], + metadata: { + name: '', + keyring: { type: KeyringTypes.ledger }, + importTime: 1691565967600, + lastSelected: 1955565967656, + }, +}; + +describe('AccountGroupController', () => { + describe('updateAccountGroups', () => { + it('should group accounts by entropy source, then snapId, then wallet type', async () => { + const { controller, messenger } = setup({}); + + messenger.registerActionHandler( + 'AccountsController:listMultichainAccounts', + () => { + return [ + MOCK_HD_ACCOUNT_1, + MOCK_HD_ACCOUNT_2, + MOCK_SNAP_ACCOUNT_1, + MOCK_SNAP_ACCOUNT_2, + MOCK_HARDWARE_ACCOUNT_1, + ]; + }, + ); + + await controller.updateAccountGroups(); + + expect(controller.state.accountGroups.groups).toStrictEqual({ + 'mock-keyring-id-1': { + [DEFAULT_SUB_GROUP]: [MOCK_HD_ACCOUNT_1.id], + }, + 'mock-keyring-id-2': { + [DEFAULT_SUB_GROUP]: [MOCK_HD_ACCOUNT_2.id, MOCK_SNAP_ACCOUNT_1.id], + }, + 'mock-snap-id-2': { + [DEFAULT_SUB_GROUP]: [MOCK_SNAP_ACCOUNT_2.id], + }, + [KeyringTypes.ledger]: { + [DEFAULT_SUB_GROUP]: [MOCK_HARDWARE_ACCOUNT_1.id], + }, + }); + }); + + it('should warn and fall back to wallet type grouping if an HD account is missing entropySource', async () => { + const consoleWarnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => undefined); + const { controller, messenger } = setup({}); + + const mockHdAccountWithoutEntropy: InternalAccount = { + id: 'hd-account-no-entropy', + address: '0xHDADD', + metadata: { + name: 'HD Account Without Entropy', + keyring: { + type: KeyringTypes.hd, + }, + importTime: Date.now(), + lastSelected: Date.now(), + }, + methods: [...ETH_EOA_METHODS], + options: {}, + type: EthAccountType.Eoa, + scopes: [EthScope.Eoa], + }; + + const listAccountsMock = jest + .fn() + .mockReturnValue([mockHdAccountWithoutEntropy]); + messenger.registerActionHandler( + 'AccountsController:listMultichainAccounts', + listAccountsMock, + ); + + await controller.updateAccountGroups(); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + "! Found an HD account with no entropy source: account won't be associated to its wallet", + ); + + expect( + controller.state.accountGroups.groups[KeyringTypes.hd]?.[ + DEFAULT_SUB_GROUP + ], + ).toContain(mockHdAccountWithoutEntropy.id); + + expect( + controller.state.accountGroups.groups[ + undefined as unknown as AccountGroupId + ], + ).toBeUndefined(); + + consoleWarnSpy.mockRestore(); + }); + }); + + describe('listAccountGroups', () => { + it('should return an empty array if no groups exist', async () => { + const { controller } = setup({ + state: { + accountGroups: { groups: {} }, + accountGroupsMetadata: {}, + }, + }); + + const result = await controller.listAccountGroups(); + expect(result).toStrictEqual([]); + }); + + it('should correctly map group data and metadata to AccountGroup objects', async () => { + const group1Id = 'group-id-1' as AccountGroupId; + const group2Id = 'group-id-2' as AccountGroupId; + + const initialState: AccountGroupControllerState = { + accountGroups: { + groups: { + [group1Id]: { + [DEFAULT_SUB_GROUP]: ['account-1', 'account-2'], + }, + [group2Id]: { + 'sub-group-x': ['account-3'], + }, + }, + }, + accountGroupsMetadata: { + [group1Id]: { name: 'Group 1 Name' }, + [group2Id]: { name: 'Group 2 Name' }, + }, + }; + + const { controller } = setup({ state: initialState }); + const result = await controller.listAccountGroups(); + + expect(result).toStrictEqual([ + { + id: group1Id, + name: 'Group 1 Name', + subGroups: { + [DEFAULT_SUB_GROUP]: ['account-1', 'account-2'], + }, + }, + { + id: group2Id, + name: 'Group 2 Name', + subGroups: { + 'sub-group-x': ['account-3'], + }, + }, + ]); + }); + + it('should throw a TypeError if metadata is missing for a group', async () => { + const groupIdWithMissingMetadata = 'group-missing-meta' as AccountGroupId; + const initialState: Partial = { + accountGroups: { + groups: { + [groupIdWithMissingMetadata]: { + [DEFAULT_SUB_GROUP]: ['account-x'], + }, + }, + }, + // Metadata for groupIdWithMissingMetadata is deliberately omitted + accountGroupsMetadata: {}, + }; + + const { controller } = setup({ state: initialState }); + + // Current implementation will throw: Cannot read properties of undefined (reading 'name') + // which is a TypeError. + await expect(controller.listAccountGroups()).rejects.toThrow(TypeError); + }); + }); +}); diff --git a/packages/account-group-controller/src/AccountGroupController.ts b/packages/account-group-controller/src/AccountGroupController.ts new file mode 100644 index 00000000000..8247f30567b --- /dev/null +++ b/packages/account-group-controller/src/AccountGroupController.ts @@ -0,0 +1,268 @@ +import type { + AccountId, + AccountsControllerAccountAddedEvent, + AccountsControllerAccountRemovedEvent, + AccountsControllerListMultichainAccountsAction, +} from '@metamask/accounts-controller'; +import type { StateMetadata } from '@metamask/base-controller'; +import { + type ControllerGetStateAction, + type ControllerStateChangeEvent, + type RestrictedMessenger, + BaseController, +} from '@metamask/base-controller'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; + +const controllerName = 'AccountGroupController'; + +export type AccountGroupId = string; + +// NOTES: +// - Maybe add a `metadata` / `flags` for each groups (or at least, top-level ones) + +export type AccountGroup = { + id: AccountGroupId; + name: string; + subGroups: { + [accountSubGroup: AccountGroupId]: AccountId[]; + }; +}; + +export type AccountGroupMetadata = { + name: string; +}; + +export type AccountGroupControllerState = { + accountGroups: { + groups: { + // Wallet + [accountGroup: AccountGroupId]: { + // Multichain Account OR Account Group + [accountSubGroup: AccountGroupId]: AccountId[]; // Blockchain Accounts + }; + }; + }; + accountGroupsMetadata: { + [accountGroup: AccountGroupId]: AccountGroupMetadata; + }; +}; + +export type AccountGroupControllerGetStateAction = ControllerGetStateAction< + typeof controllerName, + AccountGroupControllerState +>; + +export type AccountGroupControllerListAccountGroupsAction = { + type: `${typeof controllerName}:listAccountGroups`; + handler: AccountGroupController['listAccountGroups']; +}; + +export type AllowedActions = AccountsControllerListMultichainAccountsAction; + +export type AccountGroupControllerActions = + AccountGroupControllerListAccountGroupsAction; + +export type AccountGroupControllerChangeEvent = ControllerStateChangeEvent< + typeof controllerName, + AccountGroupControllerState +>; + +export type AllowedEvents = + | AccountsControllerAccountAddedEvent + | AccountsControllerAccountRemovedEvent; + +export type AccountGroupControllerEvents = never; + +export type AccountGroupControllerMessenger = RestrictedMessenger< + typeof controllerName, + AccountGroupControllerActions | AllowedActions, + AccountGroupControllerEvents | AllowedEvents, + AllowedActions['type'], + AllowedEvents['type'] +>; + +const accountGroupControllerMetadata: StateMetadata = + { + accountGroups: { + persist: false, // We do re-recompute this state everytime. + anonymous: false, + }, + accountGroupsMetadata: { + persist: false, // TODO: Change it to true once we have data to persist. + anonymous: false, + }, + }; + +/** + * Gets default state of the `AccountGroupController`. + * + * @returns The default state of the `AccountGroupController`. + */ +export function getDefaultAccountGroupControllerState(): AccountGroupControllerState { + return { + accountGroups: { + groups: {}, + }, + accountGroupsMetadata: {}, + }; +} + +// TODO: For now we use this for the 2nd-level of the tree until we implements proper multichain accounts. +// QUESTION: This might still be useful for accounts that are not multichains? +export const DEFAULT_SUB_GROUP = 'default'; + +/** + * Cast a generic ID to a group ID. + * + * @param id - Generic ID. + * @returns The group ID. + */ +function asAccountGroupId(id: unknown): AccountGroupId { + // For now, we're just casting, but we could think of something better (e.g. "wallet:", + // "snap:", "keyring:", etc..). + return id as AccountGroupId; +} + +export class AccountGroupController extends BaseController< + typeof controllerName, + AccountGroupControllerState, + AccountGroupControllerMessenger +> { + /** + * Constructor for AccountGroupController. + * + * @param options - The controller options. + * @param options.messenger - The messenger object. + * @param options.state - Initial state to set on this controller + */ + constructor({ + messenger, + state, + }: { + messenger: AccountGroupControllerMessenger; + state?: AccountGroupControllerState; + }) { + super({ + messenger, + name: controllerName, + metadata: accountGroupControllerMetadata, + state: { + ...getDefaultAccountGroupControllerState(), + ...state, + }, + }); + } + + #hasKeyringType(account: InternalAccount, type: KeyringTypes): boolean { + return account.metadata.keyring.type === (type as string); + } + + #groupByEntropySource(account: InternalAccount): AccountGroupId | undefined { + if (this.#hasKeyringType(account, KeyringTypes.hd)) { + // TODO: Maybe use superstruct to validate the structure of HD account since they are not strongly-typed for now? + if (!account.options.entropySource) { + console.warn( + "! Found an HD account with no entropy source: account won't be associated to its wallet", + ); + return undefined; + } + + return asAccountGroupId(account.options.entropySource); + } + + // TODO: For now, we're not checking if the Snap is a preinstalled one, and we probably should... + if ( + this.#hasKeyringType(account, KeyringTypes.snap) && + account.metadata.snap?.enabled + ) { + // Not all Snaps have an entropy-source and options are not typed yet, so we have to check manually here. + const { entropySource } = account.options; + + if (entropySource) { + // We blindly trust the `entropySource` for now, but it could be wrong since it comes from a Snap. + return asAccountGroupId(entropySource); + } + } + + return undefined; + } + + #groupBySnapId(account: InternalAccount): AccountGroupId | undefined { + if ( + this.#hasKeyringType(account, KeyringTypes.snap) && + account.metadata.snap && + account.metadata.snap.enabled + ) { + return account.metadata.snap.id; + } + + return undefined; + } + + #groupByWalletType(account: InternalAccount): AccountGroupId | undefined { + return account.metadata.keyring.type as AccountGroupId; + } + + async updateAccountGroups(): Promise { + const rules = [ + // 1. We group by entropy-source + (account: InternalAccount) => this.#groupByEntropySource(account), + // 2. We group by Snap ID + (account: InternalAccount) => this.#groupBySnapId(account), + // 3. We group by wallet type + (account: InternalAccount) => this.#groupByWalletType(account), + ]; + const groups: AccountGroupControllerState['accountGroups']['groups'] = {}; + + for (const account of this.#listAccounts()) { + for (const rule of rules) { + const groupId = rule(account); + + if (!groupId) { + // If none group ID got found, we continue and use the next rule. + continue; + } + + if (!groups[groupId]) { + // For now, we add a default sub-group. + groups[groupId] = { + [DEFAULT_SUB_GROUP]: [], + }; + } + groups[groupId][DEFAULT_SUB_GROUP].push(account.id); + + // We found a matching rule, stop and continue with the next account. + break; + } + } + + this.update((state) => { + state.accountGroups.groups = groups; + }); + } + + async listAccountGroups(): Promise { + return Object.keys(this.state.accountGroups.groups).map( + (groupId: AccountGroupId) => { + const subGroups = this.state.accountGroups.groups[groupId]; + return { + id: groupId, + name: this.state.accountGroupsMetadata[groupId].name, + subGroups, + }; + }, + ); + } + + /** + * Lists the multichain accounts coming from the `AccountsController`. + * + * @returns A list of multichain accounts. + */ + #listAccounts(): InternalAccount[] { + return this.messagingSystem.call( + 'AccountsController:listMultichainAccounts', + ); + } +} diff --git a/packages/account-group-controller/src/index.ts b/packages/account-group-controller/src/index.ts new file mode 100644 index 00000000000..7ab0bdcaa8e --- /dev/null +++ b/packages/account-group-controller/src/index.ts @@ -0,0 +1,10 @@ +export type { + AccountGroupId, + AccountGroupControllerState, + AccountGroupControllerGetStateAction, + AccountGroupControllerActions, + AccountGroupControllerChangeEvent, + AccountGroupControllerEvents, + AccountGroupControllerMessenger, +} from './AccountGroupController'; +export { AccountGroupController } from './AccountGroupController'; diff --git a/packages/account-group-controller/tsconfig.build.json b/packages/account-group-controller/tsconfig.build.json new file mode 100644 index 00000000000..5e3f6b10dd6 --- /dev/null +++ b/packages/account-group-controller/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [ + { "path": "../accounts-controller/tsconfig.build.json" }, + { "path": "../base-controller/tsconfig.build.json" }, + { "path": "../keyring-controller/tsconfig.build.json" } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/account-group-controller/tsconfig.json b/packages/account-group-controller/tsconfig.json new file mode 100644 index 00000000000..8b6228af6b8 --- /dev/null +++ b/packages/account-group-controller/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [ + { + "path": "../base-controller" + }, + { + "path": "../keyring-controller" + }, + { + "path": "../accounts-controller" + } + ], + "include": ["../../types", "./src"] +} diff --git a/packages/account-group-controller/typedoc.json b/packages/account-group-controller/typedoc.json new file mode 100644 index 00000000000..c9da015dbf8 --- /dev/null +++ b/packages/account-group-controller/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 3f4ed8fb383..1c86094f1aa 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,5 +1,6 @@ { "references": [ + { "path": "./packages/account-group-controller/tsconfig.build.json" }, { "path": "./packages/accounts-controller/tsconfig.build.json" }, { "path": "./packages/address-book-controller/tsconfig.build.json" }, { "path": "./packages/announcement-controller/tsconfig.build.json" }, @@ -24,12 +25,12 @@ { "path": "./packages/keyring-controller/tsconfig.build.json" }, { "path": "./packages/logging-controller/tsconfig.build.json" }, { "path": "./packages/message-manager/tsconfig.build.json" }, + { "path": "./packages/multichain-api-middleware/tsconfig.build.json" }, { "path": "./packages/multichain-network-controller/tsconfig.build.json" }, { "path": "./packages/multichain-transactions-controller/tsconfig.build.json" }, { "path": "./packages/multichain/tsconfig.build.json" }, - { "path": "./packages/multichain-api-middleware/tsconfig.build.json" }, { "path": "./packages/name-controller/tsconfig.build.json" }, { "path": "./packages/network-controller/tsconfig.build.json" }, { diff --git a/tsconfig.json b/tsconfig.json index e107fb6e545..81e6ce9af81 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "noEmit": true }, "references": [ + { "path": "./packages/account-group-controller" }, { "path": "./packages/accounts-controller" }, { "path": "./packages/address-book-controller" }, { "path": "./packages/announcement-controller" }, diff --git a/yarn.lock b/yarn.lock index c06348c6398..dd407564902 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2427,6 +2427,28 @@ __metadata: languageName: node linkType: hard +"@metamask/account-group-controller@workspace:packages/account-group-controller": + version: 0.0.0-use.local + resolution: "@metamask/account-group-controller@workspace:packages/account-group-controller" + dependencies: + "@metamask/accounts-controller": "npm:^29.0.0" + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/base-controller": "npm:^8.0.1" + "@metamask/keyring-api": "npm:^17.4.0" + "@metamask/keyring-controller": "npm:^22.0.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" + peerDependencies: + "@metamask/accounts-controller": ^29.0.0 + "@metamask/keyring-controller": ^22.0.0 + languageName: unknown + linkType: soft + "@metamask/accounts-controller@npm:^29.0.0, @metamask/accounts-controller@workspace:packages/accounts-controller": version: 0.0.0-use.local resolution: "@metamask/accounts-controller@workspace:packages/accounts-controller"