diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index a4c876b..dacbc33 100644 Binary files a/res/fonts/NotoSansCJK-Subset.ttf and b/res/fonts/NotoSansCJK-Subset.ttf differ diff --git a/res/lang/de.json b/res/lang/de.json index 7858a20..bb6e95e 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -1294,6 +1294,8 @@ "sweep_to": "Gefegt an:", "sweep_toggle": "In meine Wallet fegen (Schlüssel nicht behalten)", "sweep_tx": "Transaktion:", + "switch_corrupt_body": "Diese Wallet scheint beschädigt zu sein – der Knoten konnte sie nicht öffnen. Aus einem Backup wiederherstellen, neu erstellen oder eine Reparatur versuchen.", + "switch_corrupt_repair": "Reparatur versuchen (Salvage)", "switch_progress_background": "Im Hintergrund fortsetzen", "switch_progress_default_wallet": "Standard-Wallet", "switch_progress_elapsed": "Vergangen", diff --git a/res/lang/es.json b/res/lang/es.json index 46d924a..8a8762b 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -1294,6 +1294,8 @@ "sweep_to": "Barrido a:", "sweep_toggle": "Barrer a mi monedero (no conservar la clave)", "sweep_tx": "Transacción:", + "switch_corrupt_body": "Esta cartera parece dañada: el nodo no pudo abrirla. Restáurala desde una copia de seguridad, vuelve a crearla o intenta repararla.", + "switch_corrupt_repair": "Intentar reparar (salvage)", "switch_progress_background": "Continuar en segundo plano", "switch_progress_default_wallet": "Cartera predeterminada", "switch_progress_elapsed": "Transcurrido", diff --git a/res/lang/fr.json b/res/lang/fr.json index 865c409..3097e50 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -1294,6 +1294,8 @@ "sweep_to": "Balayé vers :", "sweep_toggle": "Balayer vers mon portefeuille (ne pas conserver la clé)", "sweep_tx": "Transaction :", + "switch_corrupt_body": "Ce portefeuille semble corrompu — le nœud n'a pas pu l'ouvrir. Restaurez-le depuis une sauvegarde, recréez-le ou tentez de le réparer.", + "switch_corrupt_repair": "Tenter une réparation (salvage)", "switch_progress_background": "Continuer en arrière-plan", "switch_progress_default_wallet": "Portefeuille par défaut", "switch_progress_elapsed": "Écoulé", diff --git a/res/lang/ja.json b/res/lang/ja.json index 131a97e..cd6f9ba 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -1294,6 +1294,8 @@ "sweep_to": "集約先:", "sweep_toggle": "ウォレットに集約(鍵は保持しない)", "sweep_tx": "取引:", + "switch_corrupt_body": "このウォレットは破損しているようです。ノードが開けませんでした。バックアップから復元するか、作り直すか、修復を試してください。", + "switch_corrupt_repair": "修復を試す(salvage)", "switch_progress_background": "バックグラウンドで続行", "switch_progress_default_wallet": "既定のウォレット", "switch_progress_elapsed": "経過", diff --git a/res/lang/ko.json b/res/lang/ko.json index 9d1dadb..fb3f94f 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -1294,6 +1294,8 @@ "sweep_to": "쓸어담은 주소:", "sweep_toggle": "내 지갑으로 쓸어담기 (키 보관 안 함)", "sweep_tx": "거래:", + "switch_corrupt_body": "이 지갑이 손상된 것 같습니다. 노드가 열 수 없습니다. 백업에서 복원하거나 다시 만들거나 복구를 시도하세요.", + "switch_corrupt_repair": "복구 시도(salvage)", "switch_progress_background": "백그라운드에서 계속", "switch_progress_default_wallet": "기본 지갑", "switch_progress_elapsed": "경과", diff --git a/res/lang/pt.json b/res/lang/pt.json index c0b1986..ed13eb3 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -1294,6 +1294,8 @@ "sweep_to": "Varrido para:", "sweep_toggle": "Varrer para minha carteira (não manter a chave)", "sweep_tx": "Transação:", + "switch_corrupt_body": "Esta carteira parece corrompida — o nó não conseguiu abri-la. Restaure de um backup, recrie-a ou tente repará-la.", + "switch_corrupt_repair": "Tentar reparar (salvage)", "switch_progress_background": "Continuar em segundo plano", "switch_progress_default_wallet": "Carteira padrão", "switch_progress_elapsed": "Decorrido", diff --git a/res/lang/ru.json b/res/lang/ru.json index e6ec3f2..62d0fc7 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -1294,6 +1294,8 @@ "sweep_to": "Переведено на:", "sweep_toggle": "Перевести в мой кошелёк (не сохранять ключ)", "sweep_tx": "Транзакция:", + "switch_corrupt_body": "Похоже, этот кошелёк повреждён — узел не смог его открыть. Восстановите из резервной копии, создайте заново или попробуйте восстановить.", + "switch_corrupt_repair": "Попробовать восстановить (salvage)", "switch_progress_background": "Продолжить в фоне", "switch_progress_default_wallet": "Кошелёк по умолчанию", "switch_progress_elapsed": "Прошло", diff --git a/res/lang/zh.json b/res/lang/zh.json index cbfa78a..b28ec38 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -1294,6 +1294,8 @@ "sweep_to": "归集到:", "sweep_toggle": "归集到我的钱包(不保留密钥)", "sweep_tx": "交易:", + "switch_corrupt_body": "此钱包似乎已损坏——节点无法打开它。请从备份恢复、重新创建,或尝试修复。", + "switch_corrupt_repair": "尝试修复(salvage)", "switch_progress_background": "在后台继续", "switch_progress_default_wallet": "默认钱包", "switch_progress_elapsed": "已用时", diff --git a/src/app.cpp b/src/app.cpp index 0ff27fb..2031c7a 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -3919,6 +3919,15 @@ void App::renderSwitchStopDaemonDialog() // rather than repeating the title. ui::material::DialogWarningHeader(wallet_switch_error_.c_str()); ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd())); + // If the node reported the target wallet as corrupt, offer a one-click -salvagewallet repair + // (retries the switch to that wallet with recovery enabled). + if (switch_wallet_corrupt_.load()) { + if (ui::material::TactileButton(TR("switch_corrupt_repair"), ImVec2(240.0f * dp, 0))) { + const std::string target = wallet_switch_target_file_; + switchToWallet(target, /*stopDaemonConfirmed=*/true, /*salvage=*/true); // reopens the progress modal + } + ImGui::SameLine(); + } if (ui::material::TactileButton(TR("close"), ImVec2(110.0f * dp, 0))) { wallet_switch_dialog_open_.store(false); wallet_switch_phase_.store(static_cast(WalletSwitchPhase::None)); diff --git a/src/app.h b/src/app.h index 7e7a788..58065c9 100644 --- a/src/app.h +++ b/src/app.h @@ -492,8 +492,8 @@ public: // (rescan only if it was never synced in this datadir). Per-wallet data follows automatically // via the identity-scoped caches (P1). No-op if that wallet is already active. // stopDaemonConfirmed: skip the "stop the running node?" confirmation (set true when re-entered from - // that dialog). The confirmation only appears when the node to restart was ADOPTED (externally running). - void switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed = false); + // that dialog). salvage: start the target node with -salvagewallet (repair a corrupt wallet). + void switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed = false, bool salvage = false); // Main-thread continuation: if a wallet switch's daemon failed to start, revert active_wallet_file. void processWalletSwitchRevert(); @@ -780,6 +780,10 @@ private: std::atomic wallet_switch_dialog_open_{false}; std::string wallet_switch_error_; double wallet_switch_started_time_ = 0.0; // ImGui::GetTime() captured on first progress frame (elapsed) + // Set when a failed switch's node output indicates the target wallet is CORRUPT — the Failed modal then + // offers a one-click "-salvagewallet" repair, retrying the switch to wallet_switch_target_file_. + std::atomic switch_wallet_corrupt_{false}; + std::string wallet_switch_target_file_; // the wallet we were switching TO (for a salvage retry) // Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast // and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it. diff --git a/src/app_network.cpp b/src/app_network.cpp index 823b4c3..35ca3b0 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -189,6 +189,17 @@ static WarmupText translateWarmup(const std::string& raw) return {raw.c_str(), ""}; } +// A node told to open a wallet it can't prints one of these to its console: the BDB corrupt-wallet +// recovery ("Failed to rename … .bak", "wallet.dat corrupt, salvage failed") or a generic load error. +// Used to offer a -salvagewallet repair when a switch fails because the target wallet is corrupt. +static bool walletOutputLooksCorrupt(const std::string& out) +{ + return out.find("Failed to rename") != std::string::npos + || out.find("salvage failed") != std::string::npos + || out.find("wallet.dat corrupt") != std::string::npos + || out.find("Error loading wallet") != std::string::npos; +} + // Phrases dragonxd prints to its console while initializing, in the order translateWarmup() // understands them. The most recent matching console line tells us which stage the node is in // even when the RPC probe just times out (no -28 reply to read). @@ -1029,7 +1040,7 @@ void App::updateWalletIndexForActiveWallet(bool markOpened) if (wallet_index_.upsert(e)) wallet_index_.save(); } -void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed) +void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed, bool salvage) { if (!supportsFullNodeLifecycleActions()) { ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build"); @@ -1079,8 +1090,10 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed const std::string prevWallet = settings_->getActiveWalletFile(); // for revert if the new one fails wallet_switch_prev_file_ = prevWallet; + wallet_switch_target_file_ = walletFile; // remembered for a "salvage" retry from the failure modal wallet_switch_failed_.store(false); switch_stop_failed_.store(false); // reason latch for the revert message; a stale one must not leak + switch_wallet_corrupt_.store(false); wallet_switch_pending_confirm_.store(true); // cleared on a successful connect (onConnected) settings_->setActiveWalletFile(walletFile); settings_->save(); @@ -1117,8 +1130,12 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed wallet_switch_phase_.store(static_cast(WalletSwitchPhase::Stopping)); wallet_switch_dialog_open_.store(true); - async_tasks_.submit("Switch wallet", [this, needRescan](const util::AsyncTaskManager::Token&) { + async_tasks_.submit("Switch wallet", [this, needRescan, salvage](const util::AsyncTaskManager::Token&) { bool ok = false; + // Watermark the node's captured console output so we scan only THIS start's lines for a + // corruption signature (below), not stale output from a previous daemon. + std::size_t out_off = 0; + if (daemon_controller_) daemon_controller_->outputSince(out_off); try { // Stop the node so it releases the datadir .lock + RPC port before we relaunch. For an adopted // (external) daemon this sends a graceful RPC "stop" — stopEmbeddedDaemon()'s policy would leave @@ -1137,7 +1154,8 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed // as owned (stop/isRunning/exit behave normally for the next switch and app exit). wallet_switch_phase_.store(static_cast(WalletSwitchPhase::Starting)); if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected(); - if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); + if (salvage && daemon_controller_) daemon_controller_->setSalvageOnNextStart(true); // repair a corrupt wallet + else if (needRescan && daemon_controller_) daemon_controller_->setRescanOnNextStart(true); // (salvage implies rescan) // Start ONCE. Do NOT retry-spawn: a second start while the first is still shutting down leaves // two dragonxd holding wallet.dat against each other (BDB "Failed to rename … Error"). The // stopDaemonForWalletSwitch() wait already ensured the old node's process is gone, so a valid @@ -1149,6 +1167,13 @@ void App::switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed std::this_thread::sleep_for(std::chrono::milliseconds(1500)); if (!isEmbeddedDaemonRunning()) ok = false; } + // If it died in init, scan this start's captured output for a corrupt-wallet signature so the + // failure modal can offer a repair. Give the dying node a moment to flush its final lines. + if (!ok && !shutting_down_ && daemon_controller_) { + for (int i = 0; i < 12 && !shutting_down_; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (walletOutputLooksCorrupt(daemon_controller_->outputSince(out_off))) + switch_wallet_corrupt_.store(true); + } // Process is up and staying up — now waiting for RPC to answer (onConnected closes the modal). if (ok) wallet_switch_phase_.store(static_cast(WalletSwitchPhase::Reconnecting)); } @@ -1181,12 +1206,15 @@ void App::processWalletSwitchRevert() } // Clear the crash-wedge so the (good) previous wallet's daemon is allowed to start again. if (daemon_controller_) daemon_controller_->resetCrashCount(); - // Accurate reason: "port wouldn't free" (the running node kept the connection) vs. a genuinely bad - // target wallet. The former is the adopted-external-daemon case; the latter a daemon that exited in init. - const std::string reason = switch_stop_failed_.exchange(false) - ? std::string("Couldn't switch wallets — the running node didn't release its connection in time. " - "It's still on the previous wallet.") - : std::string("Couldn't open that wallet — reverted to the previous one."); + // Accurate reason: "port wouldn't free" (the running node kept the connection); "wallet is corrupt" + // (the node's recovery couldn't open it); else a generic bad-wallet failure. + const std::string reason = + switch_stop_failed_.exchange(false) + ? std::string("Couldn't switch wallets — the running node didn't release its connection in time. " + "It's still on the previous wallet.") + : switch_wallet_corrupt_.load() + ? std::string(TR("switch_corrupt_body")) + : std::string("Couldn't open that wallet — reverted to the previous one."); // Surface it in the progress modal if it's still up (keep it open on Failed until the user closes); // otherwise (the user chose "continue in background") fall back to a toast. if (wallet_switch_dialog_open_.load()) { diff --git a/src/daemon/daemon_controller.cpp b/src/daemon/daemon_controller.cpp index 927745d..6f27c48 100644 --- a/src/daemon/daemon_controller.cpp +++ b/src/daemon/daemon_controller.cpp @@ -121,6 +121,11 @@ void DaemonController::setZapOnNextStart(bool enabled) daemon_->setZapOnNextStart(enabled); } +void DaemonController::setSalvageOnNextStart(bool enabled) +{ + daemon_->setSalvageOnNextStart(enabled); +} + bool DaemonController::zapOnNextStart() const { return daemon_->zapOnNextStart(); diff --git a/src/daemon/daemon_controller.h b/src/daemon/daemon_controller.h index 8e67ec1..5f2fe25 100644 --- a/src/daemon/daemon_controller.h +++ b/src/daemon/daemon_controller.h @@ -107,6 +107,7 @@ public: bool rescanOnNextStart() const; void setZapOnNextStart(bool enabled); bool zapOnNextStart() const; + void setSalvageOnNextStart(bool enabled); static ShutdownDecision evaluateShutdownPolicy(bool hasDaemon, bool externalDaemonDetected, diff --git a/src/daemon/embedded_daemon.cpp b/src/daemon/embedded_daemon.cpp index b334e99..2e67fd6 100644 --- a/src/daemon/embedded_daemon.cpp +++ b/src/daemon/embedded_daemon.cpp @@ -524,9 +524,16 @@ bool EmbeddedDaemon::start(const std::string& binary_path) args.push_back("-wallet=" + wallet_file_); } - // Add wallet-repair flag if requested (one-shot). -zapwallettxes=2 wipes all wallet tx/note - // records and rebuilds them from the chain; it implies -rescan, so don't also pass -rescan. - if (zap_on_next_start_.exchange(false)) { + // Add wallet-repair flag if requested (one-shot). Precedence: salvage > zap > rescan; each implies a + // rescan in the daemon, so we don't stack them. + if (salvage_on_next_start_.exchange(false)) { + // -salvagewallet recovers readable keypairs from a corrupt wallet.dat; the daemon then implies -rescan. + DEBUG_LOGF("[INFO] Adding -salvagewallet flag to recover a corrupt wallet\n"); + args.push_back("-salvagewallet"); + zap_on_next_start_.store(false); + rescan_on_next_start_.store(false); + } else if (zap_on_next_start_.exchange(false)) { + // -zapwallettxes=2 wipes all wallet tx/note records and rebuilds them from the chain (implies -rescan). DEBUG_LOGF("[INFO] Adding -zapwallettxes=2 flag for wallet repair (zap & rebuild)\n"); args.push_back("-zapwallettxes=2"); rescan_on_next_start_.store(false); // implied by zap; avoid redundant -rescan diff --git a/src/daemon/embedded_daemon.h b/src/daemon/embedded_daemon.h index 682ed8d..4021cab 100644 --- a/src/daemon/embedded_daemon.h +++ b/src/daemon/embedded_daemon.h @@ -201,6 +201,11 @@ public: void setZapOnNextStart(bool v) { zap_on_next_start_ = v; } bool zapOnNextStart() const { return zap_on_next_start_.load(); } + // -salvagewallet: attempt to recover keys from a corrupt wallet.dat on startup (implies -rescan in + // the daemon). One-shot, consumed on the next start. Used to repair a wallet a switch flagged corrupt. + void setSalvageOnNextStart(bool v) { salvage_on_next_start_ = v; } + bool salvageOnNextStart() const { return salvage_on_next_start_.load(); } + /** * @brief One-shot isolated-datadir override for the NEXT start(): run the daemon against a * different datadir (with its own DRAGONX.conf) plus the given extra args. Used by the @@ -274,6 +279,7 @@ private: std::atomic crash_count_{0}; // consecutive crash counter std::atomic rescan_on_next_start_{false}; // -rescan flag for next start std::atomic zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start + std::atomic salvage_on_next_start_{false}; // -salvagewallet flag for next start std::string override_datadir_; // one-shot: -datadir for the next start std::vector override_extra_args_; // one-shot: extra args for the next start bool skip_port_check_ = false; // isolated instance on a non-default port diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 9672ac5..d93cd01 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1057,6 +1057,8 @@ void I18n::loadBuiltinEnglish() strings_["switch_progress_elapsed"] = "Elapsed"; strings_["switch_progress_default_wallet"] = "Default wallet"; strings_["switch_progress_external_wallet"] = "External wallet"; + strings_["switch_corrupt_body"] = "This wallet appears corrupt — the node couldn't open it. Restore it from a backup, re-create it, or try to repair it."; + strings_["switch_corrupt_repair"] = "Try to repair (salvage)"; // Receive Tab strings_["receiving_addresses"] = "Your Receiving Addresses";