feat(network): a clear auth-failed node banner with a fix-it action

When the node rejects the wallet's RPC login (bad rpcuser/rpcpassword), the
status bar showed developer jargon ("Auth failed — check rpcuser/rpcpassword")
and the only action was Reconnect, which just repeats the failure. Now a
dedicated AuthFailed banner reason surfaces "Wallet can't sign in to the node"
with a plain-language detail and a real fix: Restart node for the embedded
daemon (regenerates its cookie creds) or Open config folder for an external one
(to fix DRAGONX.conf). Driven by a new rpc_auth_failed_ flag (set on the auth
path, cleared on each connect attempt), with evaluator unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 11:53:29 -05:00
co-authored by Claude Opus 4.8
parent adf61e43d6
commit 0101cc707c
6 changed files with 66 additions and 7 deletions
+13 -3
View File
@@ -2662,6 +2662,7 @@ void App::renderNodeStatusBanner()
const NodeBusyText busy = nodeBusyText();
in.node_busy = busy.busy;
in.node_busy_detail = busy.compact; // one line; the overlay and the quit prompt explain more
in.auth_failed = rpc_auth_failed_;
const ui::NodeBannerState banner = ui::evaluateNodeStatusBanner(in);
if (!banner.show) return;
@@ -2698,6 +2699,8 @@ void App::renderNodeStatusBanner()
title = TR("node_banner_no_peers_title"); icon = ICON_MD_CLOUD_OFF; break;
case ui::NodeBannerReason::NodeBusy:
title = busy.title.c_str(); icon = ICON_MD_HOURGLASS_EMPTY; break;
case ui::NodeBannerReason::AuthFailed:
title = TR("node_banner_auth_title"); icon = ICON_MD_LOCK; break;
case ui::NodeBannerReason::FullNodeOffline:
default:
title = TR("node_banner_offline_title"); icon = ICON_MD_CLOUD_OFF; break;
@@ -2706,13 +2709,15 @@ void App::renderNodeStatusBanner()
const char* actionLabel = nullptr;
if (banner.action == ui::NodeBannerAction::Reconnect) actionLabel = TR("node_banner_reconnect");
else if (banner.action == ui::NodeBannerAction::RestartNode) actionLabel = TR("node_banner_restart");
else if (banner.action == ui::NodeBannerAction::OpenConfigFolder) actionLabel = TR("node_banner_open_config");
// The NoPeers case deliberately carries no passthrough detail (connection_status is just
// "Connected" there, which reads as a contradiction under the "no peers" headline). Show a
// translated, actionable hint instead; every other reason keeps its live status/error text.
const std::string detailText = (banner.reason == ui::NodeBannerReason::NoPeers)
? std::string(TR("node_banner_no_peers_detail"))
: banner.detail;
std::string detailText;
if (banner.reason == ui::NodeBannerReason::NoPeers) detailText = TR("node_banner_no_peers_detail");
else if (banner.reason == ui::NodeBannerReason::AuthFailed) detailText = TR("node_banner_auth_detail");
else detailText = banner.detail;
const float padX = ui::Layout::spacingLg();
@@ -2801,6 +2806,11 @@ void App::renderNodeStatusBanner()
if (m::TactileButton(actionLabel, ImVec2(btnW, btnH))) {
if (banner.action == ui::NodeBannerAction::RestartNode) restartDaemon();
else if (banner.action == ui::NodeBannerAction::Reconnect) tryConnect();
else if (banner.action == ui::NodeBannerAction::OpenConfigFolder) {
const std::string conf = rpc::Connection::getDefaultConfPath();
util::Platform::openFolder(std::filesystem::path(conf).parent_path().string(),
/*createIfMissing=*/false);
}
}
}
+1
View File
@@ -1559,6 +1559,7 @@ private:
// Connection
std::string connection_status_ = "Disconnected";
bool rpc_auth_failed_ = false; // last connect hit an auth failure (bad rpcuser/rpcpassword); drives the banner
bool connection_in_progress_ = false;
bool remote_rpc_plaintext_warning_shown_ = false;
// Startup daemon-launch diagnostics: bound the "RPC port busy, no config" wait before warning,
+3
View File
@@ -406,6 +406,7 @@ void App::tryConnect()
++connect_attempt;
connection_in_progress_ = true;
rpc_auth_failed_ = false; // re-arm: a fresh connect attempt clears any prior auth failure
connection_status_ = TR("sb_loading_config");
// Auto-detect configuration (file I/O — fast, safe on main thread)
@@ -608,6 +609,7 @@ void App::tryConnect()
} else {
state_.connected = false;
connection_status_ = TR("sb_auth_failed");
rpc_auth_failed_ = true;
VERBOSE_LOGF("[connect #%d] .cookie auth also failed\n", attempt);
ui::Notifications::instance().error(
"RPC authentication failed (HTTP 401). "
@@ -621,6 +623,7 @@ void App::tryConnect()
state_.connected = false;
std::string confPath = rpc::Connection::getDefaultConfPath();
connection_status_ = TR("sb_auth_failed");
rpc_auth_failed_ = true;
VERBOSE_LOGF("[connect #%d] HTTP 401 — rpcuser/rpcpassword in %s don't match the daemon. "
"Edit the file or restart the daemon to regenerate credentials.\n",
attempt, confPath.c_str());
+22 -3
View File
@@ -19,9 +19,10 @@ enum class NodeBannerSeverity {
// 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)
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
};
// Why the banner is up. App maps this to a translated headline; `detail` carries the live,
@@ -33,6 +34,7 @@ enum class NodeBannerReason {
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)
};
struct NodeBannerState {
@@ -71,6 +73,8 @@ struct NodeBannerInputs {
bool node_busy = false;
std::string node_busy_detail;
bool auth_failed = false; // the node rejected the wallet's RPC login (bad rpcuser/rpcpassword)
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
@@ -146,6 +150,21 @@ inline NodeBannerState evaluateNodeStatusBanner(const NodeBannerInputs& in) {
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;
+4 -1
View File
@@ -1553,7 +1553,7 @@ void I18n::loadBuiltinEnglish()
strings_["sb_no_conf"] = "No DRAGONX.conf found";
strings_["sb_starting_daemon"] = "Starting dragonxd...";
strings_["sb_connecting_daemon"] = "Connecting to dragonxd...";
strings_["sb_auth_failed"] = "Auth failed — check rpcuser/rpcpassword";
strings_["sb_auth_failed"] = "Couldn't sign in to the node";
strings_["sb_waiting_daemon"] = "Waiting for dragonxd to start...";
strings_["sb_waiting_daemon_err"] = "Waiting for dragonxd — %s";
strings_["sb_connecting_external"] = "Connecting to external daemon...";
@@ -1571,6 +1571,9 @@ void I18n::loadBuiltinEnglish()
strings_["node_banner_lite_open_failed"] = "Couldn't open your wallet";
strings_["node_banner_reconnect"] = "Reconnect";
strings_["node_banner_restart"] = "Restart node";
strings_["node_banner_open_config"] = "Open config folder";
strings_["node_banner_auth_title"] = "Wallet can't sign in to the node";
strings_["node_banner_auth_detail"] = "The node rejected the wallet's saved login. Restart the node, or open the config folder to fix the login in DRAGONX.conf.";
// Refresh-staleness badge (W6-2) on the Total Balance card.
strings_["data_stale_prefix"] = "Updated";
strings_["data_stale_tooltip"] =