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";