44 Commits
Author SHA1 Message Date
dan_s dc4426810f fix: accurate sync speed display, add missing i18n keys, native language names
- Fix blk/s calculation that was inflated ~10x due to resetting the
  time baseline every frame instead of only when blocks advanced
- Add decay when no new blocks arrive for 10s so rate doesn't stay stale
- Add 7 missing translation keys (timeout_off/1min/5min/15min/30min/1hour,
  slider_off) to all 8 language files so settings dropdowns translate
- Show language names in native script (中文, Русский, 日本語, 한국어)
2026-04-12 15:12:36 -05:00
dan_s 915c1b4d23 feat: non-blocking warmup — connect during daemon initialization
Instead of blocking the entire UI with "Activating best chain..." until
the daemon finishes warmup, treat warmup responses as a successful
connection. The wallet now:

- Sets connected=true + warming_up=true when daemon returns RPC -28
- Shows warmup status with block progress in the loading overlay
- Polls getinfo every few seconds to detect warmup completion
- Allows Console, Peers, Settings tabs during warmup
- Shows orange status indicator with warmup message in status bar
- Skips balance/tx/address refresh until warmup completes
- Triggers full data refresh once daemon is ready

Also: fix curl handle/header leak on reconnect, fill in empty
externalDetected error branch, bump version to v1.2.0 in build scripts.
2026-04-12 14:32:57 -05:00
dan_s 28b9e0dffb fix: auto-refresh peers list, show warmup status during daemon startup
- Fix peer timer calling refreshEncryptionState() instead of
  refreshPeerInfo(), so the Network tab now auto-updates every 5s
- Reorder RPC error handling so warmup messages (Loading block index,
  Verifying blocks, etc.) display in the status bar instead of being
  masked by the generic "Waiting for dragonxd" message
2026-04-12 13:43:45 -05:00
dan_s 6be0a58c26 feat: use DragonX DNS seed nodes, pass -maxconnections to daemon, show sync speed
- Replace hardcoded IP addnodes with node.dragonx.is, node1–4.dragonx.is
  in both daemon launch params and auto-generated DRAGONX.conf
- Add max_connections setting (persisted, default 0 = daemon default);
  passed as -maxconnections= flag to dragonxd on startup
- Show blocks/sec in status bar during sync with exponential smoothing
  (e.g. "Syncing 45.2% (12340 left, 85 blk/s)")
2026-04-12 13:22:22 -05:00
DanS fbdba1a001 feat: CJK font rendering, force quit confirmation, settings i18n
- Rebuild CJK font subset (1421 glyphs) and convert CFF→TTF for
  stb_truetype compatibility, fixing Chinese/Japanese/Korean rendering
- Add force quit confirmation dialog with cancel/confirm actions
- Show force quit tooltip immediately on hover (no delay)
- Translate hardcoded English strings in settings dropdowns
  (auto-lock timeouts, slider "Off" labels)
- Fix mojibake en-dashes in 7 translation JSON files
- Add helper scripts: build_cjk_subset, convert_cjk_to_ttf,
  check_font_coverage, fix_mojibake
2026-04-12 10:32:58 -05:00
DanS 821c54ba2b Redesign benchmark to measure sustained (thermally throttled) hashrate
instead of initial burst performance. Previously the benchmark used a
fixed 20s warmup + 10s peak measurement, which reported inflated
results on thermally constrained hardware (e.g. 179 H/s vs actual
sustained 117 H/s on a MacBook Pro).

- Adaptive warmup with stability detection: mine for at least 90s,
  then compare rolling 10s hashrate windows. Require 3 consecutive
  windows within 5% before declaring thermal equilibrium (cap 300s)
- Average-based measurement: record mean hashrate over 30s instead
  of peak, reflecting real sustained throughput
- Start candidates at half the system cores — lower thread counts
  are rarely optimal and waste time warming up
- Add CoolingDown phase: 5s idle pause between tests so each starts
  from a similar thermal baseline
- Adaptive time estimates: use observed warmup durations from
  completed tests to predict remaining time
- UI shows Stabilizing when waiting for thermal equilibrium past
  the minimum warmup, Cooling during idle pauses"
2026-04-06 13:51:56 -05:00
DanS 3ff62ca248 v1.2.0: UX audit — security fixes, accessibility, and polish
Security (P0):
- Fix sidebar remaining interactive behind lock screen
- Extend auto-lock idle detection to include active widget interactions
- Distinguish missing PIN vault from wrong PIN; auto-switch to passphrase

Blocking UX (P1):
- Add 15s timeout for encryption state check to prevent indefinite loading
- Show restart reason in loading overlay after wallet encryption
- Add Force Quit button on shutdown screen after 10s
- Warn user if embedded daemon fails to start during wizard completion

Polish (P2):
- Use configured explorer URL in Receive tab instead of hardcoded URL
- Increase request memo buffer from 256 to 512 bytes to match Send tab
- Extend notification duration to 5s for critical operations (tx sent,
  wallet encrypted, key import, backup, export)
- Add Reduce Motion accessibility setting (disables page fade + balance lerp)
- Show estimated remaining time during mining thread benchmark
- Add staleness indicator to market price data (warning after 5 min)

New i18n keys: incorrect_pin, incorrect_passphrase, pin_not_set,
restarting_after_encryption, force_quit, reduce_motion, tt_reduce_motion,
ago, wizard_daemon_start_failed
2026-04-04 19:10:58 -05:00
DanS bbf53a130c refactor: tab-aware prioritized refresh system
Split monolithic refreshData() into independent sub-functions
(refreshCoreData, refreshAddressData, refreshTransactionData,
refreshEncryptionState) each with its own timer and atomic guard.

Per-category timers replace the single 5s refresh_timer_:
- core_timer_: balance + blockchain info (5s default)
- transaction_timer_: tx list + enrichment (10s default)
- address_timer_: z/t address lists (15s default)
- peer_timer_: encryption state (10s default)

Tab-switching via setCurrentPage() adjusts active intervals so
the current tab's data refreshes faster (e.g. 3s core on Overview,
5s transactions on History) while background categories slow down.

Use fast_worker_ for core data on Overview tab to avoid blocking
behind the main refresh batch.

Bump version to 1.1.2.
2026-04-04 13:05:00 -05:00
DanS 1f9e43d7b2 refactor: extract AI/agent files into separate repo
ObsidianDragon-agent/ is now a standalone git repo (future submodule)
so AI configuration files are not pushed to the main repository.

- Remove copilot-instructions.md and ARCHITECTURE.md from main tracking
- Remove symlinks from .github/ and docs/
- Add ObsidianDragon-agent/ and .github/ to .gitignore
2026-04-04 11:36:04 -05:00
DanS 5ebccceffc refactor: move AI/agent files into ObsidianDragon-agent/
- copilot-instructions.md → ObsidianDragon-agent/copilot-instructions.md
- ARCHITECTURE.md → ObsidianDragon-agent/ARCHITECTURE.md
- Symlinks at original locations preserve Copilot auto-discovery
2026-04-04 11:29:12 -05:00
DanS 9d2d581474 docs: add ARCHITECTURE.md with project overview
Covers directory layout, threading model, RPC architecture,
connection lifecycle, UI system, build system, and key conventions.
2026-04-04 11:17:21 -05:00
DanS 096f8ee90e docs: add copilot-instructions.md and file-level comments
- Create .github/copilot-instructions.md with project coding standards,
  architecture overview, threading model, and key rules for AI sessions
- Add module description comments to app.cpp, rpc_client.cpp, rpc_worker.cpp,
  embedded_daemon.cpp, xmrig_manager.cpp, console_tab.cpp, settings.cpp
- Add ASCII connection state diagram to app_network.cpp
- Remove /.github/ from .gitignore so instructions file is tracked
2026-04-04 11:14:31 -05:00
DanS ca199ef195 fix: console not connected when fast-lane RPC still connecting
The console tab was passed fast_rpc_ even before its async connection
completed, causing 'Not connected to daemon' errors despite the main
RPC being connected and sync data flowing. Fall back to the main
rpc_/worker_ until fast_rpc_ reports isConnected().
2026-04-03 11:34:32 -05:00
DanS 97bd2f8168 build: macOS universal binary (arm64+x86_64) with deployment target 11.0
- Set CMAKE_OSX_DEPLOYMENT_TARGET and CMAKE_OSX_ARCHITECTURES before
  project() so they propagate to all FetchContent dependencies (SDL3, etc.)
- build.sh: native mac release builds universal binary, detects and
  rebuilds single-arch libsodium, verifies with lipo, exports
  MACOSX_DEPLOYMENT_TARGET; dev build uses correct build/mac directory
- fetch-libsodium.sh: build arm64 and x86_64 separately then merge with
  lipo on native macOS; fix sha256sum unavailable on macOS (use shasum)
2026-04-03 10:55:07 -05:00
dan_s 09f287fbc5 feat: thread benchmark, GPU-aware idle mining, thread scaling fix
- Add pool mining thread benchmark: cycles through thread counts with
  20s warmup + 10s measurement to find optimal setting for CPU
- Add GPU-aware idle detection: GPU utilization >= 10% (video, games)
  treats system as active; toggle in mining tab header (default: on)
  Supports AMD sysfs, NVIDIA nvidia-smi, Intel freq ratio; -1 on macOS
- Fix idle thread scaling: use getRequestedThreads() for immediate
  thread count instead of xmrig API threads_active which lags on restart
- Apply active thread count on initial mining start when user is active
- Skip idle mining adjustments while benchmark is running
- Disable thread grid drag-to-select during benchmark
- Add idle_gpu_aware setting with JSON persistence (default: true)
- Add 7 i18n English strings for benchmark and GPU-aware tooltips
2026-04-01 17:06:05 -05:00
dan_s b3d43ba0ad update build output filenames to include version info 2026-03-25 11:24:21 -05:00
DanS 430290f97a update hardcoded version for mac dmg build 2026-03-25 11:18:03 -05:00
dan_s 30fc5da520 feat: track shielded send txids via z_viewtransaction
Extract txids from completed z_sendmany operations and store in
send_txids_ so pure shielded sends are discoverable. The network
thread includes them in the enrichment set, calls z_viewtransaction,
caches results in viewtx_cache_, and removes them from send_txids_.
2026-03-25 11:06:09 -05:00
DanS f02c965929 fix: macOS block index corruption, dbcache auto-sizing, import key rescan height
- Shutdown: 3-phase stop (wait for RPC stop → SIGTERM → SIGKILL) prevents
  LevelDB flush interruption on macOS/APFS that caused full re-sync on restart
- dbcache: auto-detect RAM and set -dbcache to 12.5% (clamped 450-4096 MB)
  on macOS (sysctl), Linux (sysconf), and Windows (GlobalMemoryStatusEx)
- Import key: pass user-entered start height to z_importkey and trigger
  rescanblockchain from that height for t-key imports
- Bump version to 1.1.1
2026-03-25 11:00:14 -05:00
DanS f0b7b88ef2 update mac icons 2026-03-19 14:46:33 -05:00
M 53d08de639 macOS port: build, rendering, daemon, and mining fixes
Build & setup:
- Fix setup.sh and build.sh for macOS (bundle daemon, xmrig, sapling params, asmap.dat into .app)
- Fix CMakeLists.txt libsodium linking for macOS
- Fix incbin.h to use __DATA,__const section on macOS
- Remove vendored libsodium-1.0.18 source tree (use fetch script instead)
- Remove prebuilt-binaries/xmrig (replaced by xmrig-hac)
- Add .DS_Store to .gitignore

Rendering & UI:
- Use GLSL #version 150 and OpenGL 3.2 Core Profile on macOS
- Force dpiScale=1.0 on macOS to fix Retina double-scaling
- Set default window/UI opacity to 100% on Mac/Linux
- Add scroll fade shader guard for macOS GL compatibility
- Add ImGui error recovery around render loop and mining tab

Daemon & bootstrap:
- Fix getDragonXDataDir() to return ~/Library/Application Support/Hush/DRAGONX/ on macOS
- Fix isPortInUse() with connect() fallback (no /proc/net/tcp on macOS)
- Increase daemon watchdog timeout from 3s to 15s
- Add daemon status indicator (colored dot + label) in wizard bootstrap phases

Mining tab:
- Fix EmbeddedDaemon::getMemoryUsageMB() crash on macOS (was using Linux /proc)
- Fix XmrigManager::getMemoryUsageMB() to use ps on macOS instead of /proc
- Restructure RenderMiningTab with wrapper pattern for exception safety
- Fix default pool URL to include port (pool.dragonx.is:3433)
2026-03-19 14:26:04 -05:00
dan_s 8645a82e4f feat: sync thread grid during idle scaling, skip lock screen while pool mining, add paste preview to import key dialog
- Mining tab: sync s_selected_threads with actual thread count when idle
  thread scaling adjusts threads (solo via genproclimit, pool via
  threads_active), skipping sync during user drag
- Auto-lock: bypass lock screen overlay when xmrig pool mining is active
  so the mining UI remains accessible
- Import key dialog: add clipboard hover preview with transparent overlay
  on the input field, inline key type validation next to title (matching
  send tab paste button pattern), configurable via ui.toml
2026-03-19 06:10:46 -05:00
dan_s 9e94952e0a v1.1.0: explorer tab, bootstrap fixes, full theme overlay merge
Explorer tab:
- New block explorer tab with search, chain stats, mempool info,
  recent blocks table, block detail modal with tx expansion
- Sidebar nav entry, i18n strings, ui.toml layout values

Bootstrap fixes:
- Move wizard Done handler into render() — was dead code, preventing
  startEmbeddedDaemon() and tryConnect() from firing post-wizard
- Stop deleting BDB database/ dir during cleanup — caused LSN mismatch
  that salvaged wallet.dat into wallet.{timestamp}.bak
- Add banlist.dat, db.log, .lock to cleanup file list
- Fatal extraction failure for blocks/ and chainstate/ files
- Verification progress: split SHA-256 (0-50%) and MD5 (50-100%)

Theme system:
- Expand overlay merge to apply ALL sections (tabs, dialogs, components,
  screens, flat sections), not just theme+backdrop+effects
- Add screens and security section parsing to UISchema
- Build-time theme expansion via expand_themes.py (CMake + build.sh)

Other:
- Version bump to 1.1.0
- WalletState::clear() resets all fields (sync, daemon info, etc.)
- Sidebar item-height 42 → 36
2026-03-17 18:49:46 -05:00
dan_s 4a841fd032 daemon version check, idle mining control, bootstrap mirror, import key paste, and cleanup
- Add startup binary version checking for dragonxd/xmrig
- Display daemon version in UI
- Add idle mining thread count adjustment
- Add bootstrap mirror option (bootstrap2.dragonx.is) in setup wizard
- Add paste button to import private key dialog with address validation
- Add z-address generation UI feedback (loading indicator)
- Add option to delete blockchain data while preserving wallet.dat
- Add font scale slider hotkey tooltip (Ctrl+Plus/Ctrl+Minus)
- Fix Windows RPC auth: trim \r from config values, add .cookie fallback
- Fix connection status message during block index loading
- Improve application shutdown to prevent lingering background process
2026-03-17 14:57:12 -05:00
dan_s f0c87e4092 update version to v1.0.2 2026-03-12 02:29:08 -05:00
dan_s c5ef4899bb fix: remove D3D11 debug layer flag that prevented startup on user machines
DRAGONX_DEBUG was defined unconditionally, causing D3D11CreateDevice() to
request the debug layer via D3D11_CREATE_DEVICE_DEBUG. This layer is only
available on machines with the Windows SDK or Graphics Tools installed,
so the call fails with DXGI_ERROR_SDK_COMPONENT_MISSING on regular user
machines — causing the app to silently exit.
2026-03-12 00:13:27 -05:00
dan_s 36b67e69d0 fix xmrig bundling issues 2026-03-11 21:14:03 -05:00
dan_s 06c80ef51c fix scrolling bug 2026-03-11 03:15:31 -05:00
dan_s 6bd5341507 build: Linux release outputs binaries zip + AppImage, bundle sapling params
- Linux --linux-release now produces both ObsidianDragon-Linux-x64.zip
  (raw binaries) and ObsidianDragon.AppImage (single-file)
- Windows --win-release keeps standalone exe alongside zip with binaries
- Bundle sapling-spend.params and sapling-output.params in Linux release
2026-03-11 01:38:59 -05:00
dan_s 5284c0dbb6 ui: add idle delay combo to mining tab
Add inline combo box (30s/1m/2m/5m/10m) next to the idle mining
toggle so users can choose how long to wait before idle mining starts.
2026-03-11 01:38:48 -05:00
dan_s cf520fdf40 ui: reorganize settings page with collapsible sections
- Rename APPEARANCE section to THEME & LANGUAGE
- Move font scale slider out of effects into main section
- Collapse visual effects into "Advanced Effects..." toggle
- Collapse wallet tools into "Tools & Actions..." toggle
- Remove redundant Tools & Actions divider/section from wallet card
- Add i18n strings: theme_language, advanced_effects, tools_actions
2026-03-11 01:38:40 -05:00
dan_s 96c27bb949 feat: Full UI internationalization, pool hashrate stats, and layout caching
- Replace all hardcoded English strings with TR() translation keys across
  every tab, dialog, and component (~20 UI files)
- Expand all 8 language files (de, es, fr, ja, ko, pt, ru, zh) with
  complete translations (~37k lines added)
- Improve i18n loader with exe-relative path fallback and English base
  fallback for missing keys
- Add pool-side hashrate polling via pool stats API in xmrig_manager
- Introduce Layout::beginFrame() per-frame caching and refresh balance
  layout config only on schema generation change
- Offload daemon output parsing to worker thread
- Add CJK subset fallback font for Chinese/Japanese/Korean glyphs
2026-03-11 00:40:50 -05:00
dan_s cc617dd5be Add mine-when-idle, default banlist, and console parsing improvements
Mine-when-idle:
- Auto-start/stop mining based on system idle time detection
- Platform::getSystemIdleSeconds() via XScreenSaver (Linux) / GetLastInputInfo (Win)
- Settings: mine_when_idle toggle + configurable delay (30s–10m)
- Settings page UI with checkbox and delay combo

Console tab:
- Shell-like argument parsing with quote and JSON bracket support
- Pass JSON objects/arrays directly as RPC params
- Fix selection indices when lines are evicted from buffer

Connection & status bar:
- Reduce RPC connect timeout to 1s for localhost fast-fail
- Fast retry timer on daemon startup and external daemon detection
- Show pool mining hashrate in status bar; sidebar badge reflects pool state

UI polish:
- Add logo to About card in settings; expose logo dimensions on App
- Header title offset-y support; adjust content-area margins
- Fix banned peers row cursor position (rawRowPosB.x)

Branding:
- Update copyright to "DragonX Developers" in RC and About section
- Replace logo/icon assets with updated versions

Misc:
- setup.sh: checkout dragonx branch before pulling
- Remove stale prebuilt-binaries/xmrig/.gitkeep
2026-03-07 13:42:31 -06:00
dan_s 653a90de62 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
2026-03-05 22:43:27 -06:00
dan_s 4b16a2a2c4 improve diagnostics, security UX, and network tab refresh
Diagnostics & logging:
- add verbose logging system (VERBOSE_LOGF) with toggle in Settings
- forward app-level log messages to Console tab for in-UI visibility
- add detailed connection attempt logging (attempt #, daemon state,
  config paths, auth failures, port owner identification)
- detect HTTP 401 auth failures and show actionable error messages
- identify port owner process (PID + name) on both Linux and Windows
- demote noisy acrylic/shader traces from DEBUG_LOGF to VERBOSE_LOGF
- persist verbose_logging preference in settings.json
- link iphlpapi on Windows for GetExtendedTcpTable

Security & encryption:
- update local encryption state immediately after encryptwallet RPC
  so Settings reflects the change before daemon restarts
- show notifications for encrypt success/failure and PIN skip
- use dedicated RPC client for z_importwallet during decrypt flow
  to avoid blocking main rpc_ curl_mutex (which starved peer/tx refresh)
- force full state refresh (addresses, transactions, peers) after
  successful wallet import

Network tab:
- redesign peers refresh button as glass-panel with icon + label,
  matching the mining button style
- add spinning arc animation while peer data is loading
  (peer_refresh_in_progress_ atomic flag set/cleared in refreshPeerInfo)
- prevent double-click spam during refresh
- add refresh-button size to ui.toml

Other:
- use fast_rpc_ for rescan polling to avoid blocking on main rpc_
- enable DRAGONX_DEBUG in all build configs (was debug-only)
- setup.sh: pull latest xmrig-hac when repo already exists
2026-03-05 05:26:04 -06:00
dan_s c51d3dafff fix text shifting in status bar from font scale changes 2026-03-05 01:29:03 -06:00
dan_s 68c2a59d09 improved font scaling text and window adjustment, added ctrl + scroll hotkey for font scaling 2026-03-05 01:22:20 -06:00
dan_s 45a2ccd9f3 refresh network info instantly when switching to network tab 2026-03-04 15:16:32 -06:00
dan_s 0ca1caf148 feat: RPC caching, background decrypt import, fast-lane peers, mining fix
RPC client:
- Add call() overload with per-call timeout parameter
- z_exportwallet uses 300s, z_importwallet uses 1200s timeout

Decrypt wallet (app_security.cpp, app.cpp):
- Show per-step and overall elapsed timers during decrypt flow
- Reduce dialog to 5 steps; close before key import begins
- Run z_importwallet on detached background thread
- Add pulsing "Importing keys..." status bar indicator
- Report success/failure via notifications instead of dialog

RPC caching (app_network.cpp, app.h):
- Cache z_viewtransaction results in viewtx_cache_ across refresh cycles
- Skip RPC calls for already-cached txids (biggest perf win)
- Build confirmed_tx_cache_ for deeply-confirmed transactions
- Clear all caches on disconnect
- Remove unused refreshTransactions() dead code

Peers (app_network.cpp, peers_tab.cpp):
- Route refreshPeerInfo() through fast_worker_ to avoid head-of-line blocking
- Replace footer "Refresh Peers" button with ICON_MD_REFRESH in toggle header
- Refresh button triggers both peer list and full blockchain data refresh

Mining (mining_tab.cpp):
- Allow pool mining toggle when blockchain is not synced
- Pool mining only needs xmrig, not local daemon sync
2026-03-04 15:12:24 -06:00
dan_s 7fb1f1de9d Rename hush→dragonx across wallet codebase
- Rename RESOURCE_HUSHD/HUSH_CLI/HUSH_TX to RESOURCE_DRAGONXD/DRAGONX_CLI/DRAGONX_TX
- Remove unused .bat resource constants (DRAGONXD_BAT, DRAGONX_CLI_BAT)
- Update INCBIN symbols: g_hushd_exe → g_dragonxd_exe, etc.
- Update daemon search paths, removing hush-arrakis-chain fallbacks
- Update process detection (Windows findProcessByName, Linux /proc/comm, macOS pgrep)
- Update build.sh: embed dragonxd.exe/dragonx-cli.exe/dragonx-tx.exe
- Overhaul setup.sh: fix binary names, release paths, add -j passthrough
- Update getDaemonPath/needsDaemonExtraction/hasDaemonAvailable for new names
2026-03-04 03:17:32 -06:00
dan_s 386cc857b0 setup script improvements, automatically clone xmrig-hac and build for multiple platforms 2026-03-03 01:47:44 -06:00
dan_s 3e6136983a update links 2026-03-03 01:20:03 -06:00
dan_s 2c1862aed3 change release output names 2026-02-28 15:28:40 -06:00
dan_s 4b815fc9d1 feat: blockchain rescan via daemon restart + status bar progress
- Fix z_importwallet to use full path instead of filename only
- Add rescanBlockchain() method that restarts daemon with -rescan flag
- Track rescan progress via daemon output parsing and getrescaninfo RPC
- Display rescan progress in status bar with animated indicator when starting
- Improve dark theme card contrast: lighter surface-variant, tinted borders, stronger rim-light
2026-02-28 15:06:35 -06:00
65 changed files with 7619 additions and 5667 deletions
+1
View File
@@ -37,6 +37,7 @@ asmap.dat
/memory
/todo.md
/.github/
/ObsidianDragon-agent/
# macOS
.DS_Store
+26 -1
View File
@@ -3,8 +3,19 @@
# Released under the GPLv3
cmake_minimum_required(VERSION 3.20)
# macOS: set deployment target and universal architectures BEFORE project()
# so they propagate to all targets, including FetchContent dependencies (SDL3, etc.)
if(APPLE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum macOS version" FORCE)
# Build universal binary (Apple Silicon + Intel) unless the user explicitly set architectures
if(NOT DEFINED CMAKE_OSX_ARCHITECTURES OR CMAKE_OSX_ARCHITECTURES STREQUAL "")
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures" FORCE)
endif()
endif()
project(ObsidianDragon
VERSION 1.1.0
VERSION 1.2.0
LANGUAGES C CXX
DESCRIPTION "DragonX Cryptocurrency Wallet"
)
@@ -408,6 +419,20 @@ configure_file(
@ONLY
)
# INCBIN uses .incbin assembler directives that reference font files at
# assembly time — CMake doesn't track these implicit dependencies.
# Tell CMake that the generated source depends on the actual font binaries
# so a font file change triggers recompilation.
set_source_files_properties(
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
PROPERTIES OBJECT_DEPENDS
"${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-R.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
)
add_executable(ObsidianDragon
${APP_SOURCES}
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
+52 -12
View File
@@ -20,7 +20,7 @@
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERSION="1.0.0"
VERSION="1.2.0"
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
@@ -197,7 +197,14 @@ bundle_linux_daemon() {
# ═══════════════════════════════════════════════════════════════════════════════
build_dev() {
header "Dev Build ($(uname -s) / $BUILD_TYPE)"
local bd="$SCRIPT_DIR/build/linux"
# Use platform-appropriate build directory
if [[ "$(uname -s)" == "Darwin" ]]; then
local bd="$SCRIPT_DIR/build/mac"
export MACOSX_DEPLOYMENT_TARGET="11.0"
else
local bd="$SCRIPT_DIR/build/linux"
fi
if $CLEAN; then
info "Cleaning $bd ..."; rm -rf "$bd"
@@ -261,7 +268,7 @@ build_release_linux() {
rm -rf "$out"
mkdir -p "$out"
local DIST="ObsidianDragon-Linux-x64"
local DIST="ObsidianDragon-${VERSION}-Linux-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
@@ -379,9 +386,9 @@ APPRUN
local ARCH
ARCH=$(uname -m)
cd "$bd"
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${ARCH}.AppImage" 2>/dev/null && {
cp "ObsidianDragon-${ARCH}.AppImage" "$out/ObsidianDragon.AppImage"
info "AppImage: $out/ObsidianDragon.AppImage ($(du -h "$out/ObsidianDragon.AppImage" | cut -f1))"
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
cp "ObsidianDragon-${VERSION}-${ARCH}.AppImage" "$out/ObsidianDragon-${VERSION}.AppImage"
info "AppImage: $out/ObsidianDragon-${VERSION}.AppImage ($(du -h "$out/ObsidianDragon-${VERSION}.AppImage" | cut -f1))"
} || warn "AppImage creation failed — binaries zip still in release/linux/"
info "Linux release artifacts: $out/"
@@ -604,7 +611,7 @@ HDR
rm -rf "$out"
mkdir -p "$out"
local DIST="ObsidianDragon-Windows-x64"
local DIST="ObsidianDragon-${VERSION}-Windows-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp bin/ObsidianDragon.exe "$dist_dir/"
@@ -628,8 +635,8 @@ HDR
cp -r bin/res "$dist_dir/" 2>/dev/null || true
# ── Single-file exe (all resources embedded) ────────────────────────────
cp bin/ObsidianDragon.exe "$out/"
info "Single-file exe: $out/ObsidianDragon.exe ($(du -h "$out/ObsidianDragon.exe" | cut -f1))"
cp bin/ObsidianDragon.exe "$out/ObsidianDragon-${VERSION}.exe"
info "Single-file exe: $out/ObsidianDragon-${VERSION}.exe ($(du -h "$out/ObsidianDragon-${VERSION}.exe" | cut -f1))"
# ── Zip ──────────────────────────────────────────────────────────────────
if command -v zip &>/dev/null; then
@@ -732,7 +739,9 @@ build_release_mac() {
fi
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
else
MAC_ARCH=$(uname -m)
# Native macOS: build universal binary (arm64 + x86_64)
MAC_ARCH="universal"
export MACOSX_DEPLOYMENT_TARGET="11.0"
fi
header "Release: macOS ($MAC_ARCH$(${IS_CROSS} && echo ' — cross-compile'))"
@@ -811,12 +820,31 @@ TOOLCHAIN
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"}
else
info "Configuring (native) ..."
# Build libsodium as universal if needed
local need_sodium=false
if [[ ! -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]] && \
[[ ! -f "$SCRIPT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
need_sodium=true
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
# Rebuild if existing lib is not universal (single-arch won't link)
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then
info "Existing libsodium is not universal — rebuilding ..."
rm -rf "$SCRIPT_DIR/libs/libsodium"
need_sodium=true
fi
fi
if $need_sodium; then
info "Building libsodium (universal) ..."
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
fi
info "Configuring (native universal arm64+x86_64) ..."
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
fi
info "Building with $JOBS jobs ..."
@@ -836,6 +864,11 @@ TOOLCHAIN
else
info "Stripping ..."
strip bin/ObsidianDragon
# Verify universal binary
if command -v lipo &>/dev/null; then
info "Architecture info:"
lipo -info bin/ObsidianDragon
fi
fi
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
@@ -1019,6 +1052,13 @@ PLIST
info ".app bundle created: $APP"
# ── Zip the .app bundle ──────────────────────────────────────────────────
local APP_ZIP="ObsidianDragon-${VERSION}-macOS-${MAC_ARCH}.app.zip"
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$APP_ZIP" "ObsidianDragon.app")
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
fi
# ── Create DMG ───────────────────────────────────────────────────────────
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"
+1 -1
View File
@@ -5,7 +5,7 @@
<assemblyIdentity
type="win32"
name="DragonX.ObsidianDragon.Wallet"
version="1.1.0.0"
version="1.2.0.0"
processorArchitecture="amd64"
/>
Binary file not shown.
Binary file not shown.
+8 -1
View File
@@ -652,8 +652,9 @@
"show": "Anzeigen",
"show_hidden": "Ausgeblendete anzeigen (%d)",
"show_qr_code": "QR-Code anzeigen",
"showing_transactions": "Zeige %d–%d von %d Transaktionen (gesamt: %zu)",
"showing_transactions": "Zeige %d–%d von %d Transaktionen (gesamt: %zu)",
"simple_background": "Einfacher Hintergrund",
"slider_off": "Aus",
"start_mining": "Mining starten",
"status": "Status",
"stop_external": "Externen Daemon stoppen",
@@ -670,6 +671,12 @@
"time_hours_ago": "vor %d Stunden",
"time_minutes_ago": "vor %d Minuten",
"time_seconds_ago": "vor %d Sekunden",
"timeout_15min": "15 Min",
"timeout_1hour": "1 Stunde",
"timeout_1min": "1 Min",
"timeout_30min": "30 Min",
"timeout_5min": "5 Min",
"timeout_off": "Aus",
"to": "An",
"to_upper": "AN",
"tools": "WERKZEUGE",
+7
View File
@@ -654,6 +654,7 @@
"show_qr_code": "Mostrar Código QR",
"showing_transactions": "Mostrando %d–%d de %d transacciones (total: %zu)",
"simple_background": "Fondo simple",
"slider_off": "Apagado",
"start_mining": "Iniciar Minería",
"status": "Estado",
"stop_external": "Detener daemon externo",
@@ -670,6 +671,12 @@
"time_hours_ago": "hace %d horas",
"time_minutes_ago": "hace %d minutos",
"time_seconds_ago": "hace %d segundos",
"timeout_15min": "15 min",
"timeout_1hour": "1 hora",
"timeout_1min": "1 min",
"timeout_30min": "30 min",
"timeout_5min": "5 min",
"timeout_off": "Apagado",
"to": "Para",
"to_upper": "PARA",
"tools": "HERRAMIENTAS",
+8 -1
View File
@@ -652,8 +652,9 @@
"show": "Afficher",
"show_hidden": "Afficher masqués (%d)",
"show_qr_code": "Afficher le code QR",
"showing_transactions": "Affichage %d–%d sur %d transactions (total : %zu)",
"showing_transactions": "Affichage %d–%d sur %d transactions (total : %zu)",
"simple_background": "Arrière-plan simple",
"slider_off": "Désactivé",
"start_mining": "Démarrer le minage",
"status": "Statut",
"stop_external": "Arrêter le daemon externe",
@@ -670,6 +671,12 @@
"time_hours_ago": "il y a %d heures",
"time_minutes_ago": "il y a %d minutes",
"time_seconds_ago": "il y a %d secondes",
"timeout_15min": "15 min",
"timeout_1hour": "1 heure",
"timeout_1min": "1 min",
"timeout_30min": "30 min",
"timeout_5min": "5 min",
"timeout_off": "Désactivé",
"to": "À",
"to_upper": "À",
"tools": "OUTILS",
+8 -1
View File
@@ -652,8 +652,9 @@
"show": "表示",
"show_hidden": "非表示を表示 (%d)",
"show_qr_code": "QRコードを表示",
"showing_transactions": "%d–%d / %d 件の取引を表示中(合計:%zu)",
"showing_transactions": "%d–%d / %d 件の取引を表示中(合計:%zu)",
"simple_background": "シンプル背景",
"slider_off": "オフ",
"start_mining": "マイニング開始",
"status": "ステータス",
"stop_external": "外部デーモンを停止",
@@ -670,6 +671,12 @@
"time_hours_ago": "%d時間前",
"time_minutes_ago": "%d分前",
"time_seconds_ago": "%d秒前",
"timeout_15min": "15分",
"timeout_1hour": "1時間",
"timeout_1min": "1分",
"timeout_30min": "30分",
"timeout_5min": "5分",
"timeout_off": "オフ",
"to": "宛先",
"to_upper": "宛先",
"tools": "ツール",
+8 -1
View File
@@ -652,8 +652,9 @@
"show": "표시",
"show_hidden": "숨겨진 항목 표시 (%d)",
"show_qr_code": "QR 코드 표시",
"showing_transactions": "%d–%d / %d건의 거래 표시 중 (총: %zu)",
"showing_transactions": "%d–%d / %d건의 거래 표시 중 (총: %zu)",
"simple_background": "단순 배경",
"slider_off": "끔",
"start_mining": "채굴 시작",
"status": "상태",
"stop_external": "외부 데몬 중지",
@@ -670,6 +671,12 @@
"time_hours_ago": "%d시간 전",
"time_minutes_ago": "%d분 전",
"time_seconds_ago": "%d초 전",
"timeout_15min": "15분",
"timeout_1hour": "1시간",
"timeout_1min": "1분",
"timeout_30min": "30분",
"timeout_5min": "5분",
"timeout_off": "끔",
"to": "받는 곳",
"to_upper": "받는 곳",
"tools": "도구",
+8 -1
View File
@@ -652,8 +652,9 @@
"show": "Mostrar",
"show_hidden": "Mostrar ocultos (%d)",
"show_qr_code": "Mostrar Código QR",
"showing_transactions": "Mostrando %d–%d de %d transações (total: %zu)",
"showing_transactions": "Mostrando %d–%d de %d transações (total: %zu)",
"simple_background": "Fundo simples",
"slider_off": "Desligado",
"start_mining": "Iniciar Mineração",
"status": "Status",
"stop_external": "Parar daemon externo",
@@ -670,6 +671,12 @@
"time_hours_ago": "há %d horas",
"time_minutes_ago": "há %d minutos",
"time_seconds_ago": "há %d segundos",
"timeout_15min": "15 min",
"timeout_1hour": "1 hora",
"timeout_1min": "1 min",
"timeout_30min": "30 min",
"timeout_5min": "5 min",
"timeout_off": "Desligado",
"to": "Para",
"to_upper": "PARA",
"tools": "FERRAMENTAS",
+8 -1
View File
@@ -652,8 +652,9 @@
"show": "Показать",
"show_hidden": "Показать скрытые (%d)",
"show_qr_code": "Показать QR-код",
"showing_transactions": "Показано %d–%d из %d транзакций (всего: %zu)",
"showing_transactions": "Показано %d–%d из %d транзакций (всего: %zu)",
"simple_background": "Простой фон",
"slider_off": "Выкл",
"start_mining": "Начать майнинг",
"status": "Статус",
"stop_external": "Остановить внешний daemon",
@@ -670,6 +671,12 @@
"time_hours_ago": "%d часов назад",
"time_minutes_ago": "%d минут назад",
"time_seconds_ago": "%d секунд назад",
"timeout_15min": "15 мин",
"timeout_1hour": "1 час",
"timeout_1min": "1 мин",
"timeout_30min": "30 мин",
"timeout_5min": "5 мин",
"timeout_off": "Выкл",
"to": "Кому",
"to_upper": "КОМУ",
"tools": "ИНСТРУМЕНТЫ",
+8 -1
View File
@@ -652,8 +652,9 @@
"show": "显示",
"show_hidden": "显示已隐藏 (%d)",
"show_qr_code": "显示二维码",
"showing_transactions": "显示第 %d–%d 笔,共 %d 笔交易(总计:%zu)",
"showing_transactions": "显示第 %d–%d 笔,共 %d 笔交易(总计:%zu)",
"simple_background": "简单背景",
"slider_off": "关闭",
"start_mining": "开始挖矿",
"status": "状态",
"stop_external": "停止外部守护进程",
@@ -670,6 +671,12 @@
"time_hours_ago": "%d 小时前",
"time_minutes_ago": "%d 分钟前",
"time_seconds_ago": "%d 秒前",
"timeout_15min": "15分钟",
"timeout_1hour": "1小时",
"timeout_1min": "1分钟",
"timeout_30min": "30分钟",
"timeout_5min": "5分钟",
"timeout_off": "关闭",
"to": "至",
"to_upper": "至",
"tools": "工具",
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""
Build a NotoSansCJK subset font containing all characters used by
the zh, ja, and ko translation files, plus common CJK punctuation
and symbols.
Usage:
python3 scripts/build_cjk_subset.py
Requires: pip install fonttools brotli
"""
import json
import os
from fontTools.ttLib import TTFont
from fontTools import subset as ftsubset
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
SOURCE_FONT = '/tmp/NotoSansCJKsc-Regular.otf'
OUTPUT_FONT = os.path.join(ROOT, 'res', 'fonts', 'NotoSansCJK-Subset.ttf')
# Collect all characters used in CJK translation files
needed = set()
for lang in ['zh', 'ja', 'ko']:
path = os.path.join(LANG_DIR, f'{lang}.json')
if not os.path.exists(path):
continue
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
for v in data.values():
if isinstance(v, str):
for c in v:
cp = ord(c)
if cp > 0x7F: # non-ASCII only (ASCII handled by Ubuntu font)
needed.add(cp)
# Also add common CJK ranges that future translations might use:
# - CJK punctuation and symbols (3000-303F)
# - Hiragana (3040-309F)
# - Katakana (30A0-30FF)
# - Bopomofo (3100-312F)
# - CJK quotation marks, brackets
for cp in range(0x3000, 0x3100):
needed.add(cp)
for cp in range(0x3100, 0x3130):
needed.add(cp)
# Fullwidth ASCII variants (commonly mixed in CJK text)
for cp in range(0xFF01, 0xFF5F):
needed.add(cp)
print(f"Total non-ASCII characters to include: {len(needed)}")
# Check which of these the source font supports
font = TTFont(SOURCE_FONT)
cmap = font.getBestCmap()
supportable = needed & set(cmap.keys())
unsupported = needed - set(cmap.keys())
print(f"Supported by source font: {len(supportable)}")
if unsupported:
print(f"Not in source font (will use fallback): {len(unsupported)}")
for cp in sorted(unsupported)[:10]:
print(f" U+{cp:04X} {chr(cp)}")
# Build the subset using pyftsubset CLI-style API
args = [
SOURCE_FONT,
f'--output-file={OUTPUT_FONT}',
f'--unicodes={",".join(f"U+{cp:04X}" for cp in sorted(supportable))}',
'--no-hinting',
'--desubroutinize',
]
ftsubset.main(args)
# Convert CFF outlines to TrueType (glyf) outlines.
# stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly.
from fontTools.pens.cu2quPen import Cu2QuPen
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.ttLib import newTable
tmp_otf = OUTPUT_FONT + '.tmp.otf'
os.rename(OUTPUT_FONT, tmp_otf)
conv = TTFont(tmp_otf)
if 'CFF ' in conv:
print("Converting CFF -> TrueType outlines...")
glyphOrder = conv.getGlyphOrder()
glyphSet = conv.getGlyphSet()
glyf_table = newTable("glyf")
glyf_table.glyphs = {}
glyf_table.glyphOrder = glyphOrder
loca_table = newTable("loca")
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
for gname in glyphOrder:
try:
ttPen = TTGlyphPen(glyphSet)
cu2quPen = Cu2QuPen(ttPen, max_err=1.0, reverse_direction=True)
glyphSet[gname].draw(cu2quPen)
glyf_table.glyphs[gname] = ttPen.glyph()
except Exception:
glyf_table.glyphs[gname] = TTGlyph()
del conv['CFF ']
if 'VORG' in conv:
del conv['VORG']
conv['glyf'] = glyf_table
conv['loca'] = loca_table
conv['head'].indexToLocFormat = 1
if 'maxp' in conv:
conv['maxp'].version = 0x00010000
conv.sfntVersion = "\x00\x01\x00\x00"
conv.save(OUTPUT_FONT)
conv.close()
os.remove(tmp_otf)
size = os.path.getsize(OUTPUT_FONT)
print(f"\nOutput: {OUTPUT_FONT}")
print(f"Size: {size / 1024:.0f} KB")
# Verify
verify = TTFont(OUTPUT_FONT)
verify_cmap = set(verify.getBestCmap().keys())
still_missing = needed - verify_cmap
print(f"Verified glyphs in subset: {len(verify_cmap)}")
if still_missing:
# These are chars not in the source font - expected for some Hangul/Hiragana
print(f"Not coverable by this font: {len(still_missing)} (need additional font)")
for cp in sorted(still_missing)[:10]:
print(f" U+{cp:04X} {chr(cp)}")
else:
print("All needed characters are covered!")
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Check which characters in translation files fall outside the font glyph ranges."""
import json
import unicodedata
import glob
import os
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LANG_DIR = os.path.join(SCRIPT_DIR, '..', 'res', 'lang')
# Glyph ranges from typography.cpp (regular font + CJK merge)
RANGES = [
# Regular font ranges
(0x0020, 0x00FF), # Basic Latin + Latin-1 Supplement
(0x0100, 0x024F), # Latin Extended-A + B
(0x0370, 0x03FF), # Greek and Coptic
(0x0400, 0x04FF), # Cyrillic
(0x0500, 0x052F), # Cyrillic Supplement
(0x2000, 0x206F), # General Punctuation
(0x2190, 0x21FF), # Arrows
(0x2200, 0x22FF), # Mathematical Operators
(0x2600, 0x26FF), # Miscellaneous Symbols
# CJK ranges
(0x2E80, 0x2FDF), # CJK Radicals
(0x3000, 0x30FF), # CJK Symbols, Hiragana, Katakana
(0x3100, 0x312F), # Bopomofo
(0x31F0, 0x31FF), # Katakana Extensions
(0x3400, 0x4DBF), # CJK Extension A
(0x4E00, 0x9FFF), # CJK Unified Ideographs
(0xAC00, 0xD7AF), # Hangul Syllables
(0xFF00, 0xFFEF), # Fullwidth Forms
]
def in_ranges(cp):
return any(lo <= cp <= hi for lo, hi in RANGES)
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
lang = os.path.basename(path).replace('.json', '')
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
missing = {}
for key, val in data.items():
if not isinstance(val, str):
continue
for c in val:
cp = ord(c)
if cp > 0x7F and not in_ranges(cp):
if c not in missing:
missing[c] = []
missing[c].append(key)
if missing:
print(f"\n=== {lang}.json: {len(missing)} missing characters ===")
for c in sorted(missing, key=lambda x: ord(x)):
cp = ord(c)
name = unicodedata.name(c, 'UNKNOWN')
keys = missing[c][:3]
key_str = ', '.join(keys)
if len(missing[c]) > 3:
key_str += f' (+{len(missing[c])-3} more)'
print(f" U+{cp:04X} {c} ({name}) — used in: {key_str}")
else:
print(f"=== {lang}.json: OK (all characters covered) ===")
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Check which characters needed by translations are missing from bundled fonts."""
import json
import os
from fontTools.ttLib import TTFont
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FONTS_DIR = os.path.join(ROOT, 'res', 'fonts')
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
# Load font cmaps
cjk = TTFont(os.path.join(FONTS_DIR, 'NotoSansCJK-Subset.ttf'))
cjk_cmap = set(cjk.getBestCmap().keys())
ubuntu = TTFont(os.path.join(FONTS_DIR, 'Ubuntu-R.ttf'))
ubuntu_cmap = set(ubuntu.getBestCmap().keys())
combined = cjk_cmap | ubuntu_cmap
print(f"CJK subset font glyphs: {len(cjk_cmap)}")
print(f"Ubuntu font glyphs: {len(ubuntu_cmap)}")
print(f"Combined: {len(combined)}")
print()
for lang in ['zh', 'ja', 'ko', 'ru', 'de', 'es', 'fr', 'pt']:
path = os.path.join(LANG_DIR, f'{lang}.json')
if not os.path.exists(path):
continue
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
needed = set()
for v in data.values():
if isinstance(v, str):
for c in v:
needed.add(ord(c))
missing = sorted(needed - combined)
if missing:
print(f"{lang}.json: {len(needed)} chars needed, {len(missing)} MISSING")
for cp in missing[:20]:
c = chr(cp)
print(f" U+{cp:04X} {c}")
if len(missing) > 20:
print(f" ... and {len(missing) - 20} more")
else:
print(f"{lang}.json: OK ({len(needed)} chars, all covered)")
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Convert CJK subset from CID-keyed CFF/OTF to TrueType/TTF.
stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly,
so we need glyf-based TrueType outlines instead.
Two approaches:
1. Direct CFF->TTF conversion via cu2qu (fontTools)
2. Download NotoSansSC-Regular.ttf (already TTF) and re-subset
This script tries approach 1 first, falls back to approach 2.
"""
import os
import sys
import json
import glob
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
FONT_DIR = os.path.join(PROJECT_ROOT, "res", "fonts")
LANG_DIR = os.path.join(PROJECT_ROOT, "res", "lang")
SRC_OTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.otf")
DST_TTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.ttf")
def get_needed_codepoints():
"""Collect all unique codepoints from CJK translation files."""
codepoints = set()
for lang_file in glob.glob(os.path.join(LANG_DIR, "*.json")):
with open(lang_file, "r", encoding="utf-8") as f:
data = json.load(f)
for value in data.values():
if isinstance(value, str):
for ch in value:
cp = ord(ch)
# Include CJK + Hangul + fullwidth + CJK symbols/kana
if cp >= 0x2E80:
codepoints.add(cp)
return codepoints
def convert_cff_to_ttf():
"""Convert existing OTF/CFF font to TTF using fontTools cu2qu."""
from fontTools.ttLib import TTFont
from fontTools.pens.cu2quPen import Cu2QuPen
from fontTools.pens.ttGlyphPen import TTGlyphPen
print(f"Loading {SRC_OTF}...")
font = TTFont(SRC_OTF)
# Verify it's CFF
if "CFF " not in font:
print("Font is not CFF, skipping conversion")
return False
cff = font["CFF "]
top = cff.cff.topDictIndex[0]
print(f"ROS: {getattr(top, 'ROS', None)}")
print(f"CID-keyed: {getattr(top, 'FDSelect', None) is not None}")
glyphOrder = font.getGlyphOrder()
print(f"Glyphs: {len(glyphOrder)}")
# Use fontTools' built-in otf2ttf if available
try:
from fontTools.otf2ttf import otf_to_ttf
otf_to_ttf(font)
font.save(DST_TTF)
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
font.close()
return True
except ImportError:
pass
# Manual conversion using cu2qu
print("Using manual CFF->TTF conversion with cu2qu...")
from fontTools.pens.recordingPen import RecordingPen
from fontTools.pens.pointPen import SegmentToPointPen
from fontTools import ttLib
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
import struct
# Get glyph set
glyphSet = font.getGlyphSet()
# Create new glyf table
from fontTools.ttLib import newTable
glyf_table = newTable("glyf")
glyf_table.glyphs = {}
glyf_table.glyphOrder = glyphOrder
loca_table = newTable("loca")
max_error = 1.0 # em-units tolerance for cubic->quadratic
for gname in glyphOrder:
try:
ttPen = TTGlyphPen(glyphSet)
cu2quPen = Cu2QuPen(ttPen, max_err=max_error, reverse_direction=True)
glyphSet[gname].draw(cu2quPen)
glyf_table.glyphs[gname] = ttPen.glyph()
except Exception as e:
# Fallback: empty glyph
glyf_table.glyphs[gname] = TTGlyph()
# Replace CFF with glyf
del font["CFF "]
if "VORG" in font:
del font["VORG"]
font["glyf"] = glyf_table
font["loca"] = loca_table
# Add required tables for TTF
# head table needs indexToLocFormat
font["head"].indexToLocFormat = 1 # long format
# Create maxp for TrueType
if "maxp" in font:
font["maxp"].version = 0x00010000
# Update sfntVersion
font.sfntVersion = "\x00\x01\x00\x00" # TrueType
font.save(DST_TTF)
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
font.close()
return True
def download_and_subset():
"""Download NotoSansSC-Regular.ttf and subset it."""
import urllib.request
from fontTools.ttLib import TTFont
from fontTools import subset
# Google Fonts provides static TTF files
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/SubsetOTF/SC/NotoSansSC-Regular.otf"
# Actually, we want TTF. Let's try the variable font approach.
# Or better: use google-fonts API for static TTF
# NotoSansSC static TTF from Google Fonts CDN
tmp_font = "/tmp/NotoSansSC-Regular.ttf"
if not os.path.exists(tmp_font):
print(f"Downloading NotoSansSC-Regular.ttf...")
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTC/NotoSansCJK-Regular.ttc"
# This is a TTC (font collection), too large.
# Use the OTF we already have and convert it.
return False
print(f"Using {tmp_font}")
font = TTFont(tmp_font)
cmap = font.getBestCmap()
print(f"Source has {len(cmap)} cmap entries")
needed = get_needed_codepoints()
print(f"Need {len(needed)} CJK codepoints")
# Subset
subsetter = subset.Subsetter()
subsetter.populate(unicodes=needed)
subsetter.subset(font)
font.save(DST_TTF)
print(f"Saved: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
font.close()
return True
def verify_result():
"""Verify the output TTF has glyf outlines and correct characters."""
from fontTools.ttLib import TTFont
font = TTFont(DST_TTF)
cmap = font.getBestCmap()
print(f"\n--- Verification ---")
print(f"Format: {font.sfntVersion!r}")
print(f"Has glyf: {'glyf' in font}")
print(f"Has CFF: {'CFF ' in font}")
print(f"Cmap entries: {len(cmap)}")
# Check key characters
test_chars = {
"历": 0x5386, "史": 0x53F2, # Chinese: history
"概": 0x6982, "述": 0x8FF0, # Chinese: overview
"设": 0x8BBE, "置": 0x7F6E, # Chinese: settings
}
for name, cp in test_chars.items():
status = "YES" if cp in cmap else "NO"
print(f" {name} (U+{cp:04X}): {status}")
size = os.path.getsize(DST_TTF)
print(f"File size: {size} bytes ({size/1024:.1f} KB)")
font.close()
if __name__ == "__main__":
print("=== CJK Font CFF -> TTF Converter ===\n")
if convert_cff_to_ttf():
verify_result()
else:
print("Direct conversion failed, trying download approach...")
if download_and_subset():
verify_result()
else:
print("ERROR: Could not convert font")
sys.exit(1)
+1 -1
View File
@@ -8,7 +8,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="${SCRIPT_DIR}/build/linux"
APPDIR="${BUILD_DIR}/AppDir"
VERSION="1.0.0"
VERSION="1.2.0"
# Colors
GREEN='\033[0;32m'
+77 -6
View File
@@ -50,12 +50,20 @@ if [[ ! -f "$TARBALL" ]]; then
curl -fSL -o "$TARBALL" "$SODIUM_URL"
fi
# Verify checksum
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
rm -f "$TARBALL"
exit 1
}
# Verify checksum (sha256sum on Linux, shasum on macOS)
if command -v sha256sum &>/dev/null; then
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
rm -f "$TARBALL"
exit 1
}
elif command -v shasum &>/dev/null; then
echo "$SODIUM_SHA256 $TARBALL" | shasum -a 256 -c - || {
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
rm -f "$TARBALL"
exit 1
}
fi
# ── Extract ─────────────────────────────────────────────────────────────────
if [[ ! -d "$SRC_DIR" ]]; then
@@ -115,6 +123,69 @@ case "$TARGET" in
;;
esac
# ── Native macOS: build universal binary (arm64 + x86_64) ───────────────────
IS_MACOS_NATIVE=false
if [[ "$TARGET" == "native" && "$(uname -s)" == "Darwin" ]]; then
IS_MACOS_NATIVE=true
fi
if $IS_MACOS_NATIVE; then
echo "[fetch-libsodium] Building universal (arm64 + x86_64) for macOS..."
export MACOSX_DEPLOYMENT_TARGET="11.0"
INSTALL_ARM64="$PROJECT_DIR/libs/libsodium-arm64"
INSTALL_X86_64="$PROJECT_DIR/libs/libsodium-x86_64"
for ARCH in arm64 x86_64; do
echo "[fetch-libsodium] Building for $ARCH..."
cd "$SRC_DIR"
make clean 2>/dev/null || true
make distclean 2>/dev/null || true
if [[ "$ARCH" == "arm64" ]]; then
ARCH_INSTALL="$INSTALL_ARM64"
HOST_TRIPLE="aarch64-apple-darwin"
else
ARCH_INSTALL="$INSTALL_X86_64"
HOST_TRIPLE="x86_64-apple-darwin"
fi
ARCH_CFLAGS="-arch $ARCH -mmacosx-version-min=11.0"
./configure \
--prefix="$ARCH_INSTALL" \
--disable-shared \
--enable-static \
--with-pic \
--host="$HOST_TRIPLE" \
CFLAGS="$ARCH_CFLAGS" \
LDFLAGS="-arch $ARCH" \
> /dev/null
make -j"$(sysctl -n hw.ncpu 2>/dev/null || echo 4)" > /dev/null 2>&1
make install > /dev/null
done
# Merge with lipo
echo "[fetch-libsodium] Creating universal binary with lipo..."
mkdir -p "$INSTALL_DIR/lib" "$INSTALL_DIR/include"
lipo -create \
"$INSTALL_ARM64/lib/libsodium.a" \
"$INSTALL_X86_64/lib/libsodium.a" \
-output "$INSTALL_DIR/lib/libsodium.a"
cp -R "$INSTALL_ARM64/include/"* "$INSTALL_DIR/include/"
# Clean up per-arch builds
rm -rf "$INSTALL_ARM64" "$INSTALL_X86_64"
cd "$PROJECT_DIR"
rm -rf "$SRC_DIR"
rm -f "$TARBALL"
echo "[fetch-libsodium] Done (universal): $INSTALL_DIR/lib/libsodium.a"
lipo -info "$INSTALL_DIR/lib/libsodium.a"
exit 0
fi
echo "[fetch-libsodium] Configuring for target: $TARGET ..."
./configure "${CONFIGURE_ARGS[@]}" > /dev/null
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Fix mojibake en-dash (and other common patterns) in translation JSON files."""
import os
import glob
LANG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'res', 'lang')
# Common mojibake patterns: UTF-8 bytes interpreted as Latin-1
MOJIBAKE_FIXES = {
'\u00e2\u0080\u0093': '\u2013', # en dash
'\u00e2\u0080\u0094': '\u2014', # em dash
'\u00e2\u0080\u0099': '\u2019', # right single quote
'\u00e2\u0080\u009c': '\u201c', # left double quote
'\u00e2\u0080\u009d': '\u201d', # right double quote
'\u00e2\u0080\u00a6': '\u2026', # ellipsis
}
total_fixed = 0
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
with open(path, 'r', encoding='utf-8') as f:
raw = f.read()
original = raw
for bad, good in MOJIBAKE_FIXES.items():
if bad in raw:
count = raw.count(bad)
raw = raw.replace(bad, good)
lang = os.path.basename(path)
print(f" {lang}: fixed {count} x {repr(good)}")
total_fixed += count
if raw != original:
with open(path, 'w', encoding='utf-8') as f:
f.write(raw)
print(f"\nTotal fixes: {total_fixed}")
+1 -1
View File
@@ -7,7 +7,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="${SCRIPT_DIR}/build/linux"
VERSION="1.0.0"
VERSION="1.2.0"
# Colors for output
RED='\033[0;31m'
+226 -41
View File
@@ -1,6 +1,9 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// app.cpp — Main application: init, shutdown, ImGui render loop, NavPage
// dispatch, dialog rendering, and frame-level state management.
#include "app.h"
#include "config/version.h"
@@ -313,7 +316,8 @@ void App::update()
// Track user interaction for auto-lock
if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0 ||
io.MouseClicked[0] || io.MouseClicked[1] ||
io.InputQueueCharacters.Size > 0) {
io.InputQueueCharacters.Size > 0 ||
ImGui::IsAnyItemActive()) {
last_interaction_ = std::chrono::steady_clock::now();
}
@@ -340,7 +344,10 @@ void App::update()
}
// Update timers
refresh_timer_ += io.DeltaTime;
core_timer_ += io.DeltaTime;
address_timer_ += io.DeltaTime;
transaction_timer_ += io.DeltaTime;
peer_timer_ += io.DeltaTime;
price_timer_ += io.DeltaTime;
fast_refresh_timer_ += io.DeltaTime;
tx_age_timer_ += io.DeltaTime;
@@ -564,31 +571,64 @@ void App::update()
};
}
}
return [this, done, anySuccess]() {
// Extract txids from successful operations so shielded
// sends are discoverable by z_viewtransaction.
std::vector<std::string> successTxids;
for (const auto& op : result) {
if (op.value("status", "") == "success"
&& op.contains("result") && op["result"].contains("txid")) {
successTxids.push_back(op["result"]["txid"].get<std::string>());
}
}
return [this, done, anySuccess,
successTxids = std::move(successTxids)]() {
for (const auto& id : done) {
pending_opids_.erase(
std::remove(pending_opids_.begin(), pending_opids_.end(), id),
pending_opids_.end());
}
if (anySuccess) {
for (const auto& txid : successTxids) {
send_txids_.insert(txid);
}
// Transaction confirmed by daemon — force immediate data refresh
transactions_dirty_ = true;
addresses_dirty_ = true;
last_tx_block_height_ = -1;
refresh_timer_ = REFRESH_INTERVAL;
core_timer_ = active_core_interval_;
transaction_timer_ = active_tx_interval_;
address_timer_ = active_addr_interval_;
}
};
});
}
// Regular refresh every 5 seconds
// Per-category refresh with tab-aware intervals
// Skip when wallet is locked — same reason as above.
if (refresh_timer_ >= REFRESH_INTERVAL) {
refresh_timer_ = 0.0f;
if (state_.connected && !state_.isLocked()) {
refreshData();
} else if (!connection_in_progress_ &&
wizard_phase_ == WizardPhase::None) {
if (state_.connected && !state_.isLocked()) {
if (core_timer_ >= active_core_interval_) {
core_timer_ = 0.0f;
refreshCoreData();
}
// Skip balance/tx/address refresh during warmup — RPC calls fail with -28
if (!state_.warming_up) {
if (transaction_timer_ >= active_tx_interval_) {
transaction_timer_ = 0.0f;
refreshTransactionData();
}
if (address_timer_ >= active_addr_interval_) {
address_timer_ = 0.0f;
refreshAddressData();
}
}
if (peer_timer_ >= active_peer_interval_) {
peer_timer_ = 0.0f;
refreshPeerInfo();
}
} else if (core_timer_ >= active_core_interval_) {
core_timer_ = 0.0f;
if (!connection_in_progress_ &&
wizard_phase_ == WizardPhase::None) {
tryConnect();
}
}
@@ -603,7 +643,7 @@ void App::update()
// Keyboard shortcut: Ctrl+, to open Settings page
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Comma)) {
current_page_ = ui::NavPage::Settings;
setCurrentPage(ui::NavPage::Settings);
}
// Keyboard shortcut: Ctrl+Left/Right to cycle themes
@@ -988,7 +1028,8 @@ void App::render()
bool prevCollapsed = sidebar_collapsed_;
{
PERF_SCOPE("Render.Sidebar");
ui::RenderSidebar(current_page_, sidebarW, sidebarH, sbStatus, sidebar_collapsed_);
ui::RenderSidebar(current_page_, sidebarW, sidebarH, sbStatus, sidebar_collapsed_,
state_.isLocked());
}
if (sbStatus.exitClicked) {
requestQuit();
@@ -1003,7 +1044,7 @@ void App::render()
// Page transition: detect change, ramp alpha
if (current_page_ != prev_page_) {
page_alpha_ = ui::effects::isLowSpecMode() ? 1.0f : 0.0f;
page_alpha_ = (ui::effects::isLowSpecMode() || (settings_ && settings_->getReduceMotion())) ? 1.0f : 0.0f;
prev_page_ = current_page_;
}
if (page_alpha_ < 1.0f) {
@@ -1040,16 +1081,17 @@ void App::render()
int fgVtxStart = fgDL->VtxBuffer.Size;
// ---------------------------------------------------------------
// Loading overlay — show only while daemon is not yet connected.
// Once connected, all tabs render normally; individual tabs show
// inline sync-progress indicators when the chain is still syncing.
// Pages that remain accessible even without a connection:
// Console, Peers, Settings
// Loading overlay — show only on tabs that truly need wallet data.
// Tabs that work without full daemon connection:
// Console, Peers, Settings, Market, Mining
// Tabs that need balance/transaction data (show overlay):
// Overview, Send, Receive, History
// ---------------------------------------------------------------
bool pageNeedsDaemon = (current_page_ != ui::NavPage::Console &&
current_page_ != ui::NavPage::Peers &&
current_page_ != ui::NavPage::Settings);
bool daemonReady = state_.connected; // don't gate on sync state
bool pageNeedsWalletData = (current_page_ == ui::NavPage::Overview ||
current_page_ == ui::NavPage::Send ||
current_page_ == ui::NavPage::Receive ||
current_page_ == ui::NavPage::History);
bool daemonReady = state_.connected && !state_.warming_up;
// Don't show lock screen while pool mining — xmrig runs independently
// of the wallet and locking would block the mining UI needlessly.
@@ -1057,7 +1099,24 @@ void App::render()
if (state_.isLocked() && !poolMiningActive) {
// Lock screen — covers tab content just like the loading overlay
renderLockScreen();
} else if (pageNeedsDaemon && (!daemonReady || (state_.connected && !state_.encryption_state_known))) {
} else if (pageNeedsWalletData && state_.warming_up) {
// Daemon is reachable but still initializing — show warmup overlay
// only on wallet-data tabs. Other tabs render normally.
lock_screen_was_visible_ = false;
renderLoadingOverlay(contentH);
} else if (pageNeedsWalletData && (!daemonReady || (state_.connected && !state_.encryption_state_known))) {
// Track how long we've been waiting for encryption state
if (state_.connected && !state_.encryption_state_known) {
encryption_check_timer_ += ImGui::GetIO().DeltaTime;
if (encryption_check_timer_ >= 15.0f) {
DEBUG_LOGF("[App] Encryption state check timed out after 15s — assuming unencrypted\n");
state_.encryption_state_known = true;
state_.encrypted = false;
encryption_check_timer_ = 0.0f;
}
} else {
encryption_check_timer_ = 0.0f;
}
// Reset lock screen focus flag so it auto-focuses next time
lock_screen_was_visible_ = false;
// Show loading overlay instead of tab content
@@ -1095,9 +1154,10 @@ void App::render()
case ui::NavPage::Console:
// Use fast-lane worker for console commands to avoid head-of-line
// blocking behind the consolidated refreshData() batch.
// Fall back to main rpc/worker if fast-lane hasn't connected yet.
console_tab_.render(embedded_daemon_.get(),
fast_rpc_ ? fast_rpc_.get() : rpc_.get(),
fast_worker_ ? fast_worker_.get() : worker_.get(),
(fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get(),
(fast_rpc_ && fast_rpc_->isConnected() && fast_worker_) ? fast_worker_.get() : worker_.get(),
xmrig_manager_.get());
break;
case ui::NavPage::Settings:
@@ -1370,7 +1430,16 @@ void App::renderStatusBar()
// Connection status
float dotOpacity = S.drawElement("components.status-bar", "connection-dot").opacity;
if (dotOpacity < 0.0f) dotOpacity = 1.0f;
if (state_.connected) {
if (state_.warming_up) {
ImGui::PushFont(ui::material::Type().iconSmall());
ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, dotOpacity), ICON_MD_CIRCLE);
ImGui::PopFont();
ImGui::SameLine(0, sbIconTextGap);
// Show truncated warmup status (e.g. "Activating best chain... (Block 12345)")
const char* warmupText = state_.warmup_status.empty()
? "Warming up..." : state_.warmup_status.c_str();
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", warmupText);
} else if (state_.connected) {
ImGui::PushFont(ui::material::Type().iconSmall());
ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, dotOpacity), ICON_MD_CIRCLE);
ImGui::PopFont();
@@ -1410,8 +1479,40 @@ void App::renderStatusBar()
int chainTip = state_.longestchain > 0 ? state_.longestchain : state_.sync.headers;
int blocksLeft = chainTip - state_.sync.blocks;
if (blocksLeft < 0) blocksLeft = 0;
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left)",
state_.sync.verification_progress * 100.0, blocksLeft);
// Calculate sync speed (blocks/sec) using a smoothed rolling average
static int s_prev_blocks = 0;
static double s_prev_time = 0.0;
static double s_blocks_per_sec = 0.0;
double now = ImGui::GetTime();
if (s_prev_time == 0.0) {
// First sample — seed baseline, don't compute rate yet
s_prev_blocks = state_.sync.blocks;
s_prev_time = now;
} else if (state_.sync.blocks > s_prev_blocks) {
double dt = now - s_prev_time;
if (dt > 0.5) {
double raw = (state_.sync.blocks - s_prev_blocks) / dt;
// Exponential smoothing (alpha ~0.3 per update)
s_blocks_per_sec = s_blocks_per_sec > 0.0
? s_blocks_per_sec * 0.7 + raw * 0.3
: raw;
s_prev_blocks = state_.sync.blocks;
s_prev_time = now;
}
} else if (now - s_prev_time > 10.0) {
// No new blocks for 10s — decay the displayed rate
s_blocks_per_sec *= 0.5;
s_prev_time = now;
}
if (s_blocks_per_sec > 0.1) {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left, %.0f blk/s)",
state_.sync.verification_progress * 100.0, blocksLeft, s_blocks_per_sec);
} else {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Syncing %.1f%% (%d left)",
state_.sync.verification_progress * 100.0, blocksLeft);
}
} else if (state_.connected) {
ImGui::Text("Peers: %zu", state_.peers.size());
}
@@ -1869,7 +1970,11 @@ void App::renderAntivirusHelpDialog()
void App::refreshNow()
{
refresh_timer_ = REFRESH_INTERVAL; // Trigger immediate refresh
// Trigger immediate refresh on all categories
core_timer_ = active_core_interval_;
transaction_timer_ = active_tx_interval_;
address_timer_ = active_addr_interval_;
peer_timer_ = active_peer_interval_;
transactions_dirty_ = true; // Force transaction list update
addresses_dirty_ = true; // Force address/balance update
last_tx_block_height_ = -1; // Reset tx cache
@@ -1892,7 +1997,7 @@ void App::handlePaymentURI(const std::string& uri)
pending_label_ = payment.label;
// Switch to Send page
current_page_ = ui::NavPage::Send;
setCurrentPage(ui::NavPage::Send);
// Notify user
std::string msg = "Payment request loaded";
@@ -1919,7 +2024,7 @@ void App::setCurrentTab(int tab) {
ui::NavPage::Settings, // 9 = Settings
};
if (tab >= 0 && tab < static_cast<int>(sizeof(kTabMap)/sizeof(kTabMap[0])))
current_page_ = kTabMap[tab];
setCurrentPage(kTabMap[tab]);
}
bool App::startEmbeddedDaemon()
@@ -2058,6 +2163,7 @@ bool App::startEmbeddedDaemon()
// Sync debug logging categories from user settings
if (settings_) {
embedded_daemon_->setDebugCategories(settings_->getDebugCategories());
embedded_daemon_->setMaxConnections(settings_->getMaxConnections());
}
return embedded_daemon_->start();
@@ -2124,15 +2230,19 @@ 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(500));
DEBUG_LOGF("Waiting for daemon to flush block index and shut down...\n");
shutdown_status_ = "Waiting for daemon to flush block index...";
// Give the daemon time to flush LevelDB to disk before we
// escalate to SIGTERM. On macOS/APFS, LevelDB compaction +
// fsync can take 15-20s on a large chain. The stop() method
// will wait this long for a *natural* exit (via the RPC stop
// we already sent) before falling back to SIGTERM.
}
// 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(10000);
// 20s grace period for the RPC "stop" to complete (LevelDB flush).
// Only after that does stop() escalate to SIGTERM, then SIGKILL.
embedded_daemon_->stop(20000);
}
bool App::isEmbeddedDaemonRunning() const
@@ -2358,11 +2468,14 @@ void App::renderShutdownScreen()
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.06f, 0.06f, 0.08f, 0.92f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::Begin("##ShutdownOverlay", nullptr,
ImGuiWindowFlags shutdownFlags =
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav |
ImGuiWindowFlags_NoSavedSettings);
ImGuiWindowFlags_NoSavedSettings;
// Allow input after 10s so Force Quit button is clickable
if (shutdown_timer_ < 10.0f)
shutdownFlags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav;
ImGui::Begin("##ShutdownOverlay", nullptr, shutdownFlags);
ImDrawList* dl = ImGui::GetWindowDrawList();
// Convert local centre to screen coords for draw-list primitives
@@ -2460,6 +2573,63 @@ void App::renderShutdownScreen()
ImGui::PopFont();
}
// -------------------------------------------------------------------
// 4b. Force Quit button — appears after 10 seconds
// -------------------------------------------------------------------
if (shutdown_timer_ >= 10.0f) {
ImGui::Spacing();
ImGui::Spacing();
const char* forceLabel = TR("force_quit");
ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0);
ImGui::SetCursorPosX(cx - btnSize.x * 0.5f);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
if (ImGui::Button(forceLabel, btnSize)) {
force_quit_confirm_ = true;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", TR("force_quit_warning"));
}
ImGui::PopStyleColor(3);
}
// Force Quit confirmation popup
if (force_quit_confirm_) {
ImGui::OpenPopup("##ForceQuitConfirm");
force_quit_confirm_ = false;
}
ImVec2 popupCenter(wp.x + vp_size.x * 0.5f, wp.y + vp_size.y * 0.5f);
ImGui::SetNextWindowPos(popupCenter, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("##ForceQuitConfirm", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar)) {
ImGui::PushFont(Type().subtitle1());
ImGui::TextUnformatted(TR("force_quit_confirm_title"));
ImGui::PopFont();
ImGui::Spacing();
ImGui::TextUnformatted(TR("force_quit_confirm_msg"));
ImGui::Spacing();
ImGui::Spacing();
float btnW = 120.0f;
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) * 0.5f);
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 0))) {
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.15f, 0.15f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.75f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
if (ImGui::Button(TR("force_quit_yes"), ImVec2(btnW, 0))) {
DEBUG_LOGF("Force quit confirmed by user after %.0fs\n", shutdown_timer_);
shutdown_complete_ = true;
ImGui::CloseCurrentPopup();
}
ImGui::PopStyleColor(3);
ImGui::EndPopup();
}
ImGui::Spacing();
ImGui::Spacing();
@@ -2622,6 +2792,21 @@ void App::renderLoadingOverlay(float contentH)
curY += ts.y + gap;
}
// -------------------------------------------------------------------
// 2b. Warmup description (subtitle explaining what's happening)
// -------------------------------------------------------------------
if (state_.warming_up && !state_.warmup_description.empty()) {
const char* descText = state_.warmup_description.c_str();
ImFont* capFont = Type().caption();
if (!capFont) capFont = ImGui::GetFont();
// Wrap to barW so long descriptions don't overflow
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, barW, descText);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(wp.x + cx - ts.x * 0.5f, curY),
IM_COL32(160, 160, 160, 200), descText, nullptr, barW);
curY += ts.y + gap;
}
// -------------------------------------------------------------------
// 3. Sync progress bar (if connected and syncing)
// -------------------------------------------------------------------
+55 -14
View File
@@ -173,6 +173,9 @@ public:
// Pool mining (xmrig)
void startPoolMining(int threads);
void stopPoolMining();
int getXmrigRequestedThreads() const {
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
}
// Mine-when-idle state query
bool isIdleMiningActive() const { return idle_mining_active_; }
@@ -218,13 +221,19 @@ public:
void refreshPeerInfo();
void refreshMarketData();
/// @brief Per-category refresh intervals, adjusted by active tab
struct RefreshIntervals {
float core; // balance + sync status
float transactions; // tx list + enrichment
float addresses; // address lists + balances
float peers; // peer info (0 = disabled)
};
/// @brief Get recommended refresh intervals for a given page
static RefreshIntervals getIntervalsForPage(ui::NavPage page);
// UI navigation
void setCurrentPage(ui::NavPage page) {
if (page != current_page_) {
current_page_ = page;
if (page == ui::NavPage::Peers) refreshPeerInfo();
}
}
void setCurrentPage(ui::NavPage page);
ui::NavPage getCurrentPage() const { return current_page_; }
// Dialog triggers (used by settings page to open modal dialogs)
@@ -359,17 +368,20 @@ private:
// Shutdown state
std::atomic<bool> shutting_down_{false};
std::atomic<bool> shutdown_complete_{false};
std::atomic<bool> refresh_in_progress_{false};
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
std::string shutdown_status_;
std::thread shutdown_thread_;
float shutdown_timer_ = 0.0f;
bool force_quit_confirm_ = false;
std::chrono::steady_clock::time_point shutdown_start_time_;
// Daemon restart (e.g. after changing debug log categories)
std::atomic<bool> daemon_restarting_{false};
std::thread daemon_restart_thread_;
// Encryption state check timeout
float encryption_check_timer_ = 0.0f;
// UI State
bool quit_requested_ = false;
bool show_demo_window_ = false;
@@ -435,16 +447,33 @@ private:
std::string pending_memo_;
std::string pending_label_;
// Timers (in seconds since last update)
float refresh_timer_ = 0.0f;
// Per-category timers (in seconds since last refresh)
float core_timer_ = 0.0f; // balance + sync status
float address_timer_ = 0.0f; // address lists
float transaction_timer_ = 0.0f; // transaction list
float peer_timer_ = 0.0f; // peer info
float price_timer_ = 0.0f;
float fast_refresh_timer_ = 0.0f; // For mining stats
// Refresh intervals (seconds)
static constexpr float REFRESH_INTERVAL = 5.0f;
// Default refresh intervals (seconds)
static constexpr float CORE_INTERVAL_DEFAULT = 5.0f;
static constexpr float ADDRESS_INTERVAL_DEFAULT = 15.0f;
static constexpr float TX_INTERVAL_DEFAULT = 10.0f;
static constexpr float PEER_INTERVAL_DEFAULT = 10.0f;
static constexpr float PRICE_INTERVAL = 60.0f;
static constexpr float FAST_REFRESH_INTERVAL = 1.0f;
// Active intervals — adjusted by tab priority via applyRefreshPolicy()
float active_core_interval_ = CORE_INTERVAL_DEFAULT;
float active_tx_interval_ = TX_INTERVAL_DEFAULT;
float active_addr_interval_ = ADDRESS_INTERVAL_DEFAULT;
float active_peer_interval_ = PEER_INTERVAL_DEFAULT;
// Per-category refresh guards (prevent worker queue pileup)
std::atomic<bool> core_refresh_in_progress_{false};
std::atomic<bool> address_refresh_in_progress_{false};
std::atomic<bool> tx_refresh_in_progress_{false};
// Mining refresh guard (prevents worker queue pileup)
std::atomic<bool> mining_refresh_in_progress_{false};
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
@@ -494,6 +523,12 @@ private:
float opid_poll_timer_ = 0.0f;
static constexpr float OPID_POLL_INTERVAL = 2.0f;
// Txids from completed z_sendmany operations.
// Ensures shielded sends are discoverable by z_viewtransaction
// even when they don't appear in listtransactions or
// z_listreceivedbyaddress.
std::unordered_set<std::string> send_txids_;
// First-run wizard state
WizardPhase wizard_phase_ = WizardPhase::None;
std::unique_ptr<util::Bootstrap> bootstrap_;
@@ -595,11 +630,17 @@ private:
void applyDefaultBanlist();
// Private methods - data refresh
void refreshData();
void refreshBalance();
void refreshAddresses();
void refreshData(); // Orchestrator: dispatches per-category refreshes
void refreshCoreData(); // Balance + blockchain info (can use fast_worker_)
void refreshAddressData(); // Address lists + balances
void refreshTransactionData(); // Transaction list + z_viewtransaction enrichment
void refreshEncryptionState(); // Wallet encryption/lock state
void refreshBalance(); // Legacy: balance-only refresh (used by specific callers)
void refreshAddresses(); // Legacy: standalone address refresh
void refreshPrice();
void refreshWalletEncryptionState();
void applyRefreshPolicy(ui::NavPage page);
bool shouldRefreshTransactions() const;
void checkAutoLock();
void checkIdleMining();
};
+582 -583
View File
File diff suppressed because it is too large Load Diff
+50 -11
View File
@@ -19,6 +19,7 @@
#include "ui/schema/ui_schema.h"
#include "ui/theme.h"
#include "ui/effects/imgui_acrylic.h"
#include "ui/windows/mining_tab.h"
#include "util/platform.h"
#include "util/secure_vault.h"
#include "util/perf_log.h"
@@ -68,10 +69,13 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
}
ui::Notifications::instance().info(
"Wallet encrypted successfully");
"Wallet encrypted successfully", 5.0f);
// The daemon shuts itself down after encryptwallet
// The daemon shuts itself down after encryptwallet.
// Update connection_status_ so the loading overlay
// explains why the daemon is restarting.
if (isUsingEmbeddedDaemon()) {
connection_status_ = TR("restarting_after_encryption");
// Give daemon a moment to shut down, then restart
// (do this off the main thread to avoid stalling the UI)
std::thread([this]() {
@@ -163,13 +167,13 @@ void App::processDeferredEncryption() {
if (pinStored) {
settings_->setPinEnabled(true);
settings_->save();
ui::Notifications::instance().info("Wallet encrypted & PIN set");
ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f);
} else {
ui::Notifications::instance().warning(
"Wallet encrypted but PIN vault failed");
}
} else {
ui::Notifications::instance().info("Wallet encrypted successfully");
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f);
}
// Securely clear deferred state
@@ -263,7 +267,7 @@ void App::unlockWallet(const std::string& passphrase, int timeout) {
state_.unlocked_until = std::time(nullptr) + timeout;
} else {
lock_attempts_++;
lock_error_msg_ = "Incorrect passphrase";
lock_error_msg_ = TR("incorrect_passphrase");
lock_error_timer_ = 3.0f;
memset(lock_passphrase_buf_, 0, sizeof(lock_passphrase_buf_));
@@ -439,12 +443,25 @@ void App::checkIdleMining() {
return;
}
// Skip idle mining adjustments while thread benchmark is running
if (ui::IsMiningBenchmarkActive()) return;
int idleSec = util::Platform::getSystemIdleSeconds();
int delay = settings_->getMineIdleDelay();
bool isPool = settings_->getPoolMode();
bool threadScaling = settings_->getIdleThreadScaling();
int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());
// GPU-aware idle detection: if enabled, treat GPU utilization >= 10%
// as "user active" (e.g. watching a video). Disabled = unrestricted
// mode that only looks at keyboard/mouse input.
bool gpuBusy = false;
if (settings_->getIdleGpuAware()) {
int gpuUtil = util::Platform::getGpuUtilization();
gpuBusy = (gpuUtil >= 10);
}
bool systemIdle = (idleSec >= delay) && !gpuBusy;
// Check if mining is already running (manually started by user)
bool miningActive = isPool
? (xmrig_manager_ && xmrig_manager_->isRunning())
@@ -461,7 +478,7 @@ void App::checkIdleMining() {
if (activeThreads <= 0) activeThreads = std::max(1, maxThreads / 2);
if (idleThreads <= 0) idleThreads = maxThreads;
if (idleSec >= delay) {
if (systemIdle) {
// System is idle — scale up to idle thread count
if (!idle_scaled_to_idle_) {
idle_scaled_to_idle_ = true;
@@ -474,7 +491,7 @@ void App::checkIdleMining() {
DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (idle)\n", activeThreads, idleThreads);
}
} else {
// User is active — scale down to active thread count
// User is active (or GPU busy) — scale down to active thread count
if (idle_scaled_to_idle_) {
idle_scaled_to_idle_ = false;
if (isPool) {
@@ -484,11 +501,26 @@ void App::checkIdleMining() {
startMining(activeThreads);
}
DEBUG_LOGF("[App] Idle thread scaling: %d -> %d threads (active)\n", idleThreads, activeThreads);
} else {
// Mining just started while user is active — ensure active
// thread count is applied (grid selection may differ).
int currentThreads = isPool
? xmrig_manager_->getStats().threads_active
: state_.mining.genproclimit;
if (currentThreads > 0 && currentThreads != activeThreads) {
if (isPool) {
stopPoolMining();
startPoolMining(activeThreads);
} else {
startMining(activeThreads);
}
DEBUG_LOGF("[App] Idle thread scaling: initial %d -> %d threads (active)\n", currentThreads, activeThreads);
}
}
}
} else {
// --- Start/Stop mode (original behavior) ---
if (idleSec >= delay) {
if (systemIdle) {
// System is idle — start mining if not already running
if (!miningActive && !idle_mining_active_ && !mining_toggle_in_progress_.load()) {
// For solo mining, need daemon connected and synced
@@ -759,10 +791,17 @@ void App::renderLockScreen() {
bool vaultOk = vault_ && vault_->retrieve(pin, passphrase);
if (!vaultOk) {
return [this]() {
bool noVault = !vault_ || !vault_->hasVault();
return [this, noVault]() {
lock_unlock_in_progress_ = false;
lock_attempts_++;
lock_error_msg_ = "Incorrect PIN";
if (noVault) {
// Vault file missing — switch to passphrase mode
lock_error_msg_ = TR("pin_not_set");
lock_use_pin_ = false;
} else {
lock_attempts_++;
lock_error_msg_ = TR("incorrect_pin");
}
lock_error_timer_ = 3.0f;
float baseDelay = ui::schema::UI().drawElement("security", "lockout-base-delay").sizeOr(2.0f);
+8 -2
View File
@@ -1294,7 +1294,10 @@ void App::renderFirstRunWizard() {
// Start daemon + finish wizard immediately
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
startEmbeddedDaemon();
if (!startEmbeddedDaemon()) {
ui::Notifications::instance().warning(
TR("wizard_daemon_start_failed"));
}
}
tryConnect();
wizard_phase_ = WizardPhase::Done;
@@ -1313,7 +1316,10 @@ void App::renderFirstRunWizard() {
settings_->setWizardCompleted(true);
settings_->save();
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
startEmbeddedDaemon();
if (!startEmbeddedDaemon()) {
ui::Notifications::instance().warning(
TR("wizard_daemon_start_failed"));
}
}
tryConnect();
}
+9
View File
@@ -1,6 +1,9 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// settings.cpp — JSON settings persistence. Loads/saves user preferences
// to ~/.config/ObsidianDragon/settings.json (Linux/macOS) or %APPDATA% (Windows).
#include "settings.h"
#include "version.h"
@@ -129,6 +132,7 @@ bool Settings::load(const std::string& path)
if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get<bool>();
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get<bool>();
if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get<bool>();
if (j.contains("max_connections")) max_connections_ = j["max_connections"].get<int>();
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
debug_categories_.clear();
@@ -137,6 +141,7 @@ bool Settings::load(const std::string& path)
}
if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get<bool>();
if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get<bool>();
if (j.contains("reduce_motion")) reduce_motion_ = j["reduce_motion"].get<bool>();
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>();
@@ -153,6 +158,7 @@ bool Settings::load(const std::string& path)
if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get<bool>();
if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get<int>();
if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get<int>();
if (j.contains("idle_gpu_aware")) idle_gpu_aware_ = j["idle_gpu_aware"].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"])
@@ -230,12 +236,14 @@ bool Settings::save(const std::string& path)
j["pin_enabled"] = pin_enabled_;
j["keep_daemon_running"] = keep_daemon_running_;
j["stop_external_daemon"] = stop_external_daemon_;
j["max_connections"] = max_connections_;
j["verbose_logging"] = verbose_logging_;
j["debug_categories"] = json::array();
for (const auto& cat : debug_categories_)
j["debug_categories"].push_back(cat);
j["theme_effects_enabled"] = theme_effects_enabled_;
j["low_spec_mode"] = low_spec_mode_;
j["reduce_motion"] = reduce_motion_;
j["selected_exchange"] = selected_exchange_;
j["selected_pair"] = selected_pair_;
j["pool_url"] = pool_url_;
@@ -250,6 +258,7 @@ bool Settings::save(const std::string& path)
j["idle_thread_scaling"] = idle_thread_scaling_;
j["idle_threads_active"] = idle_threads_active_;
j["idle_threads_idle"] = idle_threads_idle_;
j["idle_gpu_aware"] = idle_gpu_aware_;
j["saved_pool_urls"] = json::array();
for (const auto& u : saved_pool_urls_)
j["saved_pool_urls"].push_back(u);
+13
View File
@@ -165,6 +165,10 @@ public:
bool getStopExternalDaemon() const { return stop_external_daemon_; }
void setStopExternalDaemon(bool v) { stop_external_daemon_ = v; }
// Daemon — maximum peer connections (0 = daemon default)
int getMaxConnections() const { return max_connections_; }
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
bool getVerboseLogging() const { return verbose_logging_; }
void setVerboseLogging(bool v) { verbose_logging_ = v; }
@@ -186,6 +190,10 @@ public:
bool getLowSpecMode() const { return low_spec_mode_; }
void setLowSpecMode(bool v) { low_spec_mode_ = v; }
// Reduce motion — disables animated transitions for accessibility
bool getReduceMotion() const { return reduce_motion_; }
void setReduceMotion(bool v) { reduce_motion_ = v; }
// Market — last selected exchange + pair
std::string getSelectedExchange() const { return selected_exchange_; }
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
@@ -221,6 +229,8 @@ public:
void setIdleThreadsActive(int v) { idle_threads_active_ = std::max(0, v); }
int getIdleThreadsIdle() const { return idle_threads_idle_; }
void setIdleThreadsIdle(int v) { idle_threads_idle_ = std::max(0, v); }
bool getIdleGpuAware() const { return idle_gpu_aware_; }
void setIdleGpuAware(bool v) { idle_gpu_aware_ = v; }
// Saved pool URLs (user-managed favorites dropdown)
const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; }
@@ -297,10 +307,12 @@ private:
bool pin_enabled_ = false;
bool keep_daemon_running_ = false;
bool stop_external_daemon_ = false;
int max_connections_ = 0; // 0 = daemon default
bool verbose_logging_ = false;
std::set<std::string> debug_categories_;
bool theme_effects_enabled_ = true;
bool low_spec_mode_ = false;
bool reduce_motion_ = false;
std::string selected_exchange_ = "TradeOgre";
std::string selected_pair_ = "DRGX/BTC";
@@ -317,6 +329,7 @@ private:
bool idle_thread_scaling_ = false; // scale threads instead of start/stop
int idle_threads_active_ = 0; // threads when user active (0 = auto)
int idle_threads_idle_ = 0; // threads when idle (0 = auto = all)
bool idle_gpu_aware_ = true; // treat GPU activity as non-idle
std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
+2 -2
View File
@@ -7,9 +7,9 @@
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
#define DRAGONX_VERSION "1.1.0"
#define DRAGONX_VERSION "1.2.0"
#define DRAGONX_VERSION_MAJOR 1
#define DRAGONX_VERSION_MINOR 1
#define DRAGONX_VERSION_MINOR 2
#define DRAGONX_VERSION_PATCH 0
#define DRAGONX_APP_NAME "ObsidianDragon"
+81 -13
View File
@@ -1,6 +1,9 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// embedded_daemon.cpp — Manages the dragonxd child process lifecycle:
// binary discovery, process spawn, stdout/stderr monitoring, crash recovery.
#include "embedded_daemon.h"
#include "../config/version.h"
@@ -34,6 +37,9 @@
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#ifdef __APPLE__
#include <sys/sysctl.h>
#endif
#endif
namespace fs = std::filesystem;
@@ -152,6 +158,41 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
// DragonX chain parameters.
// On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout.
// On Linux, -printtoconsole is used for pipe-based output capture.
// Auto-detect a reasonable -dbcache based on available physical RAM.
// Default LevelDB cache is small (~450MB); larger caches improve sync
// performance and reduce disk I/O — especially on macOS with APFS.
std::string dbcache_arg = "-dbcache=450";
{
#ifdef __APPLE__
// sysctl hw.memsize returns total physical RAM in bytes
int64_t memsize = 0;
size_t len = sizeof(memsize);
if (sysctlbyname("hw.memsize", &memsize, &len, nullptr, 0) == 0 && memsize > 0) {
int totalMB = static_cast<int>(memsize / (1024 * 1024));
// Use ~12.5% of RAM for dbcache, clamped to [450, 4096]
int cache = std::max(450, std::min(4096, totalMB / 8));
dbcache_arg = "-dbcache=" + std::to_string(cache);
}
#elif defined(__linux__)
long pages = sysconf(_SC_PHYS_PAGES);
long page_size = sysconf(_SC_PAGE_SIZE);
if (pages > 0 && page_size > 0) {
int totalMB = static_cast<int>((static_cast<int64_t>(pages) * page_size) / (1024 * 1024));
int cache = std::max(450, std::min(4096, totalMB / 8));
dbcache_arg = "-dbcache=" + std::to_string(cache);
}
#elif defined(_WIN32)
MEMORYSTATUSEX memInfo;
memInfo.dwLength = sizeof(memInfo);
if (GlobalMemoryStatusEx(&memInfo)) {
int totalMB = static_cast<int>(memInfo.ullTotalPhys / (1024 * 1024));
int cache = std::max(450, std::min(4096, totalMB / 8));
dbcache_arg = "-dbcache=" + std::to_string(cache);
}
#endif
DEBUG_LOGF("[INFO] Using %s\n", dbcache_arg.c_str());
}
return {
"-tls=only",
#ifndef _WIN32
@@ -164,9 +205,14 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
"-ac_reward=300000000",
"-ac_blocktime=36",
"-ac_private=1",
"-addnode=176.126.87.241",
"-addnode=node.dragonx.is",
"-addnode=node1.dragonx.is",
"-addnode=node2.dragonx.is",
"-addnode=node3.dragonx.is",
"-addnode=node4.dragonx.is",
"-experimentalfeatures",
"-developerencryptwallet"
"-developerencryptwallet",
dbcache_arg
};
}
@@ -431,6 +477,11 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
args.push_back("-debug=" + cat);
}
// Append max connections if configured (0 = daemon default)
if (max_connections_ > 0) {
args.push_back("-maxconnections=" + std::to_string(max_connections_));
}
// Add -rescan flag if requested (one-shot)
if (rescan_on_next_start_.exchange(false)) {
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
@@ -1045,14 +1096,12 @@ void EmbeddedDaemon::stop(int wait_ms)
if (process_pid_ > 0) {
setState(State::Stopping, "Stopping dragonxd...");
// Send SIGTERM to the entire process group (negative PID).
// This ensures that if dragonxd is a shell script wrapper,
// both bash AND the actual dragonxd child receive the signal.
// Without this, only bash is killed and dragonxd is orphaned.
DEBUG_LOGF("Sending SIGTERM to process group -%d\n", process_pid_);
kill(-process_pid_, SIGTERM);
// Wait for process to exit, draining stdout each iteration
// Phase 1: Wait for the daemon to exit naturally.
// The caller (stopEmbeddedDaemon) already sent an RPC "stop" which
// tells the daemon to flush LevelDB, close sockets, and exit cleanly.
// On macOS/APFS the LevelDB flush can take several seconds — we must
// NOT send SIGTERM until the daemon has had enough time to finish.
DEBUG_LOGF("Waiting up to %d ms for daemon to exit after RPC stop...\n", wait_ms);
auto start = std::chrono::steady_clock::now();
while (isRunning()) {
drainOutput();
@@ -1061,15 +1110,34 @@ void EmbeddedDaemon::stop(int wait_ms)
std::chrono::steady_clock::now() - start).count();
if (elapsed >= wait_ms) {
// Force kill the entire process group
DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_);
kill(-process_pid_, SIGKILL);
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// Phase 2: If still running, send SIGTERM and wait a further 10s.
if (isRunning()) {
DEBUG_LOGF("Daemon did not exit gracefully — sending SIGTERM to process group -%d\n", process_pid_);
kill(-process_pid_, SIGTERM);
auto sigterm_start = std::chrono::steady_clock::now();
while (isRunning()) {
drainOutput();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - sigterm_start).count();
if (elapsed >= 10000) {
// Phase 3: Force kill
DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_);
kill(-process_pid_, SIGKILL);
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
} else {
DEBUG_LOGF("Daemon exited cleanly after RPC stop\n");
}
drainOutput(); // read any remaining output
// Reap the child process
+26
View File
@@ -116,6 +116,26 @@ public:
* @brief Get last N lines of daemon output (thread-safe snapshot)
*/
std::vector<std::string> getRecentLines(int maxLines = 8) const;
/**
* @brief Extract the latest block height from daemon output (thread-safe).
* Parses the last "height=N" from UpdateTip lines without copying
* the entire output buffer. Returns -1 if no UpdateTip found.
*/
int getLastBlockHeight() const {
std::lock_guard<std::mutex> lk(output_mutex_);
// Search backwards from the end for "height="
size_t pos = process_output_.rfind("height=");
if (pos == std::string::npos) return -1;
pos += 7; // skip "height="
int h = 0;
for (size_t i = pos; i < process_output_.size(); ++i) {
char c = process_output_[i];
if (c >= '0' && c <= '9') h = h * 10 + (c - '0');
else break;
}
return h > 0 ? h : -1;
}
/**
* @brief Whether start() detected an existing daemon on the RPC port.
@@ -152,6 +172,11 @@ public:
void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
const std::set<std::string>& getDebugCategories() const { return debug_categories_; }
/**
* @brief Set maximum peer connections (0 = use daemon default)
*/
void setMaxConnections(int v) { max_connections_ = v; }
/**
* @brief Request a blockchain rescan on the next daemon start
*/
@@ -194,6 +219,7 @@ private:
std::thread monitor_thread_;
std::atomic<bool> should_stop_{false};
std::set<std::string> debug_categories_;
int max_connections_ = 0; // 0 = daemon default
std::atomic<int> crash_count_{0}; // consecutive crash counter
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
};
+3
View File
@@ -1,6 +1,9 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// xmrig_manager.cpp — Pool mining process management via xmrig-hac.
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
#include "xmrig_manager.h"
#include "../resources/embedded_resources.h"
+4
View File
@@ -88,6 +88,10 @@ public:
const PoolStats& getStats() const { return stats_; }
const std::string& getLastError() const { return last_error_; }
/// Thread count requested at start() — available immediately, unlike
/// PoolStats::threads_active which requires an API response.
int getRequestedThreads() const { return threads_; }
/**
* @brief Get last N lines of xmrig stdout (thread-safe snapshot).
*/
+7
View File
@@ -134,6 +134,7 @@ struct MarketInfo {
double change_24h = 0.0;
double market_cap = 0.0;
std::string last_updated;
std::chrono::steady_clock::time_point last_fetch_time{};
// Price history for chart
std::vector<double> price_history;
@@ -179,6 +180,9 @@ struct PoolMiningState {
struct WalletState {
// Connection
bool connected = false;
bool warming_up = false; // daemon reachable but in RPC warmup (error -28)
std::string warmup_status; // user-friendly title, e.g. "Processing blocks..."
std::string warmup_description; // subtitle explaining the stage
int daemon_version = 0;
std::string daemon_subversion;
int protocol_version = 0;
@@ -249,6 +253,9 @@ struct WalletState {
void clear() {
connected = false;
warming_up = false;
warmup_status.clear();
warmup_description.clear();
daemon_version = 0;
daemon_subversion.clear();
protocol_version = 0;
+1 -1
View File
@@ -12,4 +12,4 @@ INCBIN(ubuntu_regular, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-R.ttf");
INCBIN(ubuntu_light, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Light.ttf");
INCBIN(ubuntu_medium, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Medium.ttf");
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf");
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.otf");
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf");
+684 -667
View File
File diff suppressed because it is too large Load Diff
+681 -664
View File
File diff suppressed because it is too large Load Diff
+77 -59
View File
@@ -2799,66 +2799,84 @@ unsigned char res_lang_fr_json[] = {
0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x68, 0x6f, 0x77,
0x69, 0x6e, 0x67, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x22, 0x41, 0x66, 0x66, 0x69,
0x63, 0x68, 0x61, 0x67, 0x65, 0x20, 0x25, 0x64, 0xc3, 0xa2, 0xc2, 0x80,
0xc2, 0x93, 0x25, 0x64, 0x20, 0x73, 0x75, 0x72, 0x20, 0x25, 0x64, 0x20,
0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73,
0x20, 0x28, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x20, 0x3a, 0x20, 0x25, 0x7a,
0x75, 0x29, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x69,
0x6d, 0x70, 0x6c, 0x65, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f,
0x75, 0x6e, 0x64, 0x22, 0x3a, 0x20, 0x22, 0x41, 0x72, 0x72, 0x69, 0xc3,
0xa8, 0x72, 0x65, 0x2d, 0x70, 0x6c, 0x61, 0x6e, 0x20, 0x73, 0x69, 0x6d,
0x70, 0x6c, 0x65, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73,
0x74, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x22,
0x3a, 0x20, 0x22, 0x44, 0xc3, 0xa9, 0x6d, 0x61, 0x72, 0x72, 0x65, 0x72,
0x20, 0x6c, 0x65, 0x20, 0x6d, 0x69, 0x6e, 0x61, 0x67, 0x65, 0x22, 0x2c,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
0x22, 0x3a, 0x20, 0x22, 0x53, 0x74, 0x61, 0x74, 0x75, 0x74, 0x22, 0x2c,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x74, 0x6f, 0x70, 0x5f, 0x65,
0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x41,
0x72, 0x72, 0xc3, 0xaa, 0x74, 0x65, 0x72, 0x20, 0x6c, 0x65, 0x20, 0x64,
0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x20, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e,
0x65, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x74, 0x6f,
0x70, 0x5f, 0x6d, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0x3a, 0x20, 0x22,
0x41, 0x72, 0x72, 0xc3, 0xaa, 0x74, 0x65, 0x72, 0x20, 0x6c, 0x65, 0x20,
0x6d, 0x69, 0x6e, 0x61, 0x67, 0x65, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20,
0x20, 0x22, 0x73, 0x75, 0x62, 0x6d, 0x69, 0x74, 0x74, 0x69, 0x6e, 0x67,
0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x22, 0x3a, 0x20, 0x22, 0x53, 0x6f, 0x75, 0x6d, 0x69, 0x73, 0x73, 0x69,
0x6f, 0x6e, 0x20, 0x64, 0x65, 0x20, 0x6c, 0x61, 0x20, 0x74, 0x72, 0x61,
0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x2e, 0x2e, 0x22,
0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x63, 0x63, 0x65,
0x73, 0x73, 0x22, 0x3a, 0x20, 0x22, 0x53, 0x75, 0x63, 0x63, 0xc3, 0xa8,
0x73, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x6d,
0x6d, 0x61, 0x72, 0x79, 0x22, 0x3a, 0x20, 0x22, 0x52, 0xc3, 0xa9, 0x73,
0x75, 0x6d, 0xc3, 0xa9, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22,
0x73, 0x79, 0x6e, 0x63, 0x69, 0x6e, 0x67, 0x22, 0x3a, 0x20, 0x22, 0x53,
0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x73, 0x61, 0x74, 0x69,
0x63, 0x68, 0x61, 0x67, 0x65, 0x20, 0x25, 0x64, 0xe2, 0x80, 0x93, 0x25,
0x64, 0x20, 0x73, 0x75, 0x72, 0x20, 0x25, 0x64, 0x20, 0x74, 0x72, 0x61,
0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, 0x28, 0x74,
0x6f, 0x74, 0x61, 0x6c, 0x20, 0x3a, 0x20, 0x25, 0x7a, 0x75, 0x29, 0x22,
0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x69, 0x6d, 0x70, 0x6c,
0x65, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64,
0x22, 0x3a, 0x20, 0x22, 0x41, 0x72, 0x72, 0x69, 0xc3, 0xa8, 0x72, 0x65,
0x2d, 0x70, 0x6c, 0x61, 0x6e, 0x20, 0x73, 0x69, 0x6d, 0x70, 0x6c, 0x65,
0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x6c, 0x69, 0x64,
0x65, 0x72, 0x5f, 0x6f, 0x66, 0x66, 0x22, 0x3a, 0x20, 0x22, 0x44, 0xc3,
0xa9, 0x73, 0x61, 0x63, 0x74, 0x69, 0x76, 0xc3, 0xa9, 0x22, 0x2c, 0x0a,
0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x6d,
0x69, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0x3a, 0x20, 0x22, 0x44, 0xc3, 0xa9,
0x6d, 0x61, 0x72, 0x72, 0x65, 0x72, 0x20, 0x6c, 0x65, 0x20, 0x6d, 0x69,
0x6e, 0x61, 0x67, 0x65, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22,
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x3a, 0x20, 0x22, 0x53, 0x74,
0x61, 0x74, 0x75, 0x74, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22,
0x73, 0x74, 0x6f, 0x70, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x6c, 0x22, 0x3a, 0x20, 0x22, 0x41, 0x72, 0x72, 0xc3, 0xaa, 0x74, 0x65,
0x72, 0x20, 0x6c, 0x65, 0x20, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x20,
0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x22, 0x2c, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x22, 0x73, 0x74, 0x6f, 0x70, 0x5f, 0x6d, 0x69, 0x6e, 0x69,
0x6e, 0x67, 0x22, 0x3a, 0x20, 0x22, 0x41, 0x72, 0x72, 0xc3, 0xaa, 0x74,
0x65, 0x72, 0x20, 0x6c, 0x65, 0x20, 0x6d, 0x69, 0x6e, 0x61, 0x67, 0x65,
0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x75, 0x62, 0x6d,
0x69, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73,
0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x53, 0x6f,
0x75, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x64, 0x65, 0x20,
0x6c, 0x61, 0x20, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x2e, 0x2e, 0x2e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x22, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73,
0x22, 0x3a, 0x20, 0x22, 0x41, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73,
0x20, 0x54, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x65,
0x73, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54, 0x65, 0x73, 0x74, 0x65, 0x72, 0x22,
0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x68, 0x65, 0x6d, 0x65,
0x22, 0x3a, 0x20, 0x22, 0x54, 0x68, 0xc3, 0xa8, 0x6d, 0x65, 0x22, 0x2c,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x5f,
0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x73, 0x22, 0x3a, 0x20, 0x22, 0x45,
0x66, 0x66, 0x65, 0x74, 0x73, 0x20, 0x64, 0x65, 0x20, 0x74, 0x68, 0xc3,
0xa8, 0x6d, 0x65, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74,
0x69, 0x6d, 0x65, 0x5f, 0x64, 0x61, 0x79, 0x73, 0x5f, 0x61, 0x67, 0x6f,
0x22, 0x3a, 0x20, 0x22, 0x69, 0x6c, 0x20, 0x79, 0x20, 0x61, 0x20, 0x25,
0x64, 0x20, 0x6a, 0x6f, 0x75, 0x72, 0x73, 0x22, 0x2c, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x22, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x68, 0x6f, 0x75, 0x72,
0x73, 0x5f, 0x61, 0x67, 0x6f, 0x22, 0x3a, 0x20, 0x22, 0x69, 0x6c, 0x20,
0x79, 0x20, 0x61, 0x20, 0x25, 0x64, 0x20, 0x68, 0x65, 0x75, 0x72, 0x65,
0x22, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x3a, 0x20, 0x22,
0x53, 0x75, 0x63, 0x63, 0xc3, 0xa8, 0x73, 0x22, 0x2c, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x22, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x22, 0x3a,
0x20, 0x22, 0x52, 0xc3, 0xa9, 0x73, 0x75, 0x6d, 0xc3, 0xa9, 0x22, 0x2c,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x79, 0x6e, 0x63, 0x69, 0x6e,
0x67, 0x22, 0x3a, 0x20, 0x22, 0x53, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f,
0x6e, 0x69, 0x73, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x2e, 0x2e, 0x22,
0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x5f, 0x61, 0x64, 0x64,
0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x22, 0x41, 0x64,
0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x20, 0x54, 0x22, 0x2c, 0x0a, 0x20,
0x20, 0x20, 0x20, 0x22, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x6e,
0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x54,
0x65, 0x73, 0x74, 0x65, 0x72, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x22, 0x74, 0x68, 0x65, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x54, 0x68,
0xc3, 0xa8, 0x6d, 0x65, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22,
0x74, 0x68, 0x65, 0x6d, 0x65, 0x5f, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74,
0x73, 0x22, 0x3a, 0x20, 0x22, 0x45, 0x66, 0x66, 0x65, 0x74, 0x73, 0x20,
0x64, 0x65, 0x20, 0x74, 0x68, 0xc3, 0xa8, 0x6d, 0x65, 0x22, 0x2c, 0x0a,
0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x64, 0x61,
0x79, 0x73, 0x5f, 0x61, 0x67, 0x6f, 0x22, 0x3a, 0x20, 0x22, 0x69, 0x6c,
0x20, 0x79, 0x20, 0x61, 0x20, 0x25, 0x64, 0x20, 0x6a, 0x6f, 0x75, 0x72,
0x73, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, 0x6d,
0x65, 0x5f, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x5f, 0x61, 0x67,
0x6f, 0x22, 0x3a, 0x20, 0x22, 0x69, 0x6c, 0x20, 0x79, 0x20, 0x61, 0x20,
0x25, 0x64, 0x20, 0x6d, 0x69, 0x6e, 0x75, 0x74, 0x65, 0x73, 0x22, 0x2c,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73,
0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x5f, 0x61, 0x67, 0x6f, 0x22, 0x3a,
0x20, 0x22, 0x69, 0x6c, 0x20, 0x79, 0x20, 0x61, 0x20, 0x25, 0x64, 0x20,
0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x65, 0x73, 0x22, 0x2c, 0x0a, 0x20,
0x65, 0x5f, 0x68, 0x6f, 0x75, 0x72, 0x73, 0x5f, 0x61, 0x67, 0x6f, 0x22,
0x3a, 0x20, 0x22, 0x69, 0x6c, 0x20, 0x79, 0x20, 0x61, 0x20, 0x25, 0x64,
0x20, 0x68, 0x65, 0x75, 0x72, 0x65, 0x73, 0x22, 0x2c, 0x0a, 0x20, 0x20,
0x20, 0x20, 0x22, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d, 0x69, 0x6e, 0x75,
0x74, 0x65, 0x73, 0x5f, 0x61, 0x67, 0x6f, 0x22, 0x3a, 0x20, 0x22, 0x69,
0x6c, 0x20, 0x79, 0x20, 0x61, 0x20, 0x25, 0x64, 0x20, 0x6d, 0x69, 0x6e,
0x75, 0x74, 0x65, 0x73, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22,
0x74, 0x69, 0x6d, 0x65, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73,
0x5f, 0x61, 0x67, 0x6f, 0x22, 0x3a, 0x20, 0x22, 0x69, 0x6c, 0x20, 0x79,
0x20, 0x61, 0x20, 0x25, 0x64, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64,
0x65, 0x73, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69,
0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x31, 0x35, 0x6d, 0x69, 0x6e, 0x22,
0x3a, 0x20, 0x22, 0x31, 0x35, 0x20, 0x6d, 0x69, 0x6e, 0x22, 0x2c, 0x0a,
0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74,
0x5f, 0x31, 0x68, 0x6f, 0x75, 0x72, 0x22, 0x3a, 0x20, 0x22, 0x31, 0x20,
0x68, 0x65, 0x75, 0x72, 0x65, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x22, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x31, 0x6d, 0x69,
0x6e, 0x22, 0x3a, 0x20, 0x22, 0x31, 0x20, 0x6d, 0x69, 0x6e, 0x22, 0x2c,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75,
0x74, 0x5f, 0x33, 0x30, 0x6d, 0x69, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x33,
0x30, 0x20, 0x6d, 0x69, 0x6e, 0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20,
0x22, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x35, 0x6d, 0x69,
0x6e, 0x22, 0x3a, 0x20, 0x22, 0x35, 0x20, 0x6d, 0x69, 0x6e, 0x22, 0x2c,
0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75,
0x74, 0x5f, 0x6f, 0x66, 0x66, 0x22, 0x3a, 0x20, 0x22, 0x44, 0xc3, 0xa9,
0x73, 0x61, 0x63, 0x74, 0x69, 0x76, 0xc3, 0xa9, 0x22, 0x2c, 0x0a, 0x20,
0x20, 0x20, 0x20, 0x22, 0x74, 0x6f, 0x22, 0x3a, 0x20, 0x22, 0xc3, 0x80,
0x22, 0x2c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x6f, 0x5f, 0x75,
0x70, 0x70, 0x65, 0x72, 0x22, 0x3a, 0x20, 0x22, 0xc3, 0x80, 0x22, 0x2c,
@@ -3515,4 +3533,4 @@ unsigned char res_lang_fr_json[] = {
0x20, 0x22, 0x41, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x20, 0x5a,
0x22, 0x0a, 0x7d, 0x0a
};
unsigned int res_lang_fr_json_len = 42172;
unsigned int res_lang_fr_json_len = 42388;
+742 -725
View File
File diff suppressed because it is too large Load Diff
+650 -634
View File
File diff suppressed because it is too large Load Diff
+679 -661
View File
File diff suppressed because it is too large Load Diff
+953 -934
View File
File diff suppressed because it is too large Load Diff
+598 -580
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -234,8 +234,11 @@ bool Connection::createDefaultConfig(const std::string& path)
file << "exportdir=" << dataDir << "\n";
file << "experimentalfeatures=1\n";
file << "developerencryptwallet=1\n";
file << "addnode=195.201.20.230\n";
file << "addnode=195.201.137.219\n";
file << "addnode=node.dragonx.is\n";
file << "addnode=node1.dragonx.is\n";
file << "addnode=node2.dragonx.is\n";
file << "addnode=node3.dragonx.is\n";
file << "addnode=node4.dragonx.is\n";
file.close();
+28 -3
View File
@@ -1,6 +1,9 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// rpc_client.cpp — JSON-RPC client over HTTPS using libcurl.
// All calls are blocking; run on RPCWorker threads, never on main thread.
#include "rpc_client.h"
#include "../config/version.h"
@@ -70,6 +73,16 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
impl_->url = "http://" + host + ":" + port + "/";
VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str());
// Clean up previous curl handle/headers to avoid leaks on retries
if (impl_->headers) {
curl_slist_free_all(impl_->headers);
impl_->headers = nullptr;
}
if (impl_->curl) {
curl_easy_cleanup(impl_->curl);
impl_->curl = nullptr;
}
// Initialize curl handle
impl_->curl = curl_easy_init();
if (!impl_->curl) {
@@ -78,7 +91,7 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
}
// Set up headers - daemon expects text/plain, not application/json
impl_->headers = curl_slist_append(impl_->headers, "Content-Type: text/plain");
impl_->headers = curl_slist_append(nullptr, "Content-Type: text/plain");
std::string auth_header = "Authorization: Basic " + auth_;
impl_->headers = curl_slist_append(impl_->headers, auth_header.c_str());
@@ -94,6 +107,8 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
json result = call("getinfo");
if (result.contains("version")) {
connected_ = true;
warming_up_ = false;
warmup_status_.clear();
last_connect_error_.clear();
DEBUG_LOGF("Connected to dragonxd v%d\n", result["version"].get<int>());
return true;
@@ -101,7 +116,9 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
} catch (const std::exception& e) {
last_connect_error_ = e.what();
// Daemon warmup messages (Loading block index, Verifying blocks, etc.)
// are normal startup progress — don't label them "Connection failed".
// are normal startup progress — the daemon is reachable and auth works,
// it just hasn't finished initializing yet. Mark as connected+warmup
// so the wallet can show the UI instead of a blocking overlay.
std::string msg = e.what();
bool isWarmup = (msg.find("Loading") != std::string::npos ||
msg.find("Verifying") != std::string::npos ||
@@ -110,13 +127,19 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
msg.find("Rescanning") != std::string::npos ||
msg.find("Pruning") != std::string::npos);
if (isWarmup) {
DEBUG_LOGF("Daemon starting: %s\n", msg.c_str());
connected_ = true;
warming_up_ = true;
warmup_status_ = msg;
DEBUG_LOGF("Daemon warming up: %s\n", msg.c_str());
return true;
} else {
DEBUG_LOGF("Connection failed: %s\n", msg.c_str());
}
}
connected_ = false;
warming_up_ = false;
warmup_status_.clear();
return false;
}
@@ -124,6 +147,8 @@ void RPCClient::disconnect()
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
connected_ = false;
warming_up_ = false;
warmup_status_.clear();
if (impl_->curl) {
curl_easy_cleanup(impl_->curl);
impl_->curl = nullptr;
+15
View File
@@ -53,6 +53,19 @@ public:
*/
bool isConnected() const { return connected_; }
/**
* @brief True if the last connect() succeeded but daemon returned a warmup error.
* The curl handle is valid and auth succeeded — RPC calls will throw warmup errors
* until the daemon finishes initializing.
*/
bool isWarmingUp() const { return warming_up_; }
/**
* @brief The warmup status message (e.g. "Activating best chain...").
* Empty when not in warmup.
*/
const std::string& getWarmupStatus() const { return warmup_status_; }
/**
* @brief Get the error message from the last failed connect() attempt.
*/
@@ -182,6 +195,8 @@ private:
std::string port_;
std::string auth_; // Base64 encoded "user:password"
bool connected_ = false;
bool warming_up_ = false;
std::string warmup_status_;
std::string last_connect_error_;
mutable std::recursive_mutex curl_mutex_; // serializes all curl handle access
+3
View File
@@ -1,6 +1,9 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// rpc_worker.cpp — Background work queue. Executes WorkFn on its own thread,
// returns MainCb callbacks drained each frame on the main thread.
#include "rpc_worker.h"
#include <cstdio>
+8 -1
View File
@@ -300,7 +300,14 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
cjkCfg.GlyphRanges = cjkRanges;
snprintf(cjkCfg.Name, sizeof(cjkCfg.Name), "NotoSansCJK %.0fpx (merge)", size);
io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg);
ImFont* mergeResult = io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg);
if (mergeResult) {
DEBUG_LOGF("Typography: Merged CJK (%u bytes) into %s OK\n",
g_noto_cjk_subset_size, name);
} else {
DEBUG_LOGF("Typography: WARNING — CJK merge FAILED for %s (size=%u)\n",
name, g_noto_cjk_subset_size);
}
}
} else {
DEBUG_LOGF("Typography: Failed to load %s\n", name);
+21 -5
View File
@@ -93,6 +93,9 @@ static bool sp_gradient_background = false;
// Low-spec mode
static bool sp_low_spec_mode = false;
// Reduce motion (accessibility)
static bool sp_reduce_motion = false;
// Font scale (user accessibility, 1.0–3.0)
static float sp_font_scale = 1.0f;
@@ -179,6 +182,7 @@ static void loadSettingsPageState(config::Settings* settings) {
sp_theme_effects_enabled = settings->getThemeEffectsEnabled();
sp_low_spec_mode = settings->getLowSpecMode();
effects::setLowSpecMode(sp_low_spec_mode);
sp_reduce_motion = settings->getReduceMotion();
sp_font_scale = settings->getFontScale();
Layout::setUserFontScale(sp_font_scale); // sync with Layout on load
sp_keep_daemon_running = settings->getKeepDaemonRunning();
@@ -232,6 +236,7 @@ static void saveSettingsPageState(config::Settings* settings) {
settings->setScanlineEnabled(sp_scanline_enabled);
settings->setThemeEffectsEnabled(sp_theme_effects_enabled);
settings->setLowSpecMode(sp_low_spec_mode);
settings->setReduceMotion(sp_reduce_motion);
settings->setFontScale(sp_font_scale);
settings->setKeepDaemonRunning(sp_keep_daemon_running);
settings->setStopExternalDaemon(sp_stop_external_daemon);
@@ -672,6 +677,12 @@ void RenderSettingsPage(App* app) {
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg"));
ImGui::SameLine(0, Layout::spacingLg());
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &sp_reduce_motion)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion"));
ImGui::BeginDisabled(sp_low_spec_mode);
ImGui::SameLine(0, Layout::spacingLg());
@@ -702,7 +713,7 @@ void RenderSettingsPage(App* app) {
{
char blur_fmt[16];
if (sp_blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
snprintf(blur_fmt, sizeof(blur_fmt), "%s", TR("slider_off"));
else
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f);
if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
@@ -724,7 +735,7 @@ void RenderSettingsPage(App* app) {
{
char noise_fmt[16];
if (sp_noise_opacity < 0.01f)
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off"));
else
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f);
if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt,
@@ -962,6 +973,11 @@ void RenderSettingsPage(App* app) {
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg_alt"));
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &sp_reduce_motion)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion"));
ImGui::BeginDisabled(sp_low_spec_mode);
if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &sp_scanline_enabled)) {
@@ -985,7 +1001,7 @@ void RenderSettingsPage(App* app) {
{
char blur_fmt[16];
if (sp_blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
snprintf(blur_fmt, sizeof(blur_fmt), "%s", TR("slider_off"));
else
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f);
if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
@@ -1003,7 +1019,7 @@ void RenderSettingsPage(App* app) {
{
char noise_fmt[16];
if (sp_noise_opacity < 0.01f)
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off"));
else
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f);
if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt,
@@ -1570,7 +1586,7 @@ void RenderSettingsPage(App* app) {
float comboW = S.drawElement("components.settings-page", "security-combo-width").sizeOr(120.0f);
int timeout = app->settings()->getAutoLockTimeout();
const char* timeoutLabels[] = { "Off", "1 min", "5 min", "15 min", "30 min", "1 hour" };
const char* timeoutLabels[] = { TR("timeout_off"), TR("timeout_1min"), TR("timeout_5min"), TR("timeout_15min"), TR("timeout_30min"), TR("timeout_1hour") };
int timeoutValues[] = { 0, 60, 300, 900, 1800, 3600 };
int selTimeout = 0;
for (int i = 0; i < 6; i++) {
+8 -4
View File
@@ -380,7 +380,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
// collapsed: when true, sidebar is in icon-only mode (narrow width).
// The caller can toggle collapsed via a reference if a toggle button is desired.
inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHeight,
SidebarStatus& status, bool& collapsed)
SidebarStatus& status, bool& collapsed, bool locked = false)
{
using namespace material;
bool changed = false;
@@ -610,8 +610,12 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
// Click detection
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
// Click detection — block pages that require unlock when locked
bool pageNeedsUnlock = locked &&
item.page != NavPage::Console &&
item.page != NavPage::Peers &&
item.page != NavPage::Settings;
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !pageNeedsUnlock) {
current = item.page;
changed = true;
}
@@ -620,7 +624,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
float iconS = iconHalfSize; // icon half-size in pixels
float iconCY = cursor.y + btnH * 0.5f;
float textY = cursor.y + (btnH - ImGui::GetTextLineHeight()) * 0.5f;
ImU32 textCol = selected ? Primary() : OnSurfaceMedium();
ImU32 textCol = selected ? Primary() : (pageNeedsUnlock ? OnSurfaceDisabled() : OnSurfaceMedium());
if (showLabels) {
// Measure total width of icon + gap + label, then center.
+1 -1
View File
@@ -144,7 +144,7 @@ void BackupWalletDialog::render(App* app)
s_status = statusMsg;
s_backing_up = false;
if (success) {
Notifications::instance().success(TR("backup_created"));
Notifications::instance().success(TR("backup_created"), 5.0f);
} else {
Notifications::instance().warning(statusMsg);
}
+2 -2
View File
@@ -326,7 +326,7 @@ static void RenderBalanceClassic(App* app)
// Lerp displayed balances toward actual values
{
float dt = ImGui::GetIO().DeltaTime;
float speed = kBalanceLerpSpeed;
float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed;
auto lerp = [](double& disp, double target, float dt, float spd) {
double diff = target - disp;
if (std::abs(diff) < 1e-9) { disp = target; return; }
@@ -1278,7 +1278,7 @@ static void UpdateBalanceLerp(App* app) {
const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f);
const auto& state = app->state();
float dt = ImGui::GetIO().DeltaTime;
float speed = kBalanceLerpSpeed;
float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed;
auto lerp = [](double& disp, double target, float dt, float spd) {
double diff = target - disp;
if (std::abs(diff) < 1e-9) { disp = target; return; }
+3
View File
@@ -1,6 +1,9 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// console_tab.cpp — Interactive RPC console with command history,
// tab completion, daemon log display, and color-coded output.
#include "console_tab.h"
#include "../material/colors.h"
+1 -1
View File
@@ -213,7 +213,7 @@ void ExportAllKeysDialog::render(App* app)
s_exporting = false;
if (writeOk) {
s_status = "Exported to: " + filepath;
Notifications::instance().success(TR("export_keys_success"));
Notifications::instance().success(TR("export_keys_success"), 5.0f);
} else {
s_status = "Failed to write file";
Notifications::instance().error("Failed to save key file");
@@ -142,7 +142,7 @@ void ExportTransactionsDialog::render(App* app)
s_status = "Exported " + std::to_string(state.transactions.size()) +
" transactions to: " + filepath;
Notifications::instance().success(TR("export_tx_success"));
Notifications::instance().success(TR("export_tx_success"), 5.0f);
}
}
}
+28 -4
View File
@@ -307,8 +307,9 @@ void ImportKeyDialog::render(App* app)
// Import keys on worker thread to avoid freezing UI
bool rescan = s_rescan;
int rescanHeight = s_rescan_height;
if (app->worker()) {
app->worker()->post([rpc = app->rpc(), keys, rescan]() -> rpc::RPCWorker::MainCb {
app->worker()->post([rpc = app->rpc(), keys, rescan, rescanHeight]() -> rpc::RPCWorker::MainCb {
int imported = 0;
int failed = 0;
@@ -317,10 +318,22 @@ void ImportKeyDialog::render(App* app)
try {
if (keyType == "z-spending") {
rpc->call("z_importkey", {key, rescan ? "yes" : "no"});
// z_importkey "key" "yes"|"no" startheight
if (rescan && rescanHeight > 0) {
rpc->call("z_importkey", {key, "yes", rescanHeight});
} else {
rpc->call("z_importkey", {key, rescan ? "yes" : "no"});
}
imported++;
} else if (keyType == "t-privkey") {
rpc->call("importprivkey", {key, "", rescan});
// importprivkey does not accept a startheight;
// import without rescan, then rescanblockchain
// from the requested height afterward.
if (rescan && rescanHeight > 0) {
rpc->call("importprivkey", {key, "", false});
} else {
rpc->call("importprivkey", {key, "", rescan});
}
imported++;
} else {
failed++;
@@ -329,6 +342,17 @@ void ImportKeyDialog::render(App* app)
failed++;
}
}
// If t-keys were imported without rescan (because a
// specific start height was requested), trigger a
// single rescanblockchain from that height now.
if (rescan && rescanHeight > 0 && imported > 0) {
try {
rpc->call("rescanblockchain", {rescanHeight});
} catch (...) {
// rescan failure is non-fatal; user can retry
}
}
return [imported, failed]() {
s_imported_keys = imported;
@@ -339,7 +363,7 @@ void ImportKeyDialog::render(App* app)
imported, failed);
s_status = buf;
if (imported > 0) {
Notifications::instance().success(TR("import_key_success"));
Notifications::instance().success(TR("import_key_success"), 5.0f);
}
};
});
+20
View File
@@ -237,6 +237,26 @@ void RenderMarketTab(App* app)
ImVec2(centerX - valSz.x * 0.5f, valY), stats[i].valueCol, stats[i].value.c_str());
}
// ---- STALENESS INDICATOR ----
{
auto fetchTime = market.last_fetch_time;
if (fetchTime.time_since_epoch().count() > 0) {
auto elapsed = std::chrono::steady_clock::now() - fetchTime;
int ageSecs = (int)std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();
bool stale = ageSecs > 300; // 5 minutes
if (ageSecs < 60)
snprintf(buf, sizeof(buf), "%s %ds %s", stale ? ICON_MD_WARNING : "", ageSecs, TR("ago"));
else
snprintf(buf, sizeof(buf), "%s %dm %s", stale ? ICON_MD_WARNING : "", ageSecs / 60, TR("ago"));
ImFont* staleFont = capFont;
ImU32 staleCol = stale ? Warning() : WithAlpha(OnSurface(), 100);
float staleY = statsY + ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + Layout::spacingSm();
ImVec2 staleSz = staleFont->CalcTextSizeA(staleFont->LegacySize, FLT_MAX, 0, buf);
dl->AddText(staleFont, staleFont->LegacySize,
ImVec2(cardMin.x + Layout::spacingLg(), staleY), staleCol, buf);
}
}
// ---- TRADE BUTTON (top-right of card) ----
if (!currentExchange.pairs.empty()) {
const char* pairName = currentExchange.pairs[s_pair_idx].displayName.c_str();
+474 -6
View File
@@ -43,6 +43,132 @@ static int s_drag_anchor_thread = 0; // thread# where drag started
// Earnings filter: 0 = All, 1 = Solo, 2 = Pool
static int s_earnings_filter = 0;
// Thread benchmark state
struct ThreadBenchmark {
enum class Phase { Idle, Starting, WarmingUp, Measuring, Advancing, CoolingDown, Done };
Phase phase = Phase::Idle;
std::vector<int> candidates;
int current_index = 0;
struct Result {
int threads;
double hashrate;
};
std::vector<Result> results;
float phase_timer = 0.0f;
// Warmup: wait at least MIN then check for hashrate stability; cap at MAX.
// Laptops need 90s+ for thermal throttling to fully manifest.
static constexpr float MIN_WARMUP_SECS = 90.0f;
static constexpr float MAX_WARMUP_SECS = 300.0f;
static constexpr float MEASURE_SECS = 30.0f;
static constexpr float COOLDOWN_SECS = 5.0f;
// Stability detection — compare rolling 10s hashrate windows.
// Require STABLE_WINDOWS_NEEDED consecutive stable readings.
static constexpr float STABILITY_WINDOW_SECS = 10.0f;
static constexpr float STABILITY_THRESHOLD = 0.05f; // 5% change → stable
static constexpr int STABLE_WINDOWS_NEEDED = 3;
double prev_window_avg = 0.0;
double window_sum = 0.0;
int window_samples = 0;
float window_timer = 0.0f;
int consecutive_stable = 0; // count of consecutive stable windows
// Measurement: average-based (sustained performance, not peak burst)
double measure_sum = 0.0;
int measure_samples = 0;
int optimal_threads = 0;
double optimal_hashrate = 0.0;
bool was_pool_running = false;
int prev_threads = 0;
// Track actual warmup durations for better time estimates
float total_warmup_secs = 0.0f;
void reset() {
phase = Phase::Idle;
candidates.clear();
current_index = 0;
results.clear();
phase_timer = 0.0f;
prev_window_avg = 0.0;
window_sum = 0.0;
window_samples = 0;
window_timer = 0.0f;
consecutive_stable = 0;
measure_sum = 0.0;
measure_samples = 0;
optimal_threads = 0;
optimal_hashrate = 0.0;
was_pool_running = false;
prev_threads = 0;
total_warmup_secs = 0.0f;
}
void buildCandidates(int max_threads) {
candidates.clear();
// Start at half the cores — lower counts are rarely optimal and
// testing them first would waste time warming up the CPU before
// reaching the thread counts that actually matter.
int start = std::max(1, max_threads / 2);
for (int t = start; t <= max_threads; t++)
candidates.push_back(t);
}
/// Average warmup duration based on tests completed so far
float avgWarmupSecs() const {
if (current_index > 0)
return total_warmup_secs / (float)current_index;
return (MIN_WARMUP_SECS + MAX_WARMUP_SECS) * 0.5f; // initial estimate
}
/// Estimated seconds per test (uses observed warmup average)
float perTestSecs() const {
return avgWarmupSecs() + MEASURE_SECS;
}
float totalEstimatedSecs() const {
int n = (int)candidates.size();
if (n <= 0) return 0.0f;
// Completed tests use actual time; remaining use estimate
float completed_time = total_warmup_secs
+ (float)current_index * (MEASURE_SECS + COOLDOWN_SECS);
int remaining = n - current_index;
float remaining_time = (float)remaining * (avgWarmupSecs() + MEASURE_SECS)
+ (float)std::max(0, remaining - 1) * COOLDOWN_SECS;
return completed_time + remaining_time;
}
float elapsedSecs() const {
float completed = total_warmup_secs
+ (float)current_index * (MEASURE_SECS + COOLDOWN_SECS);
return completed + phase_timer;
}
float progress() const {
float total = totalEstimatedSecs();
return (total > 0.0f) ? std::min(1.0f, elapsedSecs() / total) : 0.0f;
}
void resetStabilityTracking() {
prev_window_avg = 0.0;
window_sum = 0.0;
window_samples = 0;
window_timer = 0.0f;
consecutive_stable = 0;
}
};
static ThreadBenchmark s_benchmark;
bool IsMiningBenchmarkActive() {
return s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
s_benchmark.phase != ThreadBenchmark::Phase::Done;
}
// Pool mode state
static bool s_pool_mode = false;
static char s_pool_url[256] = "pool.dragonx.is:3433";
@@ -162,9 +288,16 @@ static void RenderMiningTabContent(App* app)
}
// Sync thread grid with actual count when idle thread scaling adjusts threads
if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active) {
if (s_pool_mode && state.pool_mining.xmrig_running && state.pool_mining.threads_active > 0) {
s_selected_threads = std::min(state.pool_mining.threads_active, max_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 = std::min(reqThreads, max_threads);
else if (state.pool_mining.threads_active > 0)
s_selected_threads = std::min(state.pool_mining.threads_active, max_threads);
} else if (mining.generate && mining.genproclimit > 0) {
s_selected_threads = std::min(mining.genproclimit, max_threads);
}
@@ -239,6 +372,135 @@ static void RenderMiningTabContent(App* app)
? state.pool_mining.xmrig_running
: (mining.generate || state.pool_mining.xmrig_running);
// ================================================================
// Thread Benchmark state machine — runs pool mining at each candidate
// thread count to find the optimal setting for this CPU.
// ================================================================
if (s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
s_benchmark.phase != ThreadBenchmark::Phase::Done) {
float dt = ImGui::GetIO().DeltaTime;
s_benchmark.phase_timer += dt;
switch (s_benchmark.phase) {
case ThreadBenchmark::Phase::Starting:
// Start pool mining at current candidate
if (s_benchmark.current_index < (int)s_benchmark.candidates.size()) {
int t = s_benchmark.candidates[s_benchmark.current_index];
app->stopPoolMining();
app->startPoolMining(t);
s_benchmark.phase = ThreadBenchmark::Phase::WarmingUp;
s_benchmark.phase_timer = 0.0f;
s_benchmark.resetStabilityTracking();
s_benchmark.measure_sum = 0.0;
s_benchmark.measure_samples = 0;
} else {
s_benchmark.phase = ThreadBenchmark::Phase::Done;
}
break;
case ThreadBenchmark::Phase::WarmingUp: {
// Adaptive warmup: wait for hashrate to stabilize (thermal steady state).
// After MIN_WARMUP (90s), compare rolling 10s hashrate windows.
// Require 3 consecutive windows within 5% to confirm equilibrium.
// Laptops can take 2-3+ minutes for thermal throttling to fully
// manifest, so a single stable window isn't sufficient.
bool past_min = s_benchmark.phase_timer >= ThreadBenchmark::MIN_WARMUP_SECS;
bool past_max = s_benchmark.phase_timer >= ThreadBenchmark::MAX_WARMUP_SECS;
// Accumulate samples into current window
if (state.pool_mining.hashrate_10s > 0.0) {
s_benchmark.window_sum += state.pool_mining.hashrate_10s;
s_benchmark.window_samples++;
}
s_benchmark.window_timer += dt;
bool stable = false;
if (past_min && s_benchmark.window_timer >= ThreadBenchmark::STABILITY_WINDOW_SECS
&& s_benchmark.window_samples > 0) {
double current_avg = s_benchmark.window_sum / s_benchmark.window_samples;
if (s_benchmark.prev_window_avg > 0.0) {
double change = std::abs(current_avg - s_benchmark.prev_window_avg)
/ s_benchmark.prev_window_avg;
if (change < ThreadBenchmark::STABILITY_THRESHOLD)
s_benchmark.consecutive_stable++;
else
s_benchmark.consecutive_stable = 0; // reset on instability
if (s_benchmark.consecutive_stable >= ThreadBenchmark::STABLE_WINDOWS_NEEDED)
stable = true;
}
// Shift window
s_benchmark.prev_window_avg = current_avg;
s_benchmark.window_sum = 0.0;
s_benchmark.window_samples = 0;
s_benchmark.window_timer = 0.0f;
}
if (stable || past_max) {
s_benchmark.total_warmup_secs += s_benchmark.phase_timer;
s_benchmark.phase = ThreadBenchmark::Phase::Measuring;
s_benchmark.phase_timer = 0.0f;
s_benchmark.measure_sum = 0.0;
s_benchmark.measure_samples = 0;
}
break;
}
case ThreadBenchmark::Phase::Measuring:
// Sample average hashrate — reflects sustained (thermally throttled) performance
if (state.pool_mining.hashrate_10s > 0.0) {
s_benchmark.measure_sum += state.pool_mining.hashrate_10s;
s_benchmark.measure_samples++;
}
if (s_benchmark.phase_timer >= ThreadBenchmark::MEASURE_SECS) {
int t = s_benchmark.candidates[s_benchmark.current_index];
double avg = (s_benchmark.measure_samples > 0)
? s_benchmark.measure_sum / s_benchmark.measure_samples
: 0.0;
s_benchmark.results.push_back({t, avg});
if (avg > s_benchmark.optimal_hashrate) {
s_benchmark.optimal_hashrate = avg;
s_benchmark.optimal_threads = t;
}
s_benchmark.phase = ThreadBenchmark::Phase::Advancing;
s_benchmark.phase_timer = 0.0f;
}
break;
case ThreadBenchmark::Phase::Advancing:
app->stopPoolMining();
s_benchmark.current_index++;
if (s_benchmark.current_index < (int)s_benchmark.candidates.size()) {
// Cool down before next test to reduce thermal throttling bias
s_benchmark.phase = ThreadBenchmark::Phase::CoolingDown;
s_benchmark.phase_timer = 0.0f;
} else {
// Done — apply optimal thread count
s_benchmark.phase = ThreadBenchmark::Phase::Done;
if (s_benchmark.optimal_threads > 0) {
s_selected_threads = s_benchmark.optimal_threads;
app->settings()->setPoolThreads(s_selected_threads);
app->settings()->save();
}
// Restart mining if it was running before, using optimal count
if (s_benchmark.was_pool_running && s_benchmark.optimal_threads > 0) {
app->startPoolMining(s_benchmark.optimal_threads);
}
}
break;
case ThreadBenchmark::Phase::CoolingDown:
// Idle pause — let CPU temps drop before starting next test
if (s_benchmark.phase_timer >= ThreadBenchmark::COOLDOWN_SECS) {
s_benchmark.phase = ThreadBenchmark::Phase::Starting;
s_benchmark.phase_timer = 0.0f;
}
break;
default:
break;
}
}
// ================================================================
// Proportional section budget — ensures all content fits without
// scrolling at the minimum window size (1024×775).
@@ -936,6 +1198,41 @@ static void RenderMiningTabContent(App* app)
idleRightEdge = sBtnX - 4.0f * dp;
}
// GPU-aware idle toggle (to the left, when idle is on)
// When ON (default): GPU utilization >= 10% counts as "not idle"
// When OFF: unrestricted mode, only keyboard/mouse input matters
if (idleOn) {
bool gpuAware = app->settings()->getIdleGpuAware();
const char* gpuIcon = gpuAware ? ICON_MD_MONITOR : ICON_MD_MONITOR;
float gBtnX = idleRightEdge - btnSz;
float gBtnY = btnY;
if (gpuAware) {
dl->AddRectFilled(ImVec2(gBtnX, gBtnY), ImVec2(gBtnX + btnSz, gBtnY + btnSz),
WithAlpha(Primary(), 40), btnSz * 0.5f);
}
ImVec2 gIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, gpuIcon);
ImU32 gIcoCol = gpuAware ? Primary() : OnSurfaceDisabled();
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(gBtnX + (btnSz - gIcoSz.x) * 0.5f, gBtnY + (btnSz - gIcoSz.y) * 0.5f),
gIcoCol, gpuIcon);
ImGui::SetCursorScreenPos(ImVec2(gBtnX, gBtnY));
ImGui::InvisibleButton("##IdleGpuAware", ImVec2(btnSz, btnSz));
if (ImGui::IsItemClicked()) {
app->settings()->setIdleGpuAware(!gpuAware);
app->settings()->save();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", gpuAware
? TR("mining_idle_gpu_on_tooltip")
: TR("mining_idle_gpu_off_tooltip"));
}
idleRightEdge = gBtnX - 4.0f * dp;
}
// Idle delay combo (to the left, when idle is enabled and NOT in thread scaling mode)
if (idleOn && !threadScaling) {
struct DelayOption { int seconds; const char* label; };
@@ -1076,6 +1373,175 @@ static void RenderMiningTabContent(App* app)
ImGui::SetCursorScreenPos(savedCur);
}
// --- Thread Benchmark button / progress (left of idle toggle) ---
{
ImVec2 benchSavedCur = ImGui::GetCursorScreenPos();
bool benchRunning = s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
s_benchmark.phase != ThreadBenchmark::Phase::Done;
bool benchDone = s_benchmark.phase == ThreadBenchmark::Phase::Done;
ImFont* icoFont = Type().iconSmall();
if (benchRunning) {
// Show progress bar + current test info
float barW = std::min(180.0f * hs, idleRightEdge - (cardMin.x + pad) - 10.0f * dp);
float barH = 4.0f * dp;
float barX = idleRightEdge - barW;
float barY = curY + headerH - barH - 2.0f * dp;
// Progress bar track
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
WithAlpha(OnSurface(), 30), barH * 0.5f);
// Progress bar fill
float pct = s_benchmark.progress();
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW * pct, barY + barH),
Primary(), barH * 0.5f);
// Status text above bar
int ct = s_benchmark.current_index < (int)s_benchmark.candidates.size()
? s_benchmark.candidates[s_benchmark.current_index] : 0;
// Estimated remaining time (uses observed warmup for better accuracy)
int remaining_tests = (int)s_benchmark.candidates.size() - s_benchmark.current_index;
float elapsed_in_phase = s_benchmark.phase_timer;
float phase_total;
if (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp)
phase_total = s_benchmark.avgWarmupSecs(); // adaptive estimate
else if (s_benchmark.phase == ThreadBenchmark::Phase::CoolingDown)
phase_total = ThreadBenchmark::COOLDOWN_SECS;
else
phase_total = ThreadBenchmark::MEASURE_SECS;
float remaining_in_current = std::max(0.0f, phase_total - elapsed_in_phase);
// Remaining tests after current each need warmup + measure + cooldown
float est_secs = remaining_in_current
+ (remaining_tests - 1) * (s_benchmark.avgWarmupSecs() + ThreadBenchmark::MEASURE_SECS + ThreadBenchmark::COOLDOWN_SECS);
int est_min = (int)(est_secs / 60.0f);
int est_sec = (int)est_secs % 60;
const char* phase_label;
if (s_benchmark.phase == ThreadBenchmark::Phase::CoolingDown)
phase_label = TR("mining_benchmark_cooling");
else if (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp
&& s_benchmark.phase_timer >= ThreadBenchmark::MIN_WARMUP_SECS)
phase_label = TR("mining_benchmark_stabilizing");
else
phase_label = TR("mining_benchmark_testing");
if (est_min > 0)
snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%dm%ds",
phase_label,
s_benchmark.current_index + 1,
(int)s_benchmark.candidates.size(), ct, est_min, est_sec);
else
snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%ds",
phase_label,
s_benchmark.current_index + 1,
(int)s_benchmark.candidates.size(), ct, est_sec);
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(barX + (barW - txtSz.x) * 0.5f, barY - txtSz.y - 2.0f * dp),
OnSurfaceMedium(), buf);
// Cancel button (small X)
float cancelSz = icoFont->LegacySize + 4.0f * dp;
float cancelX = barX - cancelSz - 4.0f * dp;
float cancelY = curY + (headerH - cancelSz) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(cancelX, cancelY));
ImGui::InvisibleButton("##BenchCancel", ImVec2(cancelSz, cancelSz));
if (ImGui::IsItemClicked()) {
app->stopPoolMining();
if (s_benchmark.was_pool_running)
app->startPoolMining(s_benchmark.prev_threads);
s_benchmark.reset();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", TR("mining_benchmark_cancel"));
}
const char* cancelIcon = ICON_MD_CLOSE;
ImVec2 cIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, cancelIcon);
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(cancelX + (cancelSz - cIcoSz.x) * 0.5f,
cancelY + (cancelSz - cIcoSz.y) * 0.5f),
OnSurfaceMedium(), cancelIcon);
idleRightEdge = cancelX - 4.0f * dp;
} else if (benchDone && s_benchmark.optimal_threads > 0) {
// Show result briefly, then reset on next click
snprintf(buf, sizeof(buf), "%s: %dt (%.1f H/s)",
TR("mining_benchmark_result"),
s_benchmark.optimal_threads, s_benchmark.optimal_hashrate);
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
float txtX = idleRightEdge - txtSz.x;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(txtX, curY + (headerH - txtSz.y) * 0.5f),
WithAlpha(Success(), 220), buf);
// Dismiss button
float dismissSz = icoFont->LegacySize + 4.0f * dp;
float dismissX = txtX - dismissSz - 4.0f * dp;
float dismissY = curY + (headerH - dismissSz) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(dismissX, dismissY));
ImGui::InvisibleButton("##BenchDismiss", ImVec2(dismissSz, dismissSz));
if (ImGui::IsItemClicked())
s_benchmark.reset();
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", TR("mining_benchmark_dismiss"));
}
const char* okIcon = ICON_MD_CHECK;
ImVec2 oIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, okIcon);
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(dismissX + (dismissSz - oIcoSz.x) * 0.5f,
dismissY + (dismissSz - oIcoSz.y) * 0.5f),
WithAlpha(Success(), 200), okIcon);
idleRightEdge = dismissX - 4.0f * dp;
} else if (s_pool_mode) {
// Show benchmark button (only in pool mode)
float btnSz = icoFont->LegacySize + 8.0f * dp;
float btnX = idleRightEdge - btnSz;
float btnY = curY + (headerH - btnSz) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
ImGui::InvisibleButton("##BenchStart", ImVec2(btnSz, btnSz));
bool benchHovered = ImGui::IsItemHovered();
bool benchClicked = ImGui::IsItemClicked();
// Hover highlight
if (benchHovered) {
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
StateHover(), btnSz * 0.5f);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", TR("mining_benchmark_tooltip"));
}
const char* benchIcon = ICON_MD_SPEED;
ImVec2 bIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, benchIcon);
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnX + (btnSz - bIcoSz.x) * 0.5f,
btnY + (btnSz - bIcoSz.y) * 0.5f),
OnSurfaceMedium(), benchIcon);
if (benchClicked) {
// Require a wallet address for pool mining
std::string worker(s_pool_worker);
if (!worker.empty()) {
s_benchmark.reset();
s_benchmark.was_pool_running = state.pool_mining.xmrig_running;
s_benchmark.prev_threads = s_selected_threads;
s_benchmark.buildCandidates(max_threads);
s_benchmark.phase = ThreadBenchmark::Phase::Starting;
// Stop any active solo mining first
if (mining.generate)
app->stopMining();
}
}
idleRightEdge = btnX - 4.0f * dp;
}
ImGui::SetCursorScreenPos(benchSavedCur);
}
// Active mining indicator (left of idle toggle)
if (mining.generate) {
float pulse = effects::isLowSpecMode()
@@ -1115,11 +1581,13 @@ static void RenderMiningTabContent(App* app)
}
// Show pointer cursor when hovering the thread grid
if (hovered_thread > 0)
bool benchActive = s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
s_benchmark.phase != ThreadBenchmark::Phase::Done;
if (hovered_thread > 0 && !benchActive)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
// Drag-to-select logic
if (ImGui::IsMouseClicked(0) && hovered_thread > 0) {
// Drag-to-select logic (disabled during benchmark)
if (!benchActive && ImGui::IsMouseClicked(0) && hovered_thread > 0) {
// Begin drag
s_drag_active = true;
s_drag_anchor_thread = hovered_thread;
+6
View File
@@ -15,5 +15,11 @@ namespace ui {
*/
void RenderMiningTab(App* app);
/**
* @brief Returns true when the thread benchmark is actively running.
* Used by idle mining to avoid interfering with measurements.
*/
bool IsMiningBenchmarkActive();
} // namespace ui
} // namespace dragonx
+7 -12
View File
@@ -11,7 +11,9 @@
#include "receive_tab.h"
#include "send_tab.h"
#include "../../app.h"
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../../config/version.h"
#include "../../data/wallet_state.h"
#include "../../ui/widgets/qr_code.h"
@@ -45,7 +47,7 @@ static std::string TrId(const char* key, const char* id) {
// ============================================================================
static int s_selected_address_idx = -1;
static double s_request_amount = 0.0;
static char s_request_memo[256] = "";
static char s_request_memo[512] = "";
static double s_request_usd_amount = 0.0;
static bool s_request_usd_mode = false;
static std::string s_cached_qr_data;
@@ -76,16 +78,9 @@ static std::string TruncateAddress(const std::string& addr, size_t maxLen = 40)
return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen);
}
static void OpenExplorerURL(const std::string& address) {
std::string url = "https://explorer.dragonx.com/address/" + address;
#ifdef _WIN32
std::string cmd = "start \"\" \"" + url + "\"";
#elif __APPLE__
std::string cmd = "open \"" + url + "\"";
#else
std::string cmd = "xdg-open \"" + url + "\"";
#endif
system(cmd.c_str());
static void OpenExplorerURL(App* app, const std::string& address) {
std::string url = app->settings()->getAddressExplorerUrl() + address;
dragonx::util::Platform::openUrl(url);
}
// ============================================================================
@@ -978,7 +973,7 @@ void RenderReceiveTab(App* app)
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.receive", "explorer-btn-border-size").size);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight()));
if (TactileButton(TrId("explorer", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
OpenExplorerURL(selected.address);
OpenExplorerURL(app, selected.address);
}
ImGui::PopStyleVar(); // FrameBorderSize
ImGui::PopStyleColor(4);
+1 -1
View File
@@ -804,7 +804,7 @@ void RenderSendConfirmPopup(App* app) {
s_tx_status = TR("send_tx_sent");
s_result_txid = result;
s_status_success = true;
Notifications::instance().success(TR("send_tx_success"));
Notifications::instance().success(TR("send_tx_success"), 5.0f);
s_to_address[0] = '\0';
s_amount = 0.0;
s_memo[0] = '\0';
+33 -4
View File
@@ -33,13 +33,13 @@ I18n::I18n()
// Cyrillic glyphs are included in the Ubuntu font glyph ranges.
registerLanguage("en", "English");
registerLanguage("es", "Español");
registerLanguage("zh", "Chinese");
registerLanguage("ru", "Russian");
registerLanguage("zh", "中文");
registerLanguage("ru", "Русский");
registerLanguage("de", "Deutsch");
registerLanguage("fr", "Français");
registerLanguage("pt", "Português");
registerLanguage("ja", "Japanese");
registerLanguage("ko", "Korean");
registerLanguage("ja", "日本語");
registerLanguage("ko", "한국어");
// Load default English strings (built-in fallback)
loadLanguage("en");
@@ -256,6 +256,19 @@ void I18n::loadBuiltinEnglish()
strings_["settings_quick_unlock_pin"] = "Quick-unlock PIN";
strings_["settings_pin_active"] = "PIN";
strings_["settings_encrypt_first_pin"] = "Encrypt wallet first to enable PIN";
strings_["incorrect_pin"] = "Incorrect PIN";
strings_["incorrect_passphrase"] = "Incorrect passphrase";
strings_["pin_not_set"] = "PIN not set. Use passphrase to unlock.";
strings_["restarting_after_encryption"] = "Restarting daemon after encryption...";
strings_["force_quit"] = "Force Quit";
strings_["force_quit_warning"] = "This will immediately kill the daemon without a clean shutdown. May require a blockchain resync.";
strings_["force_quit_confirm_title"] = "Force Quit?";
strings_["force_quit_confirm_msg"] = "This will immediately kill the daemon without a clean shutdown.\nThis may corrupt the blockchain index and require a resync.";
strings_["force_quit_yes"] = "Force Quit";
strings_["reduce_motion"] = "Reduce Motion";
strings_["tt_reduce_motion"] = "Disable animated transitions and balance lerp for accessibility";
strings_["ago"] = "ago";
strings_["wizard_daemon_start_failed"] = "Failed to start daemon \xe2\x80\x94 it will be retried automatically";
strings_["settings_data_dir"] = "Data Dir:";
strings_["settings_wallet_size_label"] = "Wallet Size:";
strings_["settings_debug_changed"] = "Debug categories changed \xe2\x80\x94 restart daemon to apply";
@@ -272,6 +285,13 @@ void I18n::loadBuiltinEnglish()
strings_["settings_block_explorer_urls"] = "Block Explorer URLs";
strings_["settings_configure_explorer"] = "Configure external block explorer links";
strings_["settings_auto_lock"] = "AUTO-LOCK";
strings_["timeout_off"] = "Off";
strings_["timeout_1min"] = "1 min";
strings_["timeout_5min"] = "5 min";
strings_["timeout_15min"] = "15 min";
strings_["timeout_30min"] = "30 min";
strings_["timeout_1hour"] = "1 hour";
strings_["slider_off"] = "Off";
strings_["settings_wallet_file_size"] = "Wallet file size: %s";
strings_["settings_wallet_location"] = "Wallet location: %s";
strings_["settings_rpc_note"] = "Note: Connection settings are usually auto-detected from DRAGONX.conf";
@@ -845,6 +865,8 @@ void I18n::loadBuiltinEnglish()
strings_["mining_idle_on_tooltip"] = "Disable idle mining";
strings_["mining_idle_scale_on_tooltip"] = "Thread scaling: ON\nClick to switch to start/stop mode";
strings_["mining_idle_scale_off_tooltip"] = "Start/stop mode: ON\nClick to switch to thread scaling mode";
strings_["mining_idle_gpu_on_tooltip"] = "GPU-aware: ON\nGPU activity (video, games) prevents idle mining\nClick for unrestricted mode";
strings_["mining_idle_gpu_off_tooltip"] = "Unrestricted: ON\nOnly keyboard/mouse input determines idle state\nClick to enable GPU-aware detection";
strings_["mining_idle_threads_active_tooltip"] = "Threads when user is active";
strings_["mining_idle_threads_idle_tooltip"] = "Threads when system is idle";
strings_["mining_local_hashrate"] = "Local Hashrate";
@@ -866,6 +888,13 @@ void I18n::loadBuiltinEnglish()
strings_["mining_recent_payouts"] = "RECENT POOL PAYOUTS";
strings_["mining_remove"] = "Remove";
strings_["mining_reset_defaults"] = "Reset Defaults";
strings_["mining_benchmark_tooltip"] = "Find optimal thread count for this CPU";
strings_["mining_benchmark_testing"] = "Testing";
strings_["mining_benchmark_cooling"] = "Cooling";
strings_["mining_benchmark_stabilizing"] = "Stabilizing";
strings_["mining_benchmark_cancel"] = "Cancel benchmark";
strings_["mining_benchmark_result"] = "Optimal";
strings_["mining_benchmark_dismiss"] = "Dismiss";
strings_["mining_save_payout_address"] = "Save payout address";
strings_["mining_save_pool_url"] = "Save pool URL";
strings_["mining_saved_addresses"] = "Saved Addresses:";
+101
View File
@@ -688,5 +688,106 @@ int Platform::getSystemIdleSeconds()
#endif
}
// ============================================================================
// GPU utilization detection
// ============================================================================
int Platform::getGpuUtilization()
{
#ifdef _WIN32
// Windows: read GPU utilization via SetupAPI / D3DKMT
// Not all GPUs expose this; return -1 if unavailable.
// Use a popen fallback: nvidia-smi for NVIDIA, or return -1.
static bool s_tried_nvidia = false;
static bool s_has_nvidia = false;
if (!s_tried_nvidia) {
s_tried_nvidia = true;
FILE* f = _popen("where nvidia-smi 2>nul", "r");
if (f) {
char buf[256];
s_has_nvidia = (fgets(buf, sizeof(buf), f) != nullptr);
_pclose(f);
}
}
if (s_has_nvidia) {
FILE* f = _popen("nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>nul", "r");
if (f) {
char buf[64];
int util = -1;
if (fgets(buf, sizeof(buf), f)) {
util = atoi(buf);
if (util < 0 || util > 100) util = -1;
}
_pclose(f);
return util;
}
}
return -1;
#elif defined(__APPLE__)
return -1;
#else
// Linux: try multiple GPU sysfs paths
// AMD: /sys/class/drm/card*/device/gpu_busy_percent
{
// Try card0 through card3
char path[128];
for (int i = 0; i < 4; i++) {
snprintf(path, sizeof(path), "/sys/class/drm/card%d/device/gpu_busy_percent", i);
std::ifstream ifs(path);
if (ifs.is_open()) {
int val = -1;
ifs >> val;
if (val >= 0 && val <= 100)
return val;
}
}
}
// NVIDIA: nvidia-smi (binary may exist even without sysfs)
{
static bool s_tried = false;
static bool s_has_nvidia_smi = false;
if (!s_tried) {
s_tried = true;
FILE* f = popen("which nvidia-smi 2>/dev/null", "r");
if (f) {
char buf[256];
s_has_nvidia_smi = (fgets(buf, sizeof(buf), f) != nullptr);
pclose(f);
}
}
if (s_has_nvidia_smi) {
FILE* f = popen("nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>/dev/null", "r");
if (f) {
char buf[64];
int util = -1;
if (fgets(buf, sizeof(buf), f)) {
util = atoi(buf);
if (util < 0 || util > 100) util = -1;
}
pclose(f);
return util;
}
}
}
// Intel: compare current vs max freq as a rough proxy
{
std::ifstream curF("/sys/class/drm/card0/gt_cur_freq_mhz");
std::ifstream maxF("/sys/class/drm/card0/gt_max_freq_mhz");
if (curF.is_open() && maxF.is_open()) {
int cur = 0, mx = 0;
curF >> cur;
maxF >> mx;
if (mx > 0)
return std::min(100, (cur * 100) / mx);
}
}
return -1;
#endif
}
} // namespace util
} // namespace dragonx
+8
View File
@@ -131,6 +131,14 @@ public:
* @return Seconds since last user input, or 0 on failure
*/
static int getSystemIdleSeconds();
/**
* @brief Get GPU utilization percentage (0–100).
* Linux: reads sysfs for AMD, /proc for NVIDIA.
* Windows: queries PDH GPU engine counters.
* @return GPU busy percent, or -1 if unavailable.
*/
static int getGpuUtilization();
};
/**