Files
ObsidianDragon/src/ui/windows/mining_tab.cpp
T
DanSandClaude Opus 4.8 8fe20f6385 feat(mining): persistent start/stop error caption + timeout-aware pool status
- A mining start/stop/pool failure now sets a persistent App::mining_last_error_
  (cleared on the next attempt), rendered as a red caption with "Press Mine to
  try again" below the Mine button (plus a Dismiss, and a Windows-Defender
  "How to fix" on an AV block), so a crashed/blocked/failed miner is no longer
  just a button that "did nothing" once its toast faded. Two more hardcoded
  English strings (pool-payout-invalid, AV-blocked) moved to i18n.
- Pool connection status is now a persistent "Status" row in the pool stats
  card (PoolMiningState.running_since / last_error): it flips from "Connecting…"
  to "Can't reach pool — check the URL" after a 30s timeout instead of sitting
  on red forever, and no longer vanishes once logs arrive. The old transient
  banner in mining_tab.cpp was removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 12:09:49 -05:00

345 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "mining_tab.h"
#include "mining_benchmark.h"
#include "mining_tab_helpers.h"
#include "mining_pool_panel.h"
#include "mining_earnings.h"
#include "mining_stats.h"
#include "mining_controls.h"
#include "mining_mode_toggle.h"
#include "xmrig_download_dialog.h"
#include "../../util/xmrig_updater.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../config/version.h"
#include "../../data/wallet_state.h"
#include "../../config/settings.h"
#include "../../util/platform.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../effects/low_spec.h"
#include "../layout.h"
#include "../notifications.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include "imgui_internal.h"
#include <chrono>
#include <thread>
#include <algorithm>
#include <cmath>
#include <ctime>
#include <vector>
namespace dragonx {
namespace ui {
using namespace material;
// Local UI state for thread grid
static int s_selected_threads = 0;
static bool s_threads_initialized = false;
// Drag-to-select state
static bool s_drag_active = false;
static int s_drag_anchor_thread = 0; // thread# where drag started
static ThreadBenchmark s_benchmark;
bool IsMiningBenchmarkActive() {
return s_benchmark.active();
}
void CancelMiningBenchmark(App* app, bool restoreMiner) {
if (!s_benchmark.active()) return;
const ThreadBenchmarkCancel plan = PlanThreadBenchmarkCancel(s_benchmark); // read BEFORE reset() zeroes both fields
s_benchmark.reset();
if (!app) return;
// Always drop the candidate-thread miner. Restore only if the user was mining before the benchmark, using the
// was_pool_running SNAPSHOT (not the live state: the sweep may be mid inter-candidate stop). (L-04, L-11)
app->stopPoolMining();
if (restoreMiner && plan.restartMiner)
app->startPoolMining(plan.restartThreads);
}
void TickMiningBenchmark(App* app, double hashrate10s) {
static double s_lastTickSec = -1.0;
if (!s_benchmark.active()) { s_lastTickSec = -1.0; return; } // next run starts its clock from 0
const double nowSec = std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count();
// Capped like the logic timers (App::kMaxLogicDeltaSeconds): a suspend/resume must not leap a phase.
const float dt = AdvanceBenchmarkClock(s_lastTickSec, nowSec, App::kMaxLogicDeltaSeconds);
auto benchmarkUpdate = AdvanceThreadBenchmark(s_benchmark, dt, hashrate10s);
if (benchmarkUpdate.stopPoolMining) app->stopPoolMining();
if (benchmarkUpdate.saveOptimalThreads) {
s_selected_threads = benchmarkUpdate.optimalThreads;
app->settings()->setPoolThreads(s_selected_threads);
app->settings()->save();
}
if (benchmarkUpdate.startPoolMining) app->startPoolMining(benchmarkUpdate.startThreads);
if (benchmarkUpdate.inconclusive) Notifications::instance().warning(TR("grpc_benchmark_inconclusive"));
}
// Miner-update version check (one shot per session): fetches the latest DRG-XMRig release tag in
// the background so the "Update" button can show it. Network call to the project Gitea, started
// the first time the pool section is shown.
static util::XmrigUpdater s_xmrig_version_check;
static bool s_xmrig_check_started = false;
static std::string s_xmrig_latest_tag;
// Pool mode state
static bool s_pool_mode = false;
static char s_pool_url[256] = "pool.dragonx.is:3433";
static char s_pool_worker[256] = "x";
static bool s_pool_settings_dirty = false;
static bool s_pool_state_loaded = false;
static void RenderMiningTabContent(App* app);
void RenderMiningTab(App* app)
{
// Scrollable child to contain all content within available space
ImVec2 miningAvail = ImGui::GetContentRegionAvail();
ImGui::BeginChild("##MiningScroll", miningAvail, false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
ImGuiErrorRecoveryState erState;
ImGui::ErrorRecoveryStoreState(&erState);
try {
RenderMiningTabContent(app);
} catch (const std::exception& e) {
DEBUG_LOGF("[MiningTab] Exception: %s\n", e.what());
ImGui::ErrorRecoveryTryToRecoverState(&erState);
} catch (...) {
DEBUG_LOGF("[MiningTab] Unknown exception\n");
ImGui::ErrorRecoveryTryToRecoverState(&erState);
}
ImGui::EndChild(); // ##MiningScroll
}
static void RenderMiningTabContent(App* app)
{
auto& S = schema::UI();
ImVec2 miningAvail = ImGui::GetContentRegionAvail();
auto sliderInput = S.input("tabs.mining", "thread-slider");
auto startBtn = S.button("tabs.mining", "start-button");
auto lbl = S.label("tabs.mining", "label-column");
const auto& state = app->getWalletState();
const auto& mining = state.mining;
// Responsive: scale factors per frame
float availWidth = ImGui::GetContentRegionAvail().x;
float hs = Layout::hScale(availWidth);
float vs = Layout::vScale(miningAvail.y);
float pad = Layout::cardInnerPadding();
float gap = Layout::cardGap();
const float dp = Layout::dpiScale();
auto tier = Layout::currentTier(availWidth, miningAvail.y);
(void)tier;
int max_threads = GetMaxMiningThreads();
if (!s_threads_initialized) {
int saved = app->settings()->getPoolThreads();
if (mining.generate)
s_selected_threads = ClampMiningThreads(mining.genproclimit, max_threads);
else if (saved > 0)
s_selected_threads = ClampMiningThreads(saved, max_threads);
else
s_selected_threads = 1;
s_threads_initialized = true;
}
// Sync thread grid with actual count when idle thread scaling adjusts threads
// Skip during benchmark — the benchmark controls thread counts directly
// ...nor while a thread-count pick is still on its way to the miner (the grid would snap back to the old count).
if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active &&
!app->miningThreadCommitPending() && !IsMiningBenchmarkActive()) {
if (s_pool_mode && state.pool_mining.xmrig_running) {
// Use the requested thread count (available immediately) rather than threads_active from the
// xmrig API, which lags during restarts; the App's target also covers a restart still queued.
const int queued = app->poolThreadsTarget();
int reqThreads = queued > 0 ? queued : app->getXmrigRequestedThreads();
if (reqThreads > 0)
s_selected_threads = ClampMiningThreads(reqThreads, max_threads);
else if (state.pool_mining.threads_active > 0)
s_selected_threads = ClampMiningThreads(state.pool_mining.threads_active, max_threads);
} else if (mining.generate && mining.genproclimit > 0 && !app->isMiningToggleInProgress()) {
s_selected_threads = ClampMiningThreads(mining.genproclimit, max_threads);
}
}
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
glassSpec.rounding = Layout::glassRounding();
ImFont* ovFont = Type().overline();
ImFont* capFont = Type().caption();
ImFont* sub1 = Type().subtitle1();
if (!ovFont || !capFont || !sub1) {
return;
}
char buf[128];
// Load pool state from settings on first frame
if (!s_pool_state_loaded) {
s_pool_mode = app->settings()->getPoolMode();
strncpy(s_pool_url, app->settings()->getPoolUrl().c_str(), sizeof(s_pool_url) - 1);
strncpy(s_pool_worker, app->settings()->getPoolWorker().c_str(), sizeof(s_pool_worker) - 1);
s_pool_state_loaded = true;
}
// Kick off a one-shot miner-version check the first time the pool section is shown, then cache
// the latest tag once it arrives (so the "Update" button can display it). Network call.
if (s_pool_mode && !s_xmrig_check_started) {
s_xmrig_version_check.startCheck(app->settings()->getXmrigVersion());
s_xmrig_check_started = true;
}
if (s_xmrig_check_started && s_xmrig_latest_tag.empty()) {
const auto _vp = s_xmrig_version_check.getProgress();
if (!_vp.latest_tag.empty()) s_xmrig_latest_tag = _vp.latest_tag;
}
if (!app->supportsSoloMining() && !s_pool_mode) {
s_pool_mode = true;
app->settings()->setPoolMode(true);
app->settings()->save();
}
// Default pool worker to user's first shielded (z) address once available.
// For new wallets without a z-address, leave the field blank so the user
// is prompted to generate one before mining.
{
static bool s_pool_worker_defaulted = false;
std::string workerStr(s_pool_worker);
if (shouldDefaultPoolWorker(workerStr, s_pool_worker_defaulted) && !state.addresses.empty()) {
std::string defaultAddr = defaultPoolWorkerAddress(state.addresses);
if (!defaultAddr.empty()) {
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
s_pool_settings_dirty = true;
} else {
// No z-address yet — clear the placeholder "x" so field shows empty
s_pool_worker[0] = '\0';
s_pool_settings_dirty = true;
}
s_pool_worker_defaulted = true;
}
}
// Persist pool settings when dirty and no field is active
if (s_pool_settings_dirty && !ImGui::IsAnyItemActive()) {
// Trim whitespace/newlines (a pasted URL or payout address often carries a trailing newline)
// before persisting and feeding it to xmrig; write the trimmed value back so the field agrees.
auto trimmed = [](const char* b) {
std::string s(b);
while (!s.empty() && (s.front()==' '||s.front()=='\t'||s.front()=='\n'||s.front()=='\r')) s.erase(s.begin());
while (!s.empty() && (s.back()==' '||s.back()=='\t'||s.back()=='\n'||s.back()=='\r')) s.pop_back();
return s;
};
std::string poolUrl = trimmed(s_pool_url), poolWorker = trimmed(s_pool_worker);
snprintf(s_pool_url, sizeof(s_pool_url), "%s", poolUrl.c_str());
snprintf(s_pool_worker, sizeof(s_pool_worker), "%s", poolWorker.c_str());
app->settings()->setPoolUrl(poolUrl);
app->settings()->setPoolWorker(poolWorker);
app->settings()->save();
s_pool_settings_dirty = false;
// Auto-restart pool miner if it is currently running so the new
// URL / worker address takes effect immediately.
if (state.pool_mining.xmrig_running) {
app->stopPoolMining();
app->startPoolMining(s_selected_threads);
}
}
// Determine active mining state for UI
// Include pool mining running state even when user just switched to solo,
// so the button shows STOP/STOPPING while xmrig shuts down.
bool isMiningActive = IsPoolMiningActive(s_pool_mode,
state.pool_mining.xmrig_running,
mining.generate);
// ================================================================
// Thread Benchmark state machine — runs pool mining at each candidate
// thread count to find the optimal setting for this CPU.
// ================================================================
// The benchmark state machine is advanced from App::update() via TickMiningBenchmark() so it keeps running
// while this tab is hidden or the window is minimized (it used to freeze there).
// ================================================================
// Proportional section budget — ensures all content fits without
// scrolling at the minimum window size (1024×775).
// ================================================================
float sHdr = ovFont->LegacySize + Layout::spacingXs()
+ ImGui::GetStyle().ItemSpacing.y * 2.0f;
float gapOver = gap + ImGui::GetStyle().ItemSpacing.y;
// 3 sections with headers (CHART+STATS, DETAILS+EARNINGS, BLOCKS)
float totalOverhead = 3.0f * (sHdr + gapOver) + 1.0f * gapOver;
float cardBudget = std::max(schema::UI().drawElement("tabs.mining", "card-budget-min").size, miningAvail.y - totalOverhead);
Layout::SectionBudget cb(cardBudget);
float controlsBudgetH = cb.allocate(0.26f, 80.0f * dp);
float chartBudgetH = cb.allocate(0.22f, 60.0f * dp);
(void)cb; // remaining budget used by combined earnings+details card
// ================================================================
// MODE TOGGLE — SOLO | POOL segmented control
// ================================================================
RenderMiningModeToggle(app, state, mining, dl, capFont, ovFont, dp, hs, gap, availWidth,
s_pool_mode, s_pool_url, s_pool_worker, s_pool_settings_dirty);
// Payout safety (multi-wallet): in pool mode, warn if the worker looks like a DragonX address
// that isn't in the CURRENT wallet — e.g. left over from a wallet you switched away from, so
// rewards would land in the wrong wallet. Non-destructive: we only warn, never overwrite (it
// could be a deliberate external payout).
if (s_pool_mode) {
std::string w(s_pool_worker);
while (!w.empty() && (w.front()==' '||w.front()=='\t'||w.front()=='\n'||w.front()=='\r')) w.erase(w.begin());
while (!w.empty() && (w.back()==' '||w.back()=='\t'||w.back()=='\n'||w.back()=='\r')) w.pop_back();
const bool looksLikeAddr = w.size() > 10 && (w.rfind("zs", 0) == 0 || w[0] == 'R');
bool inWallet = false;
for (const auto& a : state.addresses) if (a.address == w) { inWallet = true; break; }
if (looksLikeAddr && !inWallet && !state.addresses.empty()) {
ImGui::Dummy(ImVec2(0, gap));
Type().textColored(TypeStyle::Caption, Warning(), TR("mining_payout_foreign"));
}
}
// ================================================================
// CONTROLS — Glass card with CPU core grid (no heading)
// ================================================================
RenderMiningControls(app, state, mining, dl, capFont, sub1, ovFont, dp, hs, vs, gap, pad,
availWidth, glassSpec, controlsBudgetH, max_threads, isMiningActive,
s_pool_mode, s_pool_worker, s_xmrig_latest_tag, s_benchmark,
s_selected_threads, s_drag_active, s_drag_anchor_thread);
// (The miner download/update control lives in the mining-control header row, next to the
// benchmark button — see the "Miner update" block above.)
// ================================================================
// HASHRATE + STATS — Combined glass card: stat values on top, chart below
// (Or full-card log view when toggled in pool mode)
// ================================================================
RenderMiningStats(state, mining, dl, capFont, sub1, ovFont, dp, vs, gap, pad,
availWidth, glassSpec, chartBudgetH, s_pool_mode,
app, s_pool_url, s_pool_settings_dirty);
// ================================================================
// EARNINGS — Horizontal row card (Today | Yesterday | All Time | Est. Daily)
// ================================================================
RenderMiningEarnings(app, state, mining, dl, capFont, sub1, ovFont, dp, vs, gap, pad,
isMiningActive, s_pool_mode, availWidth, glassSpec, sHdr, gapOver);
// ================================================================
// (Pool connection status now lives as a persistent, timeout-aware row in RenderLeftPoolCard
// (mining_stats.cpp) — the old transient "Connecting" banner that vanished once logs arrived and
// sat on red forever on failure was removed.)
}
} // namespace ui
} // namespace dragonx