From 3202dafa51116fe6361eaf3bf98c559910be565d Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 11 Jun 2026 18:14:02 +0200 Subject: [PATCH 1/4] feat: add group.status --- packages/account-tree-controller/CHANGELOG.md | 5 + .../src/AccountTreeController.test.ts | 63 ++++++++++ .../src/AccountTreeController.ts | 43 ++++++- packages/account-tree-controller/src/group.ts | 2 + packages/account-tree-controller/src/types.ts | 8 +- .../tests/mockMessenger.ts | 1 + .../multichain-account-service/CHANGELOG.md | 3 + .../src/MultichainAccountGroup.test.ts | 117 ++++++++++++++++++ .../src/MultichainAccountGroup.ts | 72 ++++++++++- .../src/MultichainAccountWallet.ts | 20 ++- .../multichain-account-service/src/index.ts | 2 + .../multichain-account-service/src/types.ts | 19 ++- 12 files changed, 347 insertions(+), 8 deletions(-) diff --git a/packages/account-tree-controller/CHANGELOG.md b/packages/account-tree-controller/CHANGELOG.md index 87714fcf70..aaed8e06b7 100644 --- a/packages/account-tree-controller/CHANGELOG.md +++ b/packages/account-tree-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **BREAKING:** Expose `status` on `AccountGroupMultichainAccountObject` ([#TODO](https://github.com/MetaMask/core/pull/TODO)) + - The field reflects `MultichainAccountGroupStatus` and is kept in sync via the new `MultichainAccountService:groupStatusChange` event subscription. + ### Changed - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) diff --git a/packages/account-tree-controller/src/AccountTreeController.test.ts b/packages/account-tree-controller/src/AccountTreeController.test.ts index dfdf7bfa5e..06f81b5c86 100644 --- a/packages/account-tree-controller/src/AccountTreeController.test.ts +++ b/packages/account-tree-controller/src/AccountTreeController.test.ts @@ -254,6 +254,7 @@ const MOCK_PREPOPULATED_STATE: Partial = { [MOCK_PREPOPULATED_GROUP_ID]: { id: MOCK_PREPOPULATED_GROUP_ID, type: AccountGroupType.MultichainAccount, + status: 'uninitialized', accounts: [MOCK_HD_ACCOUNT_1.id], metadata: { name: 'Account 1', @@ -562,6 +563,7 @@ describe('AccountTreeController', () => { [expectedWalletId1Group]: { id: expectedWalletId1Group, type: AccountGroupType.MultichainAccount, + status: 'uninitialized', accounts: [MOCK_HD_ACCOUNT_1.id], metadata: { name: 'Account 1', @@ -589,6 +591,7 @@ describe('AccountTreeController', () => { [expectedWalletId2Group1]: { id: expectedWalletId2Group1, type: AccountGroupType.MultichainAccount, + status: 'uninitialized', accounts: [MOCK_HD_ACCOUNT_2.id], metadata: { name: 'Account 1', // Updated: per-wallet numbering (wallet 2, account 1) @@ -603,6 +606,7 @@ describe('AccountTreeController', () => { [expectedWalletId2Group2]: { id: expectedWalletId2Group2, type: AccountGroupType.MultichainAccount, + status: 'uninitialized', accounts: [MOCK_SNAP_ACCOUNT_1.id], metadata: { name: 'Account 2', // Updated: per-wallet sequential numbering (wallet 2, account 2) @@ -1286,6 +1290,7 @@ describe('AccountTreeController', () => { [walletId1Group]: { id: walletId1Group, type: AccountGroupType.MultichainAccount, + status: 'uninitialized', metadata: { name: 'Account 1', entropy: { @@ -1377,6 +1382,7 @@ describe('AccountTreeController', () => { [walletId1Group2]: { id: walletId1Group2, type: AccountGroupType.MultichainAccount, + status: 'uninitialized', metadata: { name: 'Account 2', entropy: { @@ -1612,6 +1618,7 @@ describe('AccountTreeController', () => { [walletId1Group]: { id: walletId1Group, type: AccountGroupType.MultichainAccount, + status: 'uninitialized', metadata: { name: 'Account 1', entropy: { @@ -1720,6 +1727,7 @@ describe('AccountTreeController', () => { [walletId1Group]: { id: walletId1Group, type: AccountGroupType.MultichainAccount, + status: 'uninitialized', metadata: { name: 'Account 1', entropy: { @@ -1748,6 +1756,7 @@ describe('AccountTreeController', () => { [walletId2Group]: { id: walletId2Group, type: AccountGroupType.MultichainAccount, + status: 'uninitialized', metadata: { name: 'Account 1', // Updated: per-wallet naming (different wallet) entropy: { @@ -1871,6 +1880,60 @@ describe('AccountTreeController', () => { }); }); + describe('on MultichainAccountService:groupStatusChange', () => { + it('updates the group status when the event is published', () => { + const { controller, messenger } = setup({ + accounts: [MOCK_HD_ACCOUNT_1], + keyrings: [MOCK_HD_KEYRING_1], + }); + controller.init(); + + const walletId = MOCK_PREPOPULATED_WALLET_ID; + const groupId = MOCK_PREPOPULATED_GROUP_ID; + + expect( + controller.state.accountTree.wallets[walletId]?.groups[groupId] + ?.status, + ).toBe('uninitialized'); + + messenger.publish( + 'MultichainAccountService:groupStatusChange', + groupId, + 'in-progress:alignment', + ); + expect( + controller.state.accountTree.wallets[walletId]?.groups[groupId] + ?.status, + ).toBe('in-progress:alignment'); + + messenger.publish( + 'MultichainAccountService:groupStatusChange', + groupId, + 'aligned', + ); + expect( + controller.state.accountTree.wallets[walletId]?.groups[groupId] + ?.status, + ).toBe('aligned'); + }); + + it('does nothing when the group ID is unknown', () => { + const { controller, messenger } = setup({ + accounts: [MOCK_HD_ACCOUNT_1], + keyrings: [MOCK_HD_KEYRING_1], + }); + controller.init(); + + expect(() => + messenger.publish( + 'MultichainAccountService:groupStatusChange', + 'unknown-group-id' as ReturnType, + 'aligned', + ), + ).not.toThrow(); + }); + }); + describe('getAccountWalletObject', () => { it('gets a wallet using its ID', () => { const { controller } = setup({ diff --git a/packages/account-tree-controller/src/AccountTreeController.ts b/packages/account-tree-controller/src/AccountTreeController.ts index 5640662e9f..0b257fb779 100644 --- a/packages/account-tree-controller/src/AccountTreeController.ts +++ b/packages/account-tree-controller/src/AccountTreeController.ts @@ -1,10 +1,14 @@ -import { AccountWalletType, select } from '@metamask/account-api'; +import { + AccountGroupType, + AccountWalletType, + select, +} from '@metamask/account-api'; import type { AccountGroupId, AccountWalletId, AccountSelector, + MultichainAccountGroupId, MultichainAccountWalletId, - AccountGroupType, } from '@metamask/account-api'; import type { MultichainAccountWalletStatus } from '@metamask/account-api'; import type { AccountId } from '@metamask/accounts-controller'; @@ -13,6 +17,7 @@ import { BaseController } from '@metamask/base-controller'; import type { TraceCallback } from '@metamask/controller-utils'; import { isEvmAccountType } from '@metamask/keyring-api'; import type { InternalAccount } from '@metamask/keyring-internal-api'; +import type { MultichainAccountGroupStatus } from '@metamask/multichain-account-service'; import { assert } from '@metamask/utils'; import type { BackupAndSyncEmitAnalyticsEventParams } from './backup-and-sync/analytics'; @@ -277,6 +282,13 @@ export class AccountTreeController extends BaseController< }, ); + this.messenger.subscribe( + 'MultichainAccountService:groupStatusChange', + (groupId, status) => { + this.#handleMultichainAccountGroupStatusChange(groupId, status); + }, + ); + this.messenger.registerMethodActionHandlers( this, MESSENGER_EXPOSED_METHODS, @@ -1173,6 +1185,11 @@ export class AccountTreeController extends BaseController< ...result.group, // Type-wise, we are guaranteed to always have at least 1 account. accounts: [id], + // Entropy (multichain) groups start as 'uninitialized'; the service will + // publish a groupStatusChange event to set the real status shortly after. + ...(result.group.type === AccountGroupType.MultichainAccount && { + status: 'uninitialized', + }), metadata: { name: '', ...{ pinned: false, hidden: false, lastSelected: 0 }, // Default UI states @@ -1423,6 +1440,28 @@ export class AccountTreeController extends BaseController< }); } + /** + * Handles multichain account group status change from + * the MultichainAccountService. + * + * @param groupId - Multichain account group ID. + * @param groupStatus - New multichain account group status. + */ + #handleMultichainAccountGroupStatusChange( + groupId: MultichainAccountGroupId, + groupStatus: MultichainAccountGroupStatus, + ): void { + this.update((state) => { + const walletId = this.#groupIdToWalletId.get(groupId); + if (walletId) { + const group = state.accountTree.wallets[walletId]?.groups[groupId]; + if (group?.type === AccountGroupType.MultichainAccount) { + group.status = groupStatus; + } + } + }); + } + /** * Gets account group object. * diff --git a/packages/account-tree-controller/src/group.ts b/packages/account-tree-controller/src/group.ts index 5e53cbc5a0..e108032e7f 100644 --- a/packages/account-tree-controller/src/group.ts +++ b/packages/account-tree-controller/src/group.ts @@ -13,6 +13,7 @@ import { XlmAccountType, } from '@metamask/keyring-api'; import type { KeyringAccountType } from '@metamask/keyring-api'; +import type { MultichainAccountGroupStatus } from '@metamask/multichain-account-service'; import type { UpdatableField, ExtractFieldValues } from './type-utils'; import type { AccountTreeControllerState } from './types'; @@ -79,6 +80,7 @@ type IsAccountGroupObject< export type AccountGroupMultichainAccountObject = { type: AccountGroupType.MultichainAccount; id: MultichainAccountGroupId; + status: MultichainAccountGroupStatus; // Blockchain Accounts (at least 1 account per multichain-accounts): accounts: [AccountId, ...AccountId[]]; metadata: AccountTreeGroupMetadata & { diff --git a/packages/account-tree-controller/src/types.ts b/packages/account-tree-controller/src/types.ts index e7a6657139..e32e96e716 100644 --- a/packages/account-tree-controller/src/types.ts +++ b/packages/account-tree-controller/src/types.ts @@ -20,7 +20,10 @@ import type { MultichainAccountServiceCreateMultichainAccountGroupAction, MultichainAccountServiceCreateMultichainAccountGroupsAction, } from '@metamask/multichain-account-service'; -import type { MultichainAccountServiceWalletStatusChangeEvent } from '@metamask/multichain-account-service'; +import type { + MultichainAccountServiceGroupStatusChangeEvent, + MultichainAccountServiceWalletStatusChangeEvent, +} from '@metamask/multichain-account-service'; import type { AuthenticationController, UserStorageController, @@ -158,7 +161,8 @@ export type AllowedEvents = | AccountsControllerAccountsRemovedEvent | AccountsControllerSelectedAccountChangeEvent | UserStorageController.UserStorageControllerStateChangeEvent - | MultichainAccountServiceWalletStatusChangeEvent; + | MultichainAccountServiceWalletStatusChangeEvent + | MultichainAccountServiceGroupStatusChangeEvent; export type AccountTreeControllerEvents = | AccountTreeControllerStateChangeEvent diff --git a/packages/account-tree-controller/tests/mockMessenger.ts b/packages/account-tree-controller/tests/mockMessenger.ts index a96594dbf7..6b43a124bf 100644 --- a/packages/account-tree-controller/tests/mockMessenger.ts +++ b/packages/account-tree-controller/tests/mockMessenger.ts @@ -49,6 +49,7 @@ export function getAccountTreeControllerMessenger( 'AccountsController:selectedAccountChange', 'UserStorageController:stateChange', 'MultichainAccountService:walletStatusChange', + 'MultichainAccountService:groupStatusChange', ], actions: [ 'AccountsController:listMultichainAccounts', diff --git a/packages/multichain-account-service/CHANGELOG.md b/packages/multichain-account-service/CHANGELOG.md index d859e80892..7039950439 100644 --- a/packages/multichain-account-service/CHANGELOG.md +++ b/packages/multichain-account-service/CHANGELOG.md @@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Use this if you need to access the inner (wrapped) keyring. - Add `isAligned` ([#9039](https://github.com/MetaMask/core/pull/9039)) - This allows callers to cheaply check whether alignment has already occurred before triggering an explicit alignment operation. +- Add `MultichainAccountGroupStatus` type and `MultichainAccountServiceGroupStatusChangeEvent` event ([#TODO](https://github.com/MetaMask/core/pull/TODO)) + - `MultichainAccountGroup` now tracks a `status` field (`'uninitialized' | 'in-progress:create-accounts' | 'in-progress:alignment' | 'aligned' | 'misaligned'`). + - The service messenger emits `MultichainAccountService:groupStatusChange` whenever a group's status changes. ### Changed diff --git a/packages/multichain-account-service/src/MultichainAccountGroup.test.ts b/packages/multichain-account-service/src/MultichainAccountGroup.test.ts index 927b6d6e76..8121a9ea39 100644 --- a/packages/multichain-account-service/src/MultichainAccountGroup.test.ts +++ b/packages/multichain-account-service/src/MultichainAccountGroup.test.ts @@ -180,6 +180,123 @@ describe('MultichainAccountGroup', () => { }); }); + describe('status', () => { + it('starts as uninitialized before init()', () => { + const serviceMessenger = + getMultichainAccountServiceMessenger(getRootMessenger()); + const providers = [ + setupBip44AccountProvider({ + name: 'Provider 1', + accounts: [MOCK_WALLET_1_EVM_ACCOUNT], + }), + ]; + const wallet = new MultichainAccountWallet({ + entropySource: MOCK_WALLET_1_ENTROPY_SOURCE, + messenger: serviceMessenger, + providers, + }); + const group = new MultichainAccountGroup({ + wallet, + groupIndex: 0, + providers, + messenger: serviceMessenger, + }); + + expect(group.status).toBe('uninitialized'); + }); + + it('is aligned after init() when all providers have accounts', () => { + const { group } = setup({ + accounts: [[MOCK_WALLET_1_EVM_ACCOUNT], [MOCK_WALLET_1_SOL_ACCOUNT]], + }); + + expect(group.status).toBe('aligned'); + }); + + it('is misaligned after init() when a provider has no accounts', () => { + const { group } = setup({ + accounts: [[MOCK_WALLET_1_EVM_ACCOUNT], []], + }); + + expect(group.status).toBe('misaligned'); + }); + + it('publishes groupStatusChange event when withState transitions to in-progress then settles', async () => { + const { group, messenger: serviceMessenger } = setup({ + accounts: [[MOCK_WALLET_1_EVM_ACCOUNT], []], + }); + const publishSpy = jest.spyOn(serviceMessenger, 'publish'); + + await group.withState('in-progress:alignment', async () => { + expect(group.status).toBe('in-progress:alignment'); + }); + + expect(group.status).toBe('misaligned'); + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainAccountService:groupStatusChange', + group.id, + 'in-progress:alignment', + ); + expect(publishSpy).toHaveBeenCalledWith( + 'MultichainAccountService:groupStatusChange', + group.id, + 'misaligned', + ); + }); + + it('preserves in-progress:create-accounts through an inner in-progress:alignment withState', async () => { + const { group } = setup({ + accounts: [[MOCK_WALLET_1_EVM_ACCOUNT], []], + }); + const statusDuringInner: string[] = []; + + await group.withState('in-progress:create-accounts', async () => { + // Inner withState with a different in-progress status should not override + await group.withState('in-progress:alignment', async () => { + statusDuringInner.push(group.status); + }); + statusDuringInner.push(group.status); + }); + + // 'in-progress:create-accounts' is preserved through the inner call since + // the guard skips the entry when already in any in-progress state. + expect(statusDuringInner[0]).toBe('in-progress:create-accounts'); + // After both finally blocks fire, status is 'misaligned'. + expect(group.status).toBe('misaligned'); + }); + + it('auto-corrects status on update() when not in an in-progress state', () => { + const { group, providers } = setup({ + accounts: [[MOCK_WALLET_1_EVM_ACCOUNT], []], + }); + expect(group.status).toBe('misaligned'); + + // Simulate provider 2 now being considered aligned (e.g. disabled wrapper) + providers[1].isAligned.mockReturnValue(true); + group.update({ + 'Provider 1': [MOCK_WALLET_1_EVM_ACCOUNT.id], + }); + + expect(group.status).toBe('aligned'); + }); + + it('does not override in-progress status on update()', async () => { + const { group, providers } = setup({ + accounts: [[MOCK_WALLET_1_EVM_ACCOUNT], []], + }); + + await group.withState('in-progress:alignment', async () => { + // update() called inside withState should NOT change the status + providers[1].isAligned.mockReturnValue(true); + group.update({ 'Provider 1': [MOCK_WALLET_1_EVM_ACCOUNT.id] }); + expect(group.status).toBe('in-progress:alignment'); + }); + + // After withState finalizes, status should reflect isAligned() + expect(group.status).toBe('aligned'); + }); + }); + describe('isAligned', () => { it('returns true when every provider has at least one account in the group', () => { const { group } = setup({ diff --git a/packages/multichain-account-service/src/MultichainAccountGroup.ts b/packages/multichain-account-service/src/MultichainAccountGroup.ts index 8b7af73e08..33f770b84d 100644 --- a/packages/multichain-account-service/src/MultichainAccountGroup.ts +++ b/packages/multichain-account-service/src/MultichainAccountGroup.ts @@ -13,7 +13,10 @@ import { projectLogger as log, createModuleLogger } from './logger'; import type { ServiceState, StateKeys } from './MultichainAccountService'; import type { MultichainAccountWallet } from './MultichainAccountWallet'; import type { Bip44AccountProvider } from './providers'; -import type { MultichainAccountServiceMessenger } from './types'; +import type { + MultichainAccountGroupStatus, + MultichainAccountServiceMessenger, +} from './types'; export type GroupState = ServiceState[StateKeys['entropySource']][StateKeys['groupIndex']]; @@ -48,6 +51,8 @@ export class MultichainAccountGroup< #initialized = false; + #status: MultichainAccountGroupStatus = 'uninitialized'; + constructor({ groupIndex, wallet, @@ -114,6 +119,9 @@ export class MultichainAccountGroup< this.#log('Finished initializing group state...'); this.#initialized = true; + // Set initial status without publishing — mirrors wallet init() pattern where the tree + // hardcodes its own initial state and events only flow after the first mutation. + this.#status = this.isAligned() ? 'aligned' : 'misaligned'; } /** @@ -127,6 +135,14 @@ export class MultichainAccountGroup< this.#log('Finished updating group state...'); if (this.#initialized) { + // Auto-correct status for dynamic account changes that happen outside any + // explicit operation (e.g. a new provider added at runtime). During an + // operation, `withState` owns the status and its `finally` block finalizes it, + // so we skip the auto-update to avoid clobbering the in-progress state. + if (!this.#status.startsWith('in-progress:')) { + this.#setStatus(this.isAligned() ? 'aligned' : 'misaligned'); + } + this.#messenger.publish( 'MultichainAccountService:multichainAccountGroupUpdated', this, @@ -134,6 +150,60 @@ export class MultichainAccountGroup< } } + /** + * Gets the current status of this group. + * + * @returns The group status. + */ + get status(): MultichainAccountGroupStatus { + return this.#status; + } + + /** + * Runs an async operation under a specific in-progress status, then auto-finalizes + * the group status to `'aligned'` or `'misaligned'` in the `finally` block. + * + * Mirrors the wallet's `#withLock` pattern — without acquiring a lock (the wallet's + * mutex already serializes all mutable group operations). + * + * @param status - The in-progress status to set before the operation. + * @param operation - The operation to run. + * @returns The operation's result. + */ + async withState( + status: 'in-progress:create-accounts' | 'in-progress:alignment', + operation: () => Promise, + ): Promise { + // Do not override an in-progress status that was set by an outer caller + // (e.g. 'in-progress:create-accounts' must survive through the inner + // #alignAccountsForRange 'in-progress:alignment' withState call). + if (!this.#status.startsWith('in-progress:')) { + this.#setStatus(status); + } + try { + return await operation(); + } finally { + this.#setStatus(this.isAligned() ? 'aligned' : 'misaligned'); + } + } + + /** + * Sets the group status and publishes the status-change event. + * No-ops when the group has not been initialized yet. + * + * @param status - The new status. + */ + #setStatus(status: MultichainAccountGroupStatus): void { + this.#status = status; + if (this.#initialized) { + this.#messenger.publish( + 'MultichainAccountService:groupStatusChange', + this.#id, + this.#status, + ); + } + } + /** * Gets the multichain account group ID. * diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.ts b/packages/multichain-account-service/src/MultichainAccountWallet.ts index 731fa3f777..aec2aaafdc 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.ts @@ -447,8 +447,13 @@ export class MultichainAccountWallet< async #alignAccountsForRange( { from, to }: Required, providers: Bip44AccountProvider[], - options: { trace?: { data?: TraceRequest['data'] } } = {}, + options: { + trace?: { data?: TraceRequest['data'] }; + groupStatus?: 'in-progress:create-accounts' | 'in-progress:alignment'; + } = {}, ): Promise { + const groupStatus = options.groupStatus ?? 'in-progress:alignment'; + await this.#trace( { name: TraceName.WalletAlignment, @@ -475,7 +480,17 @@ export class MultichainAccountWallet< for (let groupIndex = from; groupIndex <= to; groupIndex++) { const groupState = groupStateByGroupIndex.get(groupIndex); if (groupState) { - this.#createOrUpdateMultichainAccountGroup(groupIndex, groupState); + const existingGroup = this.getMultichainAccountGroup(groupIndex); + assert( + existingGroup, + `Expected group at index ${groupIndex} to exist before alignment`, + ); + await existingGroup.withState(groupStatus, async () => { + this.#createOrUpdateMultichainAccountGroup( + groupIndex, + groupState, + ); + }); } } }, @@ -702,6 +717,7 @@ export class MultichainAccountWallet< post: true, // Tag to identify post-alignment traces in analytics. }, }, + groupStatus: 'in-progress:create-accounts', }); }); diff --git a/packages/multichain-account-service/src/index.ts b/packages/multichain-account-service/src/index.ts index 2c6dec78e8..f7127dd277 100644 --- a/packages/multichain-account-service/src/index.ts +++ b/packages/multichain-account-service/src/index.ts @@ -5,6 +5,8 @@ export type { MultichainAccountServiceMultichainAccountGroupCreatedEvent, MultichainAccountServiceMultichainAccountGroupUpdatedEvent, MultichainAccountServiceWalletStatusChangeEvent, + MultichainAccountGroupStatus, + MultichainAccountServiceGroupStatusChangeEvent, } from './types'; export type { MultichainAccountServiceResyncAccountsAction, diff --git a/packages/multichain-account-service/src/types.ts b/packages/multichain-account-service/src/types.ts index b640f49757..c04737201d 100644 --- a/packages/multichain-account-service/src/types.ts +++ b/packages/multichain-account-service/src/types.ts @@ -1,6 +1,7 @@ import type { Bip44Account, MultichainAccountGroup, + MultichainAccountGroupId, MultichainAccountWalletId, MultichainAccountWalletStatus, } from '@metamask/account-api'; @@ -57,6 +58,21 @@ export type MultichainAccountServiceWalletStatusChangeEvent = { payload: [MultichainAccountWalletId, MultichainAccountWalletStatus]; }; +/** + * Status of a multichain account group, mirroring the wallet-level status pattern. + */ +export type MultichainAccountGroupStatus = + | 'uninitialized' + | 'in-progress:create-accounts' + | 'in-progress:alignment' + | 'aligned' + | 'misaligned'; + +export type MultichainAccountServiceGroupStatusChangeEvent = { + type: `${typeof serviceName}:groupStatusChange`; + payload: [MultichainAccountGroupId, MultichainAccountGroupStatus]; +}; + /** * All events that {@link MultichainAccountService} publishes so that other modules * can subscribe to them. @@ -64,7 +80,8 @@ export type MultichainAccountServiceWalletStatusChangeEvent = { export type MultichainAccountServiceEvents = | MultichainAccountServiceMultichainAccountGroupCreatedEvent | MultichainAccountServiceMultichainAccountGroupUpdatedEvent - | MultichainAccountServiceWalletStatusChangeEvent; + | MultichainAccountServiceWalletStatusChangeEvent + | MultichainAccountServiceGroupStatusChangeEvent; /** * All actions registered by other modules that {@link MultichainAccountService} From 38144ff1dd70b5d8d4b41a4138684f313c168214 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 12 Jun 2026 10:59:06 +0200 Subject: [PATCH 2/4] chore: lint --- .../src/AccountTreeController.test.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/account-tree-controller/src/AccountTreeController.test.ts b/packages/account-tree-controller/src/AccountTreeController.test.ts index 06f81b5c86..0241a0e846 100644 --- a/packages/account-tree-controller/src/AccountTreeController.test.ts +++ b/packages/account-tree-controller/src/AccountTreeController.test.ts @@ -1892,8 +1892,7 @@ describe('AccountTreeController', () => { const groupId = MOCK_PREPOPULATED_GROUP_ID; expect( - controller.state.accountTree.wallets[walletId]?.groups[groupId] - ?.status, + controller.state.accountTree.wallets[walletId]?.groups[groupId]?.status, ).toBe('uninitialized'); messenger.publish( @@ -1902,8 +1901,7 @@ describe('AccountTreeController', () => { 'in-progress:alignment', ); expect( - controller.state.accountTree.wallets[walletId]?.groups[groupId] - ?.status, + controller.state.accountTree.wallets[walletId]?.groups[groupId]?.status, ).toBe('in-progress:alignment'); messenger.publish( @@ -1912,8 +1910,7 @@ describe('AccountTreeController', () => { 'aligned', ); expect( - controller.state.accountTree.wallets[walletId]?.groups[groupId] - ?.status, + controller.state.accountTree.wallets[walletId]?.groups[groupId]?.status, ).toBe('aligned'); }); From 94cb3c03cdbacc6689c8b915d24257f130424e8e Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 12 Jun 2026 11:01:24 +0200 Subject: [PATCH 3/4] chore: changelog --- packages/account-tree-controller/CHANGELOG.md | 3 ++- packages/multichain-account-service/CHANGELOG.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/account-tree-controller/CHANGELOG.md b/packages/account-tree-controller/CHANGELOG.md index aaed8e06b7..34bf01129c 100644 --- a/packages/account-tree-controller/CHANGELOG.md +++ b/packages/account-tree-controller/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **BREAKING:** Expose `status` on `AccountGroupMultichainAccountObject` ([#TODO](https://github.com/MetaMask/core/pull/TODO)) +- **BREAKING:** Expose `status` on `AccountGroupMultichainAccountObject` ([#9104](https://github.com/MetaMask/core/pull/9104)) + - The controller now requires the new event `MultichainAccountService:groupStatusChange`. - The field reflects `MultichainAccountGroupStatus` and is kept in sync via the new `MultichainAccountService:groupStatusChange` event subscription. ### Changed diff --git a/packages/multichain-account-service/CHANGELOG.md b/packages/multichain-account-service/CHANGELOG.md index 7039950439..6bab68b0ba 100644 --- a/packages/multichain-account-service/CHANGELOG.md +++ b/packages/multichain-account-service/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Use this if you need to access the inner (wrapped) keyring. - Add `isAligned` ([#9039](https://github.com/MetaMask/core/pull/9039)) - This allows callers to cheaply check whether alignment has already occurred before triggering an explicit alignment operation. -- Add `MultichainAccountGroupStatus` type and `MultichainAccountServiceGroupStatusChangeEvent` event ([#TODO](https://github.com/MetaMask/core/pull/TODO)) +- Add `MultichainAccountGroupStatus` type and `MultichainAccountServiceGroupStatusChangeEvent` event ([#9104](https://github.com/MetaMask/core/pull/9104)) - `MultichainAccountGroup` now tracks a `status` field (`'uninitialized' | 'in-progress:create-accounts' | 'in-progress:alignment' | 'aligned' | 'misaligned'`). - The service messenger emits `MultichainAccountService:groupStatusChange` whenever a group's status changes. From 0c4253dc9da5084f5d10fb6e63fe55b98078deba Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 12 Jun 2026 16:02:59 +0200 Subject: [PATCH 4/4] chore: missing jsdocs param --- .../multichain-account-service/src/MultichainAccountWallet.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.ts b/packages/multichain-account-service/src/MultichainAccountWallet.ts index aec2aaafdc..ca27a7a73f 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.ts @@ -443,6 +443,7 @@ export class MultichainAccountWallet< * @param options - Options. * @param options.trace - Trace options. * @param options.trace.data - Optional trace data. + * @param options.groupStatus - Optional status to set on groups during alignment or post-creation alignment (defaults to 'in-progress:alignment'). */ async #alignAccountsForRange( { from, to }: Required,