From 384d64ea5d8240ad97f7afffa6770442ce18c6d9 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 10 Aug 2026 00:45:47 -0500 Subject: [PATCH] feat(node): one-click "Restore original wallet" after a daemon auto-recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the restore action to the wallet-auto-recovery warning: undo the daemon's salvage by swapping the untouched original (wallet..bak) back over the salvaged copy and clearing the stale BDB env that triggered the false recovery, then restarting. Modeled on beginAdoptSeedWallet (stop daemon → file ops → restart on a worker; result pumped to the main thread for notifications). Safety (fund-adjacent file ops on a real wallet — copy/rename only, never delete user data): - picks the newest wallet..bak via the pure, unit-tested newestWalletSalvageBak(); aborts if none. - verifies the .bak is a real Berkeley DB (probeWalletFile) before touching anything — won't overwrite a working wallet with a bad backup. - stops the daemon first (stopDaemonForWalletSwitch) so wallet.dat is released. - moves the salvaged copy aside to wallet.dat.salvaged-.dat (kept), COPIES the .bak into place (the .bak stays), moves database/ aside to database.pre-restore-.bak (kept), and drops only the transient __db.* BDB region files. Rolls back the move if the copy fails. - relaunches the node even on failure so it's never left down. The warning dialog now offers Restore original wallet / Open data folder / Keep salvaged copy. Full-node only; lite-safe. Build clean, suite green (1/1). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.cpp | 11 +++- src/app.h | 8 +++ src/app_network.cpp | 121 ++++++++++++++++++++++++++++++++++++++++++++ src/util/i18n.cpp | 11 ++++ 4 files changed, 149 insertions(+), 2 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index d4cd032..eed5389 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -830,6 +830,7 @@ void App::update() // Pick up progress/result from a running seed-wallet migration (create/sweep/adopt). pumpSeedMigration(); + pumpWalletRestore(); // While confirming the sweep, poll the tx confirmations + legacy balance every ~5s. if (show_seed_migration_ && seed_migration_step_ == SeedMigrationStep::Confirming) { seed_migration_poll_timer_ -= ImGui::GetIO().DeltaTime; @@ -4302,8 +4303,14 @@ void App::renderWalletRecoveredDialog() ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); ImGui::TextWrapped("%s", TR("wallet_recovered_body")); ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd())); - if (ui::material::TactileButton(TR("wallet_recovered_open_folder"), ImVec2(240.0f * dp, 0))) { - util::Platform::openFolder(util::Platform::getDragonXDataDir()); + // Primary: one-click restore of the untouched original (stops the node, swaps the .bak back over the + // salvaged copy, clears the stale BDB env, restarts). Copy/rename-only — nothing is deleted. + if (ui::material::TactileButton(TR("wallet_recovered_restore"), ImVec2(260.0f * dp, 0))) { + restoreOriginalWallet(); // clears show_wallet_recovered_dialog_ + } + ImGui::SameLine(); + if (ui::material::TactileButton(TR("wallet_recovered_open_folder"), ImVec2(200.0f * dp, 0))) { + util::Platform::openFolder(util::Platform::getDragonXDataDir()); // manual restore instead } ImGui::SameLine(); if (ui::material::TactileButton(TR("wallet_recovered_dismiss"), ImVec2(150.0f * dp, 0))) { diff --git a/src/app.h b/src/app.h index c8353b9..dbb50a5 100644 --- a/src/app.h +++ b/src/app.h @@ -906,6 +906,12 @@ private: bool wallet_auto_recovered_ = false; // a salvage happened this session bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per session bool show_wallet_recovered_dialog_ = false; // auto-shown warning dialog + // "Restore original wallet" background op: worker sets these under the mutex, pumpWalletRestore() + // (main thread) shows the result. 0 = success, 1 = warning, 2 = error. + std::mutex wallet_restore_mutex_; + bool wallet_restore_done_ = false; + int wallet_restore_severity_ = 0; + std::string wallet_restore_msg_; // Live progress for the switch modal: it stays open from confirm through stop → wait-for-exit → start → // reconnect and auto-closes when the new node connects (onConnected). Phase is worker-updated; // dialog_open_ gates rendering — both atomic since onConnected/the worker may run off the main thread. @@ -1338,6 +1344,8 @@ private: void renderBlockDbReindexDialog(); // offer to rebuild an unreadable block database (-reindex) void reindexBlockDatabase(); // restart the daemon with -reindex to rebuild the block DB void renderWalletRecoveredDialog(); // warn that the node auto-recovered/salvaged wallet.dat + void restoreOriginalWallet(); // swap the wallet..bak back over the salvaged copy + restart + void pumpWalletRestore(); // main-thread: surface the restore op's result void processDeferredEncryption(); // Private methods - connection diff --git a/src/app_network.cpp b/src/app_network.cpp index f24e766..2b0e088 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -58,6 +58,7 @@ #include "data/exchange_candles.h" #include "data/seed_migration_resume.h" #include "util/platform.h" +#include "util/wallet_file_probe.h" // verify a salvage-backup is a real BDB before restoring it #include "util/perf_log.h" #include "util/i18n.h" #include "util/secure_vault.h" @@ -4545,6 +4546,126 @@ void App::beginAdoptSeedWallet() }); } +// Undo a daemon wallet auto-recovery: swap the untouched original (wallet..bak) back over the +// salvaged copy and clear the stale BDB env that triggered the false recovery, then restart. Modeled on +// beginAdoptSeedWallet — stop daemon → file ops (copy/rename only, NEVER delete user data) → restart. +void App::restoreOriginalWallet() +{ + if (!supportsFullNodeLifecycleActions()) { + ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build"); + return; + } + if (daemon_restarting_) { + ui::Notifications::instance().warning(TR("wallet_restore_busy")); + return; + } + show_wallet_recovered_dialog_ = false; + { std::lock_guard lk(wallet_restore_mutex_); wallet_restore_done_ = false; } + daemon_restarting_ = true; // gate the reconnect loop while we swap files + connection_status_ = TR("sb_restarting_daemon"); + if (rpc_ && rpc_->isConnected()) rpc_->disconnect(); + onDisconnected("Restoring original wallet"); + ui::Notifications::instance().info(TR("wallet_restore_started"), 12.0f); + + const std::string activeWalletName = (settings_ && !settings_->getActiveWalletFile().empty()) + ? settings_->getActiveWalletFile() : std::string("wallet.dat"); + + async_tasks_.submit("Restore original wallet", [this, activeWalletName](const util::AsyncTaskManager::Token&) { + namespace fs = std::filesystem; + std::string err, warn; + try { + const std::string datadir = util::Platform::getDragonXDataDir(); + // 1. Find the newest salvage backup (offline — no daemon needed). + std::vector files; + { + std::error_code lec; + for (const auto& e : fs::directory_iterator(datadir, lec)) + if (!lec) files.push_back(e.path().filename().string()); + } + const std::string bak = daemon::newestWalletSalvageBak(files); + if (bak.empty()) { + err = TR("wallet_restore_no_backup"); + } else if (!util::probeWalletFile(datadir + "/" + bak).isBerkeleyDB) { + err = TR("wallet_restore_bad_backup"); // don't overwrite a working wallet with a bad .bak + } else if (!stopDaemonForWalletSwitch()) { // 2. Release wallet.dat + the RPC port first. + err = TR("wallet_restore_stop_failed"); + } else { + std::error_code ec; + std::time_t t = std::time(nullptr); + std::tm tmv{}; +#ifdef _WIN32 + localtime_s(&tmv, &t); +#else + localtime_r(&t, &tmv); +#endif + char ts[32]; std::strftime(ts, sizeof(ts), "%Y%m%d-%H%M%S", &tmv); + const std::string active = datadir + "/" + activeWalletName; + const std::string salvagedAside = active + ".salvaged-" + ts + ".dat"; + // 3. Move the salvaged copy aside (NEVER delete), then copy the original .bak into place + // (copy, so the .bak itself stays as a backup). Roll back the move if the copy fails. + bool movedSalvaged = false; + if (fs::exists(active)) { + fs::rename(active, salvagedAside, ec); + if (ec) err = TR("wallet_restore_move_failed"); + else movedSalvaged = true; + } + if (err.empty()) { + fs::copy_file(datadir + "/" + bak, active, fs::copy_options::overwrite_existing, ec); + if (ec) { + if (movedSalvaged) { std::error_code e2; fs::rename(salvagedAside, active, e2); } + err = TR("wallet_restore_copy_failed"); + } + } + // 4. Clear the stale BDB environment that triggered the false recovery — otherwise the + // daemon would just re-salvage the restored wallet on the next start. Move database/ + // aside (keeps its logs) and drop the transient __db.* region files. + if (err.empty()) { + std::error_code e2; + if (fs::exists(datadir + "/database")) + fs::rename(datadir + "/database", datadir + "/database.pre-restore-" + ts + ".bak", 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); + } + } + } + } + + // 5. Bring the daemon back up (unless quitting). Even on a restore failure we relaunch so the + // node isn't left down; the connect loop reconnects and onConnected clears the gate. + if (!shutting_down_) { + if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected(); + if (!startEmbeddedDaemon() && err.empty()) + warn = TR("wallet_restore_no_restart"); + } + } catch (const std::exception& e) { + err = std::string("Restore failed: ") + e.what(); + } catch (...) { + err = "Restore failed due to an unexpected error."; + } + daemon_restarting_ = false; // ALWAYS re-arm the reconnect gate + + std::lock_guard lk(wallet_restore_mutex_); + wallet_restore_severity_ = !err.empty() ? 2 : (!warn.empty() ? 1 : 0); + wallet_restore_msg_ = !err.empty() ? err : warn; + wallet_restore_done_ = true; + }); +} + +void App::pumpWalletRestore() +{ + if (capture_mode_) return; + bool done = false; int sev = 0; std::string msg; + { + std::lock_guard lk(wallet_restore_mutex_); + if (wallet_restore_done_) { done = true; sev = wallet_restore_severity_; msg = wallet_restore_msg_; wallet_restore_done_ = false; } + } + if (!done) return; + if (sev == 2) ui::Notifications::instance().error(msg, 25.0f); + else if (sev == 1) ui::Notifications::instance().warning(msg, 20.0f); + else ui::Notifications::instance().success(TR("wallet_restore_ok"), 12.0f); +} + void App::pumpSeedMigration() { if (capture_mode_) return; // no live ops during a UI sweep (steps are set directly) diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 9201bb6..2d0e1fe 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1199,7 +1199,18 @@ void I18n::loadBuiltinEnglish() strings_["wallet_recovered_body"] = "On startup the node decided your wallet.dat looked damaged and recovered it automatically. Your ORIGINAL wallet was NOT deleted — it was renamed to \"wallet..bak\" in your data folder, and a salvaged copy is loaded now.\n\nThe salvaged copy may be incomplete, so the balance shown here could be wrong — don't treat it as final.\n\nThis is often a false alarm caused by leftover database files (e.g. after moving the wallet between machines). To restore your original: quit the wallet, then in the data folder rename the current wallet.dat aside, rename \"wallet..bak\" back to \"wallet.dat\", delete the \"database\" folder and any \"__db.*\" files, and reopen."; strings_["wallet_recovered_open_folder"] = "Open data folder"; strings_["wallet_recovered_dismiss"] = "Keep salvaged copy"; + strings_["wallet_recovered_restore"] = "Restore original wallet"; strings_["wallet_recovered_notify"] = "The node recovered your wallet and moved the original to a .bak — your shown balance may be incomplete. See the prompt to restore it."; + // One-click "Restore original wallet" flow. + strings_["wallet_restore_started"] = "Restoring your original wallet and restarting the node…"; + strings_["wallet_restore_busy"] = "The node is busy restarting — try again in a moment."; + strings_["wallet_restore_ok"] = "Original wallet restored. The node is loading it now."; + strings_["wallet_restore_no_backup"] = "Couldn't find a wallet..bak to restore. Nothing was changed."; + strings_["wallet_restore_bad_backup"] = "The backup wallet file looks unreadable, so it was NOT restored — your current wallet is unchanged. Restore from your own backup instead."; + strings_["wallet_restore_stop_failed"] = "The node didn't stop in time, so nothing was changed. Try again."; + strings_["wallet_restore_move_failed"] = "Couldn't set the current wallet aside — nothing was changed."; + strings_["wallet_restore_copy_failed"] = "Couldn't install the backup wallet; your current wallet was left in place."; + strings_["wallet_restore_no_restart"] = "Your original wallet was restored, but the node didn't restart — start it from Settings."; // Receive Tab strings_["receiving_addresses"] = "Your Receiving Addresses";