feat(ui): surface daemon DEGRADED mode + v1.3.0 auto-shield status
Rec3 — v1.3.0+ opens a wallet that lost its hdchain in DEGRADED mode (existing funds spendable, but no new HD-key derivation) instead of aborting. Add a daemon-log classifier (walletOpenedDegraded) + detectWalletDegraded(), warned once per session. Pre-1.3.0 daemons never emit that line, so it's a no-op there. O1 — probe z_autoshieldstatus once per connection (now decoupled from our own toggle/balance) and, in Settings, show whether the node handles auto-shield itself (+ its destination, or the daemon's disabled_reason). The checkbox now governs only the wallet's fallback shielder, which defers to the node. Nothing renders on pre-1.3.0 daemons (no such RPC), so behaviour is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
13
src/app.h
13
src/app.h
@@ -163,6 +163,13 @@ public:
|
|||||||
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
|
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
|
||||||
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
|
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
|
||||||
|
|
||||||
|
// Daemon (v1.3.0+) coinbase auto-shield status, from z_autoshieldstatus. "Not probed" / all-false on
|
||||||
|
// pre-1.3.0 daemons (no such RPC) — callers treat that as "the wallet handles auto-shield itself".
|
||||||
|
bool daemonAutoShieldProbed() const { return daemon_autoshield_probed_; }
|
||||||
|
bool daemonAutoShieldActive() const { return daemon_autoshield_active_; }
|
||||||
|
const std::string& daemonAutoShieldAddress() const { return daemon_autoshield_address_; }
|
||||||
|
const std::string& daemonAutoShieldDisabledReason() const { return daemon_autoshield_disabled_reason_; }
|
||||||
|
|
||||||
// W7 QoL: a plaintext support-diagnostics snapshot (version, variant, daemon/RPC/wallet/log state)
|
// W7 QoL: a plaintext support-diagnostics snapshot (version, variant, daemon/RPC/wallet/log state)
|
||||||
// for the "Copy diagnostics" action. Contains no secrets.
|
// for the "Copy diagnostics" action. Contains no secrets.
|
||||||
std::string buildDiagnosticsReport();
|
std::string buildDiagnosticsReport();
|
||||||
@@ -958,6 +965,8 @@ private:
|
|||||||
// so a possibly-incomplete salvaged wallet isn't mistaken for fund loss. Warned once per session.
|
// so a possibly-incomplete salvaged wallet isn't mistaken for fund loss. Warned once per session.
|
||||||
bool wallet_auto_recovered_ = false; // a salvage happened this session
|
bool wallet_auto_recovered_ = false; // a salvage happened this session
|
||||||
bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per session
|
bool wallet_auto_recovered_warned_ = false; // guard: only surface it once per session
|
||||||
|
bool wallet_degraded_ = false; // v1.3.0+ opened the wallet in DEGRADED mode (no new HD keys)
|
||||||
|
bool wallet_degraded_warned_ = false; // guard: surface the degraded-mode notice once per session
|
||||||
bool show_wallet_recovered_dialog_ = false; // auto-shown warning dialog
|
bool show_wallet_recovered_dialog_ = false; // auto-shown warning dialog
|
||||||
// Complementary on-disk safety net for a salvage we DIDN'T witness this launch (happened on a prior
|
// Complementary on-disk safety net for a salvage we DIDN'T witness this launch (happened on a prior
|
||||||
// run, or under an external daemon whose startup output we never captured): if the active wallet loads
|
// run, or under an external daemon whose startup output we never captured): if the active wallet loads
|
||||||
@@ -1248,6 +1257,9 @@ private:
|
|||||||
bool daemon_autoshield_probed_ = false;
|
bool daemon_autoshield_probed_ = false;
|
||||||
bool daemon_autoshield_active_ = false;
|
bool daemon_autoshield_active_ = false;
|
||||||
std::atomic<bool> daemon_autoshield_probe_inflight_{false};
|
std::atomic<bool> daemon_autoshield_probe_inflight_{false};
|
||||||
|
std::string daemon_autoshield_address_; // z_autoshieldstatus fields (O1); empty on old daemons
|
||||||
|
std::string daemon_autoshield_disabled_reason_; // daemon's reason auto-shield is off (e.g. seed not recoverable)
|
||||||
|
bool daemon_autoshield_seed_recoverable_ = false;
|
||||||
|
|
||||||
// P4: Incremental transaction cache
|
// P4: Incremental transaction cache
|
||||||
int last_tx_block_height_ = -1; // block height at last full tx fetch
|
int last_tx_block_height_ = -1; // block height at last full tx fetch
|
||||||
@@ -1440,6 +1452,7 @@ private:
|
|||||||
void renderWalletRecoveredDialog(); // warn that the node auto-recovered/salvaged wallet.dat
|
void renderWalletRecoveredDialog(); // warn that the node auto-recovered/salvaged wallet.dat
|
||||||
void renderEmptyWalletWarningDialog();// warn that the active wallet is empty while a sibling holds funds
|
void renderEmptyWalletWarningDialog();// warn that the active wallet is empty while a sibling holds funds
|
||||||
void detectWalletAutoRecovery(); // scan daemon output for a salvage; fire the warning once/session
|
void detectWalletAutoRecovery(); // scan daemon output for a salvage; fire the warning once/session
|
||||||
|
void detectWalletDegraded(); // scan daemon output for a DEGRADED-mode open; warn once/session
|
||||||
void restoreOriginalWallet(); // swap the wallet.<ts>.bak back over the salvaged copy + restart
|
void restoreOriginalWallet(); // swap the wallet.<ts>.bak back over the salvaged copy + restart
|
||||||
void pumpWalletRestore(); // main-thread: surface the restore/rebuild op's result
|
void pumpWalletRestore(); // main-thread: surface the restore/rebuild op's result
|
||||||
void rebuildWalletDatabase(); // rebuild a BDB-inconsistent wallet into a loadable one (helper)
|
void rebuildWalletDatabase(); // rebuild a BDB-inconsistent wallet into a loadable one (helper)
|
||||||
|
|||||||
@@ -249,6 +249,21 @@ void App::detectWalletAutoRecovery()
|
|||||||
VERBOSE_LOGF("[recovery] Daemon auto-recovered/salvaged wallet.dat — surfacing the recovery dialog\n");
|
VERBOSE_LOGF("[recovery] Daemon auto-recovered/salvaged wallet.dat — surfacing the recovery dialog\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v1.3.0+ opens a wallet that lost its hdchain in DEGRADED mode (funds spendable) instead of aborting,
|
||||||
|
// but can't derive NEW HD keys — z_getnewaddress / z_shieldcoinbase / t->z z_sendmany fail with "HD seed
|
||||||
|
// not found". Only a startup log line signals it, so scan the captured output and warn once. Pre-1.3.0
|
||||||
|
// daemons never emit it, so this is a no-op there (backwards compatible).
|
||||||
|
void App::detectWalletDegraded()
|
||||||
|
{
|
||||||
|
if (wallet_degraded_warned_) return;
|
||||||
|
if (!isUsingEmbeddedDaemon() || !daemon_controller_ || !daemon_controller_->daemon()) return;
|
||||||
|
if (!daemon::walletOpenedDegraded(daemon_controller_->daemon()->getOutput())) return;
|
||||||
|
wallet_degraded_ = true;
|
||||||
|
wallet_degraded_warned_ = true;
|
||||||
|
ui::Notifications::instance().warning(TR("wallet_degraded_notify"), 30.0f);
|
||||||
|
VERBOSE_LOGF("[recovery] Daemon opened wallet in DEGRADED mode — new-key derivation disabled\n");
|
||||||
|
}
|
||||||
|
|
||||||
void App::tryConnect()
|
void App::tryConnect()
|
||||||
{
|
{
|
||||||
// Lite builds have no full node / RPC daemon, so never run the RPC connection state machine
|
// Lite builds have no full node / RPC daemon, so never run the RPC connection state machine
|
||||||
@@ -259,6 +274,7 @@ void App::tryConnect()
|
|||||||
// Catch a startup wallet salvage as soon as it appears in the node's output — independent of whether
|
// Catch a startup wallet salvage as soon as it appears in the node's output — independent of whether
|
||||||
// the node ever finishes starting or connects (skip only while an orchestrated swap is mid-flight).
|
// the node ever finishes starting or connects (skip only while an orchestrated swap is mid-flight).
|
||||||
if (!daemon_restarting_) detectWalletAutoRecovery();
|
if (!daemon_restarting_) detectWalletAutoRecovery();
|
||||||
|
if (!daemon_restarting_) detectWalletDegraded();
|
||||||
|
|
||||||
if (connection_in_progress_) return;
|
if (connection_in_progress_) return;
|
||||||
|
|
||||||
@@ -1746,29 +1762,39 @@ void App::refreshCoreData()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-shield transparent funds if enabled. A v1.3.0+ daemon auto-shields coinbase
|
// Probe the daemon's auto-shield status once per connection — independent of our own
|
||||||
// itself; probe z_autoshieldstatus once (while synced, so the daemon is past warmup) and
|
// toggle/balance — so both the defer-gate below and the Settings UI (O1) can read it.
|
||||||
// defer to it when it's active — otherwise the wallet and the daemon race for the same
|
// Only when synced (daemon past warmup). Fail-closed: a pre-1.3.0 daemon lacks the RPC,
|
||||||
// coinbase UTXOs and split funds across different z-addresses. A pre-1.3.0 daemon lacks
|
// so the probe leaves active=false and the wallet keeps shielding client-side.
|
||||||
// the RPC, so the probe fails closed (active=false) and the wallet keeps shielding.
|
if (result.balanceOk && !state_.sync.syncing && !daemon_autoshield_probed_ && worker_ &&
|
||||||
const bool autoShieldEligible = result.balanceOk && settings_ && settings_->getAutoShield() &&
|
|
||||||
state_.spendableTransparentBalance > 0.0001 && !state_.sync.syncing;
|
|
||||||
if (autoShieldEligible && !daemon_autoshield_probed_ && worker_ &&
|
|
||||||
!daemon_autoshield_probe_inflight_.exchange(true)) {
|
!daemon_autoshield_probe_inflight_.exchange(true)) {
|
||||||
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
worker_->post([this]() -> rpc::RPCWorker::MainCb {
|
||||||
bool active = false;
|
bool active = false, seedRecoverable = false;
|
||||||
|
std::string addr, reason;
|
||||||
try {
|
try {
|
||||||
auto st = rpc_->call("z_autoshieldstatus", json::array());
|
auto st = rpc_->call("z_autoshieldstatus", json::array());
|
||||||
if (st.is_object() && st.contains("autoshield"))
|
if (st.is_object()) {
|
||||||
active = st["autoshield"].get<bool>();
|
if (st.contains("autoshield")) active = st["autoshield"].get<bool>();
|
||||||
} catch (...) { active = false; } // pre-1.3.0 daemon: method not found
|
if (st.contains("autoshieldaddress")) addr = st["autoshieldaddress"].get<std::string>();
|
||||||
return [this, active]() {
|
if (st.contains("disabled_reason")) reason = st["disabled_reason"].get<std::string>();
|
||||||
|
if (st.contains("seed_recoverable")) seedRecoverable = st["seed_recoverable"].get<bool>();
|
||||||
|
}
|
||||||
|
} catch (...) {} // pre-1.3.0 daemon: no such method — leave defaults (inactive)
|
||||||
|
return [this, active, addr, reason, seedRecoverable]() {
|
||||||
daemon_autoshield_active_ = active;
|
daemon_autoshield_active_ = active;
|
||||||
|
daemon_autoshield_address_ = addr;
|
||||||
|
daemon_autoshield_disabled_reason_ = reason;
|
||||||
|
daemon_autoshield_seed_recoverable_ = seedRecoverable;
|
||||||
daemon_autoshield_probed_ = true;
|
daemon_autoshield_probed_ = true;
|
||||||
daemon_autoshield_probe_inflight_ = false;
|
daemon_autoshield_probe_inflight_ = false;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-shield transparent funds — but defer to the daemon's own coinbase auto-shielder
|
||||||
|
// (v1.3.0+) when it's active, so we don't double-shield and split funds across z-addrs.
|
||||||
|
const bool autoShieldEligible = result.balanceOk && settings_ && settings_->getAutoShield() &&
|
||||||
|
state_.spendableTransparentBalance > 0.0001 && !state_.sync.syncing;
|
||||||
if (autoShieldEligible && daemon_autoshield_probed_ && !daemon_autoshield_active_ &&
|
if (autoShieldEligible && daemon_autoshield_probed_ && !daemon_autoshield_active_ &&
|
||||||
!auto_shield_pending_.exchange(true)) {
|
!auto_shield_pending_.exchange(true)) {
|
||||||
std::string targetZAddr;
|
std::string targetZAddr;
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ inline bool walletAutoRecovered(const std::string& out)
|
|||||||
&& out.find(".bak") != std::string::npos); // Recover moved wallet.dat aside
|
&& out.find(".bak") != std::string::npos); // Recover moved wallet.dat aside
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True when dragonxd (v1.3.0+) opened the wallet in DEGRADED mode: a wallet.dat that lost its hdchain
|
||||||
|
// record (e.g. an old `-salvagewallet` output) now OPENS — existing keys stay intact and spendable —
|
||||||
|
// instead of aborting, but the daemon can no longer derive NEW HD keys, so z_getnewaddress /
|
||||||
|
// z_shieldcoinbase / a t->z z_sendmany fail with "HD seed not found". The only signal is a startup log
|
||||||
|
// line; pre-1.3.0 daemons never emit it, so this classifier is naturally a no-op against them.
|
||||||
|
inline bool walletOpenedDegraded(const std::string& out)
|
||||||
|
{
|
||||||
|
return out.find("Wallet opened in DEGRADED mode") != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
// If `name` is a daemon salvage backup "wallet.<unixtime>.bak", return its timestamp; else -1.
|
// If `name` is a daemon salvage backup "wallet.<unixtime>.bak", return its timestamp; else -1.
|
||||||
inline long long parseWalletSalvageBakTs(const std::string& name)
|
inline long long parseWalletSalvageBakTs(const std::string& name)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1186,6 +1186,18 @@ void RenderSettingsPage(App* app) {
|
|||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_ztx"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_save_ztx"));
|
||||||
CB(TrId("auto_shield", "auto_shld"), &s_settingsState.auto_shield);
|
CB(TrId("auto_shield", "auto_shld"), &s_settingsState.auto_shield);
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_shield"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_shield"));
|
||||||
|
// O1: when a v1.3.0+ node auto-shields coinbase itself, show its real state — the checkbox
|
||||||
|
// above only governs the wallet's own fallback shielder (which defers to the node). Nothing
|
||||||
|
// renders on pre-1.3.0 daemons (never probed), so their behaviour is unchanged.
|
||||||
|
if (app && app->daemonAutoShieldProbed()) {
|
||||||
|
if (app->daemonAutoShieldActive()) {
|
||||||
|
ImGui::TextColored(ImVec4(0.40f, 0.78f, 0.40f, 1.0f), " %s", TR("autoshield_by_node"));
|
||||||
|
if (!app->daemonAutoShieldAddress().empty())
|
||||||
|
ImGui::TextDisabled(" %s", app->daemonAutoShieldAddress().c_str());
|
||||||
|
} else if (!app->daemonAutoShieldDisabledReason().empty()) {
|
||||||
|
ImGui::TextDisabled(" %s", app->daemonAutoShieldDisabledReason().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
CB(TrId("use_tor", "tor"), &s_settingsState.use_tor);
|
CB(TrId("use_tor", "tor"), &s_settingsState.use_tor);
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tor"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_tor"));
|
||||||
if (showDaemonOptions) {
|
if (showDaemonOptions) {
|
||||||
|
|||||||
@@ -1250,6 +1250,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["wallet_recovered_restore"] = "Restore the original file instead";
|
strings_["wallet_recovered_restore"] = "Restore the original file instead";
|
||||||
strings_["wallet_recovered_restore_sub"] = "Puts your largest untouched backup back in place, verbatim, then re-scans — slightly faster, but only as complete as that one file was. Your current file is kept as a dated backup either way.";
|
strings_["wallet_recovered_restore_sub"] = "Puts your largest untouched backup back in place, verbatim, then re-scans — slightly faster, but only as complete as that one file was. Your current file is kept as a dated backup either way.";
|
||||||
strings_["wallet_recovered_notify"] = "Your wallet file needed a repair — your original was safely backed up. Open the app to review your options.";
|
strings_["wallet_recovered_notify"] = "Your wallet file needed a repair — your original was safely backed up. Open the app to review your options.";
|
||||||
|
strings_["wallet_degraded_notify"] = "Your wallet opened in reduced-function mode: existing funds are safe and spendable, but creating new addresses and shielding are disabled. Back up your seed phrase and restore it to fully repair the wallet.";
|
||||||
|
strings_["autoshield_by_node"] = "Auto-shield is handled by your node";
|
||||||
// In-dialog recovery lifecycle (Offer → Working → Done/Failed) + disclosures.
|
// In-dialog recovery lifecycle (Offer → Working → Done/Failed) + disclosures.
|
||||||
strings_["wallet_recovery_working_label"] = "Working";
|
strings_["wallet_recovery_working_label"] = "Working";
|
||||||
strings_["wallet_recovery_done"] = "Done";
|
strings_["wallet_recovery_done"] = "Done";
|
||||||
|
|||||||
Reference in New Issue
Block a user