Repository navigation
Conversation
This was referenced Oct 26, 2026
fix(stellar): run account reads under the fetch policy and report stale balances as unavailable
#525
Closed
… scan - FullScanQueue: persistent per-account full-scan queue (reuses the rescanV1 state) with backoff 30 s..5 min; a failing account rotates to the tail, so neither new accounts nor the repair migration are blocked by a stuck one; never-synced accounts are re-queued - the balance is treated as unknown (isBalanceUnknown) until the first full scan succeeds; BitcoinAccount.hasSynchronized() tells a never-synced wallet (genesis checkpoint) apart - one scan per account: single-flight full scans, discovery scans reused for account creation (10 min TTL), at most 2 immediate scans on create; a reused discovery scan (discovery gap) that found activity is followed by a standard-gap scan - a balance request on an unknown account (the wallet's Retry) requests an on-demand full scan (30 s..2 min backoff) - the first successful sync after a failed scan requests a recovery scan within 5 s
rvelaz
force-pushed
the
fix/stale-balances-03-btc-scan-reliability
branch
from
October 28, 2026 10:27
d0acea0 to
679210f
Compare
rvelaz
changed the base branch from
fix/stale-balances-02-btc-requests
to
main
October 28, 2026 10:27
This branch has not been deployed
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
On
main(and with PR 01), a new BTC account whose first scan failed stays "Balance unavailable" long after the network is back. Nothing retries the first full scan, and the wallet's Retry does not start one. When the connection healed right after a failed scan, BTC took about 2 minutes to appear.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: Retry sends real Esplora requests and runs a full scan, and after a heal the pending first scan runs within seconds. Each account gets one scan at a time.
Actual (before this change): Retry sends nothing, and BTC stays unavailable after the heal (fixes-v1 build: no full scan about 2 min after the heal). With the v6 code, a heal 25 s after a failed scan waited about 2 min.
Without a browser:
AccountUseCases.test.ts›first heal of a never-synchronized account (v7)›requests the full scan within seconds when the first successful sync comes 25 s after a failed scanreplays the exact A-v6 timeline with fake timers.Summary
A new account's first full scan, and the discovery scan during onboarding, are retried with backoff until they succeed. Each account gets one scan at a time. The repair migration can no longer be blocked by a stuck account. The wallet's Retry (a balance request on an unknown account) now triggers a real scan. When the network comes back after a failed scan, the pending first scan runs within seconds.
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, so requests fail withFailed to fetch, often for minutes at a time. PR 01 makes a never-scanned account "unavailable" instead of 0, but on main:rescanV1repair list advanced one account per run and only after success, so one failing account blocked everything behind it;What changed
store/FullScanQueue.ts(new):rescanV1state key and shape, so existing installs continue their repair list;fullScanRetryV1, backoff 30 s doubling up to 5 min;isInitialScanPending().use-cases/AccountUseCases.ts:isBalanceUnknown= never synchronized or first full scan pending (PR 01 uses it to report the balance as unavailable);listDueFullScans()re-queues never-synced accounts;createManyenqueues new accounts and gives up to 2 of them an immediate first scan;createMany, so onboarding does not scan an account twice; there is also a discovery frontier. A reused discovery scan that found activity is followed by a standard-gap scan, so used accounts keep gap-20 coverage;fullScanper account; syncs join an in-flight scan;requestFullScan(account, reason)schedules a deduplicatedFullScanAccountbackground event, with on-demand backoff 30 s doubling up to 2 min;handlers/CronHandler.ts:#runDueFullScansreplaces#repairNextAccount: up to 2 scans per run, skipping in-flight or just-attempted accounts;fullScanAccountskips an on-demand scan whose purpose was already fulfilled.handlers/KeyringHandler.ts: a balance request on an account whose balance is unknown callsrequestFullScan(account, 'balance-request'). (With PR 01, that request also fails as "unavailable".)entities/account.ts,infra/BdkAccountAdapter.ts,infra/StoredAccountAdapter.ts:hasSynchronized()(same code as PR 01).Validation
Run on a Cursor cloud VM against
mainc2e8d19, with this PR's commit applied alone (packages/bitcoin-wallet-snap):tsc --noEmit: pass.yarn workspace @metamask/bitcoin-wallet-snap run test(with coverage) also passes. Plainmainpasses too (28 suites, 720 tests), so no failures are shared withmain.The earlier fail-before list was measured against the old parent (PR 02) and is no longer current.
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.The unfixed build had no "unavailable" state and no Retry (it showed 0), so the "before" column below uses the earlier fixes builds that had PR 01's state without this PR's logic (fixes-v1), or with the v6 timing bug.
Risks
rescanV1,fullScanRetryV1). The old{ pending: string[] }shape is kept, so a downgrade still reads it as the repair list.Changelog (
packages/bitcoin-wallet-snap/CHANGELOG.md, Unreleased → Fixed)References
Dependencies
main, on its own. It uses the shared fetch policy that feat(bitcoin): use shared fetch policy helpers #530 applied to the Esplora reads; it does not need fix(bitcoin): report a never-synced balance as unavailable, not 0 #519 or fix(bitcoin): guard Esplora reads with the shared fetch policy and timeouts #520.mainand can merge on its own. Whichever of the two merges second has one conflict: perf(bitcoin): scan new accounts with a gap of 5 and deepen only if used #522's discovery-gap first scan moves into this PR's#runFullScan.snap.manifest.jsonshasum.Stale non-EVM balances series (Chrome HTTP/2 coalescing across
*.infura.io):Checklist