// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "lite_connection_service.h" #include #include #include namespace dragonx { namespace wallet { namespace { std::string trimCopy(const std::string& value) { auto begin = value.begin(); while (begin != value.end() && std::isspace(static_cast(*begin))) ++begin; auto end = value.end(); while (end != begin && std::isspace(static_cast(*(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> usableConfiguredServers( const LiteConnectionSettings& settings) { std::vector> 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 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