Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/assets-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Bump `@metamask/transaction-controller` from `^68.1.1` to `^68.2.0` ([#9253](https://github.com/MetaMask/core/pull/9253))

### Fixed

- Fix stale token balances after transactions when switching accounts or when websocket subscriptions reconnect; `AssetsController` now fetches before re-subscribing on account switch, serializes overlapping refresh work, treats `getAssets({ forceUpdate: true })` as authoritative over recent websocket freshness guards, and prevents passive polling from overwriting websocket balances for 120 seconds ([#9265](https://github.com/MetaMask/core/pull/9265))
- `AccountsApiDataSource` bypasses the TanStack Query balance cache when `forceUpdate` is true so forced refreshes return up-to-date balances instead of 60-second cached values ([#9265](https://github.com/MetaMask/core/pull/9265))
- `BackendWebsocketDataSource` re-subscribes when subscribed accounts change (case-insensitive EVM address matching), serializes subscribe/unsubscribe to prevent races on account switch, and registers optional channel callbacks for more reliable notification delivery ([#9265](https://github.com/MetaMask/core/pull/9265))

## [9.1.0]

### Added
Expand Down
76 changes: 76 additions & 0 deletions packages/assets-controller/src/AssetsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1720,6 +1720,82 @@ describe('AssetsController', () => {
});
});

it('does not let subscription polling overwrite a recent websocket balance update', async () => {
const initialState: Partial<AssetsControllerState> = {
assetsBalance: {
[MOCK_ACCOUNT_ID]: {
[MOCK_ASSET_ID]: { amount: '8.185173' },
},
},
};

await withController({ state: initialState }, async ({ controller }) => {
await controller.handleAssetsUpdate(
{
assetsBalance: {
[MOCK_ACCOUNT_ID]: {
[MOCK_ASSET_ID]: { amount: '7.185173' },
},
},
},
'BackendWebsocketDataSource',
);

await controller.handleAssetsUpdate(
{
assetsBalance: {
[MOCK_ACCOUNT_ID]: {
[MOCK_ASSET_ID]: { amount: '8.185173' },
},
},
},
'AccountsApiDataSource',
);

expect(
controller.state.assetsBalance[MOCK_ACCOUNT_ID]?.[MOCK_ASSET_ID],
).toStrictEqual({ amount: '7.185173' });
});
});

it('applies getAssets forceUpdate over a recent websocket balance update', async () => {
const initialState: Partial<AssetsControllerState> = {
assetsBalance: {
[MOCK_ACCOUNT_ID]: {
[MOCK_ASSET_ID]: { amount: '8.185173' },
},
},
};

await withController({ state: initialState }, async ({ controller }) => {
await controller.handleAssetsUpdate(
{
assetsBalance: {
[MOCK_ACCOUNT_ID]: {
[MOCK_ASSET_ID]: { amount: '7.185173' },
},
},
},
'BackendWebsocketDataSource',
);

await controller.handleAssetsUpdate(
{
assetsBalance: {
[MOCK_ACCOUNT_ID]: {
[MOCK_ASSET_ID]: { amount: '8.185173' },
},
},
},
'getAssets:forceUpdate',
);

expect(
controller.state.assetsBalance[MOCK_ACCOUNT_ID]?.[MOCK_ASSET_ID],
).toStrictEqual({ amount: '8.185173' });
});
});

it('replaces state when full update has authoritative data', async () => {
const initialState: Partial<AssetsControllerState> = {
assetsBalance: {
Expand Down
Loading
Loading