fix(ui): only toast "Blockchain rescan complete" for user-initiated rescans

The daemon-output parser treats autonomous background witness rebuilds as a rescan (they set
state_.sync.rescanning via the foundWitness branch), so one completing fired "Blockchain rescan
complete" even though the user never started a rescan — most visibly after a minimize, where a
whole rebuild's start+finish arrives in one batch.

Add user_initiated_rescan_ (atomic — some triggers run on worker threads), set it at the wallet's
real rescan triggers (the Rescan button, a -rescan/salvage/zap/reindex restart, key import, seed
migration), and gate the three "rescan complete" toasts on it, clearing it when shown. The
rescan/witness progress state machine is untouched — only the toast is gated — so background
rebuilds no longer announce a completed rescan while genuine user rescans still do (and can't
double-toast).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 01:18:57 -05:00
parent 0343d48c13
commit 398fb274fa
3 changed files with 22 additions and 5 deletions

View File

@@ -988,7 +988,10 @@ void App::update()
// saw the rescan running. Without the confirmed-active gate, the first // saw the rescan running. Without the confirmed-active gate, the first
// poll (which hits the still-running pre-restart daemon, rescanning=false) // poll (which hits the still-running pre-restart daemon, rescanning=false)
// would fire a false "complete" the instant rescan was clicked. // would fire a false "complete" the instant rescan was clicked.
ui::Notifications::instance().success("Blockchain rescan complete"); if (user_initiated_rescan_) {
ui::Notifications::instance().success("Blockchain rescan complete");
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
}
resetWitnessRescanProgress(); resetWitnessRescanProgress();
state_.sync.rescan_progress = 1.0f; state_.sync.rescan_progress = 1.0f;
} }
@@ -1091,8 +1094,9 @@ void App::update()
// Apply results directly — we are already on the main thread. // Apply results directly — we are already on the main thread.
const std::string& status = scan.lastStatus; const std::string& status = scan.lastStatus;
if (scan.finished) { if (scan.finished) {
if (state_.sync.rescanning) { if (state_.sync.rescanning && user_initiated_rescan_) {
ui::Notifications::instance().success("Blockchain rescan complete"); ui::Notifications::instance().success("Blockchain rescan complete");
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
} }
// Witness rebuild finishes with the rescan it's part of. // Witness rebuild finishes with the rescan it's part of.
resetWitnessRescanProgress(); resetWitnessRescanProgress();
@@ -5464,6 +5468,7 @@ void App::rescanBlockchain()
// pre-restart daemon and see rescanning=false) can't be misread as instant completion. // pre-restart daemon and see rescanning=false) can't be misread as instant completion.
state_.sync.rescanning = true; state_.sync.rescanning = true;
rescan_confirmed_active_ = false; rescan_confirmed_active_ = false;
user_initiated_rescan_ = true; // user-triggered rescan → its completion should toast
state_.sync.rescan_progress = 0.0f; state_.sync.rescan_progress = 0.0f;
state_.sync.rescan_status = decision.status; state_.sync.rescan_status = decision.status;
transactions_dirty_ = true; transactions_dirty_ = true;
@@ -5515,6 +5520,7 @@ void App::repairWallet()
// confirmed-active gating as rescan: the first poll may still reach the pre-restart daemon. // confirmed-active gating as rescan: the first poll may still reach the pre-restart daemon.
state_.sync.rescanning = true; state_.sync.rescanning = true;
rescan_confirmed_active_ = false; rescan_confirmed_active_ = false;
user_initiated_rescan_ = true; // user-triggered repair (implies rescan) → completion should toast
state_.sync.rescan_progress = 0.0f; state_.sync.rescan_progress = 0.0f;
state_.sync.rescan_status = decision.status; state_.sync.rescan_status = decision.status;
transactions_dirty_ = true; transactions_dirty_ = true;
@@ -6930,6 +6936,7 @@ void App::reindexBlockDatabase()
} }
if (!daemon_controller_) return; if (!daemon_controller_) return;
daemon_controller_->setReindexOnNextStart(true); daemon_controller_->setReindexOnNextStart(true);
user_initiated_rescan_ = true; // reindex implies a rescan → its completion should toast
daemon_controller_->resetCrashCount(); // the abort no longer counts against the restart budget daemon_controller_->resetCrashCount(); // the abort no longer counts against the restart budget
show_block_db_reindex_confirm_ = false; show_block_db_reindex_confirm_ = false;
block_db_reindex_available_ = false; // un-gate → the connect loop restarts the node with -reindex block_db_reindex_available_ = false; // un-gate → the connect loop restarts the node with -reindex

View File

@@ -1302,6 +1302,11 @@ private:
// the per-second mining/rescan-status pollers are suppressed (the daemon holds cs_main for // the per-second mining/rescan-status pollers are suppressed (the daemon holds cs_main for
// the whole scan and would block them); completion is signalled by the rescan RPC callback. // the whole scan and would block them); completion is signalled by the rescan RPC callback.
bool runtime_rescan_active_ = false; bool runtime_rescan_active_ = false;
// True only for a rescan the WALLET/USER initiated (the Rescan button, a -rescan/salvage/zap/reindex
// restart, key import, seed migration) — not an autonomous background witness rebuild the daemon does
// on its own. Gates the "Blockchain rescan complete" toast so background rebuilds don't fire it;
// cleared when the toast is shown.
std::atomic<bool> user_initiated_rescan_{false}; // atomic: some rescan triggers run on worker threads
// Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan // Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan
// that reconciles the preserved wallet.dat against the freshly-imported chain. // that reconciles the preserved wallet.dat against the freshly-imported chain.
bool post_bootstrap_rescan_pending_ = false; bool post_bootstrap_rescan_pending_ = false;

View File

@@ -1365,6 +1365,7 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed
if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected(); if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected();
if (salvage && daemon_controller_) daemon_controller_->setSalvageOnNextStart(true); // repair a corrupt wallet if (salvage && daemon_controller_) daemon_controller_->setSalvageOnNextStart(true); // repair a corrupt wallet
else if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); // (salvage implies rescan) else if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); // (salvage implies rescan)
if (salvage || needRescan) user_initiated_rescan_ = true; // wallet-triggered repair/rescan → completion should toast
// Start ONCE. Do NOT retry-spawn: a second start while the first is still shutting down leaves // Start ONCE. Do NOT retry-spawn: a second start while the first is still shutting down leaves
// two dragonxd holding wallet.dat against each other (BDB "Failed to rename … Error"). The // two dragonxd holding wallet.dat against each other (BDB "Failed to rename … Error"). The
// stopDaemonForWalletSwitch() wait already ensured the old node's process is gone, so a valid // stopDaemonForWalletSwitch() wait already ensured the old node's process is gone, so a valid
@@ -1680,7 +1681,10 @@ void App::refreshCoreData()
transactions_dirty_ = true; transactions_dirty_ = true;
last_tx_block_height_ = -1; last_tx_block_height_ = -1;
invalidateShieldedHistoryScanProgress(true); invalidateShieldedHistoryScanProgress(true);
ui::Notifications::instance().success("Blockchain rescan complete"); if (user_initiated_rescan_) {
ui::Notifications::instance().success("Blockchain rescan complete");
user_initiated_rescan_ = false; // surfaced once; not for background rebuilds
}
} }
NetworkRefreshService::applyConnectionInfoResult(state_, result.info); NetworkRefreshService::applyConnectionInfoResult(state_, result.info);
@@ -4831,7 +4835,7 @@ void App::beginAdoptSeedWallet()
// 3. Rescan on next start (only if the swap happened) and bring the daemon back up — // 3. Rescan on next start (only if the swap happened) and bring the daemon back up —
// unless we're quitting, in which case don't resurrect it. // unless we're quitting, in which case don't resurrect it.
if (swapDone && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); if (swapDone && daemon_controller_) { daemon_controller_->setRescanOnNextStart(true); user_initiated_rescan_ = true; }
if (!shutting_down_) { if (!shutting_down_) {
// We stopped the daemon ourselves (port_free) — clear the adopted-external latch so the // We stopped the daemon ourselves (port_free) — clear the adopted-external latch so the
// relaunched process is treated as owned (stop/isRunning/exit behave normally afterward). // relaunched process is treated as owned (stop/isRunning/exit behave normally afterward).
@@ -5130,7 +5134,7 @@ void App::rebuildWalletDatabase()
fs::rename(datadir + "/database", datadir + "/database.prerebuild-" + std::string(ts) + ".bak", e2); fs::rename(datadir + "/database", datadir + "/database.prerebuild-" + std::string(ts) + ".bak", e2);
for (const auto& e : fs::directory_iterator(datadir, e2)) for (const auto& e : fs::directory_iterator(datadir, e2))
if (e.path().filename().string().rfind("__db.", 0) == 0) { std::error_code e3; fs::remove(e.path(), e3); } if (e.path().filename().string().rfind("__db.", 0) == 0) { std::error_code e3; fs::remove(e.path(), e3); }
if (daemon_controller_) daemon_controller_->setRescanOnNextStart(true); if (daemon_controller_) { daemon_controller_->setRescanOnNextStart(true); user_initiated_rescan_ = true; }
} }
} }
} }
@@ -5558,6 +5562,7 @@ void App::runtimeRescan(int startHeight)
runtime_rescan_active_ = true; runtime_rescan_active_ = true;
state_.sync.rescanning = true; state_.sync.rescanning = true;
rescan_confirmed_active_ = true; rescan_confirmed_active_ = true;
user_initiated_rescan_ = true; // user clicked Rescan → completion should toast
state_.sync.rescan_progress = 0.0f; state_.sync.rescan_progress = 0.0f;
state_.sync.rescan_status = "Rescanning from block " + std::to_string(startHeight) + "..."; state_.sync.rescan_status = "Rescanning from block " + std::to_string(startHeight) + "...";
transactions_dirty_ = true; transactions_dirty_ = true;