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:
+13
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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"] =
|
||||
|
||||
@@ -3559,6 +3559,29 @@ void testNodeStatusBanner()
|
||||
EXPECT_TRUE(s.action == NodeBannerAction::RestartNode);
|
||||
EXPECT_TRUE(s.detail.empty());
|
||||
}
|
||||
// Auth failure (bad rpcuser/rpcpassword): embedded → Restart (regenerates creds), external → Open
|
||||
// config folder. A hard error, detail cleared (App renders a translated actionable hint).
|
||||
{
|
||||
NodeBannerInputs in; in.auth_failed = true;
|
||||
in.using_embedded_daemon = true; in.has_daemon_controller = true;
|
||||
NodeBannerState s = evaluateNodeStatusBanner(in);
|
||||
EXPECT_TRUE(s.reason == NodeBannerReason::AuthFailed);
|
||||
EXPECT_TRUE(s.severity == NodeBannerSeverity::Error);
|
||||
EXPECT_TRUE(s.action == NodeBannerAction::RestartNode);
|
||||
EXPECT_TRUE(s.detail.empty());
|
||||
}
|
||||
{
|
||||
NodeBannerInputs in; in.auth_failed = true; // external node
|
||||
NodeBannerState s = evaluateNodeStatusBanner(in);
|
||||
EXPECT_TRUE(s.reason == NodeBannerReason::AuthFailed);
|
||||
EXPECT_TRUE(s.action == NodeBannerAction::OpenConfigFolder);
|
||||
}
|
||||
// A connected wallet never shows the auth banner (the connected branch returns earlier).
|
||||
{
|
||||
NodeBannerInputs in; in.connected = true; in.tip_known = true; in.blocks = 100;
|
||||
in.auth_failed = true;
|
||||
EXPECT_TRUE(!evaluateNodeStatusBanner(in).show);
|
||||
}
|
||||
// Connected, tip unknown, but no chain info yet (blocks==0) → no banner (avoid startup flicker).
|
||||
{
|
||||
NodeBannerInputs in; in.connected = true; in.tip_known = false; in.blocks = 0;
|
||||
|
||||
Reference in New Issue
Block a user