From 4e22912b0e26415f8e0847d68b43488d23c12f85 Mon Sep 17 00:00:00 2001 From: salimtb Date: Thu, 16 Apr 2026 12:20:09 +0200 Subject: [PATCH 1/3] fix(MultichainBalancesController): initialize balance entry when snap update arrives before account is in state Previously, `#handleOnAccountBalancesUpdated` would silently drop balance updates if `state.balances[accountId]` had not yet been populated (i.e. the key was absent). This caused a race condition where a Snap could send balances before `updateBalance` had created the entry, leaving the account with no persisted balance. The handler now initializes the entry with `??=` before merging, consistent with every other code path in the controller that writes to `state.balances`. --- packages/assets-controllers/CHANGELOG.md | 5 ++++ .../MultichainBalancesController.test.ts | 28 +++++++++++++++++++ .../MultichainBalancesController.ts | 5 ++-- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 52e0ef18897..8bf2987fff9 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/account-tree-controller` from `^7.0.0` to `^7.1.0` ([#8472](https://github.com/MetaMask/core/pull/8472)) +### Fixed + +- `MultichainBalancesController` no longer silently drops balance updates from `AccountsController:accountBalancesUpdated` when the account does not yet have an entry in `state.balances` ([#FIXME](https://github.com/MetaMask/core/pull/FIXME)) + - Previously, if a Snap sent balance updates before `updateBalance` had initialized `state.balances[accountId]`, the update was ignored because of an `accountId in state.balances` guard. The handler now initializes the entry before merging, ensuring balances are always persisted. + ## [104.0.0] ### Changed diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts index 1dfd40564c9..f0efff937f0 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.test.ts @@ -377,6 +377,34 @@ describe('MultichainBalancesController', () => { ); }); + it('initializes and stores balances when "AccountsController:accountBalancesUpdated" fires before the account has an entry in state', async () => { + const { controller, messenger } = setupController({ + state: { balances: {} }, + mocks: { + listMultichainAccounts: [], + }, + }); + + expect(controller.state.balances[mockBtcAccount.id]).toBeUndefined(); + + const balanceUpdate = { + balances: { + [mockBtcAccount.id]: mockBalanceResult, + }, + }; + + messenger.publish( + 'AccountsController:accountBalancesUpdated', + balanceUpdate, + ); + + await waitForAllPromises(); + + expect(controller.state.balances[mockBtcAccount.id]).toStrictEqual( + mockBalanceResult, + ); + }); + it('updates balances when receiving "AccountsController:accountBalancesUpdated" event', async () => { const mockInitialBalances = { [mockBtcNativeAsset]: { diff --git a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts index 88356c68bd3..7d8fbceaef4 100644 --- a/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts +++ b/packages/assets-controllers/src/MultichainBalancesController/MultichainBalancesController.ts @@ -384,9 +384,8 @@ export class MultichainBalancesController extends BaseController< this.update((state: Draft) => { Object.entries(balanceUpdate.balances).forEach( ([accountId, assetBalances]) => { - if (accountId in state.balances) { - Object.assign(state.balances[accountId], assetBalances); - } + state.balances[accountId] ??= {}; + Object.assign(state.balances[accountId], assetBalances); }, ); }); From fff2f6c2f402058a804a6fa6ec61f3dfd5eb2737 Mon Sep 17 00:00:00 2001 From: salimtb Date: Thu, 16 Apr 2026 12:25:47 +0200 Subject: [PATCH 2/3] fix: fix 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 8bf2987fff9..81bab1326ce 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- `MultichainBalancesController` no longer silently drops balance updates from `AccountsController:accountBalancesUpdated` when the account does not yet have an entry in `state.balances` ([#FIXME](https://github.com/MetaMask/core/pull/FIXME)) +- `MultichainBalancesController` no longer silently drops balance updates from `AccountsController:accountBalancesUpdated` when the account does not yet have an entry in `state.balances` ([#8484](https://github.com/MetaMask/core/pull/8484)) - Previously, if a Snap sent balance updates before `updateBalance` had initialized `state.balances[accountId]`, the update was ignored because of an `accountId in state.balances` guard. The handler now initializes the entry before merging, ensuring balances are always persisted. ## [104.0.0] From 279c3dfc313f3e6deda0e8d8b263ea4d4622c7c0 Mon Sep 17 00:00:00 2001 From: salimtb Date: Mon, 20 Apr 2026 10:07:10 +0200 Subject: [PATCH 3/3] fix: fix linter --- eslint-suppressions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 97a623f347f..7a1f398cd32 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -311,7 +311,7 @@ "count": 1 }, "no-restricted-syntax": { - "count": 2 + "count": 1 } }, "packages/assets-controllers/src/NftController.test.ts": {