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:
11
src/app.cpp
11
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))) {
|
||||
|
||||
@@ -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.<ts>.bak back over the salvaged copy + restart
|
||||
void pumpWalletRestore(); // main-thread: surface the restore op's result
|
||||
void processDeferredEncryption();
|
||||
|
||||
// Private methods - connection
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.<numbers>.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.<numbers>.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.<timestamp>.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";
|
||||
|
||||
Reference in New Issue
Block a user