Files
ObsidianDragon/src/wallet/lite_connection_service.cpp
dan_s ec8b500a42 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>
2026-06-07 11:09:27 -05:00

317 lines
11 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "lite_connection_service.h"
#include <algorithm>
#include <cctype>
#include <utility>
namespace dragonx {
namespace wallet {
namespace {
std::string trimCopy(const std::string& value)
{
auto begin = value.begin();
while (begin != value.end() && std::isspace(static_cast<unsigned char>(*begin))) ++begin;
auto end = value.end();
while (end != begin && std::isspace(static_cast<unsigned char>(*(end - 1)))) --end;
return std::string(begin, end);
}
bool startsWith(const std::string& value, const char* prefix)
{
const std::string prefixValue(prefix);
return value.size() >= prefixValue.size() &&
value.compare(0, prefixValue.size(), prefixValue) == 0;
}
LiteServerEndpoint trimmedEndpoint(const LiteServerEndpoint& endpoint)
{
LiteServerEndpoint normalized = endpoint;
normalized.url = trimCopy(normalized.url);
return normalized;
}
std::vector<std::pair<std::size_t, LiteServerEndpoint>> usableConfiguredServers(
const LiteConnectionSettings& settings)
{
std::vector<std::pair<std::size_t, LiteServerEndpoint>> servers;
for (std::size_t index = 0; index < settings.servers.size(); ++index) {
auto endpoint = trimmedEndpoint(settings.servers[index]);
if (!endpoint.enabled || !isLiteServerUrlUsable(endpoint.url)) continue;
servers.push_back({index, std::move(endpoint)});
}
return servers;
}
std::string normalizedChainName(const std::string& chainName)
{
const std::string normalized = trimCopy(chainName);
return normalized.empty() ? kDragonXLiteChainName : normalized;
}
} // namespace
std::vector<LiteServerEndpoint> defaultDragonXLiteServers()
{
return {
{"https://lite.dragonx.is", "DragonX Lite", true},
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
{"https://lite2.dragonx.is", "DragonX Lite 2", true},
{"https://lite3.dragonx.is", "DragonX Lite 3", true},
{"https://lite4.dragonx.is", "DragonX Lite 4", true},
{"https://lite5.dragonx.is", "DragonX Lite 5", true}
};
}
LiteConnectionSettings defaultLiteConnectionSettings()
{
LiteConnectionSettings settings;
settings.servers = defaultDragonXLiteServers();
settings.selectionMode = LiteServerSelectionMode::Sticky;
settings.stickyServerUrl = kDefaultDragonXLiteServer;
settings.chainName = kDragonXLiteChainName;
settings.randomSelectionSeed = 0;
return settings;
}
bool isLiteServerUrlUsable(const std::string& serverUrl)
{
const std::string normalized = trimCopy(serverUrl);
return startsWith(normalized, "https://") || startsWith(normalized, "http://");
}
bool isOfficialLiteServer(const std::string& serverUrl)
{
const std::string url = trimCopy(serverUrl);
for (const auto& s : defaultDragonXLiteServers())
if (s.url == url) return true;
return false;
}
const char* liteServerSelectionModeName(LiteServerSelectionMode mode)
{
switch (mode) {
case LiteServerSelectionMode::Sticky:
return "Sticky";
case LiteServerSelectionMode::Random:
return "Random";
}
return "Unknown";
}
const char* liteConnectionAvailabilityName(LiteConnectionAvailability availability)
{
switch (availability) {
case LiteConnectionAvailability::Ready:
return "Ready";
case LiteConnectionAvailability::UnsupportedBuild:
return "UnsupportedBuild";
case LiteConnectionAvailability::BackendUnavailable:
return "BackendUnavailable";
case LiteConnectionAvailability::BridgeUnavailable:
return "BridgeUnavailable";
case LiteConnectionAvailability::NoUsableServer:
return "NoUsableServer";
}
return "Unknown";
}
LiteServerSelectionResult selectLiteServer(const LiteConnectionSettings& settings)
{
auto usableServers = usableConfiguredServers(settings);
const std::string stickyServerUrl = trimCopy(settings.stickyServerUrl);
if (settings.selectionMode == LiteServerSelectionMode::Sticky &&
isLiteServerUrlUsable(stickyServerUrl)) {
auto match = std::find_if(usableServers.begin(), usableServers.end(), [&](const auto& candidate) {
return candidate.second.url == stickyServerUrl;
});
if (match != usableServers.end()) {
return LiteServerSelectionResult{true, match->second, match->first, false, {}};
}
return LiteServerSelectionResult{
true,
LiteServerEndpoint{stickyServerUrl, "Custom", true},
0,
true,
{}
};
}
if (usableServers.empty()) {
return LiteServerSelectionResult{false, {}, 0, false, "no usable lite servers are configured"};
}
if (settings.selectionMode == LiteServerSelectionMode::Random) {
const std::size_t selectedIndex = settings.randomSelectionSeed % usableServers.size();
return LiteServerSelectionResult{
true,
usableServers[selectedIndex].second,
usableServers[selectedIndex].first,
false,
{}
};
}
return LiteServerSelectionResult{true, usableServers.front().second, usableServers.front().first, false, {}};
}
LiteConnectionService::LiteConnectionService(WalletCapabilities capabilities,
LiteConnectionSettings settings,
LiteClientBridge bridge)
: capabilities_(capabilities), settings_(std::move(settings)), bridge_(std::move(bridge))
{
}
LiteConnectionAvailability LiteConnectionService::availability() const
{
if (!isLiteBuild(capabilities_)) return LiteConnectionAvailability::UnsupportedBuild;
if (!supportsLiteBackend(capabilities_)) return LiteConnectionAvailability::BackendUnavailable;
if (!bridge_.available()) return LiteConnectionAvailability::BridgeUnavailable;
if (!selectedServer().ok) return LiteConnectionAvailability::NoUsableServer;
return LiteConnectionAvailability::Ready;
}
WalletBackendStatus LiteConnectionService::status() const
{
const auto currentAvailability = availability();
if (currentAvailability == LiteConnectionAvailability::NoUsableServer) {
return statusFor(currentAvailability, selectedServer().error);
}
return statusFor(currentAvailability);
}
LiteServerSelectionResult LiteConnectionService::selectedServer() const
{
return selectLiteServer(settings_);
}
LiteWalletExistsRequest LiteConnectionService::walletExistsRequest() const
{
return LiteWalletExistsRequest{normalizedChainName(settings_.chainName)};
}
LiteServerHealthRequest LiteConnectionService::serverHealthRequest() const
{
auto selection = selectedServer();
if (!selection.ok) return {};
return LiteServerHealthRequest{selection.server, selection.serverIndex, selection.customServer};
}
LiteWalletExistsCheckResult LiteConnectionService::checkWalletExists()
{
LiteWalletExistsCheckResult result;
result.request = walletExistsRequest();
const auto readiness = bridgeReadinessStatus();
if (readiness.state != WalletBackendState::Disconnected) {
result.status = readiness;
result.error = readiness.message;
return result;
}
if (result.request.chainName.empty()) {
result.status = statusFor(LiteConnectionAvailability::BackendUnavailable, "lite chain name is empty");
result.error = result.status.message;
return result;
}
result.attempted = true;
result.walletExists = bridge_.walletExists(result.request.chainName);
result.ok = true;
result.status = readiness;
return result;
}
LiteServerHealthCheckResult LiteConnectionService::checkSelectedServerHealth()
{
LiteServerHealthCheckResult result;
result.request = serverHealthRequest();
const auto readiness = bridgeReadinessStatus();
if (readiness.state != WalletBackendState::Disconnected) {
result.status = readiness;
result.error = readiness.message;
return result;
}
auto selection = selectedServer();
if (!selection.ok) {
result.status = statusFor(LiteConnectionAvailability::NoUsableServer, selection.error);
result.error = result.status.message;
return result;
}
result.request = LiteServerHealthRequest{selection.server, selection.serverIndex, selection.customServer};
result.attempted = true;
result.serverOnline = bridge_.checkServerOnline(result.request.server.url);
result.ok = true;
result.status = readiness;
return result;
}
WalletBackendStatus LiteConnectionService::statusFor(LiteConnectionAvailability availability,
const std::string& detail) const
{
switch (availability) {
case LiteConnectionAvailability::Ready:
return WalletBackendStatus{
WalletBackendState::Disconnected,
detail.empty() ? "lite connection scaffold ready; wallet lifecycle is not implemented" : detail,
{},
{},
0.0
};
case LiteConnectionAvailability::UnsupportedBuild:
return WalletBackendStatus{
WalletBackendState::Unavailable,
"lite connection service is unsupported in full-node builds",
{},
{},
0.0
};
case LiteConnectionAvailability::BackendUnavailable:
return WalletBackendStatus{
WalletBackendState::Unavailable,
detail.empty() ? "lite backend is not linked" : detail,
{},
{},
0.0
};
case LiteConnectionAvailability::BridgeUnavailable:
return WalletBackendStatus{
WalletBackendState::Unavailable,
detail.empty() ? bridge_.unavailableReason() : detail,
{},
{},
0.0
};
case LiteConnectionAvailability::NoUsableServer:
return WalletBackendStatus{
WalletBackendState::Error,
detail.empty() ? "no usable lite servers are configured" : detail,
{},
{},
0.0
};
}
return WalletBackendStatus{WalletBackendState::Unavailable, "unknown lite connection state", {}, {}, 0.0};
}
WalletBackendStatus LiteConnectionService::bridgeReadinessStatus() const
{
if (!isLiteBuild(capabilities_)) return statusFor(LiteConnectionAvailability::UnsupportedBuild);
if (!supportsLiteBackend(capabilities_)) return statusFor(LiteConnectionAvailability::BackendUnavailable);
if (!bridge_.available()) return statusFor(LiteConnectionAvailability::BridgeUnavailable);
return statusFor(LiteConnectionAvailability::Ready);
}
} // namespace wallet
} // namespace dragonx