Files
ObsidianDragon/src/ui/windows/mining_tab.cpp
T
DanSandClaude Opus 4.8 d27f387d6d feat(ui): in-app FAQ, DPI-scaling audit fixes, mining/stratum polish + localize strings
Bundles the session's UI work (the touched files carry several of these
changes together, so they are committed as one coherent UI batch):

- FAQ: new RenderFaqDialog + data-driven faq_content, opened from a
  status-bar "?" (and the Windows title bar), styled like the Wallets
  modal with search, Wallet/Daemon tabs, and smooth scroll.
- DPI/font-scale audit: multiply hand-drawn absolute geometry by
  Layout::dpiScale() across ~30 files so nothing renders native-size at
  HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x).
- Mining: chart now fills the horizontal space; thread stepper +/- buttons
  match the input-box height; move the stratum-host toggle into
  Node & Security (v1.3.0+).
- Settings: fix the auto-shield status text overlapping the grid.
- Sidebar: drop the peer-count badge on the Network button.
- i18n: wrap 193 hardcoded literals with TR() (keys/translations added in
  the preceding i18n commit), so the security/PIN/lock flow, seed-backup
  wizard, and witness-rebuild dialog localize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 19:37:16 -05:00

357 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 <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) {
if (!s_benchmark.active()) return;
const int restoreThreads = s_benchmark.prev_threads;
s_benchmark.reset();
// Restore the miner to its pre-benchmark thread count. A benchmark runs in pool mode, so restore
// regardless of the instantaneous running state — the sweep may be mid inter-candidate stop, where an
// isPoolMinerRunning() check would be transiently false and silently drop the restart. (L-04, L-11)
if (app && restoreThreads > 0) {
app->stopPoolMining();
app->startPoolMining(restoreThreads);
}
}
// 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
if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active && !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.
int reqThreads = 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) {
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.
// ================================================================
if (s_benchmark.active()) {
auto benchmarkUpdate = AdvanceThreadBenchmark(
s_benchmark, ImGui::GetIO().DeltaTime, state.pool_mining.hashrate_10s);
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"));
}
}
// ================================================================
// 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 — inline indicator (pool mode, no log)
// ================================================================
if (s_pool_mode && state.pool_mining.log_lines.empty() && state.pool_mining.xmrig_running) {
ImFont* iconFont = Type().iconSmall();
const char* dotIcon = ICON_MD_CIRCLE;
ImU32 dotCol = state.pool_mining.connected ? WithAlpha(Success(), 200) : WithAlpha(Error(), 200);
const char* statusText = state.pool_mining.connected
? (state.pool_mining.pool_url.empty() ? TR("mining_connected") : state.pool_mining.pool_url.c_str())
: TR("mining_connecting");
ImVec2 pos = ImGui::GetCursorScreenPos();
ImVec2 dotSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, dotIcon);
dl->AddText(iconFont, iconFont->LegacySize, pos, dotCol, dotIcon);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(pos.x + dotSz.x + 4 * dp, pos.y + (dotSz.y - capFont->LegacySize) * 0.5f),
OnSurfaceMedium(), statusText);
ImGui::Dummy(ImVec2(availWidth, capFont->LegacySize + 4 * dp));
}
}
} // namespace ui
} // namespace dragonx