fix: Windows identity, async address creation, mining UI, and chart artifacts

Windows identity:
- Add VERSIONINFO resource (.rc) with ObsidianDragon file description
- Embed application manifest for DPI awareness and shell identity
- Patch libwinpthread/libpthread to remove competing VERSIONINFO
- Set AppUserModelID and HWND property store to override Task Manager cache
- Link patched pthread libs to eliminate "POSIX WinThreads" description

Address creation (+New button):
- Move z_getnewaddress/getnewaddress off UI thread to async worker
- Inject new address into state immediately for instant UI selection
- Trigger background refresh for balance updates

Mining tab:
- Add pool mining dropdown with saved URLs/workers and bookmarks
- Add solo mining log panel from daemon output with chart/log toggle
- Fix toggle button cursor (render after InputTextMultiline)
- Auto-restart miner on pool config change
- Migrate default pool URL to include stratum port

Transactions:
- Sort pending (0-conf) transactions to top of history
- Fall back to timereceived when timestamp is missing

Shutdown:
- Replace blocking sleep_for calls with 100ms polling loops
- Check shutting_down_ flag throughout daemon restart/bootstrap flows
- Reduce daemon stop timeout from 30s to 10s

Other:
- Fix market chart fill artifact (single concave polygon vs per-segment quads)
- Add bootstrap checksum verification state display
- Rename daemon client identifier to ObsidianDragon
This commit is contained in:
dan_s
2026-03-05 22:43:27 -06:00
parent 4b16a2a2c4
commit 653a90de62
20 changed files with 842 additions and 116 deletions

View File

@@ -373,6 +373,7 @@ endif()
# Windows application icon + VERSIONINFO (.rc -> .res -> linked into .exe)
if(WIN32)
set(OBSIDIAN_ICO_PATH "${CMAKE_SOURCE_DIR}/res/img/ObsidianDragon.ico")
set(OBSIDIAN_MANIFEST_PATH "${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest")
# Version numbers for the VERSIONINFO resource block
set(DRAGONX_VER_MAJOR 1)
set(DRAGONX_VER_MINOR 0)

View File

@@ -412,6 +412,22 @@ build_release_win() {
exit 1
fi
# ── Patch libwinpthread + libpthread to remove VERSIONINFO resources ────
# mingw-w64's libwinpthread.a and libpthread.a each ship a version.o
# with their own VERSIONINFO ("POSIX WinThreads for Windows") that
# collides with ours during .rsrc merge, causing Task Manager to show
# the wrong process description.
local PATCHED_LIB_DIR="$bd/patched-lib"
mkdir -p "$PATCHED_LIB_DIR"
for plib in libwinpthread.a libpthread.a; do
local SYS_LIB="/usr/x86_64-w64-mingw32/lib/$plib"
if [[ -f "$SYS_LIB" ]]; then
cp -f "$SYS_LIB" "$PATCHED_LIB_DIR/$plib"
x86_64-w64-mingw32-ar d "$PATCHED_LIB_DIR/$plib" version.o 2>/dev/null || true
info "Patched $plib (removed version.o VERSIONINFO resource)"
fi
done
# ── Toolchain file ───────────────────────────────────────────────────────
cat > "$bd/mingw-toolchain.cmake" <<TOOLCHAIN
set(CMAKE_SYSTEM_NAME Windows)
@@ -423,7 +439,7 @@ set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_EXE_LINKER_FLAGS "-static -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive")
set(CMAKE_EXE_LINKER_FLAGS "-static -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -L$PATCHED_LIB_DIR -lwinpthread -Wl,--no-whole-archive")
set(CMAKE_CXX_FLAGS "\${CMAKE_CXX_FLAGS} -static")
set(CMAKE_C_FLAGS "\${CMAKE_C_FLAGS} -static")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<!-- Application identity —————————————————————————————— -->
<assemblyIdentity
type="win32"
name="DragonX.ObsidianDragon.Wallet"
version="1.0.0.0"
processorArchitecture="amd64"
/>
<description>ObsidianDragon Wallet</description>
<!-- Common Controls v6 (themed buttons, etc.) ————————— -->
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<!-- DPI awareness (Per-Monitor V2) ————————————————————— -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
</windowsSettings>
</application>
<!-- Supported OS declarations (Windows 7 → 11) ———————— -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 10 / 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View File

@@ -4,6 +4,13 @@
// Use numeric ordinal 1 so LoadIcon(hInst, MAKEINTRESOURCE(1)) finds it.
1 ICON "@OBSIDIAN_ICO_PATH@"
// ---------------------------------------------------------------------------
// Application Manifest — declares DPI awareness, common controls v6,
// UTF-8 code page, and application identity. Without this, Windows may
// fall back to legacy process grouping in Task Manager.
// ---------------------------------------------------------------------------
1 24 "@OBSIDIAN_MANIFEST_PATH@"
// ---------------------------------------------------------------------------
// VERSIONINFO — sets the description shown in Task Manager, Explorer
// "Details" tab, and other Windows tools. Without this, MinGW-w64

View File

@@ -766,7 +766,7 @@ control-card-min-height = { size = 60.0 }
active-cell-border-thickness = { size = 1.5 }
cell-border-thickness = { size = 1.0 }
button-icon-y-ratio = { size = 0.42 }
button-label-y-ratio = { size = 0.78 }
button-label-y-ratio = { size = 0.72 }
chart-line-thickness = { size = 1.5 }
details-card-min-height = { size = 50.0 }
ram-bar = { height = 6.0, rounding = 3.0, opacity = 0.65 }
@@ -808,6 +808,7 @@ dir-pill-padding = { size = 4.0 }
dir-pill-y-offset = { size = 1.0 }
dir-pill-y-bottom = { size = 3.0 }
dir-pill-rounding = { size = 3.0 }
seed-badge-padding = { size = 3.0 }
tls-badge-min-width = { size = 20.0 }
tls-badge-width = { size = 28.0 }
tls-badge-rounding = { size = 3.0 }

View File

@@ -382,6 +382,11 @@ void App::update()
state_.pool_mining.xmrig_running = false;
}
// Populate solo mining log lines from daemon output
if (embedded_daemon_ && embedded_daemon_->isRunning()) {
state_.mining.log_lines = embedded_daemon_->getRecentLines(50);
}
// Check daemon output for rescan progress
if (embedded_daemon_ && embedded_daemon_->isRunning()) {
std::string newOutput = embedded_daemon_->getOutputSince(daemon_output_offset_);
@@ -1935,12 +1940,13 @@ void App::stopEmbeddedDaemon()
if (stop_sent) {
DEBUG_LOGF("Waiting for daemon to begin shutdown...\n");
shutdown_status_ = "Waiting for daemon to begin shutdown...";
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
// Wait for process to exit; SIGTERM/TerminateProcess as last resort
// Wait for process to exit; SIGTERM/TerminateProcess as last resort.
// 10 seconds is generous — if the daemon hasn't exited by then it's stuck.
shutdown_status_ = "Waiting for dragonxd process to exit...";
embedded_daemon_->stop(30000);
embedded_daemon_->stop(10000);
}
bool App::isEmbeddedDaemonRunning() const
@@ -1972,10 +1978,13 @@ void App::rescanBlockchain()
std::thread([this]() {
DEBUG_LOGF("[App] Stopping daemon for rescan...\n");
stopEmbeddedDaemon();
if (shutting_down_) return;
// Wait for daemon to fully stop
DEBUG_LOGF("[App] Waiting for daemon to fully stop...\n");
std::this_thread::sleep_for(std::chrono::seconds(3));
for (int i = 0; i < 30 && !shutting_down_; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (shutting_down_) return;
// Reset output offset so we parse fresh output for rescan progress
daemon_output_offset_ = 0;
@@ -2663,8 +2672,10 @@ void App::restartDaemon()
if (embedded_daemon_ && isEmbeddedDaemonRunning()) {
stopEmbeddedDaemon();
}
if (shutting_down_) { daemon_restarting_ = false; return; }
// Brief pause to let the port free up
std::this_thread::sleep_for(std::chrono::milliseconds(500));
for (int i = 0; i < 5 && !shutting_down_; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
startEmbeddedDaemon();
daemon_restarting_ = false;
DEBUG_LOGF("[App] Daemon restart complete — waiting for RPC...\n");

View File

@@ -493,6 +493,7 @@ void App::refreshData()
if (tx.contains("category")) info.type = tx["category"].get<std::string>();
if (tx.contains("amount")) info.amount = tx["amount"].get<double>();
if (tx.contains("time")) info.timestamp = tx["time"].get<int64_t>();
else if (tx.contains("timereceived")) info.timestamp = tx["timereceived"].get<int64_t>();
if (tx.contains("confirmations")) info.confirmations = tx["confirmations"].get<int>();
if (tx.contains("address")) info.address = tx["address"].get<std::string>();
knownTxids.insert(info.txid);
@@ -1408,25 +1409,61 @@ void App::clearBans()
void App::createNewZAddress(std::function<void(const std::string&)> callback)
{
if (!state_.connected || !rpc_) return;
rpc_->z_getNewAddress([this, callback](const json& result) {
std::string addr = result.get<std::string>();
addresses_dirty_ = true;
refreshAddresses();
if (callback) callback(addr);
if (!state_.connected || !rpc_ || !worker_) return;
worker_->post([this, callback]() -> rpc::RPCWorker::MainCb {
std::string addr;
try {
json result = rpc_->call("z_getnewaddress");
addr = result.get<std::string>();
} catch (const std::exception& e) {
DEBUG_LOGF("z_getnewaddress error: %s\n", e.what());
}
return [this, callback, addr]() {
if (!addr.empty()) {
// Inject immediately so UI can select the address next frame
AddressInfo info;
info.address = addr;
info.type = "shielded";
info.balance = 0.0;
state_.z_addresses.push_back(info);
address_list_dirty_ = true;
// Also trigger full refresh to get proper balances
addresses_dirty_ = true;
refreshAddresses();
}
if (callback) callback(addr);
};
});
}
void App::createNewTAddress(std::function<void(const std::string&)> callback)
{
if (!state_.connected || !rpc_) return;
rpc_->getNewAddress([this, callback](const json& result) {
std::string addr = result.get<std::string>();
addresses_dirty_ = true;
refreshAddresses();
if (callback) callback(addr);
if (!state_.connected || !rpc_ || !worker_) return;
worker_->post([this, callback]() -> rpc::RPCWorker::MainCb {
std::string addr;
try {
json result = rpc_->call("getnewaddress");
addr = result.get<std::string>();
} catch (const std::exception& e) {
DEBUG_LOGF("getnewaddress error: %s\n", e.what());
}
return [this, callback, addr]() {
if (!addr.empty()) {
// Inject immediately so UI can select the address next frame
AddressInfo info;
info.address = addr;
info.type = "transparent";
info.balance = 0.0;
state_.t_addresses.push_back(info);
address_list_dirty_ = true;
// Also trigger full refresh to get proper balances
addresses_dirty_ = true;
refreshAddresses();
}
if (callback) callback(addr);
};
});
}

View File

@@ -75,8 +75,11 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
// Give daemon a moment to shut down, then restart
// (do this off the main thread to avoid stalling the UI)
std::thread([this]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
for (int i = 0; i < 20 && !shutting_down_; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (shutting_down_) return;
stopEmbeddedDaemon();
if (shutting_down_) return;
startEmbeddedDaemon();
// tryConnect will be called by the update loop
}).detach();
@@ -187,8 +190,11 @@ void App::processDeferredEncryption() {
// Restart daemon (it shuts itself down after encryptwallet)
if (isUsingEmbeddedDaemon()) {
std::thread([this]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
for (int i = 0; i < 20 && !shutting_down_; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (shutting_down_) return;
stopEmbeddedDaemon();
if (shutting_down_) return;
startEmbeddedDaemon();
// tryConnect will be called by the update loop
}).detach();
@@ -1095,7 +1101,8 @@ void App::renderDecryptWalletDialog() {
}
// Wait for daemon to fully stop
std::this_thread::sleep_for(std::chrono::seconds(3));
for (int i = 0; i < 30 && !shutting_down_; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return [this, exportPath]() {
decrypt_step_ = 3;
@@ -1127,18 +1134,23 @@ void App::renderDecryptWalletDialog() {
decrypt_status_ = "Restarting daemon...";
auto restartAndImport = [this, exportPath]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
for (int i = 0; i < 20 && !shutting_down_; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (shutting_down_) return;
if (isUsingEmbeddedDaemon()) {
stopEmbeddedDaemon();
std::this_thread::sleep_for(std::chrono::seconds(1));
if (shutting_down_) return;
for (int i = 0; i < 10 && !shutting_down_; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (shutting_down_) return;
startEmbeddedDaemon();
}
// Wait for daemon to become available
int maxWait = 60;
bool daemonUp = false;
for (int i = 0; i < maxWait; i++) {
for (int i = 0; i < maxWait && !shutting_down_; i++) {
std::this_thread::sleep_for(std::chrono::seconds(1));
try {
rpc_->call("getinfo");

View File

@@ -673,8 +673,13 @@ void App::renderFirstRunWizard() {
} else {
auto prog = bootstrap_->getProgress();
const char* statusTitle = (prog.state == util::Bootstrap::State::Downloading)
? "Downloading bootstrap..." : "Extracting blockchain data...";
const char* statusTitle;
if (prog.state == util::Bootstrap::State::Downloading)
statusTitle = "Downloading bootstrap...";
else if (prog.state == util::Bootstrap::State::Verifying)
statusTitle = "Verifying checksums...";
else
statusTitle = "Extracting blockchain data...";
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, statusTitle);
cy += bodyFont->LegacySize + 12.0f * dp;

View File

@@ -140,12 +140,24 @@ bool Settings::load(const std::string& path)
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>();
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
// Migrate old default pool URL that was missing the stratum port
if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
if (j.contains("pool_algo")) pool_algo_ = j["pool_algo"].get<std::string>();
if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get<std::string>();
if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get<int>();
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
saved_pool_urls_.clear();
for (const auto& u : j["saved_pool_urls"])
if (u.is_string()) saved_pool_urls_.push_back(u.get<std::string>());
}
if (j.contains("saved_pool_workers") && j["saved_pool_workers"].is_array()) {
saved_pool_workers_.clear();
for (const auto& w : j["saved_pool_workers"])
if (w.is_string()) saved_pool_workers_.push_back(w.get<std::string>());
}
if (j.contains("font_scale") && j["font_scale"].is_number())
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>()));
if (j.contains("window_width") && j["window_width"].is_number_integer())
@@ -219,6 +231,12 @@ bool Settings::save(const std::string& path)
j["pool_tls"] = pool_tls_;
j["pool_hugepages"] = pool_hugepages_;
j["pool_mode"] = pool_mode_;
j["saved_pool_urls"] = json::array();
for (const auto& u : saved_pool_urls_)
j["saved_pool_urls"].push_back(u);
j["saved_pool_workers"] = json::array();
for (const auto& w : saved_pool_workers_)
j["saved_pool_workers"].push_back(w);
j["font_scale"] = font_scale_;
if (window_width_ > 0 && window_height_ > 0) {
j["window_width"] = window_width_;

View File

@@ -4,8 +4,10 @@
#pragma once
#include <algorithm>
#include <string>
#include <set>
#include <vector>
namespace dragonx {
namespace config {
@@ -206,6 +208,31 @@ public:
bool getPoolMode() const { return pool_mode_; }
void setPoolMode(bool v) { pool_mode_ = v; }
// Saved pool URLs (user-managed favorites dropdown)
const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; }
void addSavedPoolUrl(const std::string& url) {
// Don't add duplicates
for (const auto& u : saved_pool_urls_) if (u == url) return;
saved_pool_urls_.push_back(url);
}
void removeSavedPoolUrl(const std::string& url) {
saved_pool_urls_.erase(
std::remove(saved_pool_urls_.begin(), saved_pool_urls_.end(), url),
saved_pool_urls_.end());
}
// Saved pool worker addresses (user-managed favorites dropdown)
const std::vector<std::string>& getSavedPoolWorkers() const { return saved_pool_workers_; }
void addSavedPoolWorker(const std::string& addr) {
for (const auto& a : saved_pool_workers_) if (a == addr) return;
saved_pool_workers_.push_back(addr);
}
void removeSavedPoolWorker(const std::string& addr) {
saved_pool_workers_.erase(
std::remove(saved_pool_workers_.begin(), saved_pool_workers_.end(), addr),
saved_pool_workers_.end());
}
// Font scale (user accessibility setting, 1.01.5)
float getFontScale() const { return font_scale_; }
void setFontScale(float v) { font_scale_ = std::max(1.0f, std::min(1.5f, v)); }
@@ -255,13 +282,15 @@ private:
std::string selected_pair_ = "DRGX/BTC";
// Pool mining
std::string pool_url_ = "pool.dragonx.is";
std::string pool_url_ = "pool.dragonx.is:3433";
std::string pool_algo_ = "rx/hush";
std::string pool_worker_ = "x";
int pool_threads_ = 0;
bool pool_tls_ = false;
bool pool_hugepages_ = true;
bool pool_mode_ = false; // false=solo, true=pool
std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
// Font scale (user accessibility, 1.03.0; 1.0 = default)
float font_scale_ = 1.0f;

View File

@@ -154,7 +154,7 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
#ifndef _WIN32
"-printtoconsole",
#endif
"-clientname=DragonXImGui",
"-clientname=ObsidianDragon",
"-ac_name=DRAGONX",
"-ac_algo=randomx",
"-ac_halving=3500000",

View File

@@ -101,6 +101,9 @@ struct MiningInfo {
// History for chart
std::vector<double> hashrate_history; // Last N samples
static constexpr int MAX_HISTORY = 300; // 5 minutes at 1s intervals
// Recent daemon log lines for the mining log panel
std::vector<std::string> log_lines;
};
/**

View File

@@ -46,6 +46,15 @@
#include "platform/windows_backdrop.h"
#include <windows.h>
#include <dwmapi.h>
#include <shlobj.h>
#include <propkey.h>
#include <propsys.h>
// SetCurrentProcessExplicitAppUserModelID lives behind NTDDI_WIN7 in
// MinGW's shobjidl.h. Rather than forcing the version macro globally,
// declare just the one function we need (available on Windows 7+).
extern "C" HRESULT __stdcall SetCurrentProcessExplicitAppUserModelID(PCWSTR AppID);
// SHGetPropertyStoreForWindow is also behind NTDDI_WIN7 in MinGW headers.
extern "C" HRESULT __stdcall SHGetPropertyStoreForWindow(HWND hwnd, REFIID riid, void** ppv);
// Not defined in older MinGW SDK headers
#ifndef WS_EX_NOREDIRECTIONBITMAP
#define WS_EX_NOREDIRECTIONBITMAP 0x00200000L
@@ -107,6 +116,45 @@ static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep)
} catch (...) {}
return EXCEPTION_EXECUTE_HANDLER;
}
// Set the window's shell property store so Task Manager, taskbar, and shell
// always show "ObsidianDragon" regardless of any cached VERSIONINFO metadata.
static void SetWindowIdentity(HWND hwnd)
{
IPropertyStore* pps = nullptr;
HRESULT hr = SHGetPropertyStoreForWindow(hwnd, IID_IPropertyStore, (void**)&pps);
if (SUCCEEDED(hr) && pps) {
// Set AppUserModel.ID on the window (overrides process-level ID for this window)
PROPVARIANT pvId;
PropVariantInit(&pvId);
pvId.vt = VT_LPWSTR;
pvId.pwszVal = const_cast<LPWSTR>(L"DragonX.ObsidianDragon.Wallet");
pps->SetValue(PKEY_AppUserModel_ID, pvId);
// Don't PropVariantClear — the string is a static literal
// Set RelaunchDisplayNameResource so the shell shows our name
PROPVARIANT pvName;
PropVariantInit(&pvName);
pvName.vt = VT_LPWSTR;
pvName.pwszVal = const_cast<LPWSTR>(L"ObsidianDragon");
pps->SetValue(PKEY_AppUserModel_RelaunchDisplayNameResource, pvName);
// Set RelaunchCommand (required alongside RelaunchDisplayNameResource)
PROPVARIANT pvCmd;
PropVariantInit(&pvCmd);
pvCmd.vt = VT_LPWSTR;
wchar_t exePath[MAX_PATH] = {};
GetModuleFileNameW(nullptr, exePath, MAX_PATH);
pvCmd.pwszVal = exePath;
pps->SetValue(PKEY_AppUserModel_RelaunchCommand, pvCmd);
pps->Commit();
pps->Release();
DEBUG_LOGF("Window property store: identity set to ObsidianDragon\n");
} else {
DEBUG_LOGF("SHGetPropertyStoreForWindow failed: 0x%08lx\n", (unsigned long)hr);
}
}
#endif
// ---------------------------------------------------------------
@@ -353,6 +401,11 @@ int main(int argc, char* argv[])
}
// Install crash handler for diagnostics
SetUnhandledExceptionFilter(CrashHandler);
// Set the Application User Model ID so Windows Task Manager, taskbar,
// and jump lists show "ObsidianDragon" instead of inheriting a
// description from the MinGW runtime ("POSIX WinThreads for Windows").
SetCurrentProcessExplicitAppUserModelID(L"DragonX.ObsidianDragon.Wallet");
#endif
// Check for payment URI in command line
@@ -499,6 +552,10 @@ int main(int argc, char* argv[])
UpdateWindow(nativeHwnd);
DEBUG_LOGF("Borderless window: native title bar removed\n");
// Set shell property store on the HWND so Task Manager and the taskbar
// always show "ObsidianDragon" (overrides any cached metadata).
SetWindowIdentity(nativeHwnd);
// Initialize DirectX 11 context with DXGI alpha swap chain
dragonx::platform::DX11Context dx;
if (!dx.init(window)) {
@@ -1719,6 +1776,10 @@ int main(int argc, char* argv[])
}
}
// Hide the window immediately so the user perceives the app as closed
// while background cleanup (thread joins, RPC disconnect) continues.
SDL_HideWindow(window);
// Final cleanup (daemon already stopped by beginShutdown)
app.shutdown();
#ifdef DRAGONX_USE_DX11
@@ -1727,6 +1788,20 @@ int main(int argc, char* argv[])
Shutdown(window, gl_context);
#endif
// Explicitly release the single-instance lock before exit so a new
// instance can start immediately.
g_single_instance.unlock();
// Force-terminate the process. All important cleanup (daemon stop,
// settings save, RPC disconnect, SDL teardown) has completed above.
// On Windows with mingw-w64 POSIX threads, normal CRT cleanup
// deadlocks waiting for detached pthreads. On Linux, static
// destructors and atexit handlers can also block. _Exit() bypasses
// all of that.
fflush(stdout);
fflush(stderr);
_Exit(0);
return 0;
}

View File

@@ -320,15 +320,13 @@ void RenderMarketTab(App* app)
points[i] = ImVec2(x, y);
}
// Fill under curve
for (size_t i = 0; i + 1 < n; i++) {
ImVec2 quad[4] = {
points[i],
points[i + 1],
ImVec2(points[i + 1].x, plotBottom),
ImVec2(points[i].x, plotBottom)
};
dl->AddConvexPolyFilled(quad, 4, fillCol);
// Fill under curve (single concave polygon to avoid AA seam artifacts)
if (n >= 2) {
for (size_t i = 0; i < n; i++)
dl->PathLineTo(points[i]);
dl->PathLineTo(ImVec2(points[n - 1].x, plotBottom));
dl->PathLineTo(ImVec2(points[0].x, plotBottom));
dl->PathFillConcave(fillCol);
}
// Line

View File

@@ -40,11 +40,12 @@ static int s_drag_anchor_thread = 0; // thread# where drag started
// Pool mode state
static bool s_pool_mode = false;
static char s_pool_url[256] = "pool.dragonx.is";
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 bool s_show_pool_log = false; // Toggle: false=chart, true=log
static bool s_show_solo_log = false; // Toggle: false=chart, true=log (solo mode)
// Get max threads based on hardware
static int GetMaxMiningThreads()
@@ -175,12 +176,21 @@ void RenderMiningTab(App* app)
app->settings()->setPoolWorker(s_pool_worker);
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 = s_pool_mode
? state.pool_mining.xmrig_running
: mining.generate;
: (mining.generate || state.pool_mining.xmrig_running);
// ================================================================
// Proportional section budget — ensures all content fits without
@@ -310,20 +320,193 @@ void RenderMiningTab(App* app)
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8 * dp, 4 * dp));
// Calculate remaining width from inputs start to end of content region
float inputFrameH2 = ImGui::GetFrameHeight();
float resetBtnW = inputFrameH2; // Square button matching input height
float iconBtnW = inputFrameH2;
float resetBtnW = iconBtnW;
float contentEndX = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW - Layout::spacingSm();
// Each input group: [input][▼][bookmark]
// Layout: [URL group] [spacing] [Worker group] [spacing] [reset]
float perGroupExtra = iconBtnW * 2; // dropdown + bookmark
float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW
- Layout::spacingSm() - perGroupExtra * 2;
float urlW = std::max(60.0f, remainW * 0.30f);
float wrkW = std::max(40.0f, remainW * 0.70f);
// Track positions for popup alignment
float urlGroupStartX = ImGui::GetCursorScreenPos().x;
float urlGroupStartY = ImGui::GetCursorScreenPos().y;
float urlGroupW = urlW + perGroupExtra;
// === Pool URL input ===
ImGui::SetNextItemWidth(urlW);
if (ImGui::InputTextWithHint("##PoolURL", "Pool URL", s_pool_url, sizeof(s_pool_url))) {
s_pool_settings_dirty = true;
}
// --- URL: Dropdown arrow button ---
ImGui::SameLine(0, 0);
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(iconBtnW, inputFrameH2);
ImGui::InvisibleButton("##PoolDropdown", btnSize);
bool btnHov = ImGui::IsItemHovered();
bool btnClk = ImGui::IsItemClicked();
ImDrawList* dl2 = ImGui::GetWindowDrawList();
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Saved pools");
}
ImFont* icoFont = Type().iconSmall();
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon);
dl2->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
OnSurfaceMedium(), dropIcon);
if (btnClk) {
ImGui::OpenPopup("##SavedPoolsPopup");
}
}
// --- URL: Bookmark button ---
ImGui::SameLine(0, 0);
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(iconBtnW, inputFrameH2);
ImGui::InvisibleButton("##SavePoolUrl", btnSize);
bool btnHov = ImGui::IsItemHovered();
bool btnClk = ImGui::IsItemClicked();
ImDrawList* dl2 = ImGui::GetWindowDrawList();
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
std::string currentUrl(s_pool_url);
bool alreadySaved = false;
for (const auto& u : app->settings()->getSavedPoolUrls()) {
if (u == currentUrl) { alreadySaved = true; break; }
}
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip(alreadySaved ? "Already saved" : "Save pool URL");
}
ImFont* icoFont = Type().iconSmall();
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
dl2->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
if (btnClk && !currentUrl.empty() && !alreadySaved) {
app->settings()->addSavedPoolUrl(currentUrl);
app->settings()->save();
}
}
// --- URL: Popup positioned below the input group ---
// Match popup width to input group; zero horizontal padding so
// item highlights are flush with the popup container edges.
ImGui::SetNextWindowPos(ImVec2(urlGroupStartX, urlGroupStartY + inputFrameH2));
ImGui::SetNextWindowSize(ImVec2(urlGroupW, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
if (ImGui::BeginPopup("##SavedPoolsPopup")) {
const auto& savedUrls = app->settings()->getSavedPoolUrls();
if (savedUrls.empty()) {
ImGui::SetCursorPosX(8 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("No saved pools");
ImGui::PopFont();
ImGui::SetCursorPosX(8 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("Click " ICON_MD_BOOKMARK_BORDER " to save");
ImGui::PopFont();
} else {
std::string urlToRemove;
float popupInnerW = ImGui::GetContentRegionAvail().x;
float xZoneW = ImGui::GetFrameHeight();
float textPadX = 8 * dp;
ImFont* rowFont = ImGui::GetFont();
float rowFontSz = ImGui::GetFontSize();
float rowH = ImGui::GetFrameHeight();
for (const auto& url : savedUrls) {
ImGui::PushID(url.c_str());
bool isCurrent = (std::string(s_pool_url) == url);
ImVec2 rowMin = ImGui::GetCursorScreenPos();
ImVec2 rowMax(rowMin.x + popupInnerW, rowMin.y + rowH);
ImGui::InvisibleButton("##row", ImVec2(popupInnerW, rowH));
bool rowHov = ImGui::IsItemHovered();
bool rowClk = ImGui::IsItemClicked();
ImDrawList* pdl = ImGui::GetWindowDrawList();
bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - xZoneW;
// Row background — flush with popup edges
if (isCurrent)
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
if (rowHov && !inXZone)
pdl->AddRectFilled(rowMin, rowMax, StateHover());
// Item text with internal padding
float textY = rowMin.y + (rowH - rowFontSz) * 0.5f;
pdl->AddText(rowFont, rowFontSz,
ImVec2(rowMin.x + textPadX, textY),
isCurrent ? Primary() : OnSurface(), url.c_str());
// X button — flush with right edge, icon centered
{
ImVec2 xMin(rowMax.x - xZoneW, rowMin.y);
ImVec2 xMax(rowMax.x, rowMax.y);
if (inXZone) {
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Remove");
} else if (rowHov) {
// Show faint X when row is hovered
ImFont* icoF = Type().iconSmall();
const char* xIcon = ICON_MD_CLOSE;
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
pdl->AddText(icoF, icoF->LegacySize,
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
OnSurfaceDisabled(), xIcon);
}
// Always draw icon when hovering X zone
if (inXZone) {
ImFont* icoF = Type().iconSmall();
const char* xIcon = ICON_MD_CLOSE;
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
pdl->AddText(icoF, icoF->LegacySize,
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
Error(), xIcon);
}
}
// Click handling
if (rowClk) {
if (inXZone) {
urlToRemove = url;
} else {
strncpy(s_pool_url, url.c_str(), sizeof(s_pool_url) - 1);
s_pool_url[sizeof(s_pool_url) - 1] = '\0';
s_pool_settings_dirty = true;
ImGui::CloseCurrentPopup();
}
}
if (rowHov && !inXZone)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::PopID();
}
if (!urlToRemove.empty()) {
app->settings()->removeSavedPoolUrl(urlToRemove);
app->settings()->save();
}
}
ImGui::EndPopup();
}
ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for URL popup
ImGui::SameLine(0, Layout::spacingSm());
float wrkGroupStartX = ImGui::GetCursorScreenPos().x;
float wrkGroupStartY = ImGui::GetCursorScreenPos().y;
float wrkGroupW = wrkW + perGroupExtra;
ImGui::SetNextItemWidth(wrkW);
if (ImGui::InputTextWithHint("##PoolWorker", "Payout Address", s_pool_worker, sizeof(s_pool_worker))) {
s_pool_settings_dirty = true;
@@ -332,7 +515,169 @@ void RenderMiningTab(App* app)
ImGui::SetTooltip("Your DRAGONX address for receiving pool payouts");
}
// Reset to defaults button (matching input height)
// --- Worker: Dropdown arrow button ---
ImGui::SameLine(0, 0);
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(iconBtnW, inputFrameH2);
ImGui::InvisibleButton("##WorkerDropdown", btnSize);
bool btnHov = ImGui::IsItemHovered();
bool btnClk = ImGui::IsItemClicked();
ImDrawList* dl2 = ImGui::GetWindowDrawList();
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Saved addresses");
}
ImFont* icoFont = Type().iconSmall();
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon);
dl2->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
OnSurfaceMedium(), dropIcon);
if (btnClk) {
ImGui::OpenPopup("##SavedWorkersPopup");
}
}
// --- Worker: Bookmark button ---
ImGui::SameLine(0, 0);
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(iconBtnW, inputFrameH2);
ImGui::InvisibleButton("##SaveWorkerAddr", btnSize);
bool btnHov = ImGui::IsItemHovered();
bool btnClk = ImGui::IsItemClicked();
ImDrawList* dl2 = ImGui::GetWindowDrawList();
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
std::string currentWorker(s_pool_worker);
bool alreadySaved = false;
for (const auto& w : app->settings()->getSavedPoolWorkers()) {
if (w == currentWorker) { alreadySaved = true; break; }
}
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip(alreadySaved ? "Already saved" : "Save payout address");
}
ImFont* icoFont = Type().iconSmall();
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
dl2->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
if (btnClk && !currentWorker.empty() && currentWorker != "x" && !alreadySaved) {
app->settings()->addSavedPoolWorker(currentWorker);
app->settings()->save();
}
}
// --- Worker: Popup positioned below the input group ---
// Popup sized to fit full z-addresses without truncation;
// zero horizontal padding so item highlights are flush with edges.
float addrPopupW = std::max(wrkGroupW, availWidth * 0.55f);
ImGui::SetNextWindowPos(ImVec2(wrkGroupStartX, wrkGroupStartY + inputFrameH2));
ImGui::SetNextWindowSize(ImVec2(addrPopupW, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
if (ImGui::BeginPopup("##SavedWorkersPopup")) {
const auto& savedWorkers = app->settings()->getSavedPoolWorkers();
if (savedWorkers.empty()) {
ImGui::SetCursorPosX(8 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("No saved addresses");
ImGui::PopFont();
ImGui::SetCursorPosX(8 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("Click " ICON_MD_BOOKMARK_BORDER " to save");
ImGui::PopFont();
} else {
std::string addrToRemove;
float wPopupInnerW = ImGui::GetContentRegionAvail().x;
float wXZoneW = ImGui::GetFrameHeight();
float wTextPadX = 8 * dp;
ImFont* wRowFont = ImGui::GetFont();
float wRowFontSz = ImGui::GetFontSize();
float wRowH = ImGui::GetFrameHeight();
for (const auto& addr : savedWorkers) {
ImGui::PushID(addr.c_str());
bool isCurrent = (std::string(s_pool_worker) == addr);
ImVec2 rowMin = ImGui::GetCursorScreenPos();
ImVec2 rowMax(rowMin.x + wPopupInnerW, rowMin.y + wRowH);
ImGui::InvisibleButton("##row", ImVec2(wPopupInnerW, wRowH));
bool rowHov = ImGui::IsItemHovered();
bool rowClk = ImGui::IsItemClicked();
ImDrawList* pdl = ImGui::GetWindowDrawList();
bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - wXZoneW;
// Row background — flush with popup edges
if (isCurrent)
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
if (rowHov && !inXZone)
pdl->AddRectFilled(rowMin, rowMax, StateHover());
// Full address text with internal padding
float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f;
pdl->AddText(wRowFont, wRowFontSz,
ImVec2(rowMin.x + wTextPadX, textY),
isCurrent ? Primary() : OnSurface(), addr.c_str());
// Tooltip for long addresses
if (rowHov && !inXZone)
ImGui::SetTooltip("%s", addr.c_str());
// X button — flush with right edge, icon centered
{
ImVec2 xMin(rowMax.x - wXZoneW, rowMin.y);
ImVec2 xMax(rowMax.x, rowMax.y);
if (inXZone) {
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Remove");
} else if (rowHov) {
ImFont* icoF = Type().iconSmall();
const char* xIcon = ICON_MD_CLOSE;
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
pdl->AddText(icoF, icoF->LegacySize,
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
OnSurfaceDisabled(), xIcon);
}
if (inXZone) {
ImFont* icoF = Type().iconSmall();
const char* xIcon = ICON_MD_CLOSE;
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
pdl->AddText(icoF, icoF->LegacySize,
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
Error(), xIcon);
}
}
// Click handling
if (rowClk) {
if (inXZone) {
addrToRemove = addr;
} else {
strncpy(s_pool_worker, addr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
s_pool_settings_dirty = true;
ImGui::CloseCurrentPopup();
}
}
if (rowHov && !inXZone)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::PopID();
}
if (!addrToRemove.empty()) {
app->settings()->removeSavedPoolWorker(addrToRemove);
app->settings()->save();
}
}
ImGui::EndPopup();
}
ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for Worker popup
// === Reset to defaults button ===
ImGui::SameLine(0, Layout::spacingSm());
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
@@ -612,9 +957,12 @@ void RenderMiningTab(App* app)
bool isToggling = app->isMiningToggleInProgress();
// Pool mining connects to an external pool via xmrig — it does not
// need the local blockchain synced or even the daemon connected.
// If pool mining is still shutting down after switching to solo,
// keep the button enabled so user can stop it.
bool poolStillRunning = !s_pool_mode && state.pool_mining.xmrig_running;
bool disabled = s_pool_mode
? (isToggling || poolBlockedBySolo)
: (!app->isConnected() || isToggling || isSyncing);
: (poolStillRunning ? false : (!app->isConnected() || isToggling || isSyncing));
// Glass panel background with state-dependent tint
GlassPanelSpec btnGlass;
@@ -755,7 +1103,11 @@ void RenderMiningTab(App* app)
else
app->startPoolMining(s_selected_threads);
} else {
if (mining.generate)
// If pool mining is still running (user just switched from pool to solo),
// stop pool mining first
if (state.pool_mining.xmrig_running)
app->stopPoolMining();
else if (mining.generate)
app->stopMining();
else
app->startMining(s_selected_threads);
@@ -776,8 +1128,12 @@ void RenderMiningTab(App* app)
ImU32 greenCol = Success();
// Determine view mode first
bool showLogView = s_pool_mode && s_show_pool_log && !state.pool_mining.log_lines.empty();
bool hasLogContent = s_pool_mode && !state.pool_mining.log_lines.empty();
bool showLogView = s_pool_mode
? (s_show_pool_log && !state.pool_mining.log_lines.empty())
: (s_show_solo_log && !mining.log_lines.empty());
bool hasLogContent = s_pool_mode
? !state.pool_mining.log_lines.empty()
: !mining.log_lines.empty();
// Use pool hashrate history when in pool mode, solo otherwise
const std::vector<double>& chartHistory = s_pool_mode
? state.pool_mining.hashrate_history
@@ -792,60 +1148,44 @@ void RenderMiningTab(App* app)
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + totalCardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
// --- Toggle button in top-right corner (pool mode only) ---
if (s_pool_mode && (hasLogContent || hasChartContent)) {
ImFont* iconFont = Type().iconSmall();
const char* toggleIcon = s_show_pool_log ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
const char* toggleTip = s_show_pool_log ? "Show hashrate chart" : "Show miner log";
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon);
float btnSize = iconSz.y + 8 * dp;
float btnX = cardMax.x - pad - btnSize;
float btnY = cardMin.y + pad * 0.5f;
ImVec2 btnMin(btnX, btnY);
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f);
bool hov = IsRectHovered(btnMin, btnMax);
if (hov) {
dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", toggleTip);
}
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
OnSurfaceMedium(), toggleIcon);
if (hov && ImGui::IsMouseClicked(0)) {
s_show_pool_log = !s_show_pool_log;
}
}
bool& showLogFlag = s_pool_mode ? s_show_pool_log : s_show_solo_log;
if (showLogView) {
// --- Full-card log view ---
// --- Full-card log view (selectable + copyable) ---
const std::vector<std::string>& logLines = s_pool_mode
? state.pool_mining.log_lines
: mining.log_lines;
// Build a single string buffer for InputTextMultiline
static std::string s_log_buf;
s_log_buf.clear();
for (const auto& line : logLines) {
if (!line.empty()) {
s_log_buf += line;
s_log_buf += '\n';
}
}
float logPad = pad * 0.5f;
float logW = availWidth - logPad * 2;
float logH = totalCardH - logPad * 2;
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + logPad, cardMin.y + logPad));
ImGui::BeginChild("##PoolLogInCard", ImVec2(availWidth - logPad * 2, totalCardH - logPad * 2), false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_HorizontalScrollbar);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface()));
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0));
ImFont* monoFont = Type().body2();
ImGui::PushFont(monoFont);
for (const auto& line : state.pool_mining.log_lines) {
if (!line.empty())
ImGui::TextUnformatted(line.c_str());
}
const char* inputId = s_pool_mode ? "##PoolLogText" : "##SoloLogText";
ImGui::InputTextMultiline(inputId,
const_cast<char*>(s_log_buf.c_str()), s_log_buf.size() + 1,
ImVec2(logW, logH),
ImGuiInputTextFlags_ReadOnly);
ImGui::PopFont();
ImGui::PopStyleColor();
ImGui::PopStyleColor(2);
// Auto-scroll to bottom only if user is already near the bottom
// This allows manual scrolling up to read history
float scrollY = ImGui::GetScrollY();
float scrollMaxY = ImGui::GetScrollMaxY();
if (scrollMaxY <= 0.0f || scrollY >= scrollMaxY - 20.0f * dp)
ImGui::SetScrollHereY(1.0f);
ImGui::EndChild();
// Reset cursor to end of card after the child window
// Reset cursor to end of card after the input
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0)); // Register position with layout system
} else {
@@ -1075,6 +1415,35 @@ void RenderMiningTab(App* app)
ImGui::Dummy(ImVec2(availWidth, totalCardH));
}
// --- Toggle button in top-right corner ---
// Rendered after content so the Hand cursor takes priority over
// the InputTextMultiline text-cursor when hovering the button.
if (hasLogContent || hasChartContent) {
ImFont* iconFont = Type().iconSmall();
const char* toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
const char* toggleTip = showLogFlag ? "Show hashrate chart" : "Show mining log";
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon);
float btnSize = iconSz.y + 8 * dp;
float btnX = cardMax.x - pad - btnSize;
float btnY = cardMin.y + pad * 0.5f;
ImVec2 btnMin(btnX, btnY);
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f);
bool hov = IsRectHovered(btnMin, btnMax);
if (hov) {
dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", toggleTip);
}
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
OnSurfaceMedium(), toggleIcon);
if (hov && ImGui::IsMouseClicked(0)) {
showLogFlag = !showLogFlag;
}
}
ImGui::Dummy(ImVec2(0, gap));
}
@@ -1106,11 +1475,17 @@ void RenderMiningTab(App* app)
double amount;
int confirmations;
bool mature;
std::string txid;
bool isPoolPayout;
};
std::vector<MinedTx> recentMined;
for (const auto& tx : state.transactions) {
if (tx.type == "generate" || tx.type == "immature" || tx.type == "mined") {
bool isSoloMined = (tx.type == "generate" || tx.type == "immature" || tx.type == "mined");
bool isPoolPayout = (tx.type == "receive"
&& !tx.memo.empty()
&& tx.memo.find("Mining Pool payout") != std::string::npos);
if (isSoloMined || isPoolPayout) {
double amt = std::abs(tx.amount);
minedAllTime += amt;
minedAllTimeCount++;
@@ -1121,8 +1496,10 @@ void RenderMiningTab(App* app)
minedYesterday += amt;
minedYesterdayCount++;
}
if (recentMined.size() < 4) {
recentMined.push_back({tx.timestamp, amt, tx.confirmations, tx.confirmations >= 100});
// Separate solo blocks from pool payouts based on current mode
bool showInCurrentMode = s_pool_mode ? isPoolPayout : isSoloMined;
if (showInCurrentMode && recentMined.size() < 4) {
recentMined.push_back({tx.timestamp, amt, tx.confirmations, tx.confirmations >= 100, tx.txid, isPoolPayout});
}
}
}
@@ -1164,18 +1541,18 @@ void RenderMiningTab(App* app)
};
snprintf(valBuf, sizeof(valBuf), "+%.4f", minedToday);
snprintf(subBuf2, sizeof(subBuf2), "(%d blk)", minedTodayCount);
snprintf(subBuf2, sizeof(subBuf2), "(%d txn)", minedTodayCount);
char todayVal[64], todaySub[64];
strncpy(todayVal, valBuf, sizeof(todayVal));
strncpy(todaySub, subBuf2, sizeof(todaySub));
char yesterdayVal[64], yesterdaySub[64];
snprintf(yesterdayVal, sizeof(yesterdayVal), "+%.4f", minedYesterday);
snprintf(yesterdaySub, sizeof(yesterdaySub), "(%d blk)", minedYesterdayCount);
snprintf(yesterdaySub, sizeof(yesterdaySub), "(%d txn)", minedYesterdayCount);
char allVal[64], allSub[64];
snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime);
snprintf(allSub, sizeof(allSub), "(%d blk)", minedAllTimeCount);
snprintf(allSub, sizeof(allSub), "(%d txn)", minedAllTimeCount);
char estVal[64];
if (estActive)
@@ -1500,16 +1877,18 @@ void RenderMiningTab(App* app)
ImGui::Dummy(ImVec2(0, gap));
// ============================================================
// RECENT BLOCKS — last 4 mined blocks
// RECENT BLOCKS — last 4 mined blocks (always shown in pool mode)
// ============================================================
if (!recentMined.empty()) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT BLOCKS");
if (!recentMined.empty() || s_pool_mode) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(),
s_pool_mode ? "RECENT POOL PAYOUTS" : "RECENT BLOCKS");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float rowH_blocks = std::max(schema::UI().drawElement("tabs.mining", "recent-row-min-height").size, schema::UI().drawElement("tabs.mining", "recent-row-height").size * vs);
// Size to remaining space — proportional budget ensures fit
float recentAvailH = ImGui::GetContentRegionAvail().y - sHdr - gapOver;
float contentH_blocks = rowH_blocks * (float)recentMined.size() + pad * 2.5f;
float minRows = recentMined.empty() ? 2.0f : (float)recentMined.size();
float contentH_blocks = rowH_blocks * minRows + pad * 2.5f;
float recentH = std::clamp(contentH_blocks, 30.0f * dp, std::max(30.0f * dp, recentAvailH));
// Glass panel wrapping the list + scroll-edge mask state
@@ -1533,6 +1912,26 @@ void RenderMiningTab(App* app)
// Top padding inside glass card
ImGui::Dummy(ImVec2(0, pad * 0.5f));
if (recentMined.empty()) {
// Empty state — card is visible but no rows yet
float emptyY = ImGui::GetCursorScreenPos().y;
float emptyX = ImGui::GetCursorScreenPos().x;
float centerX = emptyX + availWidth * 0.5f;
ImFont* icoFont = Type().iconMed();
const char* emptyIcon = ICON_MD_HOURGLASS_EMPTY;
ImVec2 iSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, emptyIcon);
miningChildDL->AddText(icoFont, icoFont->LegacySize,
ImVec2(centerX - iSz.x * 0.5f, emptyY),
OnSurfaceDisabled(), emptyIcon);
const char* emptyMsg = s_pool_mode
? "No pool payouts yet"
: "No blocks found yet";
ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, emptyMsg);
miningChildDL->AddText(capFont, capFont->LegacySize,
ImVec2(centerX - msgSz.x * 0.5f, emptyY + iSz.y + Layout::spacingXs()),
OnSurfaceDisabled(), emptyMsg);
}
for (size_t mi = 0; mi < recentMined.size(); mi++) {
const auto& mtx = recentMined[mi];
@@ -1542,10 +1941,14 @@ void RenderMiningTab(App* app)
// Subtle background on hover (inset from card edges)
bool hovered = material::IsRectHovered(rMin, rMax);
bool isClickable = !mtx.txid.empty();
if (hovered) {
dl->AddRectFilled(ImVec2(rMin.x + pad * 0.5f, rMin.y),
ImVec2(rMax.x - pad * 0.5f, rMax.y),
IM_COL32(255, 255, 255, 8), 3.0f * dp);
if (isClickable) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
}
float rx = rMin.x + pad;
@@ -1588,7 +1991,18 @@ void RenderMiningTab(App* app)
WithAlpha(Warning(), 200), buf);
}
ImGui::Dummy(ImVec2(availWidth, rH));
// Click to open in block explorer
ImGui::SetCursorScreenPos(rMin);
char blockBtnId[32];
snprintf(blockBtnId, sizeof(blockBtnId), "##RecentBlock%zu", mi);
ImGui::InvisibleButton(blockBtnId, ImVec2(availWidth, rH));
if (ImGui::IsItemClicked() && !mtx.txid.empty()) {
std::string url = app->settings()->getTxExplorerUrl() + mtx.txid;
dragonx::util::Platform::openUrl(url);
}
if (ImGui::IsItemHovered() && !mtx.txid.empty()) {
ImGui::SetTooltip("Open in explorer");
}
}
ImGui::EndChild(); // ##RecentBlocks

View File

@@ -21,6 +21,7 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <unordered_set>
namespace dragonx {
namespace ui {
@@ -45,6 +46,22 @@ static std::string ExtractIP(const std::string& addr)
return ip;
}
// Known seed/addnode IPs for the DragonX network.
// These are the official seed nodes that the daemon connects to on startup.
static bool IsSeedNode(const std::string& addr) {
static const std::unordered_set<std::string> seeds = {
"176.126.87.241", // embedded daemon -addnode
"94.72.112.24", // node1.hush.is
"37.60.252.160", // node2.hush.is
"176.57.70.185", // node3.hush.is / node6.hush.is
"185.213.209.89", // node4.hush.is
"137.74.4.198", // node5.hush.is
"18.193.113.121", // node7.hush.is
"38.60.224.94", // node8.hush.is
};
return seeds.count(ExtractIP(addr)) > 0;
}
void RenderPeersTab(App* app)
{
auto& S = schema::UI();
@@ -149,10 +166,28 @@ void RenderPeersTab(App* app)
// Blocks
float cx = cardMin.x + pad;
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Blocks");
int blocks = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
int blocks = state.sync.blocks;
if (blocks > 0) {
snprintf(buf, sizeof(buf), "%d", blocks);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
int blocksLeft = state.sync.headers - blocks;
if (blocksLeft < 0) blocksLeft = 0;
if (blocksLeft > 0) {
snprintf(buf, sizeof(buf), "%d (%d left)", blocks, blocksLeft);
float valY = ry + capFont->LegacySize + Layout::spacingXs();
// Draw block number in normal color
char blockStr[32];
snprintf(blockStr, sizeof(blockStr), "%d ", blocks);
ImVec2 numSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, blockStr);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), blockStr);
// Draw "(X left)" in warning color
char leftStr[32];
snprintf(leftStr, sizeof(leftStr), "(%d left)", blocksLeft);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx + numSz.x, valY + (sub1->LegacySize - capFont->LegacySize) * 0.5f),
Warning(), leftStr);
} else {
snprintf(buf, sizeof(buf), "%d", blocks);
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
}
} else {
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), "\xE2\x80\x94");
}
@@ -680,7 +715,16 @@ void RenderPeersTab(App* app)
float pingDotR = S.drawElement("tabs.peers", "ping-dot-radius-base").size + S.drawElement("tabs.peers", "ping-dot-radius-scale").size * hs;
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ping-dot-x-offset").size, cy + body2->LegacySize * 0.5f), pingDotR, dotCol);
dl->AddText(body2, body2->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size, cy), OnSurface(), peer.addr.c_str());
float addrX = cx + S.drawElement("tabs.peers", "address-x-offset").size;
dl->AddText(body2, body2->LegacySize, ImVec2(addrX, cy), OnSurface(), peer.addr.c_str());
// Seed node icon — rendered right after the IP address
if (IsSeedNode(peer.addr)) {
ImVec2 addrSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, peer.addr.c_str());
ImFont* iconFont = Type().iconSmall();
float iconY = cy + (body2->LegacySize - iconFont->LegacySize) * 0.5f;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(addrX + addrSz.x + Layout::spacingSm(), iconY), WithAlpha(Success(), 200), ICON_MD_GRASS);
}
{
const char* dirLabel = peer.inbound ? "In" : "Out";

View File

@@ -393,9 +393,12 @@ void RenderTransactionsTab(App* app)
}
}
// Sort by timestamp descending (same as raw list)
// Sort: pending (0-conf) transactions first, then by timestamp descending
std::sort(display_txns.begin(), display_txns.end(),
[](const DisplayTx& a, const DisplayTx& b) {
bool aPending = (a.confirmations == 0);
bool bPending = (b.confirmations == 0);
if (aPending != bPending) return aPending;
return a.timestamp > b.timestamp;
});
}

View File

@@ -512,7 +512,7 @@ std::string Bootstrap::computeSHA256(const std::string& filePath) {
snprintf(msg, sizeof(msg), "Verifying SHA-256... %.0f%% (%s / %s)",
pct, formatSize((double)processed).c_str(),
formatSize((double)fileSize).c_str());
setProgress(State::Downloading, msg, (double)processed, (double)fileSize);
setProgress(State::Verifying, msg, (double)processed, (double)fileSize);
}
}
fclose(fp);
@@ -643,7 +643,7 @@ std::string Bootstrap::computeMD5(const std::string& filePath) {
snprintf(msg, sizeof(msg), "Verifying MD5... %.0f%% (%s / %s)",
pct, formatSize((double)processed).c_str(),
formatSize((double)fileSize).c_str());
setProgress(State::Downloading, msg, (double)processed, (double)fileSize);
setProgress(State::Verifying, msg, (double)processed, (double)fileSize);
}
}
fclose(fp);
@@ -663,7 +663,7 @@ std::string Bootstrap::computeMD5(const std::string& filePath) {
// ---------------------------------------------------------------------------
bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& baseUrl) {
setProgress(State::Downloading, "Downloading checksums...");
setProgress(State::Verifying, "Downloading checksums...");
std::string sha256Url = baseUrl + "/" + kZipName + ".sha256";
std::string md5Url = baseUrl + "/" + kZipName + ".md5";
@@ -684,7 +684,7 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
// --- SHA-256 ---
if (haveSHA256) {
setProgress(State::Downloading, "Verifying SHA-256...");
setProgress(State::Verifying, "Verifying SHA-256...");
std::string expected = parseChecksumFile(sha256Content);
std::string actual = computeSHA256(zipPath);
@@ -712,7 +712,7 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
// --- MD5 ---
if (haveMD5) {
setProgress(State::Downloading, "Verifying MD5...");
setProgress(State::Verifying, "Verifying MD5...");
std::string expected = parseChecksumFile(md5Content);
std::string actual = computeMD5(zipPath);
@@ -738,7 +738,7 @@ bool Bootstrap::verifyChecksums(const std::string& zipPath, const std::string& b
DEBUG_LOGF("[Bootstrap] MD5 verified: %s\n", actual.c_str());
}
setProgress(State::Downloading, "Checksums verified \xe2\x9c\x93");
setProgress(State::Verifying, "Checksums verified \xe2\x9c\x93");
return true;
}

View File

@@ -27,6 +27,7 @@ public:
enum class State {
Idle,
Downloading,
Verifying,
Extracting,
Completed,
Failed