From 5547ab1cacfa3eae0cd05eac8865f70af00ae071 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 15 Jun 2026 17:05:27 -0500 Subject: [PATCH] feat(lite): add "Redownload blocks" (rescan from lite server) to Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a maintenance option for the lite wallet to re-download and re-scan every block from the lite server — useful when balances or history look wrong. - LiteWalletController::startRescan() runs the backend `rescan` command (which clears the wallet's synced block cache and re-syncs from its birthday) on a detached thread, reusing the existing sync progress/refresh machinery: it resets syncDone_ so refreshModel() shows progress again and refreshes data on completion. No-op if no wallet is open or a scan is already running. - scanInProgress() exposes the initial-sync-or-rescan state. - Settings (lite, open wallet) gains a "Redownload blocks" button behind a confirmation modal, disabled while a scan is running. i18n strings added. Co-Authored-By: Claude Opus 4.8 --- src/ui/pages/settings_page.cpp | 49 +++++++++++++++++++++++++++ src/util/i18n.cpp | 8 +++++ src/wallet/lite_wallet_controller.cpp | 32 +++++++++++++++++ src/wallet/lite_wallet_controller.h | 9 +++++ 4 files changed, 98 insertions(+) diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 19181e0..363b98a 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -147,6 +147,7 @@ struct SettingsPageState { bool confirm_delete_blockchain = false; bool confirm_rescan = false; bool confirm_restart_daemon = false; + bool confirm_lite_redownload = false; effects::ScrollFadeShader fade_shader; }; @@ -1819,6 +1820,24 @@ void RenderSettingsPage(App* app) { Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), s_settingsState.lite_encryption_status.c_str()); } + + // ---- Maintenance: re-download blocks from the lite server ---- + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + Type().text(TypeStyle::Body2, TR("lite_maintenance")); + + const bool scanning = app->liteWallet()->scanInProgress(); + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + ImGui::BeginDisabled(scanning); + if (TactileButton(TR("lite_redownload_blocks"), ImVec2(0, 0), S.resolveFont("button"))) { + s_settingsState.confirm_lite_redownload = true; + } + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", TR("tt_lite_redownload")); + ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), + scanning ? TR("lite_redownload_running") : TR("lite_redownload_desc")); } else if (!s_settingsState.lite_export_secret.empty()) { // Wallet closed while a backup/secret was revealed — don't leave it in memory. wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret); @@ -2640,6 +2659,36 @@ void RenderSettingsPage(App* app) { } } + // Confirm: lite wallet re-download blocks (rescan from the lite server — long but safe) + if (s_settingsState.confirm_lite_redownload) { + if (BeginOverlayDialog(TR("confirm_lite_redownload_title"), &s_settingsState.confirm_lite_redownload, 500.0f, 0.94f)) { + ImGui::PushFont(Type().iconLarge()); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_WARNING); + ImGui::PopFont(); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("warning")); + + ImGui::Spacing(); + ImGui::TextWrapped("%s", TR("confirm_lite_redownload_msg")); + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_lite_redownload_safe")); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; + if (ImGui::Button(TrId("cancel", "lite_redl_cancel").c_str(), ImVec2(btnW, 40))) { + s_settingsState.confirm_lite_redownload = false; + } + ImGui::SameLine(); + if (ImGui::Button(TrId("lite_redownload_blocks", "lite_redl_confirm").c_str(), ImVec2(btnW, 40))) { + if (auto* lite = app->liteWallet()) lite->startRescan(); + s_settingsState.confirm_lite_redownload = false; + } + EndOverlayDialog(); + } + } + } } // namespace ui diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 7148c77..54e613a 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -418,6 +418,14 @@ void I18n::loadBuiltinEnglish() strings_["confirm_rescan_safe"] = "Your wallet.dat and blockchain data are not deleted — only re-scanned."; strings_["confirm_restart_daemon_title"] = "Restart Daemon"; strings_["confirm_restart_daemon_msg"] = "This stops and restarts the daemon to apply the changed options. The wallet will briefly disconnect and reconnect."; + strings_["lite_maintenance"] = "Maintenance"; + strings_["lite_redownload_blocks"] = "Redownload blocks"; + strings_["lite_redownload_desc"] = "Re-fetch all blocks from the lite server (use if your balance or history looks wrong)."; + strings_["lite_redownload_running"] = "Re-downloading blocks…"; + strings_["tt_lite_redownload"] = "Re-download and re-scan all blocks from the lite server"; + strings_["confirm_lite_redownload_title"] = "Redownload Blocks"; + strings_["confirm_lite_redownload_msg"] = "This clears the wallet's downloaded block data and re-fetches and re-scans every block from the lite server. It can take a while; the wallet shows sync progress until it finishes."; + strings_["confirm_lite_redownload_safe"] = "Your wallet, keys, and seed are not affected — only the block data is re-downloaded."; strings_["tt_encrypt"] = "Encrypt wallet.dat with a passphrase"; strings_["tt_change_pass"] = "Change the wallet encryption passphrase"; strings_["tt_lock"] = "Lock the wallet immediately"; diff --git a/src/wallet/lite_wallet_controller.cpp b/src/wallet/lite_wallet_controller.cpp index e85c5e1..ee32d0f 100644 --- a/src/wallet/lite_wallet_controller.cpp +++ b/src/wallet/lite_wallet_controller.cpp @@ -605,6 +605,38 @@ void LiteWalletController::startSync() }); } +bool LiteWalletController::startRescan() +{ + if (!walletOpen_.load()) return false; + // Refuse if a sync/rescan is already running (would race two scans on the backend wallet lock). + if (!syncDone_ || !syncDone_->load()) return false; + + // Reset the done flag so refreshModel() re-enters its "scanning, publish progress only" path + // and the UI shows progress again; clearing it BEFORE launching the thread avoids a window + // where the worker would query balances mid-rescan. + syncDone_->store(false); + syncStarted_ = true; + liteLog("Block re-download (rescan) started"); + + // The previous sync/rescan thread has finished (syncDone_ was true above); detach it before + // reassigning syncThread_. Like startSync's thread it captures shared refs (bridge_ + syncDone_), + // never `this`, so detaching is safe. + if (syncThread_.joinable()) syncThread_.detach(); + + auto bridge = bridge_; + auto done = syncDone_; + syncThread_ = std::thread([bridge, done] { + if (bridge) { + // `rescan` clears the wallet's synced block cache and re-downloads/re-scans from the + // birthday height — a blocking, uninterruptible full scan, same as `sync`. + bridge->execute("rescan", ""); + bridge->execute("save", ""); // backend doesn't auto-save after a rescan + } + done->store(true); + }); + return true; +} + std::optional LiteWalletController::refreshModel() { if (!walletOpen_.load()) return std::nullopt; diff --git a/src/wallet/lite_wallet_controller.h b/src/wallet/lite_wallet_controller.h index 537e327..7ea90df 100644 --- a/src/wallet/lite_wallet_controller.h +++ b/src/wallet/lite_wallet_controller.h @@ -220,6 +220,15 @@ public: // op produces a ready wallet; safe to call once. void startSync(); + // Re-download and re-scan every block from the lite server: runs the backend `rescan` + // command (which clears the wallet's synced block cache and re-syncs from its birthday + // height) on a detached thread, reusing the sync progress + refresh machinery. No-op if no + // wallet is open or a scan is already running. True if a rescan was actually started. + bool startRescan(); + + // True while the initial sync OR a rescan is actively scanning (not yet complete). + bool scanInProgress() const { return syncStarted_ && !(syncDone_ && syncDone_->load()); } + // Generate a new address (shielded if true, else transparent) via the backend. Fast (local // key derivation), safe to call on the UI thread; the next refresh lists the new address. LiteNewAddressResult newAddress(bool shielded);