// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // daemon_startup_diagnosis.h — pure classifiers over a crashed daemon's captured console output, // so the app can offer a targeted one-click fix instead of a bare "daemon crashed" / silent no-funds. #pragma once #include #include #include namespace dragonx { namespace daemon { // True when dragonxd aborted because its BLOCK DATABASE could not be loaded — either a // daemon-vs-chaindata serialization-format mismatch after a daemon update (the deterministic // "non-canonical optional discriminant" → "Error loading block database" → "Aborted block database // rebuild. Exiting." sequence) or a genuinely corrupt/incomplete block index. In BOTH cases the fix // is the same: `-reindex` rebuilds the index + chainstate from the intact raw blocks (blk*.dat). // This is what otherwise silently presents as a wallet with zero balance — the node never starts. inline bool blockDbOutputLooksBroken(const std::string& out) { return out.find("Error loading block database") != std::string::npos || out.find("non-canonical optional discriminant") != std::string::npos || out.find("Aborted block database rebuild") != std::string::npos || out.find("LoadBlockIndex()") != std::string::npos; // "... : failed to read value" } // True when dragonxd AUTO-RECOVERED the wallet on startup: on any BDB-verify failure it moves the // original wallet.dat to "wallet.{timestamp}.bak", salvages readable keys into a fresh wallet.dat, and // keeps running — no flag required (CWallet::Verify → CDBEnv::Verify(walletFile, CWalletDB::Recover)). // The salvage can be incomplete (or a false positive from stale/cross-platform BDB env state), so the // node silently comes up on a possibly-empty wallet — which reads as fund loss unless we surface it. inline bool walletAutoRecovered(const std::string& out) { // Cover BOTH salvage outcomes. A successful salvage prints the "data salvaged"/"saved as wallet..bak" // warning; a FAILED one (e.g. an inconsistent-but-readable file where aggressive salvage finds no // records) prints "salvage failed"/"found no records". In every case CWalletDB::Recover first logs // "Renamed to wallet..bak" and CDBEnv::Salvage logs its own banner — those two fire the // instant a salvage begins, before the daemon may abort, so they're the earliest reliable signal. return out.find("CDBEnv::Salvage") != std::string::npos // salvage is running || out.find("wallet.dat corrupt, data salvaged") != std::string::npos // RECOVER_OK || out.find("Original wallet.dat saved as wallet.") != std::string::npos || out.find("wallet.dat corrupt, salvage failed") != std::string::npos // RECOVER_FAIL || out.find("found no records in wallet") != std::string::npos // aggressive salvage empty || (out.find("Renamed ") != std::string::npos && out.find(" to wallet.") != std::string::npos && out.find(".bak") != std::string::npos); // Recover moved wallet.dat aside } // If `name` is a daemon salvage backup "wallet..bak", return its timestamp; else -1. inline long long parseWalletSalvageBakTs(const std::string& name) { if (name.rfind("wallet.", 0) != 0) return -1; // must start "wallet." if (name.size() < 12 || name.compare(name.size() - 4, 4, ".bak") != 0) return -1; // ...and end ".bak" const std::string mid = name.substr(7, name.size() - 7 - 4); // digits between the dots if (mid.empty() || mid.size() > 18) return -1; for (char c : mid) if (c < '0' || c > '9') return -1; long long ts = 0; for (char c : mid) ts = ts * 10 + (c - '0'); return ts; } // Most RECENT salvage backup (highest timestamp). Pure, testable. inline std::string newestWalletSalvageBak(const std::vector& filenames) { long long best = -1; std::string bestName; for (const auto& f : filenames) { const long long ts = parseWalletSalvageBakTs(f); if (ts > best) { best = ts; bestName = f; } } return bestName; } // LARGEST salvage backup, from (filename, fileSize) pairs — the least-salvaged one, i.e. the original. // This is what "Restore original wallet" should use: a salvage CASCADE shrinks the wallet each round, so // the newest .bak is the WORST and the largest is the pristine pre-salvage original (an emptied salvage // is tiny; a real wallet is large). Ties break toward the newest timestamp. Returns "" if none present. inline std::string largestWalletSalvageBak(const std::vector>& files) { std::string bestName; unsigned long long bestSize = 0; long long bestTs = -1; for (const auto& fp : files) { const long long ts = parseWalletSalvageBakTs(fp.first); if (ts < 0) continue; if (fp.second > bestSize || (fp.second == bestSize && ts > bestTs)) { bestSize = fp.second; bestTs = ts; bestName = fp.first; } } return bestName; } } // namespace daemon } // namespace dragonx