#pragma once #include // Persistent node-connectivity banner shown at the top of the content column when the wallet // cannot reach its node. Distinct from the transient toast notifications: it stays visible for // as long as the fault persists, so an offline wallet is never silently mistaken for a working // one. The decision (whether to show, how severe, which action) is a pure function of a state // snapshot so it can be unit-tested; App::renderNodeStatusBanner() feeds it the live state and // draws the strip. See src/app.cpp. namespace dragonx::ui { // Visual weight. Warning (amber) = recoverable / a reconnect is offered; Error (red) = a hard // fault the user must act on (the daemon gave up crashing, or a lite wallet failed to open). enum class NodeBannerSeverity { Warning, Error, }; // What the banner's action button does. App maps this to the concrete call. enum class NodeBannerAction { None, // no button — nothing the user can usefully do from here 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, // already-human-readable status text (connection_status_ / daemon lastError / lite open error). enum class NodeBannerReason { None, FullNodeOffline, // a reachable node was lost, or never came up; reconnect offered DaemonCrashed, // the embedded daemon crashed repeatedly and auto-restart stopped LiteOpenFailed, // lite build: the wallet failed to open 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 { bool show = false; NodeBannerSeverity severity = NodeBannerSeverity::Warning; NodeBannerReason reason = NodeBannerReason::None; NodeBannerAction action = NodeBannerAction::None; std::string detail; // passthrough status/error text (may be empty) }; // Snapshot of the connection state the banner reads. Plain values so the decision is testable // without an App instance. struct NodeBannerInputs { bool lite = false; // lite build (no embedded daemon / RPC) bool connected = false; // state_.connected — the master "online" flag bool warming_up = false; // daemon reachable, RPC warmup (code -28) bool daemon_initializing = false; // daemon launching / block index loading bool connection_in_progress = false; // a connect attempt is actively running // Peer connectivity: a full node connected to the daemon but with NO peer-derived tip // (longestchain == 0 -> tip_known false) has no peers, so it silently stalls at a stale height // while balance/sync look "done". `blocks` gates the check so we don't flag the brief window // before the first chain-info refresh. (audit: no-peer-banner) bool tip_known = true; // sync.tip_known — a peer-derived network tip is known int blocks = 0; // sync.blocks — >0 once chain info has been fetched // Full-node embedded-daemon crash signal. bool using_embedded_daemon = false; bool has_daemon_controller = false; bool daemon_running = false; int daemon_crash_count = 0; // The node is running but busy with something it can't be interrupted out of (a witness rebuild, the // startup replay) or too busy to answer RPC. Wallet data may be stale; a reconnect would only add // requests, so the banner offers no action. node_busy_detail is the live, translated description. bool node_busy = false; std::string node_busy_detail; 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 }; // Auto-restart give-up threshold — mirrors the crash cap in app_network.cpp's connect loop. inline constexpr int kNodeBannerCrashGiveUpCount = 3; inline NodeBannerState evaluateNodeStatusBanner(const NodeBannerInputs& in) { NodeBannerState s; if (in.lite) { // Lite has no daemon/RPC; "online" == wallet open. Only a genuine open failure is a // fault worth a persistent banner (a not-yet-created wallet is handled by the normal // "No wallet open" prompt, and leaves lite_open_error empty). if (!in.connected && !in.lite_open_error.empty()) { s.show = true; s.severity = NodeBannerSeverity::Error; s.reason = NodeBannerReason::LiteOpenFailed; s.action = NodeBannerAction::None; s.detail = in.lite_open_error; } return s; } // Full node, busy. Before the startup-phase returns below: the overlay sets daemon_initializing while // the node is busy too, and the banner must still say why the wallet isn't updating. No action: a // "Reconnect" would only queue more requests behind the busy node. if (in.node_busy) { s.show = true; s.severity = NodeBannerSeverity::Warning; s.reason = NodeBannerReason::NodeBusy; s.action = NodeBannerAction::None; s.detail = in.node_busy_detail; return s; } // Full node. In an expected startup phase → the loading/warmup overlay owns the screen, so no // banner. An active connect attempt likewise shows progress, not an error — don't flicker a // banner over it. (These return even if `connected` has flipped early, matching prior behavior.) if (in.warming_up || in.daemon_initializing) return s; if (in.connection_in_progress) return s; // Connected to the daemon (RPC works) but with no peer-derived tip → the node has no peers and // cannot learn about new blocks; it silently stalls at a stale height while everything looks // synced. Surface it as a recoverable warning. A plain reconnect won't help, but restarting the // embedded daemon re-triggers peer discovery (it re-reads the -addnode seeds), so offer that when // we own the node; an external node is the user's own to restart, so no button there. The detail // is left empty: here connection_status is just "Connected", which would contradict the // "no peers" headline — App renders a translated, actionable hint for this reason instead. if (in.connected) { if (!in.tip_known && in.blocks > 0) { s.show = true; s.severity = NodeBannerSeverity::Warning; s.reason = NodeBannerReason::NoPeers; s.action = (in.using_embedded_daemon && in.has_daemon_controller) ? 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; } // Genuinely offline. Distinguish "the embedded daemon crashed and we stopped retrying" (a // hard fault needing a manual restart) from an ordinary lost/failed connection (retryable). if (in.using_embedded_daemon && in.has_daemon_controller && !in.daemon_running && in.daemon_crash_count >= kNodeBannerCrashGiveUpCount) { s.show = true; s.severity = NodeBannerSeverity::Error; s.reason = NodeBannerReason::DaemonCrashed; s.action = NodeBannerAction::RestartNode; s.detail = !in.daemon_last_error.empty() ? in.daemon_last_error : in.connection_status; return s; } // The node rejected the wallet's RPC login — retrying won't help (the creds are wrong). Restarting // the embedded node regenerates its cookie/creds; for an external node the user fixes DRAGONX.conf, // so offer to open its folder. App renders a translated, actionable detail (connection_status here // is the jargon "Auth failed…" line). if (in.auth_failed) { s.show = true; s.severity = NodeBannerSeverity::Error; s.reason = NodeBannerReason::AuthFailed; s.action = (in.using_embedded_daemon && in.has_daemon_controller) ? NodeBannerAction::RestartNode : NodeBannerAction::OpenConfigFolder; s.detail.clear(); return s; } s.show = true; s.severity = NodeBannerSeverity::Warning; s.reason = NodeBannerReason::FullNodeOffline; s.action = NodeBannerAction::Reconnect; s.detail = in.connection_status; return s; } } // namespace dragonx::ui