feat(lite): ObsidianDragonLite Network tab — server browser
A lite-wallet-only "Network" tab (full-node keeps the Peers tab; exactly one shows per variant) to manage lightwalletd servers, replacing the basic selector that was in Settings. - Card list of servers with per-server latency + status dot, DNS host + resolved IP, and an Official/Custom pill. Official DragonX servers get a glowing outline. - Pick a server (Sticky) by clicking its card, or toggle "use a random server" (Random mode); selection applies immediately (App::rebuildLiteWallet(force=true) tears down + rebuilds the controller against the new server and resyncs — its dtor detaches the uninterruptible sync thread, so this doesn't block). - Add custom servers; hide/unhide servers (persisted set, revealed by a "Show hidden" toggle). - Latency/IP come from a new background probe (util/LiteServerProbe): libcurl CONNECT_ONLY does the TCP+TLS handshake (works for gRPC lightwalletd, no HTTP response needed), recording APPCONNECT_TIME as latency and CURLINFO_PRIMARY_IP. Auto-runs on tab open + a Refresh button. Wiring: WalletUiSurface::LiteNetwork (gated !fullNodePagesAvailable) + NavPage::LiteNetwork in the sidebar + app.cpp dispatch; settings gains a hidden-servers set; isOfficialLiteServer() added to lite_connection_service. The Settings page lite-server selector + its plumbing are removed (single source of truth = the tab). Reuses the existing server model (LiteServerPreference, Sticky/Random, selectLiteServer) and UI primitives (DrawGlassPanel, ThemeEffects glow, peers-tab ping-dot idiom). Unit-tested (liteServerHost, isOfficialLiteServer) + an env-gated live probe (verified vs lite.dragonx.is: online, latency, IP). Both variants + lite-backend build; suite passes; hygiene clean; GUI smoke-launched without crash. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1061,6 +1061,26 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["xmrig_update_failed"] = "Update failed";
|
||||
strings_["xmrig_unknown_error"] = "Unknown error.";
|
||||
|
||||
// --- Lite Network tab (server browser) ---
|
||||
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_use_random"] = "Use a random server each time";
|
||||
strings_["lite_net_random_active"] = "Random server selection is active.";
|
||||
strings_["lite_net_refresh"] = "Refresh";
|
||||
strings_["lite_net_add"] = "Add";
|
||||
strings_["lite_net_add_url_hint"] = "https://your-lite-server";
|
||||
strings_["lite_net_add_label_hint"] = "Label (optional)";
|
||||
strings_["lite_net_invalid_url"] = "Enter a valid http(s):// URL.";
|
||||
strings_["lite_net_official"] = "Official";
|
||||
strings_["lite_net_custom"] = "Custom";
|
||||
strings_["lite_net_in_use"] = "In use";
|
||||
strings_["lite_net_offline"] = "offline";
|
||||
strings_["lite_net_checking"] = "checking…";
|
||||
strings_["lite_net_hide"] = "Hide";
|
||||
strings_["lite_net_unhide"] = "Unhide";
|
||||
strings_["lite_net_show_hidden"] = "Show hidden servers";
|
||||
strings_["lite_net_hidden_section"] = "Hidden servers";
|
||||
|
||||
// --- Peers Tab ---
|
||||
strings_["peers_avg_ping"] = "Avg Ping";
|
||||
strings_["peers_ban_24h"] = "Ban Peer 24h";
|
||||
|
||||
96
src/util/lite_server_probe.cpp
Normal file
96
src/util/lite_server_probe.cpp
Normal file
@@ -0,0 +1,96 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "lite_server_probe.h"
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
std::string liteServerHost(const std::string& url)
|
||||
{
|
||||
// Strip scheme ("scheme://") then take everything up to the first '/', '?' or '#'.
|
||||
std::string s = url;
|
||||
const auto scheme = s.find("://");
|
||||
if (scheme != std::string::npos) s = s.substr(scheme + 3);
|
||||
const auto end = s.find_first_of("/?#");
|
||||
if (end != std::string::npos) s = s.substr(0, end);
|
||||
// Drop any userinfo ("user@host").
|
||||
const auto at = s.find('@');
|
||||
if (at != std::string::npos) s = s.substr(at + 1);
|
||||
return s;
|
||||
}
|
||||
|
||||
LiteServerProbe::~LiteServerProbe()
|
||||
{
|
||||
cancel_ = true;
|
||||
if (worker_.joinable()) worker_.join();
|
||||
}
|
||||
|
||||
void LiteServerProbe::start(std::vector<std::string> urls)
|
||||
{
|
||||
// Replace any in-flight probe.
|
||||
cancel_ = true;
|
||||
if (worker_.joinable()) worker_.join();
|
||||
cancel_ = false;
|
||||
busy_ = true;
|
||||
worker_ = std::thread([this, urls = std::move(urls)]() mutable { run(std::move(urls)); });
|
||||
}
|
||||
|
||||
std::map<std::string, LiteServerProbeResult> LiteServerProbe::results() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mutex_);
|
||||
return results_;
|
||||
}
|
||||
|
||||
void LiteServerProbe::run(std::vector<std::string> urls)
|
||||
{
|
||||
for (const auto& url : urls) {
|
||||
if (cancel_.load()) break;
|
||||
|
||||
LiteServerProbeResult r;
|
||||
r.probed = true;
|
||||
|
||||
CURL* c = curl_easy_init();
|
||||
if (c) {
|
||||
curl_easy_setopt(c, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(c, CURLOPT_CONNECT_ONLY, 1L); // TCP+TLS handshake only
|
||||
curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, 5L);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT, 8L);
|
||||
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L); // thread-safe resolver
|
||||
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 0L);
|
||||
curl_easy_setopt(c, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
|
||||
curl_easy_setopt(c, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
|
||||
const CURLcode res = curl_easy_perform(c);
|
||||
if (res == CURLE_OK) {
|
||||
r.online = true;
|
||||
double t = 0.0;
|
||||
// Prefer TLS-handshake-complete time; fall back to TCP connect time.
|
||||
if (curl_easy_getinfo(c, CURLINFO_APPCONNECT_TIME, &t) != CURLE_OK || t <= 0.0)
|
||||
curl_easy_getinfo(c, CURLINFO_CONNECT_TIME, &t);
|
||||
r.latencyMs = t > 0.0 ? static_cast<int>(t * 1000.0) : 0;
|
||||
char* ip = nullptr;
|
||||
if (curl_easy_getinfo(c, CURLINFO_PRIMARY_IP, &ip) == CURLE_OK && ip)
|
||||
r.ip = ip;
|
||||
} else {
|
||||
DEBUG_LOGF("[lite-probe] %s: %s\n", url.c_str(), curl_easy_strerror(res));
|
||||
}
|
||||
curl_easy_cleanup(c);
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lk(mutex_);
|
||||
results_[url] = r;
|
||||
}
|
||||
busy_ = false;
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
63
src/util/lite_server_probe.h
Normal file
63
src/util/lite_server_probe.h
Normal file
@@ -0,0 +1,63 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// LiteServerProbe — background latency/IP probe for lite (lightwalletd) servers.
|
||||
//
|
||||
// The lite backend only exposes a yes/no checkServerOnline, so the Network tab measures latency
|
||||
// itself: for each server URL, a libcurl CONNECT_ONLY request does the TCP+TLS handshake (which
|
||||
// works for gRPC/HTTP-2 lightwalletd endpoints without needing an HTTP response) and records the
|
||||
// handshake time as latency plus the resolved IP. Runs on a background thread; results are read
|
||||
// thread-safely from the UI thread and re-run on demand (Refresh).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
struct LiteServerProbeResult {
|
||||
bool probed = false; // a probe has completed for this URL
|
||||
bool online = false; // the TCP+TLS handshake succeeded
|
||||
int latencyMs = 0; // handshake latency (ms); valid when online
|
||||
std::string ip; // resolved primary IP (CURLINFO_PRIMARY_IP)
|
||||
};
|
||||
|
||||
// Pure helper: the host[:port] authority of a URL (scheme + path stripped), for display.
|
||||
// "https://lite.dragonx.is:443/x" -> "lite.dragonx.is:443"; "" on a malformed input.
|
||||
std::string liteServerHost(const std::string& url);
|
||||
|
||||
class LiteServerProbe {
|
||||
public:
|
||||
LiteServerProbe() = default;
|
||||
~LiteServerProbe();
|
||||
LiteServerProbe(const LiteServerProbe&) = delete;
|
||||
LiteServerProbe& operator=(const LiteServerProbe&) = delete;
|
||||
|
||||
// Probe each URL on a background thread (replacing any in-flight probe). Results stream in as
|
||||
// each server completes; poll results() each frame.
|
||||
void start(std::vector<std::string> urls);
|
||||
|
||||
// Snapshot of results so far, keyed by URL.
|
||||
std::map<std::string, LiteServerProbeResult> results() const;
|
||||
|
||||
bool busy() const { return busy_.load(); }
|
||||
|
||||
private:
|
||||
void run(std::vector<std::string> urls);
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::map<std::string, LiteServerProbeResult> results_;
|
||||
std::atomic<bool> busy_{false};
|
||||
std::atomic<bool> cancel_{false};
|
||||
std::thread worker_;
|
||||
};
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
Reference in New Issue
Block a user