diff --git a/packages/assets-controllers/CHANGELOG.md b/packages/assets-controllers/CHANGELOG.md index 1307f34f3b5..ab5fc1b5264 100644 --- a/packages/assets-controllers/CHANGELOG.md +++ b/packages/assets-controllers/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - `MultichainAssetsController`: fungible `token:` assets from automatic detection are no longer added when Blockaid bulk scan fails, returns empty, or omits that address (previously fail open); an explicit non-malicious per-token result from `PhishingController:bulkScanTokens` is now required before add. ([#8400](https://github.com/MetaMask/core/pull/8400)) +- Fix `AccountTrackerController` wiping existing balances on other chains when syncing accounts for a chain that has no state entry yet ([#8505](https://github.com/MetaMask/core/pull/8505)) ## [104.0.0] diff --git a/packages/assets-controllers/src/AccountTrackerController.test.ts b/packages/assets-controllers/src/AccountTrackerController.test.ts index cabf83b84fe..c0032befce3 100644 --- a/packages/assets-controllers/src/AccountTrackerController.test.ts +++ b/packages/assets-controllers/src/AccountTrackerController.test.ts @@ -403,6 +403,72 @@ describe('AccountTrackerController', () => { ); }); + it('should not wipe existing balances when syncing accounts and the selected chain has no state entry', async () => { + const networkClientId = 'networkClientId1'; + + mockedGetTokenBalancesForMultipleAddresses.mockResolvedValueOnce({ + tokenBalances: { + '0x0000000000000000000000000000000000000000': {}, + }, + stakedBalances: {}, + }); + + await withController( + { + options: { + state: { + accountsByChainId: { + '0xe705': { + [CHECKSUM_ADDRESS_1]: { + balance: '0xabc', + stakedBalance: '0x5', + }, + }, + }, + }, + }, + isMultiAccountBalancesEnabled: true, + selectedAccount: ACCOUNT_1, + listAccounts: [ACCOUNT_1], + networkClientById: { + [networkClientId]: buildCustomNetworkClientConfiguration({ + chainId: '0x999', + }), + }, + }, + async ({ controller, refresh }) => { + // Verify initial state has the balance we expect to preserve + expect( + controller.state.accountsByChainId['0xe705'][CHECKSUM_ADDRESS_1], + ).toStrictEqual({ + balance: '0xabc', + stakedBalance: '0x5', + }); + + // Refresh for a new chain. The selected network (mainnet / 0x1) is + // NOT in accountsByChainId, so #syncAccounts sees an empty "existing" + // set. Without the fix this would overwrite every address on every + // chain with { balance: '0x0' }, wiping both balance and stakedBalance. + await refresh(['networkClientId1'], true); + + // Existing balances must be preserved + expect( + controller.state.accountsByChainId['0xe705'][CHECKSUM_ADDRESS_1], + ).toStrictEqual({ + balance: '0xabc', + stakedBalance: '0x5', + }); + + // New chain should have been initialised with a zero balance + expect( + controller.state.accountsByChainId['0x999'][CHECKSUM_ADDRESS_1], + ).toStrictEqual({ + balance: '0x0', + }); + }, + ); + }); + it('sets isActive to true when keyring is unlocked', async () => { await withController( { diff --git a/packages/assets-controllers/src/AccountTrackerController.ts b/packages/assets-controllers/src/AccountTrackerController.ts index 97b64725cef..62525440479 100644 --- a/packages/assets-controllers/src/AccountTrackerController.ts +++ b/packages/assets-controllers/src/AccountTrackerController.ts @@ -470,9 +470,11 @@ export class AccountTrackerController extends StaticIntervalPollingController { newAddresses.forEach((address) => { - accountsByChainId[chainId][address] = { - balance: '0x0', - }; + if (!accountsByChainId[chainId][address]) { + accountsByChainId[chainId][address] = { + balance: '0x0', + }; + } }); });