perf: fix redundant RPC calls on wallet unlock (cache network health metadata + deprecate legacy balance polling)#33379
Conversation
|
PR template — items to address before "Ready for review"Warnings — informational, address before merging:
See docs/readme/ready-for-review.md for the full Definition of Ready for Review. |
…hub.com:MetaMask/metamask-mobile into chore/remove-30-plus-api-calls-on-wallet-unlock
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ 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 94d764d. Configure here.
| return alreadyKnown ? [] : [id]; | ||
| } catch { | ||
| return []; | ||
| } |
There was a problem hiding this comment.
EIPS access skips network lookup
Low Severity
In lookupEnabledNetworks, alreadyKnown uses m?.EIPS[1559] without optional chaining on EIPS. When metadata has a non-Unknown status but no EIPS object, that expression throws, the flatMap catch returns an empty list, and lookupNetwork is never called for that client—so connectivity for that network is not refreshed.
Reviewed by Cursor Bugbot for commit 94d764d. Configure here.
Co-authored-by: Cursor <cursoragent@cursor.com>
🧪 Flaky unit test detectionRun history flaky detectionHistorical failure rate is a hint, not proof — review each suggestion in context. See the flaky-test-detection skill for the full pattern reference and manual audit workflow. Failures / runs sampled per window:
AI-detected flaky patterns
|
⚡ Performance Test Results
❌ 1 test failed · 7 tests · 1 device 📱 Devices tested (1)Android: Google Pixel 8 Pro (v14.0) ❌ Failed Tests (1)@metamask-mobile-platform
✅ Passed Tests (6)
Branch: |
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection: Performance Test Selection: |





Description
I recently started profiling MetaMask on mid-range Android devices and noticed something that surprised me - the wallet was making over 100 API calls the moment it unlocked. I decided to dig into it and figure out which of those were actually necessary.
Image of the 120+ API calls made on app unlock:

After tracing each call to its origin, the ones that stood out the most were the RPC calls - specifically because they were firing on every single wallet unlock, for every enabled network, before the user had done anything.
By default MetaMask ships with 8 enabled EVM popular networks. On unlock, the app was making 3 RPC calls per network, so a minimum of 24 RPC calls just to open the wallet. If you've added all popular networks that number jumps to 45.
Image of the 32 RPC API calls on a wallet of a user with 0 balance (new user). This happens on every wallet unlock!

Here's what each of those three calls was doing:
eth_blockNumber- called byAccountTrackerController's RPC balance fetcher before making a Multicall3 call. It needs a block reference to pass toeth_call.eth_callto Multicall3 (0xca11bde...) - called byAccountTrackerControllerto fetch the user's native ETH balance on chains not covered by the Accounts API v4, using a batchedaggregate3call.eth_blockNumber+eth_getBlockByNumber- called byEngine.lookupEnabledNetworks(), which was being triggered on every wallet mount by the network connection banner hook to probe whether each RPC endpoint was healthy.The thing is, MetaMask has improved a lot in the past year. We now have the Accounts API v4 which fetches balances for all major EVM networks centrally, and the new unified
AssetsControllertakes over balance fetching entirely when theassetsUnifyStateflag is on. The oldAccountTrackerControllerwas still running its unlock handler and hitting RPC on chains the Accounts API should have covered, becauseisDeprecatedwas never wired to the feature flag.For the network probe side, investigation uncovered a latent bug that was silently causing
lookupEnabledNetworksto crash on every app start, then repeatedly re-probe all networks once that crash was unintentionally "fixed" by an unrelated user action (adding a custom RPC). The root cause:NetworkEnablementControllerhad Sei (0x531) enabled by default in its initial state, butNetworkControllerhad no network client configured for0x531. CallingfindNetworkClientIdByChainId('0x531')threw synchronously and killed the entire probe function before any network could be inspected. Separately, even once that crash was bypassed,lookupEnabledNetworkshad no logic to skip networks whose health metadata was already persisted — so it would blindly re-probe every enabled network on every subsequent restart.The upstream inconsistency (Sei enabled by default in
NetworkEnablementControllerbut absent fromNetworkController) is addressed in a companion core PR: MetaMask/core#9542. That PR removes Sei from the default-enabled set so fresh installs start with a consistent state. The defensivetry/catchadded here inEngine.lookupEnabledNetworksis still necessary to protect existing users whose persisted state already has0x531: true.So this PR makes two changes:
1. Fix
Engine.lookupEnabledNetworksto be robust and skip redundant probes. The original.map().filter()chain let a single bad chain (like0x531) crash the whole function. Replacing it with a.flatMap()+try/catchgracefully skips any chain whosefindNetworkClientIdByChainIdthrows, letting all other networks be probed correctly. Additionally, a skip guard checksNetworkController.state.networksMetadata: if a network already has a known (non-Unknown) status and its EIP-1559 support has been established, it is excluded from the probe. This means theeth_blockNumber+eth_getBlockByNumberpair only fires for a given network on its first cold start — subsequent restarts skip it entirely because the metadata is persisted.2. Wire
AccountTrackerController.isDeprecatedto theassetsUnifyStateflag. When the controller is listed in the flag'sdeprecatedControllersarray, it short-circuits all its logic on unlock and makes zero RPC calls. TheAssetsControllerhandles balances from that point. This removes theeth_blockNumber+eth_call(Multicall3) pair per network on every unlock.The end result: 0 RPC calls on wallet unlock for returning users (metadata already cached), and only a one-time probe per network on first launch for new users.
Changelog
CHANGELOG entry: [performance] Eliminate redundant RPC calls on wallet unlock by caching network health metadata and deprecating legacy balance polling when the unified assets controller is active
Related issues
Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-3622
Related: MetaMask/core#9542
Manual testing steps
Screenshots/Recordings
Before
Screen.Recording.2026-07-16.at.10.08.21.mov
After
Screen.Recording.2026-07-16.at.10.17.28.mov
Pre-merge author checklist
Performance checks (if applicable)
trace()for usage andaddTokenfor an exampleFor performance guidelines and tooling, see the Performance Guide.
Pre-merge reviewer checklist
Note
Medium Risk
Touches unlock-time network probing and balance-fetch gating; behavior changes are flag-driven but affect core wallet startup and RPC usage across enabled chains.
Overview
Cuts unlock-time RPC traffic by changing how enabled networks are probed and by letting the unified assets path retire legacy balance polling.
Engine.lookupEnabledNetworksnow resolves client IDs withflatMap+try/catchso one bad enabled chain (e.g. Sei0x531with no client) no longer aborts the whole run. It also skipslookupNetworkwhen persistednetworksMetadataalready has a non-Unknownstatus and EIP-1559 is known, so health probes are not repeated every unlock.AccountTrackerControllerinit passesisDeprecatedfromselectIsControllerDeprecated('AccountTrackerController')on Redux state, so whenassetsUnifyStatelists that controller as deprecated, unlock no longer triggers its Multicall/eth_blockNumberRPC path.Tests cover throw-skipping, metadata skip behavior, and
isDeprecatedwiring.Reviewed by Cursor Bugbot for commit 09a3a63. Bugbot is set up for automated code reviews on this repo. Configure here.