fix: stop reporting "Sync 100%" while a wallet rescan is owed (#103) - #144
Merged
Conversation
Two ways the app claimed to be fully synced when it wasn't. P2: `SettingsViewModel` arms `WALLET_NEEDS_RESCAN` on descriptor load and on birthday change, but `FlorestaService` only fires the rescan 60s after filters reach the tip. The sync stepper's `allDone` consulted only `rescan_in_progress`, so for at least that minute — plus up to 10s of poll latency — the UI reported 100% with a rescan still pending. That is what put the reporter in a position to reconnect their Electrum wallets too early and see partial history. The foreground notification was worse: `applySyncState` knew only headers -> blocks -> stalled -> synced. With no filters phase and no rescan phase at all, it read "Fully synced" through the entire compact-filter sync (~3 hours in the reporter's log) and through a running rescan, while the Node screen correctly said otherwise. The phase logic existed in three hand-rolled copies (`ScreenNode`, `ScreenNodeTablet`, `FlorestaService`) which is why they drifted. Replace them with one pure `SyncPhase` computation in `domain/floresta`, shared by all three, where `WALLET_SCAN` now covers a rescan that is pending as well as one in flight. `SyncNotificationContent` maps a phase to notification text with `synced = (phase == SYNCED)`, making "Fully synced" impossible outside that phase. Extract the service's grace-window state into `WalletRescanGate`, which also bounds retries: since the UI now stays unsynced while the flag is set, a rescan that can never succeed (e.g. filters disabled) would otherwise pin it there forever. Ten attempts with backoff doubling from 60s to a 30m cap, then it clears the flag and lets the UI report synced. Timing runs off a monotonic `TimeSource` rather than the wall clock, so an NTP correction mid-session cannot make a long backoff fire instantly or never; a `TestTimeSource` asserts the whole schedule in virtual time. Verified on a mainnet device at height 959,254: with `ibd=false` and filters at 2.5%, screen and notification both report "Syncing filters" where the notification previously said "Synced - Block #959,254". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jvsena42
enabled auto-merge (rebase)
July 23, 2026 13:07
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.
Summary
Fixes P2 of #103, plus a worse sibling found while auditing it: the foreground-service notification and the Node screen disagreed about how synced we are.
P2.
SettingsViewModelarmsWALLET_NEEDS_RESCANon descriptor load (:451) and birthday change (:313), butFlorestaServiceonly fires the rescan 60s after filters reach the tip. The stepper'sallDoneconsulted onlyrescan_in_progress, so for at least that minute — plus up to 10s of poll latency — the UI reported 100% with a rescan still pending. That is what put @TheButterZone in a position to reconnect their Electrum wallets at step 20 when it wasn't yet safe to.The notification was worse.
applySyncStateknew only headers → blocks → stalled → synced. With no filters phase and no rescan phase at all, it readSynced - Block #N/ "Fully synced" through the entire compact-filter sync — ~3 hours in the reporter's log (steps 11→13) — and through a running rescan, while the Node screen correctly said "Syncing filters" / "Scanning wallet".The root cause of the drift: the phase logic existed in three hand-rolled copies (
ScreenNode,ScreenNodeTablet,FlorestaService).What changed
domain/floresta/SyncPhase.kt— one pure phase computation shared by all three surfaces.WALLET_SCANnow covers a rescan that is pending as well as one in flight. Precedence preserves today's phone-screen title order exactly. Also holdscomputeFilterSyncDecimal/computeRescanProgressDecimal, so the screen and the service stop computing them differently.presentation/service/SyncNotificationContent.kt— maps a phase to notification text, free of Android types so it is unit-testable.synced = (phase == SYNCED)is the one line that makes "Fully synced" impossible during filters or a rescan.domain/floresta/WalletRescanGate.kt— the service's grace-window state, plus a retry bound. Since the UI now stays unsynced while the flag is set, a rescan that can never succeed (e.g.NoBlockFilters) would otherwise pin it there forever. Ten attempts, backoff doubling from 60s to a 30m cap, then it clears the flag. Timing uses a monotonicTimeSource, not the wall clock — an NTP correction mid-session would otherwise make a long backoff fire instantly or never.ScreenNodeTabletnow takes aSyncPhaseinstead of five derived booleans, so it can't be handed an inconsistent set.Note
rescanblockchainflipsrescan_in_progresssynchronously before returningOk, so clearing the flag right after a successful RPC leaves no gap where both pending and in-progress read false.Tests
42 new tests (331 total). Beyond the per-phase cases,
SyncNotificationParityTestwalks all 256 combinations ofibd × progress × filters × rescan × pending × stalledand asserts the notification'ssyncedflag equals the stepper'sallDone— that is what fails today and what stops the two surfaces drifting again.Reverting just the P2 condition (
rescanInProgress || rescanPending→rescanInProgress) fails exactly four tests, one per surface, confirming they are load-bearing.Verified on device
Mainnet emulator at height 959,254. With
ibd=false, progress=1.0and filters at 2.5%:Also confirmed
WALLET_NEEDS_RESCANarms on descriptor load (read back from DataStore), and that with it armed while filters are at 12.67% both surfaces still correctly show "Syncing filters" — a pending rescan must not preempt a phase that has to finish first.Scope
P1, P3 and P4 of #103 remain open; P1 and P4 are filed upstream (getfloresta/Floresta#1225, #1226).
Checklist
feat:,fix:,chore:, ...)journeys/README.mdtestTags updated for UI changes — N/A, no testTags added or renamed./gradlew detekt,./gradlew lintDebug, and./gradlew testpass locallyPreview
Text-only change to sync labels; the before/after notification text is in the table above.