feat(node): one-click "Restore original wallet" after a daemon auto-recovery

Adds the restore action to the wallet-auto-recovery warning: undo the daemon's
salvage by swapping the untouched original (wallet.<ts>.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.<unixtime>.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-<ts>.dat (kept), COPIES
  the .bak into place (the .bak stays), moves database/ aside to
  database.pre-restore-<ts>.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) <noreply@anthropic.com>
This commit is contained in:
2026-08-10 00:45:47 -05:00
parent 3b7423f3a1
commit 384d64ea5d
4 changed files with 149 additions and 2 deletions

View File

@@ -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.<ts>.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<std::mutex> 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<std::string> 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<std::mutex> 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<std::mutex> 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)