Repository navigation
Conversation
…ailing SyncHealthStore tracks per account whether its syncs are failing and since when its stored balance was last confirmed; the streak is persisted on transitions so a snap restart does not reset it. After 90 s of failed syncs since the last success (three missed 30 s syncs, matching the client's 90 s re-check), getAccountBalances throws the expected-state BalanceStaleError (not sent to Sentry) and schedules a sync, rate limited to one per 10 s; the next successful sync clears the state and emits the balances-updated event.
This was referenced Oct 26, 2026
fix(stellar): run account reads under the fetch policy and report stale balances as unavailable
#525
Closed
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
@metamask/bitcoin-wallet-snapExplanation
Bug
When bitcoin syncs start failing after a successful one, BTC keeps showing the last balance as if it were current, with no sign that it is outdated, for as long as the failure lasts.
How to reproduce
release/13.54.0(acf000d) withyarn build:dev. For the "after" case, also build this branch and copy itsdist/bundle.js,snap.manifest.jsonandpreinstalled-snap.jsonintonode_modules/@metamask/bitcoin-wallet-snap. To see the wallet-side "Balance unavailable" + Retry, add core PR 10 and extension PR 12.bitcoin-mainnet.infura.iocan reuse the HTTP/2 connection already open to another*.infura.iohost (in our runs the Solana snap usually opened it first). The reply is an empty 404 without CORS headers, so the snap seesFailed to fetch. We reproduced this on a test machine where every*.infura.ioname 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.--disable-http2give no coalescing, and all four balances are correct.Expected: about 90 s after the last successful sync (plus the next poll), BTC shows "Balance unavailable" + Retry. It is correct again within seconds of the heal, with no Sentry event for the stale state.
Actual (before this change): BTC keeps the old amount. In fixes-v5 A-v5 it was still shown 3.5 min after its requests started failing.
Without a browser:
AccountUseCases.test.ts›stale balance after failing syncs (v6)›becomes stale 90 s after the last success while the syncs fail, and clears on the next success.Summary
After an account's syncs have kept failing for ~90 s since its last successful sync,
getAccountBalancesstops confirming the stored balance. It throwsBalanceStaleErrorinstead, an expected state that is never sent to Sentry, so the wallet shows "Balance unavailable" rather than an outdated amount. The call also schedules a sync. The next successful sync clears the state and pushes the real balance.Why
In Chrome, HTTP/2 connection coalescing across
*.infura.iocan send the snap's Esplora request over a connection opened for another Infura host. The provider router answers with an empty 404 and no CORS headers, which shows up asFailed to fetch. Once an account had synced,getAccountBalancesanswered from local state without any request. While the connection stayed broken, the snap kept confirming the last balance indefinitely as if it were current, even after funds moved. The wallet had no way to tell.What changed
store/SyncHealthStore.ts(new):staleSince);syncHealthV1, written only on transitions (its first failure and the success that ends it), so a snap restart between syncs does not reset it;BALANCE_STALE_AFTER_MS = 90_000, three missed 30 s cron syncs, matching the client's 90 s re-check (PR 11).use-cases/AccountUseCases.ts:isBalanceStale(id);scheduleStaleRefresh(id)schedules a sync, at most one per 10 s per account (STALE_SYNC_REQUEST_MIN_INTERVAL_MS).handlers/KeyringHandler.ts: after the unknown-balance check, a stale balance schedules the refresh and throwsBalanceStaleError.entities/error.ts:BalanceStaleErrorwith the stableBALANCE_STALE_MESSAGEmarker;isBalanceUnavailableError()covers both expected states (not synchronized, stale) across re-created, wrapped or serialized errors;HandlerMiddlewareandutils/errorsuse it, so neither state is tracked.Validation
Checked locally: the test files changed in this PR were run with the parent branch'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: PR 04)
KeyringHandler › getAccountBalances › throws the untracked BalanceStaleError and requests a sync when the syncs keep failing (v6)HandlerMiddleware › ... › does not track the re-created stale-balance error (v6)errors › shouldTrackError › returns false for the stale-balance state however it was wrapped (v6)AccountUseCases.test.tsandSyncHealthStore.test.tscannot run on the parent because they import the newSyncHealthStore. The new tests there (stale balance after failing syncs (v6): 4 tests;SyncHealthStore: 6) were not verified individually against the parent.Full run on this branch
jest --runInBand --testPathIgnorePatterns integration-testinpackages/bitcoin-wallet-snap: 28 suites, 746 tests pass.tsc --noEmit(package): clean.Manual A/B
A = Chrome with default HTTP/2 (reproduces the coalescing); B = Chrome with
--disable-http2(control, no coalescing). Every run used the same test wallet and a localyarn build:devof extension 13.54.0 (acf000d), with the snaps and assets-controller wired into node_modules by hand. "Unfixed 13.54.0" is that build with no fixes. The fixes-vN builds are cumulative: each one has every fix up to that pass, so an "after" number shows this PR together with the others in that build. Run names (A-v6, B-v7, ...) are our local test passes on those builds; times are CEST.snap_trackErrorBalanceStaleErrorin the consolesnap_trackError(control)Risks
BALANCE_STALE_AFTER_MSshould follow.syncHealthV1), written only on streak transitions.Changelog (
packages/bitcoin-wallet-snap/CHANGELOG.md, Unreleased → Fixed)References
Dependencies
snap.manifest.jsonshasum.Stale non-EVM balances series (Chrome HTTP/2 coalescing across
*.infura.io):Checklist