// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // Pure (no-I/O) implementation of the pool registry + hashrate helpers. Links // nlohmann_json but NOT libcurl, so it can be exercised directly from the test // binary (mirrors util/xmrig_updater_core.cpp). #include "pool_registry.h" #include #include #include #include namespace dragonx { namespace util { using json = nlohmann::json; const std::vector& knownPools() { // NOTE: only PPLNS (shared) pools belong here. The .cc SOLO pool // (dragonx-solo, ports 5555/6666) is intentionally omitted — its hashrate // is not comparable for network balancing. static const std::vector pools = { KnownPool{ "dragonx-is", "pool.dragonx.is", "pool.dragonx.is:3433", "rx/hush", "https://pool.dragonx.is/api/stats", PoolStatsSchema::DragonXIs, /*miningcorePoolId=*/"", /*feePercent=*/1.0, /*official=*/true, }, }; return pools; } namespace { std::string trimmed(const std::string& s) { const auto b = s.find_first_not_of(" \t\r\n"); if (b == std::string::npos) return {}; const auto e = s.find_last_not_of(" \t\r\n"); return s.substr(b, e - b + 1); } // Extract "host[:port]" from a URL that may carry a scheme, userinfo, path or query. // Our pools are plain host:port endpoints (no IPv6 literals), so a last-colon split // for the port is sufficient. std::string hostPortOf(const std::string& url) { std::string s = trimmed(url); if (const auto scheme = s.find("://"); scheme != std::string::npos) s = s.substr(scheme + 3); if (const auto at = s.find('@'); at != std::string::npos) s = s.substr(at + 1); if (const auto cut = s.find_first_of("/?#"); cut != std::string::npos) s = s.substr(0, cut); return s; } void splitHostPort(const std::string& hostport, std::string& host, std::string& port) { if (const auto colon = hostport.rfind(':'); colon == std::string::npos) { host = hostport; port.clear(); } else { host = hostport.substr(0, colon); port = hostport.substr(colon + 1); } std::transform(host.begin(), host.end(), host.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); } // Same endpoint when hosts match and (either side omits a port, or the ports match). bool sameEndpoint(const std::string& a, const std::string& b) { std::string ha, pa, hb, pb; splitHostPort(hostPortOf(a), ha, pa); splitHostPort(hostPortOf(b), hb, pb); if (ha.empty() || ha != hb) return false; if (pa.empty() || pb.empty()) return true; return pa == pb; } // Build a synthetic, selectable pool row for a user-supplied URL (a saved favorite // or the current custom pool). We don't know its stats API, so it carries no // statsUrl / live hashrate and an unknown (<0) fee — the UI falls back to "—". KnownPool makeUserPool(const std::string& url) { KnownPool p; const std::string hp = hostPortOf(url); std::string host, port; splitHostPort(hp, host, port); p.id = "user:" + trimmed(url); // stable + unique (used as the ImGui id) p.label = host.empty() ? hp : host; p.stratum = trimmed(url); // what the miner connects to / a row-click restores p.algo = ""; // unknown; xmrig resolves via resolvePoolAlgo's fallback p.statsUrl = ""; // no known stats endpoint -> no live hashrate/fee p.schema = PoolStatsSchema::DragonXIs; p.miningcorePoolId = ""; p.feePercent = -1.0; // unknown fee p.official = false; return p; } } // namespace const KnownPool* findPoolByUrl(const std::vector& pools, const std::string& url) { for (const auto& p : pools) if (sameEndpoint(p.stratum, url)) return &p; return nullptr; } const KnownPool* findKnownPoolByUrl(const std::string& url) { return findPoolByUrl(knownPools(), url); } std::vector effectivePools(const std::string& currentPoolUrl, const std::vector& savedPoolUrls) { std::vector pools = knownPools(); // Skip anything whose endpoint already appears (official or an earlier user row). auto listed = [&](const std::string& url) { return findPoolByUrl(pools, url) != nullptr; }; for (const auto& url : savedPoolUrls) { if (trimmed(url).empty() || listed(url)) continue; pools.push_back(makeUserPool(url)); } // The pool currently being mined, if not already shown, so the active pool is // always visible even before it's bookmarked. if (!trimmed(currentPoolUrl).empty() && !listed(currentPoolUrl)) pools.push_back(makeUserPool(currentPoolUrl)); return pools; } std::string resolvePoolAlgo(const std::string& url, const std::string& fallback) { if (const KnownPool* p = findKnownPoolByUrl(url)) return p->algo; return fallback; } double parsePoolHashrate(PoolStatsSchema schema, const std::string& jsonStr, const std::string& miningcorePoolId, bool& ok) { ok = false; try { const json j = json::parse(jsonStr); if (schema == PoolStatsSchema::DragonXIs) { // { "pools": { "dragonx": { "hashrate": , ... }, ... } } if (j.contains("pools") && j["pools"].is_object()) { const auto& pools = j["pools"]; auto readHr = [&](const json& pool, double& out) -> bool { if (pool.is_object() && pool.contains("hashrate") && pool["hashrate"].is_number()) { out = pool["hashrate"].get(); return true; } return false; }; double hr = 0.0; if (pools.contains("dragonx") && readHr(pools["dragonx"], hr)) { ok = true; return hr; } for (auto it = pools.begin(); it != pools.end(); ++it) { if (readHr(it.value(), hr)) { ok = true; return hr; } } } } else { // Miningcore // { "pools": [ { "id": "...", "poolStats": { "poolHashrate": } }, ... ] } if (j.contains("pools") && j["pools"].is_array()) { const json* chosen = nullptr; for (const auto& pool : j["pools"]) { if (!pool.is_object()) continue; if (!miningcorePoolId.empty()) { if (pool.value("id", std::string{}) == miningcorePoolId) { chosen = &pool; break; } } else if (!chosen) { chosen = &pool; // first pool when no id requested } } if (chosen && chosen->contains("poolStats") && (*chosen)["poolStats"].is_object() && (*chosen)["poolStats"].contains("poolHashrate") && (*chosen)["poolStats"]["poolHashrate"].is_number()) { ok = true; return (*chosen)["poolStats"]["poolHashrate"].get(); } } } } catch (...) { // fall through — ok stays false } return 0.0; } double parsePoolFee(PoolStatsSchema schema, const std::string& jsonStr, const std::string& miningcorePoolId, bool& ok) { ok = false; try { const json j = json::parse(jsonStr); if (schema == PoolStatsSchema::DragonXIs) { // { "pools": { "dragonx": { "poolFee": , ... }, ... } } if (j.contains("pools") && j["pools"].is_object()) { const auto& pools = j["pools"]; auto readFee = [&](const json& pool, double& out) -> bool { if (pool.is_object() && pool.contains("poolFee") && pool["poolFee"].is_number()) { out = pool["poolFee"].get(); return true; } return false; }; double fee = 0.0; if (pools.contains("dragonx") && readFee(pools["dragonx"], fee)) { ok = true; return fee; } for (auto it = pools.begin(); it != pools.end(); ++it) { if (readFee(it.value(), fee)) { ok = true; return fee; } } } } else { // Miningcore: pools[id].poolFeePercent if (j.contains("pools") && j["pools"].is_array()) { const json* chosen = nullptr; for (const auto& pool : j["pools"]) { if (!pool.is_object()) continue; if (!miningcorePoolId.empty()) { if (pool.value("id", std::string{}) == miningcorePoolId) { chosen = &pool; break; } } else if (!chosen) { chosen = &pool; // first pool when no id requested } } if (chosen && chosen->contains("poolFeePercent") && (*chosen)["poolFeePercent"].is_number()) { ok = true; return (*chosen)["poolFeePercent"].get(); } } } } catch (...) { // fall through — ok stays false } return 0.0; } std::string chooseWeightedPool(const std::vector& pools, const std::string& currentId, std::mt19937& rng) { std::vector usable; usable.reserve(pools.size()); for (const auto& p : pools) if (p.ok) usable.push_back(&p); if (usable.empty()) return currentId; if (usable.size() == 1) return usable.front()->id; // weight = 1/(hr + eps): smaller pools get more weight; eps keeps a zero-hashrate // pool finite (and most-favored). The incumbent gets a stickiness multiplier so a // periodic re-balance only rarely restarts the miner. constexpr double kEps = 1.0; // H/s std::vector weights; weights.reserve(usable.size()); double total = 0.0; for (const auto* p : usable) { double w = 1.0 / (std::max(0.0, p->hashrateHs) + kEps); if (!currentId.empty() && p->id == currentId) w *= kIncumbentStayBias; weights.push_back(w); total += w; } if (total <= 0.0) return usable.front()->id; std::uniform_real_distribution dist(0.0, total); const double roll = dist(rng); double cum = 0.0; for (std::size_t i = 0; i < usable.size(); ++i) { cum += weights[i]; if (roll <= cum) return usable[i]->id; } return usable.back()->id; } } // namespace util } // namespace dragonx