fix: refresh unified assets balances from real-time websocket pushes#31091
Merged
Conversation
When `assetsUnifyState` is enabled the UI reads balances from `AssetsController.assetsBalance`. The controller's internal `BackendWebsocketDataSource` and `AccountActivityService` both subscribe to the same account-activity channel, but the backend routes each notification to a single subscriptionId, so the data source's subscription is starved and `assetsBalance` only refreshes on the 30s poll (e.g. after a QuickBuy swap). Bridge the `AccountActivityService:balanceUpdated` event, which reliably receives the push, into the controller's public `handleAssetsUpdate` merge entrypoint so balances update in real time. The payload-to-merge transformation is extracted into a pure, unit-tested helper. Co-authored-by: Cursor <cursoragent@cursor.com>
Contributor
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3ce3557. Configure here.
`buildAssetsBalanceUpdateFromPush` only skipped `undefined` decimals/amounts, so runtime payloads with `null` (or NaN/negative/fractional) decimals or non-string amounts reached `fromTokenMinimalUnitString` and could throw. A throw in the loop aborted the entire push, dropping otherwise-valid balance rows until the next poll. Tighten the per-row guards and isolate each conversion in a try/catch so a single malformed row is skipped while the remaining valid balances still merge. Co-authored-by: Cursor <cursoragent@cursor.com>
Contributor
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection: Performance Test Selection: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Description
When the
assetsUnifyStatefeature flag is enabled, the UI reads token balances fromAssetsController.assetsBalanceinstead ofTokenBalancesController. After a swap (e.g. QuickBuy), balances did not update in the simulator until the 30s poll, even though the real-time websocket push arrived almost immediately.Root cause: both
AccountActivityServiceand the controller's internalBackendWebsocketDataSourceopen a separate websocket subscription to the sameaccount-activitychannel. The backend routes each notification to a singlesubscriptionId, so only one subscriber receives the push.AccountActivityServicewins that race, starving the data source's subscription — soAssetsController.assetsBalanceis never merged with the fresh balance until the periodic poll.Solution: bridge the
AccountActivityService:balanceUpdatedevent (which reliably receives the push) into the controller's own publichandleAssetsUpdatemerge entrypoint. The push payload is transformed into the merge shape by a pure, unit-tested helper (buildAssetsBalanceUpdateFromPush) that mirrors the data source's own raw-amount → human-readable-decimal conversion, skipping malformed rows so a single bad entry can't drop the whole push. The bridge is gated behindassetsUnifyState, and the 30s poll remains as a fallback.Why it wasn't noticeable before (and surfaced via QuickBuy)
This bug affects every balance update under
assetsUnifyState, not just QuickBuy — which is why the fix lives at the controller-bridge layer rather than in QuickBuy. It was surfaced by the QuickBuy because of how each flow ends:Routes.TRANSACTIONS_VIEW(useBridgeConfirm). Navigating away and back re-mounts the wallet token list, which under unify force-refreshesAssetsController.getAssets({ forceUpdate: true })(useRefreshTokens). That forced read masked the starved websocket path, so balances looked instant.TraderPositionViewand only shows lifecycle toasts — no navigation, no re-mount, no forced refresh. With nothing to mask it, the starved real-time path was exposed and balances only updated on the 30s poll (consistent with "pull-to-refresh fixes it" and "waiting eventually updates").Both flows submit through the same
BridgeStatusController.submitTx; the difference is purely the post-submit navigation side effect. This fix repairs the real-time path itself, so balances update live for all flows without relying on navigation or pull-to-refresh.Changelog
CHANGELOG entry: Fixed token balances not updating immediately after a swap when the unified assets state is enabled.
Related issues
Fixes:
Manual testing steps
```gherkin
Feature: Real-time balance refresh after swap
Scenario: user completes a swap with unified assets state enabled
Given the assetsUnifyState feature flag is enabled
And the user is on a screen displaying their token balances
```
Screenshots/Recordings
Before
After
Pre-merge author checklist
Pre-merge reviewer checklist