Skip to content

fix(solana): fail getAccountBalances when an RPC fetch for a requested network fails - #527

Draft
rvelaz wants to merge 1 commit into
mainfrom
fix/stale-balances-09-solana-fail-on-fetch-error
Draft

rvelaz wants to merge 1 commit into
mainfrom
fix/stale-balances-09-solana-fail-on-fetch-error

Conversation

@rvelaz

@rvelaz rvelaz commented Oct 26, 2026 •

Copy link
Copy Markdown

Draft. Manifest shasum not yet regenerated, so snap checks may fail.
Built and tested on 80b21f6; main is one commit ahead (#331, stellar only) and git merge-tree shows no conflicts.
Not yet tested in a browser. No local extension build included a rebuilt solana snap; see "Manual A/B" below.

  • Package: @metamask/solana-wallet-snap

Explanation

Bug

When the Solana RPC requests fail, getAccountBalances returns a partial or empty result instead of an error, so the wallet keeps showing the last SOL amount as if it were current, with no "Balance unavailable".

How to reproduce

  1. Build extension release/13.54.0 (acf000d) with yarn build:dev. For the "after" case, also build this branch and copy its dist/bundle.js, snap.manifest.json and preinstalled-snap.json into node_modules/@metamask/solana-wallet-snap. To see the wallet-side "Balance unavailable" + Retry, add core PR 10 and extension PR 12.
  2. Load it unpacked in Chrome or Chromium with default settings (HTTP/2 on). This is profile A.
  3. Import a test wallet that holds BTC, TRX, XLM and SOL (we used a team test wallet). Onboarding creates the Bitcoin, Tron, Stellar and Solana accounts.
  4. Let the snaps' requests coalesce. In Chrome, a request to solana-mainnet.infura.io can reuse the HTTP/2 connection already open to another *.infura.io host (in our runs the Solana snap usually opened it first). The reply is an empty 404 without CORS headers, so the snap sees Failed to fetch. We reproduced this on a test machine where every *.infura.io name resolved to the same IP. It depends on the network path and may not reproduce on every network. In our runs we moved the shared connection between hosts on purpose: we dropped the test browser's sockets while briefly blocking the other Infura hosts, which "heals" one chain and breaks the others.
  5. Wait until SOL is correct, then move the shared connection away from Solana (heal another chain) so that the solana requests fail, and watch the Solana row.
  6. Control (profile B): the same steps with Chrome started with --disable-http2 give no coalescing, and all four balances are correct.

Expected: within seconds of the first failure, the Solana row shows "Balance unavailable" + Retry; it recovers on its own after the heal.
Actual (before this change): the row keeps the old amount (fixes-v2 A-v2: SOL failed from 10:30 onwards and never showed an error; that build had neither this PR nor core PR 11).

Without a browser: Keyring.test.ts › getAccountBalances › fails instead of answering with a partial result when an RPC fetch for a requested network failed (v3).

Summary

keyring_getAccountBalances now throws when a native-balance or token-account RPC fetch fails for one of the requested networks. Before, it returned the partial or empty result. The client then flags the SOL balance as unavailable instead of quietly keeping a stale balance.

Why

In Chrome, HTTP/2 connection coalescing across *.infura.io sends the solana snap's requests over a connection that belongs to another Infura host. Each reply is an empty 404 without CORS headers, which the snap sees as Failed to fetch. SnapAssetsAdapter.fetchAccountAssets used Promise.allSettled, reported each rejection to Sentry and dropped it from the result. So getAccountBalances "succeeded" with whatever was left, and the wallet kept showing the last SOL amount with no error and no Retry.

What changed

  • SnapAssetsAdapter.fetchAccountAssets(account, { failOnNetworks }):
    • collects the per-network failures of the native and token-account fetches;
    • throws AccountAssetsFetchError (new, carries failures, cause = the first reason) if any failure is on a network in failOnNetworks.
    • Without the option, it behaves as before: skip the failure and report it.
  • AssetsService.fetchAccountAssetsFromRpc(account, options) forwards the option.
  • SolanaKeyring.getAccountBalances passes failOnNetworks = the networks of the requested assets, via getNetworkFromToken.
  • Tests: adapter (failures collected; throws only for blocking networks), AssetsService and Keyring.

Validation

Checked locally: the test files changed in this PR were run with the parent's source (tests from this branch, every other file from the parent), then on this branch.

Unit tests that fail before and pass after (parent: 80b21f6)

  • SolanaKeyring › getAccountBalances › fails instead of answering with a partial result when an RPC fetch for a requested network failed (v3), fetches live balances for the account and returns only the requested assets
  • SnapAssetsAdapter › fetchAccountAssets with failOnNetworks › throws AccountAssetsFetchError when the native balance fetch of a required network fails, throws when token accounts of a required network cannot be fetched
  • AssetsService › fetchAccountAssetsFromRpc › delegates the live fetch to the Snap adapter, forwards failOnNetworks to the Snap adapter
  • In total, 6 tests fail on the parent, 99 pass, and 2 are skipped. This run used dummy public config values (no secrets).

Full run on this branch

  • jest --runInBand in packages/solana-wallet-snap: 84 of 89 suites pass, 1043 tests pass, 11 fail. The 11 failures are in 5 env-dependent suites (getSolanaExplorerUrl, three confirmation render tests and index.test). They fail the same way on 80b21f6 locally: there is no .env and no snap bundle. All touched suites pass.
  • tsc --noEmit (package): clean.

Manual A/B

This snap change was not exercised in a browser. None of the local fixes builds (v1–v7) wired in a rebuilt solana snap: only bitcoin and stellar (and tron from v5) were rebuilt, and the solana snap was the npm package (B-v3 notes: solana 7.2.0 from npm). The SOL results in those runs come from the client side, core PR 11 (an empty balance map counts as a failure):

Check Before After (client-side fix only, core PR 11)
A: SOL when its requests fail fixes-v2 build, A-v2: SOL failed from 10:30 and never showed an error fixes-v3 build, A-v3: full failure from 15:05:32 → SOL error 15:05:38 → 'Balance unavailable' at 15:05:45 (≤ 13 s); fixes-v4, A-v4: error in the same second as the first failure (15:46:02)

This PR fixes the same case at the source: the snap stops returning a partial or empty map. That also covers a partial result (some assets fetched, the native balance failed), which the client-side check cannot see. Before merging, repeat the A/B above with this snap wired in.

Risks

  • A transient failure on one requested network now makes the whole getAccountBalances call fail. The client already handles that: it keeps the account flagged with a Retry and a backoff. That is intended, but SOL can briefly flip to unavailable on a single blip.
  • Only getAccountBalances opts in. Asset discovery and cron sync keep the lenient behaviour.
  • Nit: fetchAccountAssets has the new JSDoc block stacked above the old one. It is kept here so this branch matches the tested cumulative code exactly.

Changelog (packages/solana-wallet-snap/CHANGELOG.md, Unreleased → Fixed)

  • keyring_getAccountBalances now fails when an RPC fetch for a requested network fails, instead of returning a partial or empty balance, so the wallet shows the balance as unavailable rather than stale (#527)

References

Dependencies

  • None in internal-snaps. Wallet side: PR 10 (core error tracking, backoff and retrySnapBalances) and PR 12 (extension UI) turn the thrown error into 'Balance unavailable' + Retry.
  • Follow-up: regenerate the snap.manifest.json shasum with yarn workspace @metamask/solana-wallet-snap build. It is not included here.

Stale non-EVM balances series (Chrome HTTP/2 coalescing across *.infura.io):

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

…d network fails

keyring_getAccountBalances passes failOnNetworks for the networks of the requested assets. If a native or token-account fetch for one of them fails, SnapAssetsAdapter throws AccountAssetsFetchError instead of returning a partial or empty result, so the client flags the balance unavailable instead of keeping a stale one. Other callers keep the skip-and-report behaviour.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant