feat(recovery): offer legacy-fund recovery when a degraded wallet can't shield its transparent funds

Phase 1 of automating legacy wallet.dat migration: a persistent, actionable banner that detects a wallet
whose transparent funds are STUCK and offers the migrate-to-seed recovery flow (showSeedMigrationDialog).

Corrected trigger (the important part): it fires ONLY when wallet_degraded_ is set — i.e. z_getnewaddress
failed with "HD seed not found", so the wallet genuinely cannot create a shielded address to move its
funds — AND there is a transparent balance to move. It is NOT driven by "the wallet has no mnemonic": a
non-mnemonic but functional wallet (one that self-healed with a retrofit HD seed on load) has
wallet_degraded_ == false, can still shield/spend, and is deliberately never flagged — matching the
existing "legacy wallets are not nagged" policy. Full-node only.

Implemented in the already-unit-tested node banner: new NodeBannerReason::LegacyFundsStuck +
NodeBannerAction::RecoverLegacyFunds, gated on a new legacy_funds_stuck input (App computes it from
wallet_degraded_ + transparentBalance). NoPeers still takes priority (can't recover before the node sees
the chain); lite never shows it. Pure decision tests added: fires when stuck, never for a functional
wallet, yields to NoPeers, suppressed on lite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 22:05:11 -05:00
co-authored by Claude Opus 4.8
parent 6c2a277751
commit ae4012adb7
4 changed files with 57 additions and 0 deletions
+18
View File
@@ -23,6 +23,7 @@ enum class NodeBannerAction {
Reconnect, // full node: re-run the RPC connect state machine (App::tryConnect)
RestartNode, // full node: the embedded daemon crashed & auto-restart gave up (App::restartDaemon)
OpenConfigFolder,// external node: open the folder holding DRAGONX.conf so the user can fix the login
RecoverLegacyFunds, // a degraded legacy wallet with stuck transparent funds: open migrate-to-seed
};
// Why the banner is up. App maps this to a translated headline; `detail` carries the live,
@@ -35,6 +36,8 @@ enum class NodeBannerReason {
NoPeers, // connected to the daemon but no peer-derived tip (0 peers) — stalled, stale
NodeBusy, // the node is up but busy (rebuilding witnesses / catching up / not answering)
AuthFailed, // the node rejected the wallet's RPC login (bad rpcuser/rpcpassword in the conf)
LegacyFundsStuck, // a DEGRADED legacy wallet (can't derive a z-address) holding transparent funds it
// therefore can't shield/spend — offer the migrate-to-seed recovery
};
struct NodeBannerState {
@@ -75,6 +78,11 @@ struct NodeBannerInputs {
bool auth_failed = false; // the node rejected the wallet's RPC login (bad rpcuser/rpcpassword)
// The wallet opened DEGRADED (no usable HD seed, so z_getnewaddress / shielding fail) AND it holds
// transparent funds that are therefore stuck. App sets this ONLY from wallet_degraded_ + a transparent
// balance — never from merely-non-mnemonic status, which is a functional wallet and must not be flagged.
bool legacy_funds_stuck = false;
std::string connection_status; // human-readable status line (already translated)
std::string daemon_last_error; // DaemonController::lastError() (may be empty)
std::string lite_open_error; // lite: last wallet-open failure reason
@@ -134,6 +142,16 @@ inline NodeBannerState evaluateNodeStatusBanner(const NodeBannerInputs& in) {
? NodeBannerAction::RestartNode
: NodeBannerAction::None;
s.detail.clear();
return s;
}
// Connected and synced, but a degraded legacy wallet can't shield its transparent funds. Offer the
// recovery flow. Checked after NoPeers (can't recover until the node can see the chain anyway).
if (in.legacy_funds_stuck) {
s.show = true;
s.severity = NodeBannerSeverity::Warning;
s.reason = NodeBannerReason::LegacyFundsStuck;
s.action = NodeBannerAction::RecoverLegacyFunds;
s.detail.clear();
}
return s;
}