feat(lite): Console tab with connection + open/create diagnostics
The lite variant had no visibility into why a wallet failed to open — just a "disconnected" spinner. Add a lite-only Console tab (full-node keeps its RPC console) that shows a live diagnostic log. - LiteDiagnostics: a small thread-safe, bounded ring buffer (header-only). The controller writes to it from its background threads: each failover server attempt and result, wallet open/create/restore outcomes, sync start, and blocked-open reasons. The App logs controller (re)builds with the preferred server. - lite_console_tab: a terminal-styled, read-only view of the log (newest at the bottom, error/success lines coloured) with Clear / Copy / Auto-scroll. Reachable even when the wallet is locked (it's diagnostics, no secrets). Registered as NavPage::LiteConsole, gated lite-only via WalletUiSurface::LiteConsole. A unit test drives an open-with-failover and asserts the log records the connection attempt and the successful open. Built clean for full-node, lite, and Windows cross-compile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -440,6 +440,7 @@ set(APP_SOURCES
|
|||||||
src/ui/windows/mining_tab_helpers.cpp
|
src/ui/windows/mining_tab_helpers.cpp
|
||||||
src/ui/windows/peers_tab.cpp
|
src/ui/windows/peers_tab.cpp
|
||||||
src/ui/windows/network_tab.cpp
|
src/ui/windows/network_tab.cpp
|
||||||
|
src/ui/windows/lite_console_tab.cpp
|
||||||
src/ui/windows/explorer_tab.cpp
|
src/ui/windows/explorer_tab.cpp
|
||||||
src/ui/windows/market_tab.cpp
|
src/ui/windows/market_tab.cpp
|
||||||
src/ui/windows/console_tab.cpp
|
src/ui/windows/console_tab.cpp
|
||||||
|
|||||||
12
src/app.cpp
12
src/app.cpp
@@ -14,6 +14,7 @@
|
|||||||
#include "wallet/lite_wallet_controller.h"
|
#include "wallet/lite_wallet_controller.h"
|
||||||
#include "wallet/lite_wallet_server_selection_adapter.h"
|
#include "wallet/lite_wallet_server_selection_adapter.h"
|
||||||
#include "wallet/lite_rollout_policy.h"
|
#include "wallet/lite_rollout_policy.h"
|
||||||
|
#include "wallet/lite_diagnostics.h"
|
||||||
|
|
||||||
#include <cstdlib> // std::getenv for the lite kill-switch env var
|
#include <cstdlib> // std::getenv for the lite kill-switch env var
|
||||||
#include <sodium.h> // sodium_memzero for the lite unlock-passphrase buffer
|
#include <sodium.h> // sodium_memzero for the lite unlock-passphrase buffer
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
#include "ui/windows/mining_tab.h"
|
#include "ui/windows/mining_tab.h"
|
||||||
#include "ui/windows/peers_tab.h"
|
#include "ui/windows/peers_tab.h"
|
||||||
#include "ui/windows/network_tab.h"
|
#include "ui/windows/network_tab.h"
|
||||||
|
#include "ui/windows/lite_console_tab.h"
|
||||||
#include "ui/windows/explorer_tab.h"
|
#include "ui/windows/explorer_tab.h"
|
||||||
#include "ui/windows/market_tab.h"
|
#include "ui/windows/market_tab.h"
|
||||||
#include "ui/windows/settings_window.h"
|
#include "ui/windows/settings_window.h"
|
||||||
@@ -443,10 +445,11 @@ void App::rebuildLiteWallet(bool force)
|
|||||||
// does not block. The app's auto-open loop then reopens the wallet against the new server.
|
// does not block. The app's auto-open loop then reopens the wallet against the new server.
|
||||||
if (!force && lite_wallet_ && lite_wallet_->walletOpen()) return;
|
if (!force && lite_wallet_ && lite_wallet_->walletOpen()) return;
|
||||||
|
|
||||||
|
const auto liteConn = wallet::liteConnectionSettingsFromAppSettings(*settings_);
|
||||||
|
wallet::liteLog(std::string(force ? "Lite controller rebuilt" : "Lite controller built") +
|
||||||
|
" (preferred server: " + liteConn.stickyServerUrl + ")");
|
||||||
lite_wallet_ = wallet::LiteWalletController::createLinked(
|
lite_wallet_ = wallet::LiteWalletController::createLinked(
|
||||||
walletCapabilities(),
|
walletCapabilities(), liteConn, resolveLiteRolloutDecision(*settings_));
|
||||||
wallet::liteConnectionSettingsFromAppSettings(*settings_),
|
|
||||||
resolveLiteRolloutDecision(*settings_));
|
|
||||||
lite_wallet_->setPersistCallback([this]() { settings_->save(); });
|
lite_wallet_->setPersistCallback([this]() { settings_->save(); });
|
||||||
|
|
||||||
// The new controller starts closed. Re-arm the one-shot auto-open so the next update()
|
// The new controller starts closed. Re-arm the one-shot auto-open so the next update()
|
||||||
@@ -1368,6 +1371,9 @@ void App::render()
|
|||||||
case ui::NavPage::LiteNetwork:
|
case ui::NavPage::LiteNetwork:
|
||||||
ui::RenderLiteNetworkTab(this);
|
ui::RenderLiteNetworkTab(this);
|
||||||
break;
|
break;
|
||||||
|
case ui::NavPage::LiteConsole:
|
||||||
|
ui::RenderLiteConsoleTab(this);
|
||||||
|
break;
|
||||||
case ui::NavPage::Explorer:
|
case ui::NavPage::Explorer:
|
||||||
ui::RenderExplorerTab(this);
|
ui::RenderExplorerTab(this);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ enum class NavPage {
|
|||||||
Market,
|
Market,
|
||||||
// --- separator ---
|
// --- separator ---
|
||||||
Console,
|
Console,
|
||||||
|
LiteConsole, // lite-only diagnostics console (full-node uses the RPC Console); one per variant
|
||||||
Peers,
|
Peers,
|
||||||
LiteNetwork, // lite-only "Network" tab (full-node uses Peers); exactly one shows per variant
|
LiteNetwork, // lite-only "Network" tab (full-node uses Peers); exactly one shows per variant
|
||||||
Explorer,
|
Explorer,
|
||||||
@@ -52,9 +53,10 @@ inline const NavItem kNavItems[] = {
|
|||||||
{ "History", NavPage::History, nullptr, "history", nullptr },
|
{ "History", NavPage::History, nullptr, "history", nullptr },
|
||||||
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
|
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
|
||||||
{ "Market", NavPage::Market, nullptr, "market", nullptr },
|
{ "Market", NavPage::Market, nullptr, "market", nullptr },
|
||||||
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
|
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
|
||||||
{ "Network", NavPage::Peers, nullptr, "network", nullptr },
|
{ "Console", NavPage::LiteConsole, "ADVANCED","console", "advanced" },
|
||||||
{ "Network", NavPage::LiteNetwork, nullptr, "network", nullptr },
|
{ "Network", NavPage::Peers, nullptr, "network", nullptr },
|
||||||
|
{ "Network", NavPage::LiteNetwork, nullptr, "network", nullptr },
|
||||||
{ "Explorer", NavPage::Explorer, nullptr, "explorer", nullptr },
|
{ "Explorer", NavPage::Explorer, nullptr, "explorer", nullptr },
|
||||||
{ "Settings", NavPage::Settings, nullptr, "settings", nullptr },
|
{ "Settings", NavPage::Settings, nullptr, "settings", nullptr },
|
||||||
};
|
};
|
||||||
@@ -79,6 +81,7 @@ inline wallet::WalletUiSurface NavPageSurface(NavPage page)
|
|||||||
case NavPage::Mining: return wallet::WalletUiSurface::Mining;
|
case NavPage::Mining: return wallet::WalletUiSurface::Mining;
|
||||||
case NavPage::Market: return wallet::WalletUiSurface::Market;
|
case NavPage::Market: return wallet::WalletUiSurface::Market;
|
||||||
case NavPage::Console: return wallet::WalletUiSurface::Console;
|
case NavPage::Console: return wallet::WalletUiSurface::Console;
|
||||||
|
case NavPage::LiteConsole: return wallet::WalletUiSurface::LiteConsole;
|
||||||
case NavPage::Peers: return wallet::WalletUiSurface::Peers;
|
case NavPage::Peers: return wallet::WalletUiSurface::Peers;
|
||||||
case NavPage::LiteNetwork: return wallet::WalletUiSurface::LiteNetwork;
|
case NavPage::LiteNetwork: return wallet::WalletUiSurface::LiteNetwork;
|
||||||
case NavPage::Explorer: return wallet::WalletUiSurface::Explorer;
|
case NavPage::Explorer: return wallet::WalletUiSurface::Explorer;
|
||||||
@@ -103,6 +106,7 @@ inline const char* GetNavIconMD(NavPage page)
|
|||||||
case NavPage::Mining: return ICON_MD_CONSTRUCTION;
|
case NavPage::Mining: return ICON_MD_CONSTRUCTION;
|
||||||
case NavPage::Market: return ICON_MD_TRENDING_UP;
|
case NavPage::Market: return ICON_MD_TRENDING_UP;
|
||||||
case NavPage::Console: return ICON_MD_TERMINAL;
|
case NavPage::Console: return ICON_MD_TERMINAL;
|
||||||
|
case NavPage::LiteConsole: return ICON_MD_TERMINAL;
|
||||||
case NavPage::Peers: return ICON_MD_HUB;
|
case NavPage::Peers: return ICON_MD_HUB;
|
||||||
case NavPage::LiteNetwork: return ICON_MD_HUB;
|
case NavPage::LiteNetwork: return ICON_MD_HUB;
|
||||||
case NavPage::Explorer: return ICON_MD_EXPLORE;
|
case NavPage::Explorer: return ICON_MD_EXPLORE;
|
||||||
@@ -648,6 +652,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
|
|||||||
// Click detection
|
// Click detection
|
||||||
bool pageNeedsUnlock = locked &&
|
bool pageNeedsUnlock = locked &&
|
||||||
item.page != NavPage::Console &&
|
item.page != NavPage::Console &&
|
||||||
|
item.page != NavPage::LiteConsole &&
|
||||||
item.page != NavPage::Peers &&
|
item.page != NavPage::Peers &&
|
||||||
item.page != NavPage::Settings;
|
item.page != NavPage::Settings;
|
||||||
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !pageNeedsUnlock) {
|
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !pageNeedsUnlock) {
|
||||||
|
|||||||
98
src/ui/windows/lite_console_tab.cpp
Normal file
98
src/ui/windows/lite_console_tab.cpp
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#include "lite_console_tab.h"
|
||||||
|
|
||||||
|
#include "../../app.h"
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../wallet/lite_diagnostics.h"
|
||||||
|
#include "../layout.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "../material/colors.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
using namespace material;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Re-snapshot the (mutex-guarded) log only when it actually changes, not every frame.
|
||||||
|
std::vector<std::string> s_lines;
|
||||||
|
std::uint64_t s_cachedGeneration = static_cast<std::uint64_t>(-1);
|
||||||
|
bool s_autoScroll = true;
|
||||||
|
|
||||||
|
// Colour error/success lines for at-a-glance scanning (substring match on the messages the
|
||||||
|
// controller emits). Anything else renders in the muted default colour.
|
||||||
|
ImU32 lineColor(const std::string& line)
|
||||||
|
{
|
||||||
|
const auto has = [&line](const char* s) { return line.find(s) != std::string::npos; };
|
||||||
|
if (has("failed") || has(" unreachable") || has("blocked") || has("could not") ||
|
||||||
|
has("Error") || has("error"))
|
||||||
|
return Error();
|
||||||
|
if (has(": connected") || has("opened") || has("wallet ready") || has("Ready"))
|
||||||
|
return Success();
|
||||||
|
return OnSurfaceMedium();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void RenderLiteConsoleTab(App* app)
|
||||||
|
{
|
||||||
|
(void)app;
|
||||||
|
auto& diag = wallet::LiteDiagnostics::instance();
|
||||||
|
const std::uint64_t gen = diag.generation();
|
||||||
|
if (gen != s_cachedGeneration) {
|
||||||
|
s_lines = diag.snapshot();
|
||||||
|
s_cachedGeneration = gen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Header ──────────────────────────────────────────────────────────────────
|
||||||
|
Type().text(TypeStyle::H6, TR("lite_console_title"));
|
||||||
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("lite_console_intro"));
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// ── Toolbar ─────────────────────────────────────────────────────────────────
|
||||||
|
if (ImGui::Button(TR("lite_console_clear"))) diag.clear();
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(TR("lite_console_copy"))) {
|
||||||
|
std::string all;
|
||||||
|
for (const auto& l : s_lines) { all += l; all.push_back('\n'); }
|
||||||
|
ImGui::SetClipboardText(all.c_str());
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Checkbox(TR("lite_console_autoscroll"), &s_autoScroll);
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
// ── Log (terminal-styled scroll region) ─────────────────────────────────────
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 90));
|
||||||
|
ImGui::BeginChild("##LiteConsoleLog", ImVec2(0, 0), true,
|
||||||
|
ImGuiWindowFlags_HorizontalScrollbar);
|
||||||
|
ImGui::PushFont(Type().caption());
|
||||||
|
if (s_lines.empty()) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceDisabled());
|
||||||
|
ImGui::TextUnformatted(TR("lite_console_empty"));
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
} else {
|
||||||
|
for (const auto& line : s_lines) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, lineColor(line));
|
||||||
|
ImGui::TextUnformatted(line.c_str()); // not format-interpreted — safe for any content
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Keep pinned to the newest line only while the user is already at the bottom.
|
||||||
|
if (s_autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())
|
||||||
|
ImGui::SetScrollHereY(1.0f);
|
||||||
|
ImGui::PopFont();
|
||||||
|
ImGui::EndChild();
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
18
src/ui/windows/lite_console_tab.h
Normal file
18
src/ui/windows/lite_console_tab.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// Lite-wallet "Console" tab: a read-only, terminal-styled view of the lite diagnostics log
|
||||||
|
// (server connection attempts, wallet open/create/restore, sync milestones, errors). Lite
|
||||||
|
// builds only. See src/ui/windows/lite_console_tab.cpp.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
class App;
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
void RenderLiteConsoleTab(App* app);
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -1107,6 +1107,12 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["xmrig_unknown_error"] = "Unknown error.";
|
strings_["xmrig_unknown_error"] = "Unknown error.";
|
||||||
|
|
||||||
// --- Lite Network tab (server browser) ---
|
// --- Lite Network tab (server browser) ---
|
||||||
|
strings_["lite_console_title"] = "Console";
|
||||||
|
strings_["lite_console_intro"] = "Diagnostic log: server connections, wallet open/create, and sync events.";
|
||||||
|
strings_["lite_console_clear"] = "Clear";
|
||||||
|
strings_["lite_console_copy"] = "Copy";
|
||||||
|
strings_["lite_console_autoscroll"] = "Auto-scroll";
|
||||||
|
strings_["lite_console_empty"] = "No diagnostic output yet.";
|
||||||
strings_["lite_net_title"] = "Lite Servers";
|
strings_["lite_net_title"] = "Lite Servers";
|
||||||
strings_["lite_net_intro"] = "Pick a server to use, or let the wallet choose one at random. Changes apply immediately.";
|
strings_["lite_net_intro"] = "Pick a server to use, or let the wallet choose one at random. Changes apply immediately.";
|
||||||
strings_["lite_net_use_random"] = "Use a random server each time";
|
strings_["lite_net_use_random"] = "Use a random server each time";
|
||||||
|
|||||||
86
src/wallet/lite_diagnostics.h
Normal file
86
src/wallet/lite_diagnostics.h
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// lite_diagnostics.h — a small, thread-safe in-memory log of lite-wallet diagnostic events
|
||||||
|
// (server connection attempts, wallet open/create/restore, sync milestones, errors). Written
|
||||||
|
// from the controller's background threads and the App; read by the lite Console tab. Bounded
|
||||||
|
// so it can't grow without limit. Header-only (a Meyers singleton) so both the wallet layer
|
||||||
|
// and the UI can use it with no extra link target.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <ctime>
|
||||||
|
#include <deque>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace wallet {
|
||||||
|
|
||||||
|
class LiteDiagnostics {
|
||||||
|
public:
|
||||||
|
static LiteDiagnostics& instance() {
|
||||||
|
static LiteDiagnostics inst;
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append a timestamped line (thread-safe). Safe to call from background threads.
|
||||||
|
void log(const std::string& message) {
|
||||||
|
std::string line = timestamp();
|
||||||
|
line += message;
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
lines_.push_back(std::move(line));
|
||||||
|
if (lines_.size() > kMaxLines) lines_.pop_front();
|
||||||
|
++generation_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy of the current lines (oldest first).
|
||||||
|
std::vector<std::string> snapshot() const {
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
return std::vector<std::string>(lines_.begin(), lines_.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monotonic counter bumped on every change — lets readers re-snapshot only when it differs.
|
||||||
|
std::uint64_t generation() const {
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
return generation_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
std::lock_guard<std::mutex> lk(mutex_);
|
||||||
|
lines_.clear();
|
||||||
|
++generation_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
LiteDiagnostics() = default;
|
||||||
|
|
||||||
|
static std::string timestamp() {
|
||||||
|
std::time_t t = std::time(nullptr);
|
||||||
|
std::tm tmv{};
|
||||||
|
#ifdef _WIN32
|
||||||
|
localtime_s(&tmv, &t);
|
||||||
|
#else
|
||||||
|
localtime_r(&t, &tmv);
|
||||||
|
#endif
|
||||||
|
char buf[16];
|
||||||
|
std::snprintf(buf, sizeof(buf), "%02d:%02d:%02d ", tmv.tm_hour, tmv.tm_min, tmv.tm_sec);
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr std::size_t kMaxLines = 500;
|
||||||
|
mutable std::mutex mutex_;
|
||||||
|
std::deque<std::string> lines_;
|
||||||
|
std::uint64_t generation_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convenience: append a lite diagnostic line.
|
||||||
|
inline void liteLog(const std::string& message) { LiteDiagnostics::instance().log(message); }
|
||||||
|
|
||||||
|
} // namespace wallet
|
||||||
|
} // namespace dragonx
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "lite_wallet_controller.h"
|
#include "lite_wallet_controller.h"
|
||||||
|
|
||||||
|
#include "lite_diagnostics.h"
|
||||||
#include "../data/wallet_state.h"
|
#include "../data/wallet_state.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -279,11 +280,16 @@ std::unique_ptr<LiteWalletController> LiteWalletController::createLinked(
|
|||||||
void LiteWalletController::onLifecycleResult(const LiteWalletLifecycleResult& result)
|
void LiteWalletController::onLifecycleResult(const LiteWalletLifecycleResult& result)
|
||||||
{
|
{
|
||||||
status_ = result.status;
|
status_ = result.status;
|
||||||
|
const std::string op = liteWalletLifecycleOperationName(result.operation);
|
||||||
if (result.walletReady) {
|
if (result.walletReady) {
|
||||||
|
liteLog(op + ": wallet ready");
|
||||||
walletOpen_ = true;
|
walletOpen_ = true;
|
||||||
if (persist_) persist_();
|
if (persist_) persist_();
|
||||||
startSync(); // begin background sync on the backend
|
startSync(); // begin background sync on the backend
|
||||||
startWorker(); // begin periodic refresh -> WalletState (via takeRefreshedModel)
|
startWorker(); // begin periodic refresh -> WalletState (via takeRefreshedModel)
|
||||||
|
} else if (result.attempted) {
|
||||||
|
liteLog(op + " failed: " +
|
||||||
|
(result.error.empty() ? result.status.message : result.error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +318,7 @@ bool LiteWalletController::beginOpenExisting()
|
|||||||
status_ = WalletBackendStatus{WalletBackendState::Error,
|
status_ = WalletBackendStatus{WalletBackendState::Error,
|
||||||
reason.empty() ? "lite wallet is not available" : reason, {}, {}, 0.0};
|
reason.empty() ? "lite wallet is not available" : reason, {}, {}, 0.0};
|
||||||
lastOpenError_ = status_.message;
|
lastOpenError_ = status_.message;
|
||||||
|
liteLog("Open blocked: " + lastOpenError_);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto servers = failoverServerUrls();
|
auto servers = failoverServerUrls();
|
||||||
@@ -319,6 +326,7 @@ bool LiteWalletController::beginOpenExisting()
|
|||||||
status_ = WalletBackendStatus{WalletBackendState::Error,
|
status_ = WalletBackendStatus{WalletBackendState::Error,
|
||||||
"no usable lite servers are configured", {}, {}, 0.0};
|
"no usable lite servers are configured", {}, {}, 0.0};
|
||||||
lastOpenError_ = status_.message;
|
lastOpenError_ = status_.message;
|
||||||
|
liteLog("Open blocked: " + lastOpenError_);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,6 +337,7 @@ bool LiteWalletController::beginOpenExisting()
|
|||||||
}
|
}
|
||||||
openRunning_->store(true);
|
openRunning_->store(true);
|
||||||
status_ = WalletBackendStatus{WalletBackendState::Connecting, "opening wallet", {}, {}, 0.0};
|
status_ = WalletBackendStatus{WalletBackendState::Connecting, "opening wallet", {}, {}, 0.0};
|
||||||
|
liteLog("Opening wallet — trying " + std::to_string(servers.size()) + " server(s)");
|
||||||
|
|
||||||
// Capture only shared refs + value copies (never `this`) so the thread can safely outlive us.
|
// Capture only shared refs + value copies (never `this`) so the thread can safely outlive us.
|
||||||
auto bridge = bridge_;
|
auto bridge = bridge_;
|
||||||
@@ -340,14 +349,18 @@ bool LiteWalletController::beginOpenExisting()
|
|||||||
outcome.error = "could not reach any lite server";
|
outcome.error = "could not reach any lite server";
|
||||||
for (const auto& url : servers) {
|
for (const auto& url : servers) {
|
||||||
if (!bridge) break;
|
if (!bridge) break;
|
||||||
|
liteLog(" connecting to " + url + " ...");
|
||||||
// initialize_existing loads the wallet file but contacts the server to start the
|
// initialize_existing loads the wallet file but contacts the server to start the
|
||||||
// light client; ok && non-empty value == ready (mirrors the lifecycle's success test).
|
// light client; ok && non-empty value == ready (mirrors the lifecycle's success test).
|
||||||
const auto call = bridge->initializeExisting(/*dangerous=*/false, url);
|
const auto call = bridge->initializeExisting(/*dangerous=*/false, url);
|
||||||
if (call.ok && !call.value.empty()) {
|
if (call.ok && !call.value.empty()) {
|
||||||
|
liteLog(" " + url + ": connected");
|
||||||
outcome.ok = true;
|
outcome.ok = true;
|
||||||
outcome.serverUrl = url;
|
outcome.serverUrl = url;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
const std::string why = call.error.empty() ? "unreachable" : call.error;
|
||||||
|
liteLog(" " + url + ": " + why);
|
||||||
if (!call.error.empty()) outcome.error = call.error;
|
if (!call.error.empty()) outcome.error = call.error;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -374,12 +387,14 @@ void LiteWalletController::pumpAsyncOpen()
|
|||||||
walletOpen_ = true;
|
walletOpen_ = true;
|
||||||
lastOpenError_.clear();
|
lastOpenError_.clear();
|
||||||
status_ = WalletBackendStatus{WalletBackendState::Ready, "wallet open", {}, {}, 0.0};
|
status_ = WalletBackendStatus{WalletBackendState::Ready, "wallet open", {}, {}, 0.0};
|
||||||
|
liteLog("Wallet opened via " + outcome.serverUrl);
|
||||||
if (persist_) persist_();
|
if (persist_) persist_();
|
||||||
startSync(); // begin background sync on the backend
|
startSync(); // begin background sync on the backend
|
||||||
startWorker(); // begin periodic refresh -> WalletState
|
startWorker(); // begin periodic refresh -> WalletState
|
||||||
} else {
|
} else {
|
||||||
lastOpenError_ = outcome.error;
|
lastOpenError_ = outcome.error;
|
||||||
status_ = WalletBackendStatus{WalletBackendState::Error, outcome.error, {}, {}, 0.0};
|
status_ = WalletBackendStatus{WalletBackendState::Error, outcome.error, {}, {}, 0.0};
|
||||||
|
liteLog("Open failed: " + outcome.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,6 +402,7 @@ void LiteWalletController::startSync()
|
|||||||
{
|
{
|
||||||
if (syncLaunched_.exchange(true)) return;
|
if (syncLaunched_.exchange(true)) return;
|
||||||
syncStarted_ = true;
|
syncStarted_ = true;
|
||||||
|
liteLog("Background sync started");
|
||||||
// The backend `sync` command is a blocking, uninterruptible full chain scan, so run it on
|
// The backend `sync` command is a blocking, uninterruptible full chain scan, so run it on
|
||||||
// a detached thread. Capture shared refs (not the controller) so it is safe to outlive us.
|
// a detached thread. Capture shared refs (not the controller) so it is safe to outlive us.
|
||||||
auto bridge = bridge_;
|
auto bridge = bridge_;
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ enum class WalletUiSurface {
|
|||||||
BootstrapDownload,
|
BootstrapDownload,
|
||||||
SetupWizard,
|
SetupWizard,
|
||||||
NodeSettings,
|
NodeSettings,
|
||||||
LiteNetwork // lite-wallet-only server browser (replaces Peers in lite builds)
|
LiteNetwork, // lite-wallet-only server browser (replaces Peers in lite builds)
|
||||||
|
LiteConsole // lite-wallet-only diagnostics console (full-node uses the RPC Console)
|
||||||
};
|
};
|
||||||
|
|
||||||
struct WalletCapabilities {
|
struct WalletCapabilities {
|
||||||
@@ -155,6 +156,7 @@ constexpr bool isUiSurfaceAvailable(const WalletCapabilities& capabilities,
|
|||||||
case WalletUiSurface::Explorer:
|
case WalletUiSurface::Explorer:
|
||||||
return capabilities.fullNodePagesAvailable;
|
return capabilities.fullNodePagesAvailable;
|
||||||
case WalletUiSurface::LiteNetwork:
|
case WalletUiSurface::LiteNetwork:
|
||||||
|
case WalletUiSurface::LiteConsole:
|
||||||
return !capabilities.fullNodePagesAvailable; // lite builds only
|
return !capabilities.fullNodePagesAvailable; // lite builds only
|
||||||
case WalletUiSurface::BootstrapDownload:
|
case WalletUiSurface::BootstrapDownload:
|
||||||
case WalletUiSurface::SetupWizard:
|
case WalletUiSurface::SetupWizard:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#include "util/xmrig_updater.h"
|
#include "util/xmrig_updater.h"
|
||||||
#include "util/lite_server_probe.h"
|
#include "util/lite_server_probe.h"
|
||||||
#include "wallet/lite_connection_service.h"
|
#include "wallet/lite_connection_service.h"
|
||||||
|
#include "wallet/lite_diagnostics.h"
|
||||||
#include "wallet/lite_owned_string.h"
|
#include "wallet/lite_owned_string.h"
|
||||||
#include "wallet/lite_rollout_policy.h"
|
#include "wallet/lite_rollout_policy.h"
|
||||||
#include "wallet/lite_wallet_controller.h"
|
#include "wallet/lite_wallet_controller.h"
|
||||||
@@ -3560,6 +3561,7 @@ void testLiteWalletControllerOpenFailover()
|
|||||||
dragonx::test::resetLiteFakeCounters();
|
dragonx::test::resetLiteFakeCounters();
|
||||||
dragonx::test::g_liteFakeWalletExists = true;
|
dragonx::test::g_liteFakeWalletExists = true;
|
||||||
dragonx::test::g_liteFakeDeadServerSubstr = "dead.example";
|
dragonx::test::g_liteFakeDeadServerSubstr = "dead.example";
|
||||||
|
LiteDiagnostics::instance().clear(); // verify the open populates the console log
|
||||||
LiteWalletController controller(liteCaps, conn,
|
LiteWalletController controller(liteCaps, conn,
|
||||||
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
LiteClientBridge::fromApi(dragonx::test::makeFakeLiteApi()));
|
||||||
EXPECT_TRUE(controller.beginOpenExisting());
|
EXPECT_TRUE(controller.beginOpenExisting());
|
||||||
@@ -3569,6 +3571,17 @@ void testLiteWalletControllerOpenFailover()
|
|||||||
controller.pumpAsyncOpen();
|
controller.pumpAsyncOpen();
|
||||||
EXPECT_TRUE(controller.walletOpen());
|
EXPECT_TRUE(controller.walletOpen());
|
||||||
EXPECT_TRUE(controller.lastOpenError().empty());
|
EXPECT_TRUE(controller.lastOpenError().empty());
|
||||||
|
|
||||||
|
// The Console tab reads this log: the failover attempt + success must be recorded.
|
||||||
|
const auto log = LiteDiagnostics::instance().snapshot();
|
||||||
|
EXPECT_TRUE(!log.empty());
|
||||||
|
bool sawConnecting = false, sawOpened = false;
|
||||||
|
for (const auto& l : log) {
|
||||||
|
if (l.find("connecting to") != std::string::npos) sawConnecting = true;
|
||||||
|
if (l.find("Wallet opened via") != std::string::npos) sawOpened = true;
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(sawConnecting);
|
||||||
|
EXPECT_TRUE(sawOpened);
|
||||||
}
|
}
|
||||||
|
|
||||||
// All servers dead -> open fails, wallet stays closed, reason surfaced.
|
// All servers dead -> open fails, wallet stays closed, reason surfaced.
|
||||||
|
|||||||
Reference in New Issue
Block a user