diff --git a/src/app.cpp b/src/app.cpp index cd3f63d..8e30319 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -22,6 +22,7 @@ #include "daemon/embedded_daemon.h" #include "daemon/lifecycle_adapters.h" #include "daemon/xmrig_manager.h" +#include "util/pool_registry.h" #include "ui/windows/main_window.h" #include "ui/windows/balance_tab.h" #include "ui/windows/send_tab.h" @@ -109,9 +110,101 @@ namespace dragonx { using json = nlohmann::json; -App::App() = default; +App::App() +{ + // Seed the auto-balance RNG once per run so weighted-random pool selection isn't + // deterministic across launches. + balance_rng_.seed(std::random_device{}()); +} App::~App() = default; +namespace { +// How often auto-balance re-evaluates the pool while active. Long, because switching +// restarts the miner (drops in-flight shares + reconnect); the incumbent stickiness +// in chooseWeightedPool makes actual switches rarer still. +constexpr long long kRebalanceIntervalMs = 30LL * 60LL * 1000LL; // 30 minutes + +long long steadyNowMs() +{ + using namespace std::chrono; + return duration_cast(steady_clock::now().time_since_epoch()).count(); +} +} // namespace + +void App::updatePoolAutoBalance() +{ + if (!settings_) return; + if (!supportsPoolMining()) return; // pool mining is full-node only + if (settings_->getPoolSelectMode() != config::Settings::PoolSelectMode::AutoBalance) return; + if (!settings_->getPoolMode()) return; // only while POOL mode is selected + + const long long now = steadyNowMs(); + const bool intervalDue = (last_balance_eval_ms_ == 0) || + (now - last_balance_eval_ms_ >= kRebalanceIntervalMs); + + // Kick a background refresh when requested or due (never while one is in flight). + if ((balance_refresh_pending_ || intervalDue) && !pool_stats_service_.busy()) { + balance_refresh_pending_ = false; + last_balance_eval_ms_ = now; + balance_snapshot_seen_ = false; + pool_stats_service_.refresh(util::knownPools()); + return; // results land on a later frame + } + + // Apply the freshly-completed snapshot exactly once. + if (!pool_stats_service_.busy()) { + const auto snap = pool_stats_service_.snapshot(); + if (snap.ready && !balance_snapshot_seen_) { + balance_snapshot_seen_ = true; + applyPoolAutoBalance(snap); + } + } +} + +void App::applyPoolAutoBalance(const util::PoolStatsService::Snapshot& snap) +{ + const auto& pools = util::knownPools(); + + std::vector samples; + samples.reserve(pools.size()); + for (const auto& kp : pools) { + const auto it = snap.byId.find(kp.id); + samples.push_back(it != snap.byId.end() + ? it->second + : util::PoolHashrate{kp.id, 0.0, false}); + } + + const util::KnownPool* current = util::findKnownPoolByUrl(settings_->getPoolUrl()); + const std::string currentId = current ? current->id : std::string{}; + + const std::string chosenId = util::chooseWeightedPool(samples, currentId, balance_rng_); + if (chosenId.empty() || chosenId == currentId) return; // keep the current pool + + const util::KnownPool* chosen = nullptr; + for (const auto& kp : pools) + if (kp.id == chosenId) { chosen = &kp; break; } + if (!chosen) return; + + settings_->setPoolUrl(chosen->stratum); + settings_->save(); + + ui::Notifications::instance().info( + std::string(TR("mining_auto_balanced_to")) + " " + chosen->label); + + // Restart so the new pool (and its per-pool algo) takes effect immediately. + if (xmrig_manager_ && xmrig_manager_->isRunning()) { + const int threads = xmrig_manager_->getRequestedThreads(); + stopPoolMining(); + startPoolMining(threads); + } +} + +std::string App::poolMiningInstalledVersion() +{ + daemon::XmrigManager::startVersionDetection(); // idempotent one-shot + return daemon::XmrigManager::installedVersion(); +} + bool App::sendStopCommandSafely(rpc::RPCClient& client, const char* context) { const char* label = context ? context : "App"; @@ -706,6 +799,7 @@ void App::update() ps.pool_diff = xs.pool_diff; ps.pool_url = xs.pool_url; ps.algo = xs.algo; + ps.version = xs.version; ps.connected = xs.connected; // Get memory directly from OS (more reliable than API) double memMB = xmrig_manager_->getMemoryUsageMB(); @@ -723,6 +817,9 @@ void App::update() state_.pool_mining.xmrig_running = false; } + // Auto-balance: periodically re-pick the pool by hashrate (self-throttled). + updatePoolAutoBalance(); + // Populate solo mining log lines from daemon output if (daemon_controller_ && daemon_controller_->isRunning()) { state_.mining.log_lines = daemon_controller_->recentLines(50); diff --git a/src/app.h b/src/app.h index 3f6c7bc..6648ef7 100644 --- a/src/app.h +++ b/src/app.h @@ -19,6 +19,7 @@ #include "services/wallet_security_controller.h" #include "services/wallet_security_workflow.h" #include "util/async_task_manager.h" +#include "util/pool_stats_service.h" #include "wallet/wallet_capabilities.h" #include "ui/sidebar.h" #include "ui/windows/console_tab.h" @@ -204,6 +205,17 @@ public: return xmrig_manager_ && xmrig_manager_->isRunning(); } + // Auto-balance: latest per-pool hashrate snapshot (for the mining tab pool list), + // and a request to refresh it now (Refresh button / switching into Auto mode). + util::PoolStatsService::Snapshot poolStatsSnapshot() const { + return pool_stats_service_.snapshot(); + } + void requestPoolBalanceRefresh() { balance_refresh_pending_ = true; } + + // Installed miner version (detected from `xmrig --version`, cached; kicks the one-shot + // detection on first call) so the mining tab can show it before mining starts. + std::string poolMiningInstalledVersion(); + // Mine-when-idle state query bool isIdleMiningActive() const { return idle_mining_active_; } @@ -455,6 +467,11 @@ private: void pruneShieldedHistoryScanProgress(); void invalidateShieldedHistoryScanProgress(bool persistCache); + // Auto-balance pool selection: drive the periodic hashrate refresh and apply a + // freshly-completed snapshot (weighted-random pick + optional miner restart). + void updatePoolAutoBalance(); + void applyPoolAutoBalance(const util::PoolStatsService::Snapshot& snap); + // Subsystems std::unique_ptr rpc_; std::unique_ptr worker_; @@ -488,6 +505,13 @@ private: bool lite_startup_lock_checked_ = false; std::unique_ptr daemon_controller_; std::unique_ptr xmrig_manager_; + // Auto-balance runtime state (pool mining, full-node only). The service fetches + // pool hashrates off-thread; the RNG drives the weighted-random pick. + util::PoolStatsService pool_stats_service_; + std::mt19937 balance_rng_; + long long last_balance_eval_ms_ = 0; // steady-clock ms of the last refresh kick + bool balance_refresh_pending_ = false; // UI asked for an immediate refresh + bool balance_snapshot_seen_ = false; // the current in-flight snapshot was applied util::AsyncTaskManager async_tasks_; bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog diff --git a/src/app_network.cpp b/src/app_network.cpp index 7b74cfd..ff9efe1 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -38,6 +38,7 @@ #include "daemon/daemon_controller.h" #include "daemon/embedded_daemon.h" #include "daemon/xmrig_manager.h" +#include "util/pool_registry.h" #include "ui/notifications.h" #include "default_banlist_embedded.h" #include "util/amount_format.h" @@ -1718,7 +1719,9 @@ void App::startPoolMining(int threads) daemon::XmrigManager::Config cfg; cfg.pool_url = settings_->getPoolUrl(); cfg.worker_name = settings_->getPoolWorker(); - cfg.algo = settings_->getPoolAlgo(); + // The algo follows the pool: official pools use their own algo (pool.dragonx.cc + // needs rx/dragonx, pool.dragonx.is rx/hush); custom hosts keep the setting. + cfg.algo = util::resolvePoolAlgo(cfg.pool_url, settings_->getPoolAlgo()); cfg.threads = threads; // Use the same thread selection as solo mining cfg.tls = settings_->getPoolTls(); cfg.hugepages = settings_->getPoolHugepages(); diff --git a/src/daemon/xmrig_manager.cpp b/src/daemon/xmrig_manager.cpp index 9daa411..f2e5c0e 100644 --- a/src/daemon/xmrig_manager.cpp +++ b/src/daemon/xmrig_manager.cpp @@ -8,6 +8,7 @@ #include "xmrig_manager.h" #include "../resources/embedded_resources.h" +#include #include #include #include @@ -22,6 +23,7 @@ #include #include "../util/logger.h" +#include "../util/pool_registry.h" #ifdef _WIN32 #include @@ -718,6 +720,11 @@ void XmrigManager::fetchStatsHttp() { std::lock_guard lk(stats_mutex_); + // Miner version (top-level in /2/summary) — lets the UI show the actually + // running miner's version even when no release tag was persisted (bundled miner). + if (resp.contains("version") && resp["version"].is_string()) + stats_.version = resp["version"].get(); + if (resp.contains("hashrate") && resp["hashrate"].contains("total")) { auto& total = resp["hashrate"]["total"]; if (total.is_array() && total.size() >= 3) { @@ -768,10 +775,14 @@ void XmrigManager::fetchStatsHttp() { void XmrigManager::fetchPoolApiStats() { if (state_ != State::Running || pool_host_.empty()) return; - // Query the pool's public stats API - std::string url = "https://" + pool_host_ + "/api/stats"; - std::string responseData; + // Resolve the stats endpoint + JSON schema for this pool. Known pools carry their + // own API shape (pool.dragonx.is = custom /api/stats; pool.dragonx.cc = Miningcore + // /api/pools); unknown/custom hosts fall back to the .is convention. + const util::KnownPool* known = util::findKnownPoolByUrl(pool_host_); + const std::string url = known ? known->statsUrl + : ("https://" + pool_host_ + "/api/stats"); + std::string responseData; CURL* curl = curl_easy_init(); if (!curl) return; @@ -782,31 +793,99 @@ void XmrigManager::fetchPoolApiStats() { curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 3000L); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + // pool.dragonx.cc sits behind Cloudflare and 403s odd User-Agents. + curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; ObsidianDragon)"); + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); CURLcode res = curl_easy_perform(curl); curl_easy_cleanup(curl); - if (res != CURLE_OK) return; - try { - json resp = json::parse(responseData); + bool ok = false; + const double poolHR = util::parsePoolHashrate( + known ? known->schema : util::PoolStatsSchema::DragonXIs, + responseData, known ? known->miningcorePoolId : std::string{}, ok); + if (!ok) return; - // Pool stats API format: { "pools": { "": { "hashrate": ... } } } - double poolHR = 0; - if (resp.contains("pools") && resp["pools"].is_object()) { - for (auto& [key, pool] : resp["pools"].items()) { - if (pool.contains("hashrate") && pool["hashrate"].is_number()) { - poolHR = pool["hashrate"].get(); - break; // Use the first pool entry + std::lock_guard lk(stats_mutex_); + stats_.pool_hashrate = poolHR; +} + +// ============================================================================ +// Installed-miner version detection (` --version`, cached) +// ============================================================================ + +namespace { +std::mutex g_installed_ver_mutex; +std::string g_installed_ver; +std::atomic g_ver_detect_started{false}; + +// Extract the first "D.D[.D...]" version token from `--version` output (skips the +// build date, which uses '-' separators). Returns e.g. "6.21.0", or "" if none. +std::string parseMinerVersion(const std::string& out) +{ + for (size_t i = 0; i < out.size(); ++i) { + if (std::isdigit(static_cast(out[i]))) { + size_t j = i; + int dots = 0; + while (j < out.size() && + (std::isdigit(static_cast(out[j])) || out[j] == '.')) { + if (out[j] == '.') ++dots; + ++j; + } + if (dots >= 1 && (j - i) >= 3) { + // Include a trailing build suffix like "-hac" / "-drg1" (e.g. "6.25.1-hac"), + // matching what the running miner's API reports. + size_t end = j; + if (end < out.size() && out[end] == '-') { + size_t k = end + 1; + while (k < out.size() && std::isalnum(static_cast(out[k]))) ++k; + if (k > end + 1) end = k; } + return out.substr(i, end - i); + } + i = j; + } + } + return {}; +} +} // namespace + +void XmrigManager::startVersionDetection() +{ + if (g_ver_detect_started.exchange(true)) return; // one-shot + std::thread([]() { + const std::string bin = findXmrigBinary(); + std::string ver; + if (!bin.empty()) { + const std::string cmd = "\"" + bin + "\" --version 2>&1"; +#ifdef _WIN32 + FILE* fp = _popen(cmd.c_str(), "r"); +#else + FILE* fp = popen(cmd.c_str(), "r"); +#endif + if (fp) { + std::string out; + char buf[256]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) out.append(buf, n); +#ifdef _WIN32 + _pclose(fp); +#else + pclose(fp); +#endif + ver = parseMinerVersion(out); } } + std::lock_guard lk(g_installed_ver_mutex); + g_installed_ver = ver; + }).detach(); +} - std::lock_guard lk(stats_mutex_); - stats_.pool_hashrate = poolHR; - } catch (...) { - // Malformed response — ignore - } +std::string XmrigManager::installedVersion() +{ + std::lock_guard lk(g_installed_ver_mutex); + return g_installed_ver; } } // namespace daemon diff --git a/src/daemon/xmrig_manager.h b/src/daemon/xmrig_manager.h index 5aeffcf..3935daf 100644 --- a/src/daemon/xmrig_manager.h +++ b/src/daemon/xmrig_manager.h @@ -43,6 +43,7 @@ public: double pool_diff = 0; std::string pool_url; std::string algo; + std::string version; // miner version reported by the running xmrig API bool connected = false; // Memory usage int64_t memory_free = 0; // bytes @@ -137,6 +138,18 @@ public: */ static std::string findXmrigBinary(); + /** + * @brief Kick a one-shot background ` --version` detection (idempotent). + * Lets the UI show the installed miner's version before mining is ever started. + */ + static void startVersionDetection(); + + /** + * @brief Cached installed-miner version parsed by startVersionDetection(). + * Empty until detection completes (or if no binary/parse failed). Thread-safe. + */ + static std::string installedVersion(); + private: bool generateConfig(const Config& cfg, const std::string& outPath); bool startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads); diff --git a/src/data/wallet_state.h b/src/data/wallet_state.h index b6192f9..221dc64 100644 --- a/src/data/wallet_state.h +++ b/src/data/wallet_state.h @@ -170,6 +170,7 @@ struct PoolMiningState { bool xmrig_running = false; std::string pool_url; std::string algo; + std::string version; // running miner's version (from its API) double hashrate_10s = 0; double hashrate_60s = 0;