From 2e8c79a8dca9b1040b97febf9e1172f48e0e948a Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Fri, 24 Apr 2026 17:34:50 +0200 Subject: [PATCH 1/4] fix(assets-controllers): refetch multichain balances after keyring unlock --- packages/assets-controllers/CHANGELOG.md | 4 +++ .../MultichainBalancesController.test.ts | 1 + .../MultichainBalancesController.ts | 31 +++++++++++++++++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index b2211ffd559..3af17aeb361 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- `MultichainBalancesController`: when `MultichainAssetsController:accountAssetListUpdated` fires while the vault is locked, balance fetches are skipped and were never retried after unlock, leaving multichain token lists empty despite assets in state; subscribe to `KeyringController:stateChange` and refetch balances for snap-backed accounts that still have no cached balances after unlock. + ### Added - Expose missing public `CurrencyRateController` methods through its messenger ([#8561](https://github.com/MetaMask/core/pull/8561)) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts index 84fec30388a..14b9f72cac6 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts @@ -158,6 +158,7 @@ function getRestrictedMessenger( 'AccountsController:accountRemoved', 'AccountsController:accountBalancesUpdated', 'MultichainAssetsController:accountAssetListUpdated', + 'KeyringController:stateChange', ], }); return multichainBalancesControllerMessenger; diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index a8c58ed4a61..baba24d8681 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -16,7 +16,10 @@ import type { CaipAssetType, AccountBalancesUpdatedEventPayload, } from '@metamask/keyring-api'; -import type { KeyringControllerGetStateAction } from '@metamask/keyring-controller'; +import type { + KeyringControllerGetStateAction, + KeyringControllerStateChangeEvent, +} from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import { KeyringClient } from '@metamask/keyring-snap-client'; import type { Messenger } from '@metamask/messenger'; @@ -105,7 +108,8 @@ type AllowedEvents = | AccountsControllerAccountAddedEvent | AccountsControllerAccountRemovedEvent | AccountsControllerAccountBalancesUpdatesEvent - | MultichainAssetsControllerAccountAssetListUpdatedEvent; + | MultichainAssetsControllerAccountAssetListUpdatedEvent + | KeyringControllerStateChangeEvent; /** * Messenger type for the MultichainBalancesController. */ @@ -190,6 +194,29 @@ export class MultichainBalancesController extends BaseController< await this.#handleOnAccountAssetListUpdated(updatedAccountAssets); }, ); + + // When the keyring transitions from locked → unlocked, fetch balances for + // any non-EVM account that had its balance fetch skipped while locked. + let previouslyUnlocked = false; + this.messenger.subscribe( + 'KeyringController:stateChange', + ({ isUnlocked }) => { + const justUnlocked = isUnlocked && !previouslyUnlocked; + previouslyUnlocked = isUnlocked; + if (!justUnlocked) { + return; + } + for (const account of this.#listAccounts()) { + const hasBalance = + this.state.balances[account.id] && + Object.keys(this.state.balances[account.id]).length > 0; + if (!hasBalance) { + // eslint-disable-next-line no-void + void this.updateBalance(account.id); + } + } + }, + ); } /** From 4328a46690992452758cbef8de8029c220fe5d8a Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Fri, 24 Apr 2026 17:39:06 +0200 Subject: [PATCH 2/4] chore: update changelog --- packages/assets-controllers/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 3af17aeb361..a5606805fe0 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- `MultichainBalancesController`: when `MultichainAssetsController:accountAssetListUpdated` fires while the vault is locked, balance fetches are skipped and were never retried after unlock, leaving multichain token lists empty despite assets in state; subscribe to `KeyringController:stateChange` and refetch balances for snap-backed accounts that still have no cached balances after unlock. +- `MultichainBalancesController`: when `MultichainAssetsController:accountAssetListUpdated` fires while the vault is locked, balance fetches are skipped and were never retried after unlock, leaving multichain token lists empty despite assets in state; subscribe to `KeyringController:stateChange` and refetch balances for snap-backed accounts that still have no cached balances after unlock. ([#8579])(https://github.com/MetaMask/core/pull/8579) ### Added From 8efc78dfebd9edd7a6a5a4accbd175dea5c66115 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Fri, 24 Apr 2026 19:19:37 +0200 Subject: [PATCH 3/4] chore: updated changelog --- packages/assets-controllers/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index a5606805fe0..c8d303a1544 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- `MultichainBalancesController`: when `MultichainAssetsController:accountAssetListUpdated` fires while the vault is locked, balance fetches are skipped and were never retried after unlock, leaving multichain token lists empty despite assets in state; subscribe to `KeyringController:stateChange` and refetch balances for snap-backed accounts that still have no cached balances after unlock. ([#8579])(https://github.com/MetaMask/core/pull/8579) +- Fixed `MultichainBalancesController` not retrying balance fetches after unlock, leaving token lists empty; now retries after unlock. ([#8579](https://github.com/MetaMask/core/pull/8579)) ### Added From 9b21bf2514f39be6719b4696bb38e08302df6dc8 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Fri, 24 Apr 2026 19:25:18 +0200 Subject: [PATCH 4/4] chore: lint --- .../MultichainBalancesController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index baba24d8681..d2db1e89fd0 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -197,7 +197,9 @@ export class MultichainBalancesController extends BaseController< // When the keyring transitions from locked → unlocked, fetch balances for // any non-EVM account that had its balance fetch skipped while locked. - let previouslyUnlocked = false; + let previouslyUnlocked = this.messenger.call( + 'KeyringController:getState', + ).isUnlocked; this.messenger.subscribe( 'KeyringController:stateChange', ({ isUnlocked }) => {