53 Commits

Author SHA1 Message Date
dan_s
229373e937 feat(wallet): persist history and surface pending sends
Add an encrypted SQLite transaction history cache with cached tip metadata and
per-address shielded scan progress so startup and full refreshes avoid
re-scanning every z-address while still invalidating on wallet/address/rescan
changes.

Improve wallet history loading by paging transparent transactions, preserving
cached shielded and sent rows, keeping recent/unconfirmed activity visible, and
classifying mining-address receives. Show z_sendmany opid sends immediately in
History and Overview, pin pending rows through refreshes, and apply optimistic
address/balance debits until opids resolve.

Add timestamped RPC console tracing by source/method without logging params or
results, reduce redundant refresh/RPC calls, and cache Explorer recent block
summaries in SQLite.

Expand focused tests for transaction cache encryption, scan-progress
persistence/invalidation, history preservation, operation-status parsing,
pending send visibility, and Explorer/RPC refresh behavior.
2026-05-05 03:22:14 -05:00
dan_s
973c390df5 fix(history): keep wallet-created sends visible
Replay cached outgoing viewtransaction entries during transaction refresh so shielded sends created from the wallet remain in the History tab after send tracking is cleared.

Keep incomplete tracked sends retryable, preserve cached send timestamp/confirmation metadata, and emit a send placeholder from gettransaction metadata when viewtransaction enrichment is not yet available.

Add regression coverage for cached sends, retryable empty entries, placeholder sends, and send txid cleanup behavior.
2026-04-30 14:57:37 -05:00
dan_s
d684db446e Refactor app services and stabilize refresh/UI flows
- Add refresh scheduler and network refresh service boundaries for typed
  refresh results, ordered RPC collectors, applicators, and price parsing.
- Add daemon lifecycle and wallet security workflow helpers while preserving
  App-owned command RPC, decrypt, cancellation, and UI handoff behavior.
- Split balance, console, mining, amount formatting, and async task logic into
  focused modules with expanded Phase 4 test coverage.
- Fix market price loading by triggering price refresh immediately, avoiding
  queue-pressure drops, tracking loading/error state, and adding translations.
- Polish send, explorer, peers, settings, theme/schema, and related tab UI.
- Replace checked-in generated language headers with build-generated resources.
- Document the cleanup audit, UI static-state guidance, and architecture updates.
2026-04-29 12:47:57 -05:00
dan_s
9e1b1397ad feat(addresses): improve address labeling and view-only handling
- Add expanded address icon picker with search, bottom-aligned actions, and improved modal sizing
- Embed a pickaxe icon font subset and wire it into typography/address icon rendering
- Track view-only shielded addresses and prevent sends from non-spendable z-addresses
- Improve address transfer dialog sizing, max amount handling, and text clipping
- Tune main header layout values in ui.toml
- Update README, codebase overview, and third-party license documentation
2026-04-27 13:54:28 -05:00
dan_s
55a36e0d06 fix: drag-to-transfer drop not triggering transfer dialog
s_dropTargetIdx was reset to -1 unconditionally each frame, including
the release frame. Since drop target detection runs in PASS 2 (after
the drop handler), the target was always -1 when checked. Only reset
while mouse button is held so the previous frame's value is preserved.

Also bump version to 1.2.0-rc1 and add release notes.
2026-04-12 19:07:41 -05:00
dan_s
7937aad4fb fix: sidebar nav text overflow for long translations
- Add text scaling for section labels (TOOLS, ADVANCED) in sidebar
- Separate explorer_section key from explorer nav label to fix ALL CAPS
- Shorten long sidebar translations: es/pt settings, pt overview, ru tools/advanced
- Fix explorer translations from ALL CAPS to proper case in all languages
2026-04-12 18:45:48 -05:00
dan_s
077f9a7403 Add bootstrap download dialog and fix 100 missing translation keys
- New BootstrapDownloadDialog accessible from Settings page
  - Stops daemon before download, prevents auto-restart during bootstrap
  - Confirm/Downloading/Done/Failed states with progress display
  - Mirror support (bootstrap2.dragonx.is)
- Add bootstrap_downloading_ flag to prevent tryConnect() auto-reconnect
- Right-align Download Bootstrap + Setup Wizard buttons in settings
- Add 100 missing i18n keys to all 8 language files (de/es/fr/ja/ko/pt/ru/zh)
  - Includes bootstrap, explorer, mining benchmark, transfer, delete blockchain,
    force quit, address label, and settings section translations
- Update add_missing_translations.py with new translation batch
2026-04-12 18:19:01 -05:00
dan_s
9f23b2781c feat: modernize address list with drag-transfer, labels, and UX polish
- Rewrite RenderSharedAddressList with two-pass layout architecture
- Add drag-to-transfer: drag address onto another to open transfer dialog
- Add AddressLabelDialog with custom label text and 20-icon picker
- Add AddressTransferDialog with amount input, fee, and balance preview
- Add AddressMeta persistence (label, icon, sortOrder) in settings.json
- Gold favorite border inset 2dp from container edge
- Show hide button on all addresses, not just zero-balance
- Smaller star/hide buttons to clear favorite border
- Semi-transparent dragged row with context-aware tooltip
- Copy-to-clipboard deferred to mouse-up (no copy on drag)
- Themed colors via resolveColor() with CSS variable fallbacks
- Keyboard nav (Up/Down/J/K, Enter to copy, F2 to edit label)
- Add i18n keys for all new UI strings
2026-04-12 17:29:56 -05:00
dan_s
79d8f0d809 refactor: rewrite sidebar layout with two-pass architecture
Replace fragile Dummy()-based cursor flow with a deterministic two-pass
layout system:
- Pass 1: compute exact Y positions for all elements (pure math)
- Pass 2: render at computed positions using SetCursorScreenPos + draw list

Eliminates the dual-coordinate mismatch that caused persistent centering
and overflow bugs. Height is computed once, not estimated then measured.

Also tune sidebar spacing via ui.toml:
- button-spacing: 4 → 6
- section-gap: 4 → 8
- Add section-label-pad-bottom (4px) below category labels
- bottom-padding: 0 → 4
2026-04-12 16:34:31 -05:00
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
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
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
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
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
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
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
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
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
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
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
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
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
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
370 changed files with 28319 additions and 82321 deletions

23
.gitignore vendored
View File

@@ -11,8 +11,8 @@ prebuilt-binaries/dragonxd-win/*
!prebuilt-binaries/dragonxd-win/.gitkeep
prebuilt-binaries/dragonxd-mac/*
!prebuilt-binaries/dragonxd-mac/.gitkeep
prebuilt-binaries/drg-xmrig/*
!prebuilt-binaries/drg-xmrig/.gitkeep
prebuilt-binaries/xmrig-hac/*
!prebuilt-binaries/xmrig-hac/.gitkeep
# External sources / toolchains (created by scripts/setup.sh)
@@ -33,7 +33,7 @@ imgui.ini
*.bak*
*.params
asmap.dat
/external/drg-xmrig
/external/xmrig-hac
/memory
/todo.md
/.github/
@@ -41,20 +41,3 @@ asmap.dat
# macOS
.DS_Store
# Local-only archive of superseded lite-wallet design/planning docs (untracked)
docs/_archive/
# ed25519 release-signing keys — the secret key must NEVER be committed
*.ed25519.key
*.ed25519.pub.b64
# Lite-backend deps are fetched (or `cargo vendor`-ed locally for offline); not committed.
third_party/silentdragonxlite/lib/vendor/
# Generated by configure_file from res/ObsidianDragon.manifest.in (do not track)
res/ObsidianDragon.manifest
# Cross-built mingw FreeType (color emoji) — regenerated by scripts/build-freetype-mingw.sh
third_party/freetype-mingw/
third_party/.freetype-mingw-build/

111
CLAUDE.md
View File

@@ -1,111 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
ObsidianDragon is a portable, full-node GUI wallet for DragonX (DRGX), written in C++17 using SDL3 + Dear ImGui (immediate-mode). It drives a `dragonxd` full node over JSON-RPC and can embed/extract the daemon itself. A separate **Lite** variant (`ObsidianDragonLite`) drops the full node and instead talks to an external lite-wallet backend library.
## Build & run
`build.sh` is the single entry point for all builds. `setup.sh` (repo root) installs/validates dependencies.
```bash
./build.sh # Dev build (native, no packaging) -> build/linux/bin/ObsidianDragon
./build.sh --lite # Dev build of the Lite variant -> build/linux/bin/ObsidianDragonLite
./build.sh --clean # Wipe the build dir first
./build.sh --linux-release # Release zip + AppImage -> release/linux/
./build.sh --win-release # Windows cross-compile (mingw-w64) -> release/windows/
./build.sh --mac-release # macOS .app bundle + DMG
./setup.sh --check # Report missing build deps without installing
```
Dev builds use `build/linux/` (or `build/mac/`). To re-build incrementally without re-running CMake config: `cmake --build build/linux -j$(nproc)`.
The wallet connects to the daemon using credentials in `~/.hush/DRAGONX/DRAGONX.conf` (`rpcuser`/`rpcpassword`/`rpcport`). It searches for `dragonxd`/`dragonx-cli` binaries in the **executable's own directory first**, so dropping custom node builds next to the wallet binary overrides the bundled ones.
## Tests
Tests live in `tests/test_phase4.cpp` — a single large translation unit using a custom assertion harness (`EXPECT_TRUE`/`EXPECT_EQ`/`EXPECT_NEAR` macros, one `main()`, exit code = failure count). `include(CTest)` enables `BUILD_TESTING=ON` by default, so the `ObsidianDragonTests` executable is built alongside the app.
```bash
cd build/linux && ctest --output-on-failure # run the suite
./build/linux/bin/ObsidianDragonTests # run the binary directly (same thing)
```
There is no per-test filtering — it is one binary that runs every assertion. The suite exercises the services layer, lite-wallet bridge, and pure helpers (parsers, formatters, model classes) without launching the GUI. Fixtures are under `tests/fixtures/` (path injected as `DRAGONX_TEST_FIXTURE_DIR`).
## Architecture
**Entry & main loop.** `src/main.cpp` owns SDL3 window creation, ImGui/OpenGL(or DX11 on Windows) setup, and the frame loop. The `App` class is the central controller; because it is large it is split across four files that all implement the same class:
- `src/app.cpp` — core lifecycle, the per-frame `render()`, tab dispatch
- `src/app_network.cpp` — RPC orchestration, sync, peers, daemon lifecycle
- `src/app_security.cpp` — encryption, PIN/lock screen, key import/export, backup
- `src/app_wizard.cpp` — first-run wizard
**RPC.** All daemon calls go through `src/rpc/` (`rpc_client`, `connection`, `rpc_worker`). **Never block the main/UI thread with synchronous network I/O — dispatch through `RPCWorker`** (async). `rpc/types.h` holds the shared DTOs.
**Services** (`src/services/`) hold the non-UI state machines that the `App` owns: `NetworkRefreshService` + `RefreshScheduler` (polling/refresh of balance, peers, txs on intervals) and the `WalletSecurity*` controller/workflow stack (encryption & unlock flows).
**Data model** (`src/data/`): `WalletState`, `address_book`, `transaction_history_cache`, `exchange_info`. UI reads from these.
**UI** (`src/ui/`): `windows/` are the tabs and dialogs (one pair per screen, e.g. `send_tab`, `mining_tab`, `console_tab`), `pages/` are multi-section screens (Settings), `material/` is the design-system layer (the live helpers `color_theme`, `colors`, `type`/`typography`, `draw_helpers`, `layout`, `project_icons`, `components/buttons`), `schema/` loads the TOML UI schema/skins, `effects/` is GL post-processing (blur/acrylic).
**Lite wallet** (`src/wallet/`): the bridge to an external `litelib_*` C-ABI backend. `lite_client_bridge` loads the backend (via direct `litelib_*` externs in `linkedSdxl()`) and owns each Rust string through `lite_owned_string` (copy-before-free / free-once). On top sit `lite_connection_service`, `lite_sync_service`, `lite_result_parsers`, `lite_wallet_gateway`, `lite_wallet_state_mapper`, and `lite_wallet_lifecycle_service`, all driven by `lite_wallet_controller`. The real frontend entry points are `lite_wallet_lifecycle_ui_adapter` and `lite_wallet_server_selection_adapter` (used by `src/ui/pages/settings_page.cpp`); everything else is reachable through them. (The prebuilt-backend symbol check for `DRAGONX_ENABLE_LITE_BACKEND` is done in CMake against the symbols inventory — see below — not in C++.)
> ⚠️ **Do not regrow the `_plan`/`_batch` churn.** This directory previously held ~160 dead `lite_wallet_*_plan` / `*_batch*_receipt_custody_acceptance_confirmation_archive_handoff_*` files (filenames up to 250 chars) — auto-generated scaffolding that never reached the shipping binary. They were deleted. When extending lite-wallet behavior, **edit the named service/bridge/runtime files in place**; never add another "promotion/receipt/custody/handoff/stewardship" wrapper layer. `scripts/check-source-hygiene.sh` (wired as a `.git/hooks/pre-commit` hook) blocks >80-char filenames and chained churn-token names — run it in CI too.
**Chat** (`src/chat/*`): the HushChat protocol port (Contacts/Chat tabs, seed-derived identity, secretstream crypto, seed-encrypted sqlite store, two-variant send/receive transport). Runtime behavior is gated by `DRAGONX_ENABLE_CHAT`, now **default ON** (the sources always compile; the flag folds the feature away at runtime via `hushChatFeatureEnabledAtBuild()`). Full-node chat derives its identity from the wallet's mnemonic (`z_exportmnemonic`, portable/SDXLite-compatible) or, for legacy/non-mnemonic wallets, a stable z-address spending key (`z_exportkey`).
## Build variants & feature gating
Variants are selected with CMake options (set by `build.sh` flags), surfaced to C++ as compile definitions:
- `DRAGONX_BUILD_LITE` (`--lite`) → `DRAGONX_LITE_BUILD` define; renames the app to `ObsidianDragonLite` and excludes embedded-daemon / full-node assets (Sapling params, asmap, dragonxd).
- `DRAGONX_ENABLE_LITE_BACKEND` → links a real external lite backend. Requires `--lite`, link mode `imported`, ABI `sdxl-c-v1`, and a symbols inventory file (built by `scripts/build-lite-backend-artifact.sh`); CMake hard-fails if any required `litelib_*` symbol is missing. The backend **source is vendored in-tree** at `third_party/silentdragonxlite/` — the `qtlib` C-ABI wrapper (`lib/`, produces `libsilentdragonxlite.a`) and the `silentdragonxlitelib` core (`silentdragonxlite-cli/lib/`, with `proto/` + `res/`). `build-lite-backend-artifact.sh` defaults `--backend-dir` there, so the lite wallet builds **without** the upstream SilentDragonXLite repo. External build inputs are limited to the **Rust toolchain (rustc/cargo 1.63)** plus two project-controlled sources on `git.dragonx.is`: the librustzcash crates come from the mirror `git.dragonx.is/DragonX/librustzcash` (the 6 `git =` deps in the core `Cargo.toml`, pinned to rev `acff1444…`), and the **Sapling params are not committed** (gitignored) — the build fetches them from the `git.dragonx.is/DragonX/zcash-params` release `sapling-v1` and verifies their SHA-256 before rust-embed bakes them in (`ensure_sapling_params`; override the URL with `SAPLING_PARAMS_BASE_URL`). Other crate deps come from crates.io. For a fully offline build, `cargo vendor` into `third_party/silentdragonxlite/lib/vendor/` and add a `vendored-sources` redirect to `lib/.cargo/config.toml` (the build script symlinks `vendor/` into its prepared dir if present); `vendor/` is gitignored.
- `DRAGONX_ENABLE_CHAT``DRAGONX_ENABLE_CHAT` define gating the chat module.
Guard full-node-only code paths with `#if DRAGONX_LITE_BUILD` / chat code with `DRAGONX_ENABLE_CHAT`.
## Lite wallet status
The Lite variant is **functionally complete and runtime-verified on Linux + Windows** (work lives on branch `cleanup/lite-plan-churn`, **local-only — not pushed yet**):
- **Implemented:** lifecycle (create/open/restore + auto-open on startup), sync, refresh, send / shield / import / export / seed, persistence (the backend does *not* auto-save after sync/send/shield — the controller triggers `save` at those points), and passphrase **encryption** (encrypt/unlock/lock/decrypt + Settings UI + send-time & startup unlock; the backend locks immediately on `encrypt`). All controller-tested against the fake backend (`tests/fake_lite_backend.h`) and smoke-verified against the real SDXL backend via `tools/lite_smoke` (incl. a full sync). GUI is wired end-to-end with lite-appropriate wording; the full-node RPC connect loop / wizard / daemon strings are gated out of lite (lite "online" is derived from `lite_wallet_->walletOpen()`, not RPC).
- **Packaging:** `./build.sh --lite-backend --linux-release` (zip + AppImage, **verified**) and `--win-release` (cross-compiled `.exe`, **verified**; first build the Windows backend artifact with `scripts/build-lite-backend-artifact.sh --platform windows`). macOS `--lite-backend --mac-release` is **wired but not yet verified on this Linux box** (needs macOS/osxcross): the `.app`/launcher/rpath/`CFBundleExecutable` follow `ObsidianDragonLite`, full-node assets are skipped, and the lite variant gets its own `CFBundleName` ("DragonX Wallet Lite"), bundle id (`is.hush.dragonx.lite`), and DMG name so it can coexist with the full-node app. All variants correctly exclude full-node assets.
- **Rollout / kill-switch (implemented):** `wallet/lite_rollout_policy.{h,cpp}` is a pure, fail-open gate (local-only, no network) feeding `LiteWalletLifecycleService::availability()` (new `RolloutDisabled` reason). Inputs: the emergency env var `DRAGONX_LITE_KILL_SWITCH` (absolute — not even `force_on` bypasses it); a `lite_rollout` setting (`auto`/`force_on`/`force_off`); and an optional **locally-cached** manifest at `<config-dir>/lite_rollout.json` (`global_enabled`, `min_version`/`max_version`, `blocked_versions`, `rollout_permille`, `message`) keyed for staged rollout on a hashed, never-transmitted per-install id. A signed remote fetcher can populate that cache later without touching the policy. Resolved in `App::rebuildLiteWallet()`; the disable message surfaces via the lifecycle status. Unit-tested + runtime-verified (env / manifest / control).
- **Remaining (M5b):** verify the wired macOS `--lite` packaging on a Mac/osxcross, CI backend-artifact build + signing.
- **To publish:** rename branch → `feat/lite-wallet`, base the PR on `dev` (the full-node UX is already there), and handle the dormant gated-OFF HushChat content bundled in commit `af06b8b`.
The detailed milestone plan and design history (the v2 plan, backend artifact/ABI/signing design docs, the v1 plan, chat specs, etc.) are kept **untracked** under `docs/_archive/`.
## Miner updater (xmrig)
The mining tab's pool section has an **"Update miner…"** button that downloads/verifies/installs the latest DRG-XMRig from the project Gitea (`util/XmrigUpdater` + `ui/windows/xmrig_download_dialog.h`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest` → pick the asset for this platform (`linux-x64` / `win-x64` / `macos-x86_64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (from the release body) **and** a detached **ed25519 signature** → miniz-extract the binary (flattening the versioned subdir) into `resources::getDaemonDirectory()`. The whole archive is verified, so extracted members are trusted by transitivity (no per-member hash check). The pure, no-I/O core is split into `xmrig_updater_core.cpp` for unit tests; an env-gated (`DRAGONX_TEST_NETWORK=1`) test exercises the worker live. The dialog is a two-pane version picker (every `/releases` entry on the left, newest first, pre-releases included) so users can pin an older or pre-release build — same verify/install path via `startInstallRelease()`. It shares only the `ReleaseRow` row model with the daemon updater (`ui/windows/release_list_view.h`); each dialog renders its own tactile Material list.
**Signature verification is enforced** (`kXmrigRequireSignature = true` in `src/util/xmrig_updater.h`), checked against the public key pinned in `kXmrigSignaturePublicKeyBase64`. **Consequence for releases:** every `drg-xmrig` release MUST ship a detached signature per archive or the in-app updater refuses it. To cut a release: build the archives, then `scripts/sign-xmrig-release.sh sign <secret.key> <archive.zip>...` (OpenSSL-based, no extra deps) and upload each `<archive>.sig` as a release asset alongside its `.zip`. The signing **secret key must stay offline** (it is gitignored: `*.ed25519.key`); only its base64 public key is pinned in the source. To rotate the key, regenerate (`scripts/sign-xmrig-release.sh keygen`) and update `kXmrigSignaturePublicKeyBase64`. An emergency env override is not provided — disabling verification means setting `kXmrigSignaturePublicKeyBase64` empty (and rebuilding).
## Daemon updater (dragonxd)
Settings → **NODE & SECURITY → DAEMON BINARY** has a **"Check for updates…"** button that downloads/verifies/installs the latest **dragonxd full node** from the project Gitea — the full-node sibling of the xmrig updater (`util/DaemonUpdater` + `ui/windows/daemon_download_dialog.h`, pure no-I/O core in `daemon_updater_core.cpp`; gated full-node-only via `supportsFullNodeLifecycleActions()`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/latest` → pick the archive for this platform (`linux-amd64` / `macos` / `win64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (parsed from the release body's markdown **checksum table**, not xmrig's `<hash> <name>` lines) **and** a detached **ed25519 signature** → miniz-extract the three executables (`dragonxd`/`dragonx-cli`/`dragonx-tx`, flattening the versioned subdir) into `resources::getDaemonDirectory()`. The archive also bundles Sapling params/asmap, which the updater deliberately leaves to the wallet's own resource extraction. Install is **atomic and safe while the node runs** (POSIX `rename()` replaces the in-use binary; Windows moves the locked `.exe` aside to `.old`); the new binary takes effect on the **next daemon start**, so the Done screen offers **"Restart daemon now"** (`App::restartDaemon()`). The dialog is a two-pane version picker (every `/releases` entry on the left) so users can pin a specific/older/pre-release node build via `startInstallRelease()` — with a downgrade caution, since an older binary may not match current chain data. It shares only the `ReleaseRow` row model (`ui/windows/release_list_view.h`) with the miner updater; each renders its own tactile Material list.
**Signature verification is enforced** (`kDaemonRequireSignature = true` in `src/util/daemon_updater.h`), checked against `kDaemonSignaturePublicKeyBase64`. **Consequence for releases:** every `dragonx` release MUST ship a detached `<archive>.sig` per platform archive or the in-app updater refuses it (as of v1.0.2 the releases publish SHA-256 but **no** signatures yet — sign + upload them to enable in-app updates). To cut a release: `scripts/sign-daemon-release.sh sign <secret.key> dragonx-<ver>-{linux-amd64,macos,win64}.zip` (OpenSSL-based) and upload each `.sig` next to its `.zip`. The signing **secret key stays offline** (gitignored `*.ed25519.key`; this repo's is `dragonx-daemon.ed25519.key`); only the base64 public key is pinned. To rotate: `scripts/sign-daemon-release.sh keygen` and update `kDaemonSignaturePublicKeyBase64`. The generic SHA-256 / ed25519 primitives are shared with the miner updater (`util::sha256Hex` / `util::verifyXmrigSignature`).
## Seed phrase & migrate-to-seed (full node)
Full-node wallets are **BIP39-mnemonic-backed** and can **migrate a legacy (non-mnemonic) wallet into a seed wallet**. All of this **requires the `hd-transparent-keys`/`dev` daemon** (`z_exportmnemonic` + `-usemnemonic`); the older bundled binary lacks those RPCs, so these features degrade gracefully (chat falls back to a `z_exportkey` identity; the backup screen shows a "legacy wallet" note). The daemon source is vendored at `external/dragonx/` (build with its own `./build.sh`); deploy the built `dragonxd`/`dragonx-cli`/`dragonx-tx` into the wallet's daemon dir (`build/*/bin`) or via the daemon updater.
- **New wallets get a phrase.** `EmbeddedDaemon::getChainParams()` always passes `-usemnemonic=1`. The daemon reads it **only inside `GenerateNewSeed()` when a wallet has no seed yet**, so it is inert on existing wallets (safe to pass unconditionally) and makes every fresh wallet mnemonic-backed.
- **Back up seed phrase.** Settings → Backup & Data → "Seed phrase" opens `renderSeedBackupDialog` (`App::exportSeedPhrase``z_exportmnemonic`, wiped via `sodium_memzero`). A one-time nudge (`maybeRemindSeedBackup`, settings flag `seed_backup_reminded`) reminds mnemonic-wallet users to back up.
- **Migrate-to-seed** (`showSeedMigrationDialog` / `renderSeedMigrationDialog`, state machine `SeedMigrationStep`). **Phase 1 — create:** `daemon::SeedWalletCreator` runs a **second, isolated `dragonxd`** on its own port + throwaway datadir (`<config>/seed-migrate/DRAGONX`, basename MUST be the acname; `-usemnemonic=1 -connect=0`; RPC is plaintext http, not TLS), mints a mnemonic wallet, exports its seed + a sweep-target z-address, then stops. Uses the one-shot `EmbeddedDaemon::setNextStartOverride` + `setSkipPortCheck`. **Phase 2 — sweep + adopt:** `z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"]` sweeps all funds to the new address; a **Confirming gate** (`pollSweepStatus`) only allows adopt once the sweep tx is **mined (≥1 conf) AND the legacy balance is ~0** (offering a remainder re-sweep); adopt (`beginAdoptSeedWallet`, background) stops the daemon, moves `wallet.dat` aside to a **timestamped `.bak` (never deleted, restored on failure)**, installs the new wallet, and restarts with `-rescan`. Fund-moving code — **two rounds of adversarial review + a live mainnet run** gate it; the pending stage persists (`seed_migration_*` settings) so a restart resumes at sweep/confirm.
## Versioning
The version has a **single source of truth**: `project(... VERSION 1.2.0 ...)` plus `DRAGONX_VERSION_SUFFIX` in `CMakeLists.txt`. CMake generates `build/.../generated/dragonx_generated_version.h` from `src/config/version.h.in`. Do not hand-edit generated version output or hardcode version strings — bump the `project()` version in `CMakeLists.txt`.
## Conventions
- **C++17.** Match the surrounding code's style per file.
- **Icons:** use the Material Design icon font defines (`ICON_MD_*`); never raw Unicode glyphs.
- **UI layout values** belong in `res/themes/ui.toml`, read via `schema::UI()` — do not hardcode pixel sizes/offsets in code.
- **DPI / display scaling:** `schema::UI()` returns **logical (raw) px**; `Layout::dpiScale()` (= OS DPI × the in-app font-scale) is the single scale factor. The `Layout::k*()` helpers and `BeginOverlayDialog` already fold it in, and ImGui auto-layout scales via the DPI-rebuilt font atlas + `ScaleAllSizes` — so tabs/standard widgets scale for free. But **hand-drawn absolute geometry** (`dl->AddText` at manual `cy += 24.0f` offsets, `SetNextWindowSize`, explicit `ImVec2(width,0)` button sizes, `SameLine(x)` column strides) is immune to all of that and must be multiplied by `ui::Layout::dpiScale()` yourself, or it renders native-size (tiny) on a HiDPI display. Font metrics (`font->LegacySize`, `CalcTextSize`) are already scaled — don't double-scale those. Verify at scale with a full sweep run at `font_scale: 1.5` (same `dpiScale()==1.5` code path as OS 150%).
- **i18n:** user-facing strings are translated via `src/util/i18n`; the English source of truth is the `strings_[...]` map in `src/util/i18n.cpp`, and the per-language translations live as the **source of truth** in `res/lang/` (`de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, `zh`). **Edit those JSONs directly and additively** — write with `json.dump(..., indent=4, sort_keys=True, ensure_ascii=False)`; never bulk-regenerate/overwrite a whole file (that path silently dropped ~285 keys/language before). `scripts/add_missing_translations.py` back-fills only the keys missing from a JSON (non-destructive), and `scripts/build_cjk_subset.py` rebuilds the CJK subset font (`res/fonts/NotoSansCJK-Subset.ttf`) after new CJK glyphs are added.
- **Commits:** the history uses Conventional Commits (`feat(scope): …`, `fix(scope): …`). PRs target `master`.

View File

@@ -15,19 +15,13 @@ if(APPLE)
endif()
project(ObsidianDragon
VERSION 2.0.0
VERSION 1.2.0
LANGUAGES C CXX
DESCRIPTION "DragonX Cryptocurrency Wallet"
)
# Pre-release suffix (e.g. "-rc1", "-beta2"). Leave empty for stable releases.
set(DRAGONX_VERSION_SUFFIX "")
# ObsidianDragonLite is versioned INDEPENDENTLY of the full-node app above. The active variant's
# version flows to the generated header, the Windows .rc/manifest, and build.sh's release names via
# DRAGONX_APP_VERSION* (resolved in the lite/full block below).
set(DRAGONX_LITE_VERSION "1.0.0")
set(DRAGONX_LITE_VERSION_SUFFIX "")
set(DRAGONX_VERSION_SUFFIX "-rc1")
# C++17 standard
set(CMAKE_CXX_STANDARD 17)
@@ -42,117 +36,6 @@ endif()
# Options
option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON)
option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON)
option(DRAGONX_BUILD_LITE "Build ObsidianDragonLite variant without full-node features" OFF)
option(DRAGONX_ENABLE_LITE_BACKEND "Enable real lite wallet backend integration" OFF)
option(DRAGONX_ENABLE_CHAT "Enable the HushChat protocol/UI integration" ON)
set(DRAGONX_LITE_BACKEND_LIBRARY "" CACHE FILEPATH "Path to a prebuilt SDXL-compatible lite backend library")
set(DRAGONX_LITE_BACKEND_INCLUDE_DIR "" CACHE PATH "Optional include directory for SDXL-compatible lite backend headers")
set(DRAGONX_LITE_BACKEND_EXTRA_LIBS "" CACHE STRING "Additional libraries needed by the SDXL-compatible lite backend")
set(DRAGONX_LITE_BACKEND_LINK_MODE "imported" CACHE STRING "Lite backend link mode; Phase 1 supports imported only")
set_property(CACHE DRAGONX_LITE_BACKEND_LINK_MODE PROPERTY STRINGS imported)
set(DRAGONX_LITE_BACKEND_ABI "sdxl-c-v1" CACHE STRING "Expected lite backend C ABI version")
set(DRAGONX_LITE_BACKEND_SYMBOLS_FILE "" CACHE FILEPATH "Path to generated lite backend exported-symbol inventory")
set(DRAGONX_LITE_BACKEND_MANIFEST "" CACHE FILEPATH "Optional path to generated lite backend artifact manifest")
set(DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS
litelib_wallet_exists
litelib_initialize_new
litelib_initialize_new_from_phrase
litelib_initialize_existing
litelib_execute
litelib_rust_free_string
litelib_check_server_online
litelib_shutdown
)
if(DRAGONX_BUILD_LITE)
set(DRAGONX_APP_NAME "ObsidianDragonLite")
set(DRAGONX_BINARY_NAME "ObsidianDragonLite")
# NOTE: do NOT FORCE-write DRAGONX_ENABLE_EMBEDDED_DAEMON=OFF into the cache here. A forced
# cache write persists into a later full-node reconfigure of the same build dir and silently
# disables the embedded daemon — the binary still embeds/extracts, but isUsingEmbeddedDaemon()
# returns false, so it "unpacks dragonxd but never starts" (the 1.3.0 regression). It is also
# redundant: makeWalletCapabilities() already forces the embedded-daemon capability off for any
# lite build via `fullNodeBuild && embeddedDaemonCompiled`, so lite never launches a daemon
# regardless of this flag. build.sh sets the flag explicitly per variant to defeat stale caches.
set(DRAGONX_APP_VERSION "${DRAGONX_LITE_VERSION}")
set(DRAGONX_APP_VERSION_SUFFIX "${DRAGONX_LITE_VERSION_SUFFIX}")
else()
set(DRAGONX_APP_NAME "ObsidianDragon")
set(DRAGONX_BINARY_NAME "ObsidianDragon")
set(DRAGONX_APP_VERSION "${PROJECT_VERSION}")
set(DRAGONX_APP_VERSION_SUFFIX "${DRAGONX_VERSION_SUFFIX}")
endif()
# Split the active version into numeric components for the generated header + Windows VERSIONINFO.
string(REPLACE "." ";" _dragonx_ver_parts "${DRAGONX_APP_VERSION}")
list(GET _dragonx_ver_parts 0 DRAGONX_APP_VERSION_MAJOR)
list(GET _dragonx_ver_parts 1 DRAGONX_APP_VERSION_MINOR)
list(GET _dragonx_ver_parts 2 DRAGONX_APP_VERSION_PATCH)
set(DRAGONX_LITE_BACKEND_READY OFF)
if(DRAGONX_ENABLE_LITE_BACKEND)
if(NOT DRAGONX_BUILD_LITE)
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND is only supported with DRAGONX_BUILD_LITE=ON")
endif()
if(NOT DRAGONX_LITE_BACKEND_LINK_MODE STREQUAL "imported")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_LINK_MODE currently supports only 'imported'; runtime dynamic loading is a later bridge-runtime phase")
endif()
if(NOT DRAGONX_LITE_BACKEND_ABI STREQUAL "sdxl-c-v1")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_ABI must be sdxl-c-v1")
endif()
if(NOT DRAGONX_LITE_BACKEND_LIBRARY)
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND requires DRAGONX_LITE_BACKEND_LIBRARY to point at an SDXL-compatible artifact")
endif()
if(NOT EXISTS "${DRAGONX_LITE_BACKEND_LIBRARY}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_LIBRARY does not exist: ${DRAGONX_LITE_BACKEND_LIBRARY}")
endif()
if(NOT DRAGONX_LITE_BACKEND_SYMBOLS_FILE)
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND requires DRAGONX_LITE_BACKEND_SYMBOLS_FILE generated by scripts/build-lite-backend-artifact.sh")
endif()
if(NOT EXISTS "${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE does not exist: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
endif()
file(STRINGS "${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}" DRAGONX_LITE_BACKEND_SYMBOL_LINES)
if(NOT DRAGONX_LITE_BACKEND_SYMBOL_LINES)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE is empty: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
endif()
foreach(DRAGONX_LITE_REQUIRED_SYMBOL IN LISTS DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS)
list(FIND DRAGONX_LITE_BACKEND_SYMBOL_LINES "${DRAGONX_LITE_REQUIRED_SYMBOL}" DRAGONX_LITE_SYMBOL_INDEX)
if(DRAGONX_LITE_SYMBOL_INDEX EQUAL -1)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE is missing required symbol: ${DRAGONX_LITE_REQUIRED_SYMBOL}")
endif()
endforeach()
if(DRAGONX_LITE_BACKEND_MANIFEST AND NOT EXISTS "${DRAGONX_LITE_BACKEND_MANIFEST}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST does not exist: ${DRAGONX_LITE_BACKEND_MANIFEST}")
endif()
# Note (F15-1): the former signature-metadata gate was removed. It trusted a
# "verification_status: verified" field that scripts/build-lite-backend-artifact.sh
# self-attested with no cryptographic check (the "verified" SHA was just the artifact's
# own SHA). The trust root is now build-from-source: that script builds the backend from
# the vendored in-tree source and refuses prebuilt artifacts, so the library linked here
# is the one built from reviewed source. The required-symbol inventory check above stays.
add_library(dragonx_lite_backend UNKNOWN IMPORTED)
set_target_properties(dragonx_lite_backend PROPERTIES
IMPORTED_LOCATION "${DRAGONX_LITE_BACKEND_LIBRARY}"
)
if(APPLE)
# The Rust backend's TLS stack (security-framework / core-foundation crates)
# references Secure Transport (SSL*) + CoreFoundation symbols. Link the frameworks
# that provide them, or the static lib leaves ~130 symbols undefined at link time.
set_property(TARGET dragonx_lite_backend APPEND PROPERTY
INTERFACE_LINK_LIBRARIES "-framework Security" "-framework CoreFoundation")
endif()
if(DRAGONX_LITE_BACKEND_INCLUDE_DIR)
if(NOT IS_DIRECTORY "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_INCLUDE_DIR does not exist: ${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
endif()
set_target_properties(dragonx_lite_backend PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}"
)
endif()
set(DRAGONX_LITE_BACKEND_READY ON)
endif()
include(CTest)
@@ -282,38 +165,6 @@ else()
set(CURL_INCLUDE_DIRS ${CURL_INCLUDE_DIR})
endif()
# libwebp - WebP decode (still + animated via WebPAnimDecoder). Built from source, static, decode-only
# so Linux / mingw-Windows / macOS-osxcross all build it identically (the mingw/osx sysroots have no
# webp). Encode tools are disabled to avoid pulling in libpng/zlib that the cross sysroots lack.
message(STATUS "Fetching libwebp (decode-only, static)...")
FetchContent_Declare(
libwebp
GIT_REPOSITORY https://github.com/webmproject/libwebp.git
GIT_TAG v1.4.0
GIT_SHALLOW TRUE
# libwebp's cpu.cmake applies -mno-sse2/-mno-sse4.1 to its scalar reference DSP
# files when it can't probe SSE support. Under a macOS universal build
# (-arch arm64;x86_64) that probe fails, so the flags land on the x86_64 slice,
# where -mno-sse2 disables _Float16 and breaks the SDK's <math.h>. Neutralize
# those disable flags (SSE2 is x86_64 baseline). Portable + idempotent; a no-op
# for single-arch Linux/Windows/x86_64 builds. See cmake/patch-libwebp-simd.cmake.
PATCH_COMMAND ${CMAKE_COMMAND}
-DCPU_CMAKE=<SOURCE_DIR>/cmake/cpu.cmake
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/patch-libwebp-simd.cmake
)
set(WEBP_LINK_STATIC ON CACHE BOOL "" FORCE)
set(WEBP_BUILD_ANIM_UTILS OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_CWEBP OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_DWEBP OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_GIF2WEBP OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_IMG2WEBP OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_VWEBP OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_WEBPINFO OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_LIBWEBPMUX OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_WEBPMUX OFF CACHE BOOL "" FORCE)
set(WEBP_BUILD_EXTRAS OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(libwebp)
# libsodium - platform-specific
# Search order per platform:
# 1. Local pre-built in libs/libsodium{-mac,-win}/ (downloaded by scripts/fetch-libsodium.sh)
@@ -402,35 +253,6 @@ else()
list(APPEND IMGUI_HEADERS ${IMGUI_DIR}/backends/imgui_impl_opengl3.h)
endif()
# Optional FreeType font loader — enables color-emoji rendering (COLR/CPAL Twemoji) when the chat
# "color emoji" setting is on; otherwise the wallet falls back to the monochrome emoji subset.
# - Native Linux/macOS: use the system FreeType via find_package.
# - Windows (mingw cross): the toolchain ships no FreeType, so build.sh --win-release cross-builds a
# static one (scripts/build-freetype-mingw.sh) and passes -DDRAGONX_MINGW_FREETYPE_PREFIX here.
# - Other cross builds (osxcross) without FreeType: silently fall back to monochrome.
set(DRAGONX_FREETYPE OFF)
set(DRAGONX_FREETYPE_LIB "")
set(DRAGONX_FREETYPE_INC "")
if(DEFINED DRAGONX_MINGW_FREETYPE_PREFIX AND EXISTS "${DRAGONX_MINGW_FREETYPE_PREFIX}/lib/libfreetype.a")
set(DRAGONX_FREETYPE ON)
set(DRAGONX_FREETYPE_LIB "${DRAGONX_MINGW_FREETYPE_PREFIX}/lib/libfreetype.a")
set(DRAGONX_FREETYPE_INC "${DRAGONX_MINGW_FREETYPE_PREFIX}/include/freetype2")
message(STATUS "FreeType (mingw cross-built) found — chat color emoji enabled")
elseif(NOT CMAKE_CROSSCOMPILING)
find_package(Freetype QUIET)
if(FREETYPE_FOUND)
set(DRAGONX_FREETYPE ON)
set(DRAGONX_FREETYPE_LIB Freetype::Freetype) # imported target carries include dirs
message(STATUS "FreeType ${FREETYPE_VERSION_STRING} found — chat color emoji enabled")
endif()
endif()
if(DRAGONX_FREETYPE)
list(APPEND IMGUI_SOURCES ${IMGUI_DIR}/misc/freetype/imgui_freetype.cpp)
list(APPEND IMGUI_HEADERS ${IMGUI_DIR}/misc/freetype/imgui_freetype.h)
else()
message(STATUS "FreeType not found — chat color emoji falls back to monochrome")
endif()
# -----------------------------------------------------------------------------
# QR Code library (bundled)
# -----------------------------------------------------------------------------
@@ -452,33 +274,12 @@ set(APP_SOURCES
src/app.cpp
src/app_network.cpp
src/app_security.cpp
src/app_sweep.cpp
src/app_wizard.cpp
src/services/network_refresh_service.cpp
src/services/refresh_scheduler.cpp
src/services/wallet_security_controller.cpp
src/services/wallet_security_workflow.cpp
src/services/wallet_security_workflow_executor.cpp
src/chat/chat_protocol.cpp
src/chat/chat_crypto.cpp
src/chat/chat_identity.cpp
src/chat/chat_store.cpp
src/chat/chat_service.cpp
src/chat/chat_database.cpp
src/chat/chat_outgoing.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_diagnostics.cpp
src/wallet/lite_wallet_controller.cpp
src/wallet/lite_result_parsers.cpp
src/wallet/lite_sync_service.cpp
src/wallet/lite_wallet_gateway.cpp
src/wallet/lite_wallet_state_mapper.cpp
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
src/wallet/lite_wallet_server_selection_adapter.cpp
src/wallet/lite_wallet_lifecycle_service.cpp
src/data/wallet_state.cpp
src/data/transaction_history_cache.cpp
src/ui/theme.cpp
@@ -489,7 +290,6 @@ set(APP_SOURCES
src/ui/notifications.cpp
src/ui/windows/main_window.cpp
src/ui/windows/balance_tab.cpp
src/ui/windows/balance_components.cpp
src/ui/windows/balance_address_list.cpp
src/ui/windows/balance_recent_tx.cpp
src/ui/windows/balance_tab_helpers.cpp
@@ -497,27 +297,17 @@ set(APP_SOURCES
src/ui/windows/receive_tab.cpp
src/ui/windows/transactions_tab.cpp
src/ui/windows/mining_tab.cpp
src/ui/windows/mining_earnings.cpp
src/ui/windows/mining_stats.cpp
src/ui/windows/mining_controls.cpp
src/ui/windows/mining_mode_toggle.cpp
src/ui/windows/mining_benchmark.cpp
src/ui/windows/mining_pool_panel.cpp
src/ui/windows/mining_tab_helpers.cpp
src/ui/windows/peers_tab.cpp
src/ui/windows/network_tab.cpp
src/ui/windows/explorer_tab.cpp
src/ui/windows/market_tab.cpp
src/ui/windows/console_tab.cpp
src/ui/windows/console_command_executor.cpp
src/ui/windows/console_command_reference.cpp
src/ui/windows/console_input_model.cpp
src/ui/windows/console_model.cpp
src/ui/windows/console_output_model.cpp
src/ui/windows/console_scroll_controller.cpp
src/ui/windows/console_selection_controller.cpp
src/ui/windows/console_tab_helpers.cpp
src/ui/windows/console_text_layout.cpp
src/ui/windows/settings_window.cpp
src/ui/pages/settings_page.cpp
src/ui/windows/about_dialog.cpp
@@ -525,48 +315,36 @@ set(APP_SOURCES
src/ui/windows/transaction_details_dialog.cpp
src/ui/windows/qr_popup_dialog.cpp
src/ui/windows/validate_address_dialog.cpp
src/ui/windows/contacts_tab.cpp
src/ui/windows/chat_tab.cpp
src/ui/windows/address_book_dialog.cpp
src/ui/windows/shield_dialog.cpp
src/ui/windows/request_payment_dialog.cpp
src/ui/windows/block_info_dialog.cpp
src/ui/windows/import_key_dialog.cpp
src/ui/windows/export_all_keys_dialog.cpp
src/ui/windows/export_transactions_dialog.cpp
src/ui/windows/backup_wallet_dialog.cpp
src/ui/widgets/qr_code.cpp
src/rpc/rpc_client.cpp
src/rpc/rpc_worker.cpp
src/rpc/connection.cpp
src/config/settings.cpp
src/data/address_book.cpp
src/data/wallet_index.cpp
src/data/exchange_info.cpp
src/util/logger.cpp
src/util/async_task_manager.cpp
src/util/amount_format.cpp
src/util/address_validation.cpp
src/util/base64.cpp
src/util/single_instance.cpp
src/util/i18n.cpp
src/util/text_format.cpp
src/util/platform.cpp
src/util/payment_uri.cpp
src/util/texture_loader.cpp
src/util/svg_texture.cpp
src/util/noise_texture.cpp
src/daemon/embedded_daemon.cpp
src/daemon/seed_wallet_creator.cpp
src/daemon/daemon_controller.cpp
src/daemon/lifecycle_adapters.cpp
src/daemon/xmrig_manager.cpp
src/util/bootstrap.cpp
src/util/lite_server_probe.cpp
src/util/pool_registry_core.cpp
src/util/pool_stats_service.cpp
src/util/http_download.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
src/util/daemon_updater.cpp
src/util/daemon_updater_core.cpp
src/util/secure_vault.cpp
src/ui/effects/framebuffer.cpp
src/ui/effects/blur_shader.cpp
@@ -602,27 +380,6 @@ set(APP_HEADERS
src/services/wallet_security_controller.h
src/services/wallet_security_workflow.h
src/services/wallet_security_workflow_executor.h
src/wallet/wallet_capabilities.h
src/wallet/wallet_backend.h
src/wallet/lite_owned_string.h
src/wallet/lite_rollout_policy.h
src/wallet/lite_client_bridge.h
src/wallet/lite_connection_service.h
src/wallet/lite_result_parsers.h
src/wallet/lite_sync_service.h
src/wallet/lite_wallet_gateway.h
src/wallet/lite_wallet_state_mapper.h
src/wallet/lite_wallet_lifecycle_ui_adapter.h
src/wallet/lite_wallet_server_selection_adapter.h
src/wallet/lite_wallet_lifecycle_service.h
src/chat/chat_protocol.h
src/chat/chat_crypto.h
src/chat/chat_identity.h
src/chat/chat_message.h
src/chat/chat_store.h
src/chat/chat_service.h
src/chat/chat_database.h
src/chat/chat_outgoing.h
src/config/version.h
src/data/wallet_state.h
src/data/transaction_history_cache.h
@@ -645,13 +402,9 @@ set(APP_HEADERS
src/ui/windows/peers_tab.h
src/ui/windows/explorer_tab.h
src/ui/windows/market_tab.h
src/ui/windows/console_channel.h
src/ui/windows/console_command_reference.h
src/ui/windows/console_input_model.h
src/ui/windows/console_model.h
src/ui/windows/console_output_model.h
src/ui/windows/console_scroll_controller.h
src/ui/windows/console_selection_controller.h
src/ui/windows/console_tab.h
src/ui/windows/console_tab_helpers.h
src/ui/windows/settings_window.h
@@ -660,14 +413,14 @@ set(APP_HEADERS
src/ui/windows/transaction_details_dialog.h
src/ui/windows/qr_popup_dialog.h
src/ui/windows/validate_address_dialog.h
src/ui/windows/contacts_tab.h
src/ui/windows/chat_tab.h
src/ui/windows/contact_picker.h
src/ui/windows/address_book_dialog.h
src/ui/windows/shield_dialog.h
src/ui/windows/request_payment_dialog.h
src/ui/windows/block_info_dialog.h
src/ui/windows/import_key_dialog.h
src/ui/windows/export_all_keys_dialog.h
src/ui/windows/export_transactions_dialog.h
src/ui/windows/backup_wallet_dialog.h
src/ui/widgets/qr_code.h
src/rpc/rpc_client.h
src/rpc/rpc_worker.h
@@ -686,7 +439,6 @@ set(APP_HEADERS
src/util/payment_uri.h
src/util/secure_vault.h
src/daemon/embedded_daemon.h
src/daemon/seed_wallet_creator.h
src/daemon/daemon_controller.h
src/daemon/lifecycle_adapters.h
src/daemon/xmrig_manager.h
@@ -727,12 +479,10 @@ if(WIN32)
set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc)
endif()
# Generate version values from the single project(VERSION ...) declaration.
# Keep the build-specific app name in the build tree so full/lite configures do
# not rewrite a tracked source header.
# Generate version.h from the single project(VERSION ...) declaration
configure_file(
${CMAKE_SOURCE_DIR}/src/config/version.h.in
${CMAKE_BINARY_DIR}/generated/dragonx_generated_version.h
${CMAKE_SOURCE_DIR}/src/config/version.h
@ONLY
)
@@ -753,12 +503,9 @@ set_source_files_properties(
"${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/UbuntuMono-R.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/NotoEmoji-Subset.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/TwemojiMozilla-Color.ttf"
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
)
add_executable(ObsidianDragon
@@ -771,8 +518,6 @@ add_executable(ObsidianDragon
${WIN_RC_FILE}
)
set_target_properties(ObsidianDragon PROPERTIES OUTPUT_NAME "${DRAGONX_BINARY_NAME}")
target_include_directories(ObsidianDragon PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/embedded
@@ -786,7 +531,6 @@ target_include_directories(ObsidianDragon PRIVATE
${GLAD_INCLUDE}
${CURL_INCLUDE_DIRS}
${MINIZ_DIR}
${libwebp_SOURCE_DIR}/src # <webp/decode.h>, <webp/demux.h> (FetchContent build tree)
)
target_link_libraries(ObsidianDragon PRIVATE
@@ -796,62 +540,8 @@ target_link_libraries(ObsidianDragon PRIVATE
sqlite3_amalgamation
${CURL_LIBRARIES}
${SODIUM_LIBRARY}
webp
webpdemux # WebPAnimDecoder (animated WebP); transitively pulls in webp + sharpyuv
)
if(DRAGONX_LITE_BACKEND_READY)
target_link_libraries(ObsidianDragon PRIVATE dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS})
# Real-backend smoke tool (only built when a real lite backend is linked).
add_executable(lite_smoke
tools/lite_smoke.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_result_parsers.cpp
)
target_include_directories(lite_smoke PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/generated
${SODIUM_INCLUDE_DIR}
)
target_compile_definitions(lite_smoke PRIVATE DRAGONX_ENABLE_LITE_BACKEND=1)
target_link_libraries(lite_smoke PRIVATE
dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS}
nlohmann_json::nlohmann_json
${SODIUM_LIBRARY}
)
if(UNIX)
target_link_libraries(lite_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
endif()
# Real-backend SEND smoke tool — drives the exact GUI send path (bridge.execute("send", ...)).
add_executable(lite_send_smoke
tools/lite_send_smoke.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_result_parsers.cpp
)
target_include_directories(lite_send_smoke PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/generated
${SODIUM_INCLUDE_DIR}
)
target_compile_definitions(lite_send_smoke PRIVATE DRAGONX_ENABLE_LITE_BACKEND=1)
target_link_libraries(lite_send_smoke PRIVATE
dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS}
nlohmann_json::nlohmann_json
${SODIUM_LIBRARY}
)
if(UNIX)
target_link_libraries(lite_send_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
endif()
endif()
# Platform-specific settings
if(WIN32)
target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi iphlpapi d3d11 dxgi d3dcompiler dcomp)
@@ -880,10 +570,6 @@ endif()
# Compile definitions
target_compile_definitions(ObsidianDragon PRIVATE
DRAGONX_DEBUG
DRAGONX_LITE_BUILD=$<BOOL:${DRAGONX_BUILD_LITE}>
DRAGONX_ENABLE_EMBEDDED_DAEMON=$<BOOL:${DRAGONX_ENABLE_EMBEDDED_DAEMON}>
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
DRAGONX_ENABLE_CHAT=$<BOOL:${DRAGONX_ENABLE_CHAT}>
)
if(WIN32)
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11)
@@ -891,35 +577,6 @@ else()
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAS_GLAD)
endif()
# Color-emoji font loader (FreeType) — linked + flagged only when found (see DRAGONX_FREETYPE above).
if(DRAGONX_FREETYPE)
target_link_libraries(ObsidianDragon PRIVATE ${DRAGONX_FREETYPE_LIB})
if(DRAGONX_FREETYPE_INC)
target_include_directories(ObsidianDragon PRIVATE ${DRAGONX_FREETYPE_INC})
endif()
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAVE_FREETYPE)
endif()
add_executable(HushChatFixtureCheck
tools/hushchat_fixture_check.cpp
src/chat/chat_protocol.cpp
src/chat/chat_fixture_tooling.cpp
)
target_include_directories(HushChatFixtureCheck PRIVATE
${CMAKE_SOURCE_DIR}/src
${SODIUM_INCLUDE_DIR}
)
target_link_libraries(HushChatFixtureCheck PRIVATE
nlohmann_json::nlohmann_json
${SODIUM_LIBRARY}
)
target_compile_definitions(HushChatFixtureCheck PRIVATE
DRAGONX_ENABLE_CHAT=0
)
# -----------------------------------------------------------------------------
# Copy resources
# -----------------------------------------------------------------------------
@@ -1070,7 +727,7 @@ install(TARGETS ObsidianDragon
)
install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res
DESTINATION share/${DRAGONX_BINARY_NAME}
DESTINATION share/ObsidianDragon
OPTIONAL
)
@@ -1086,63 +743,25 @@ if(BUILD_TESTING)
src/services/wallet_security_controller.cpp
src/services/wallet_security_workflow.cpp
src/services/wallet_security_workflow_executor.cpp
src/chat/chat_protocol.cpp
src/chat/chat_crypto.cpp
src/chat/chat_identity.cpp
src/chat/chat_store.cpp
src/chat/chat_service.cpp
src/chat/chat_database.cpp
src/chat/chat_outgoing.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_diagnostics.cpp
src/wallet/lite_wallet_controller.cpp
src/wallet/lite_result_parsers.cpp
src/wallet/lite_sync_service.cpp
src/wallet/lite_wallet_gateway.cpp
src/wallet/lite_wallet_state_mapper.cpp
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
src/wallet/lite_wallet_server_selection_adapter.cpp
src/wallet/lite_wallet_lifecycle_service.cpp
src/ui/explorer/explorer_block_cache.cpp
src/ui/windows/balance_address_list.cpp
src/ui/windows/balance_recent_tx.cpp
src/ui/windows/console_input_model.cpp
src/ui/windows/console_model.cpp
src/ui/windows/console_output_model.cpp
src/ui/windows/console_scroll_controller.cpp
src/ui/windows/console_selection_controller.cpp
src/ui/windows/console_tab_helpers.cpp
src/ui/windows/console_text_layout.cpp
src/ui/windows/mining_benchmark.cpp
src/ui/windows/mining_pool_panel.cpp
src/ui/windows/mining_tab_helpers.cpp
src/util/payment_uri.cpp
src/util/amount_format.cpp
src/util/address_validation.cpp
src/util/i18n.cpp
src/util/text_format.cpp
src/data/wallet_state.cpp
src/data/transaction_history_cache.cpp
src/data/address_book.cpp
src/data/wallet_index.cpp
src/daemon/lifecycle_adapters.cpp
src/rpc/connection.cpp
src/config/settings.cpp
src/resources/embedded_resources.cpp
src/util/secure_vault.cpp
src/util/platform.cpp
src/util/logger.cpp
src/util/lite_server_probe.cpp
src/util/pool_registry_core.cpp
src/util/http_download.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
src/util/daemon_updater.cpp
src/util/daemon_updater_core.cpp
${MINIZ_SOURCES}
)
target_include_directories(ObsidianDragonTests PRIVATE
@@ -1152,29 +771,14 @@ if(BUILD_TESTING)
${CMAKE_BINARY_DIR}/generated
${IMGUI_DIR}
${SODIUM_INCLUDE_DIR}
${CURL_INCLUDE_DIRS}
${MINIZ_DIR}
)
target_link_libraries(ObsidianDragonTests PRIVATE
nlohmann_json::nlohmann_json
sqlite3_amalgamation
${SODIUM_LIBRARY}
${CURL_LIBRARIES}
)
target_compile_definitions(ObsidianDragonTests PRIVATE
DRAGONX_ENABLE_CHAT=$<BOOL:${DRAGONX_ENABLE_CHAT}>
DRAGONX_LITE_BUILD=$<BOOL:${DRAGONX_BUILD_LITE}>
DRAGONX_ENABLE_EMBEDDED_DAEMON=$<BOOL:${DRAGONX_ENABLE_EMBEDDED_DAEMON}>
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
DRAGONX_TEST_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures"
)
if(DRAGONX_LITE_BACKEND_READY)
target_link_libraries(ObsidianDragonTests PRIVATE dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS})
endif()
if(UNIX)
target_link_libraries(ObsidianDragonTests PRIVATE ${CMAKE_DL_LIBS})
endif()
@@ -1188,17 +792,10 @@ endif()
message(STATUS "")
message(STATUS "DragonX ImGui Wallet Configuration:")
message(STATUS " Version: ${DRAGONX_APP_VERSION}${DRAGONX_APP_VERSION_SUFFIX} (${DRAGONX_APP_NAME})")
message(STATUS " Version: ${PROJECT_VERSION}")
message(STATUS " Build type: ${CMAKE_BUILD_TYPE}")
message(STATUS " C++ Standard: ${CMAKE_CXX_STANDARD}")
message(STATUS " ImGui dir: ${IMGUI_DIR}")
message(STATUS " SDL3 found: ${SDL3_FOUND}")
message(STATUS " Sodium lib: ${SODIUM_LIBRARY}")
message(STATUS " Lite build: ${DRAGONX_BUILD_LITE}")
message(STATUS " Lite requested: ${DRAGONX_ENABLE_LITE_BACKEND}")
message(STATUS " Lite backend: ${DRAGONX_LITE_BACKEND_READY}")
message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}")
message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
message(STATUS " Lite manifest: ${DRAGONX_LITE_BACKEND_MANIFEST}")
message(STATUS " Lite trust: built-from-source (vendored third_party/silentdragonxlite)")
message(STATUS "")

View File

@@ -81,8 +81,8 @@ Download linux and windows binaries of latest releases and place in binary direc
- prebuilt-binaries/dragonxd-win/
- prebuilt-binaries/dragonxd-mac/
**DRG-XMRig fork** (https://git.dragonx.is/DragonX/drg-xmrig):
- prebuilt-binaries/drg-xmrig/
**xmrig HAC fork** (https://git.dragonx.is/dragonx/xmrig-hac):
- prebuilt-binaries/xmrig-hac/
## Build Steps

384
build.sh
View File

@@ -20,9 +20,7 @@
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# VERSION is resolved per-variant from CMakeLists.txt (the single source of truth) after arg
# parsing — see the APP_BASENAME block below. Placeholder until then.
VERSION=""
VERSION="1.2.0-rc1"
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
@@ -43,8 +41,6 @@ DO_DEV=false
DO_LINUX=false
DO_WIN=false
DO_MAC=false
DO_LITE=false
DO_LITE_BACKEND=false
CLEAN=false
BUILD_TYPE="Release"
@@ -58,10 +54,6 @@ Targets (at least one required, or none for dev build):
--linux-release Linux release (zip + AppImage) -> release/linux/
--win-release Windows cross-compile (mingw-w64) -> release/windows/
--mac-release macOS .app bundle + DMG -> release/mac/
--lite Build ObsidianDragonLite variant (no embedded daemon/full-node features)
--lite-backend Like --lite, and link the real SDXL litelib backend artifact
(auto-discovers build/lite-backend/<platform>/; build it with
scripts/build-lite-backend-artifact.sh, or set DRAGONX_LITE_BACKEND_DIR)
Build trees are stored under build/{linux,windows,mac}/
@@ -82,7 +74,6 @@ Examples:
$0 --linux-release # Linux release (zip + AppImage)
$0 --win-release # Windows cross-compile
$0 --mac-release # macOS bundle + DMG (native or osxcross)
$0 --lite-backend --mac-release # macOS ObsidianDragonLite.app + DMG (lite backend)
$0 --clean --linux-release --win-release # Clean + both
EOF
exit 0
@@ -94,8 +85,6 @@ while [[ $# -gt 0 ]]; do
--linux-release) DO_LINUX=true; shift ;;
--win-release) DO_WIN=true; shift ;;
--mac-release) DO_MAC=true; shift ;;
--lite) DO_LITE=true; shift ;;
--lite-backend) DO_LITE=true; DO_LITE_BACKEND=true; shift ;;
-c|--clean) CLEAN=true; shift ;;
-d|--debug) BUILD_TYPE="Debug"; shift ;;
-j) JOBS="$2"; shift 2 ;;
@@ -109,92 +98,6 @@ if ! $DO_LINUX && ! $DO_WIN && ! $DO_MAC; then
DO_DEV=true
fi
APP_BASENAME="ObsidianDragon"
CMAKE_LITE_ARGS=()
# Always set the variant flag EXPLICITLY (ON and OFF) so switching variants in a shared build dir
# can't reuse a stale cached value (e.g. a prior --lite build leaving DRAGONX_BUILD_LITE=ON).
if $DO_LITE; then
APP_BASENAME="ObsidianDragonLite"
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=ON")
# Lite never embeds/launches a daemon; set it explicitly too for cache hygiene.
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=OFF")
info "Lite mode enabled: building ${APP_BASENAME}"
else
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=OFF")
# Re-assert the embedded daemon ON for full-node builds, EXPLICITLY, so a build dir whose cache
# was poisoned OFF by a prior --lite configure (or any stale value) is healed — otherwise the
# full-node app extracts dragonxd but never launches it (isUsingEmbeddedDaemon() == false).
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=ON")
fi
# Resolve the release version string for the active variant from CMakeLists.txt (single source of
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
_cml="$SCRIPT_DIR/CMakeLists.txt"
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]][[:space:]]*\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
_full_suffix=$(sed -n 's/^set(DRAGONX_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
_lite_ver=$(sed -n 's/^set(DRAGONX_LITE_VERSION[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
_lite_suffix=$(sed -n 's/^set(DRAGONX_LITE_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
if $DO_LITE; then
VERSION="${_lite_ver}${_lite_suffix}"
else
VERSION="${_full_ver}${_full_suffix}"
fi
[ -n "$_full_ver" ] && [ -n "$VERSION" ] || { err "Could not parse version from CMakeLists.txt"; exit 1; }
info "Release version: ${VERSION} (${APP_BASENAME})"
# ── Lite backend (real SDXL litelib) linking ─────────────────────────────────
# Enables DRAGONX_ENABLE_LITE_BACKEND with an imported artifact produced by
# scripts/build-lite-backend-artifact.sh. Auto-discovers build/lite-backend/<platform>/;
# override the directory with DRAGONX_LITE_BACKEND_DIR.
if $DO_LITE_BACKEND; then
# Artifact platform follows the cross target when exactly one non-host release is requested,
# so `--lite-backend --win-release` links the Windows backend (not the host's) automatically.
case "$(uname -s)" in
Linux) lb_platform="linux" ;;
Darwin) lb_platform="macos" ;;
*) lb_platform="linux" ;;
esac
if $DO_WIN && ! $DO_LINUX && ! $DO_MAC; then lb_platform="windows"; fi
if $DO_MAC && ! $DO_LINUX && ! $DO_WIN; then lb_platform="macos"; fi
lb_dir="${DRAGONX_LITE_BACKEND_DIR:-$SCRIPT_DIR/build/lite-backend/$lb_platform}"
lb_lib=""
for cand in "$lb_dir"/libsilentdragonxlite.a "$lb_dir"/libsilentdragonxlite.so "$lb_dir"/silentdragonxlite.lib; do
[[ -f "$cand" ]] && { lb_lib="$cand"; break; }
done
lb_symbols="$lb_dir/lite-backend-symbols.txt"
lb_manifest="$lb_dir/lite-backend-artifact-manifest.json"
if [[ -z "$lb_lib" || ! -f "$lb_symbols" ]]; then
err "Lite backend artifact not found under: $lb_dir"
err "Build it first: ./scripts/build-lite-backend-artifact.sh --platform $lb_platform"
err "Or set DRAGONX_LITE_BACKEND_DIR to an existing artifact directory."
exit 1
fi
CMAKE_LITE_ARGS+=(
"-DDRAGONX_ENABLE_LITE_BACKEND=ON"
"-DDRAGONX_LITE_BACKEND_LIBRARY=$lb_lib"
"-DDRAGONX_LITE_BACKEND_SYMBOLS_FILE=$lb_symbols"
"-DDRAGONX_LITE_BACKEND_LINK_MODE=imported"
"-DDRAGONX_LITE_BACKEND_ABI=sdxl-c-v1"
)
[[ -f "$lb_manifest" ]] && CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_MANIFEST=$lb_manifest")
# A Rust x86_64-pc-windows-gnu staticlib pulls in Win32 system libs (rustls/schannel, ring,
# dirs, std) that the app doesn't already link. The set is rustc's `--print native-static-libs`
# for the backend (winapi_* shims mapped to the real mingw import libs); all exist in mingw-w64.
if [[ "$lb_platform" == "windows" ]]; then
CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_EXTRA_LIBS=advapi32;ws2_32;kernel32;bcrypt;cfgmgr32;credui;crypt32;cryptnet;fwpuclnt;gdi32;msimg32;ncrypt;ntdll;ole32;opengl32;secur32;shell32;synchronization;user32;winspool;userenv")
fi
info "Lite backend enabled ($lb_platform): $lb_lib"
else
# Explicit OFF so a prior --lite-backend configure in a shared build dir can't leave it ON
# (which would then fail the BUILD_LITE=OFF guard in CMake).
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_LITE_BACKEND=OFF")
fi
should_bundle_full_node_assets() {
! $DO_LITE
}
# ── Helper: find resource files ──────────────────────────────────────────────
find_sapling_params() {
local dirs=(
@@ -312,14 +215,13 @@ build_dev() {
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=ON \
"${CMAKE_LITE_ARGS[@]}"
-DDRAGONX_USE_SYSTEM_SDL3=ON
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/${APP_BASENAME}" ]] || { err "Build failed"; exit 1; }
info "Dev binary: $bd/bin/${APP_BASENAME} ($(du -h "bin/${APP_BASENAME}" | cut -f1))"
[[ -f "bin/ObsidianDragon" ]] || { err "Build failed"; exit 1; }
info "Dev binary: $bd/bin/ObsidianDragon ($(du -h bin/ObsidianDragon | cut -f1))"
}
# ═══════════════════════════════════════════════════════════════════════════════
@@ -340,53 +242,44 @@ build_release_linux() {
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=ON \
"${CMAKE_LITE_ARGS[@]}"
-DDRAGONX_USE_SYSTEM_SDL3=ON
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; }
[[ -f "bin/ObsidianDragon" ]] || { err "Linux build failed"; exit 1; }
info "Stripping ..."
strip "bin/${APP_BASENAME}"
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
strip bin/ObsidianDragon
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
if should_bundle_full_node_assets; then
# ── Bundle daemon ────────────────────────────────────────────────────
# ── Bundle daemon ────────────────────────────────────────────────────────
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
# ── Bundle Sapling params ────────────────────────────────────────────
# ── Bundle Sapling params ────────────────────────────────────────────────
SAPLING_SPEND="" SAPLING_OUTPUT=""
find_sapling_params && {
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
info "Bundled Sapling params"
} || warn "Sapling params not found — not bundled"
else
info "Lite mode: skipping daemon and Sapling/asmap bundling"
fi
# ── Package: release/linux/ ──────────────────────────────────────────────
# Remove only THIS variant's prior artifacts so full-node and lite releases can coexist in the
# same output dir (both ObsidianDragon* and ObsidianDragonLite* end up under release/linux/).
rm -rf "$out"
mkdir -p "$out"
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.AppImage"
local DIST="${APP_BASENAME}-${VERSION}-Linux-x64"
local DIST="ObsidianDragon-${VERSION}-Linux-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp "bin/${APP_BASENAME}" "$dist_dir/"
if should_bundle_full_node_assets; then
cp bin/ObsidianDragon "$dist_dir/"
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
fi
# Bundle xmrig for mining support
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
cp -r bin/res "$dist_dir/" 2>/dev/null || true
@@ -406,10 +299,9 @@ build_release_linux() {
"$APPDIR/usr/share/icons/hicolor/256x256/apps" \
"$APPDIR/usr/share/ObsidianDragon/res"
cp "bin/${APP_BASENAME}" "$APPDIR/usr/bin/"
cp bin/ObsidianDragon "$APPDIR/usr/bin/"
cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true
if should_bundle_full_node_assets; then
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$APPDIR/usr/bin/"
# Daemon data files must be alongside the daemon binary (usr/bin/)
@@ -417,18 +309,17 @@ build_release_linux() {
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
fi
# Bundle xmrig for mining support
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
# Desktop entry
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<DESK
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<'DESK'
[Desktop Entry]
Type=Application
Name=DragonX Wallet
Comment=DragonX Cryptocurrency Wallet
Exec=${APP_BASENAME}
Exec=ObsidianDragon
Icon=ObsidianDragon
Categories=Finance;Network;
Terminal=false
@@ -459,14 +350,14 @@ SVG
cp "$APPDIR/ObsidianDragon.svg" "$APPDIR/ObsidianDragon.png" 2>/dev/null || true
# AppRun
cat > "$APPDIR/AppRun" <<APPRUN
cat > "$APPDIR/AppRun" <<'APPRUN'
#!/bin/bash
SELF=\$(readlink -f "\$0")
HERE=\${SELF%/*}
export DRAGONX_RES_PATH="\${HERE}/usr/share/ObsidianDragon/res"
export LD_LIBRARY_PATH="\${HERE}/usr/lib:\${LD_LIBRARY_PATH}"
cd "\${HERE}/usr/share/ObsidianDragon"
exec "\${HERE}/usr/bin/${APP_BASENAME}" "\$@"
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
export DRAGONX_RES_PATH="${HERE}/usr/share/ObsidianDragon/res"
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}"
cd "${HERE}/usr/share/ObsidianDragon"
exec "${HERE}/usr/bin/ObsidianDragon" "$@"
APPRUN
chmod +x "$APPDIR/AppRun"
@@ -478,36 +369,26 @@ APPRUN
done
[[ -f "$bd/_deps/sdl3-build/libSDL3.so" ]] && cp "$bd/_deps/sdl3-build/libSDL3.so"* "$APPDIR/usr/lib/" 2>/dev/null || true
# appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
# The old "continuous" tag is a MOVING build fetched over the network and run on the release
# builder; a compromised/MITM'd artifact would execute here. Verify, or refuse to package.
local APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
local APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
# appimagetool
local APPIMAGETOOL=""
if command -v appimagetool &>/dev/null; then
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
APPIMAGETOOL="appimagetool"
elif [[ -f "$bd/appimagetool-x86_64.AppImage" ]]; then
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage"
else
local at="$bd/appimagetool-x86_64.AppImage"
# Re-verify any cached copy too; a stale unverified download must not be trusted.
if [[ ! -f "$at" ]] || ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
info "Downloading appimagetool 1.9.0 (pinned) ..."
wget -q -O "$at" "$APPIMAGETOOL_URL"
if ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
err "appimagetool SHA-256 verification failed — refusing to use it"
rm -f "$at"
return 1
fi
chmod +x "$at"
fi
APPIMAGETOOL="$at"
info "Downloading appimagetool ..."
wget -q -O "$bd/appimagetool-x86_64.AppImage" \
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x "$bd/appimagetool-x86_64.AppImage"
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage"
fi
local ARCH
ARCH=$(uname -m)
cd "$bd"
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
cp "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" "$out/${APP_BASENAME}-${VERSION}.AppImage"
info "AppImage: $out/${APP_BASENAME}-${VERSION}.AppImage ($(du -h "$out/${APP_BASENAME}-${VERSION}.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/"
@@ -616,7 +497,6 @@ HDR
# ── Daemon binaries ──────────────────────────────────────────────
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
if should_bundle_full_node_assets; then
if [[ -d "$DD" && -f "$DD/dragonxd.exe" ]]; then
info "Embedding daemon binaries ..."
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
@@ -634,30 +514,9 @@ HDR
else
warn "prebuilt-binaries/dragonxd-win/ not found — wallet-only build"
fi
else
info "Lite mode: skipping embedded daemon binaries"
fi
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
# The published DRG-XMRig archives ship the binary inside a versioned subdir, not as a flat
# xmrig.exe. Extract it from the matching win-x64 zip if it isn't already staged — otherwise
# the embed below never fires (HAS_EMBEDDED_XMRIG stays undefined) and the wallet ships with
# no miner ("xmrig binary not found" at runtime).
if [[ ! -f "$XMRIG_DIR/xmrig.exe" ]]; then
local _xz; _xz=$(ls "$XMRIG_DIR"/drg-xmrig-*-win-x64.zip 2>/dev/null | head -1)
if [[ -n "$_xz" ]] && command -v unzip >/dev/null 2>&1; then
local _xtmp; _xtmp=$(mktemp -d)
# -j flattens the versioned subdir; check the file (not unzip's exit code, which is
# non-zero if a pattern matches nothing).
unzip -j -o "$_xz" '*xmrig.exe' -d "$_xtmp" >/dev/null 2>&1 || true
if [[ -f "$_xtmp/xmrig.exe" ]]; then
cp -f "$_xtmp/xmrig.exe" "$XMRIG_DIR/xmrig.exe"
info " Extracted xmrig.exe from $(basename "$_xz")"
fi
rm -rf "$_xtmp"
fi
fi
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
if [[ -f "$XMRIG_DIR/xmrig.exe" ]]; then
cp -f "$XMRIG_DIR/xmrig.exe" "$RES/xmrig.exe"
info " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
@@ -735,47 +594,29 @@ HDR
"$SCRIPT_DIR/scripts/fetch-libsodium.sh" --win
fi
# ── FreeType for Windows (color-emoji rendering) ───────────────────────
# The mingw toolchain ships no FreeType; cross-build a minimal static one (COLR/CPAL, no external
# deps). Failure is non-fatal — the wallet just falls back to monochrome emoji.
local FT_MINGW_PREFIX="$SCRIPT_DIR/third_party/freetype-mingw"
if [[ ! -f "$FT_MINGW_PREFIX/lib/libfreetype.a" ]]; then
info "Cross-building FreeType for Windows (color emoji) ..."
"$SCRIPT_DIR/scripts/build-freetype-mingw.sh" "$FT_MINGW_PREFIX" \
|| warn "FreeType cross-build failed — Windows build will use monochrome emoji"
fi
local FT_CMAKE_ARG=()
if [[ -f "$FT_MINGW_PREFIX/lib/libfreetype.a" ]]; then
FT_CMAKE_ARG=(-DDRAGONX_MINGW_FREETYPE_PREFIX="$FT_MINGW_PREFIX")
fi
# ── CMake + build ────────────────────────────────────────────────────────
info "Configuring (cross-compile) ..."
cmake "$SCRIPT_DIR" \
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
"${FT_CMAKE_ARG[@]}" \
"${CMAKE_LITE_ARGS[@]}"
-DDRAGONX_USE_SYSTEM_SDL3=OFF
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; }
info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)"
[[ -f "bin/ObsidianDragon.exe" ]] || { err "Windows build failed"; exit 1; }
info "Binary: $(du -h bin/ObsidianDragon.exe | cut -f1)"
# ── Package: release/windows/ ────────────────────────────────────────────
# Remove only THIS variant's prior artifacts so full-node and lite releases coexist here.
rm -rf "$out"
mkdir -p "$out"
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.exe"
local DIST="${APP_BASENAME}-${VERSION}-Windows-x64"
local DIST="ObsidianDragon-${VERSION}-Windows-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp "bin/${APP_BASENAME}.exe" "$dist_dir/"
cp bin/ObsidianDragon.exe "$dist_dir/"
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
if should_bundle_full_node_assets; then
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
@@ -786,19 +627,16 @@ HDR
for f in sapling-spend.params sapling-output.params asmap.dat; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
else
info "Lite mode: skipping daemon and Sapling/asmap assets in Windows zip"
fi
# Bundle xmrig for mining support
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig.exe"
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
cp -r bin/res "$dist_dir/" 2>/dev/null || true
# ── Single-file exe (all resources embedded) ────────────────────────────
cp "bin/${APP_BASENAME}.exe" "$out/${APP_BASENAME}-${VERSION}.exe"
info "Single-file exe: $out/${APP_BASENAME}-${VERSION}.exe ($(du -h "$out/${APP_BASENAME}-${VERSION}.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
@@ -901,26 +739,8 @@ build_release_mac() {
fi
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
else
# Native macOS: build universal (arm64 + x86_64) by default. Override with
# DRAGONX_MAC_ARCHS (e.g. "x86_64").
MAC_ARCHS="${DRAGONX_MAC_ARCHS:-arm64;x86_64}"
# When linking the real lite backend, the app can only include architectures
# the backend static library actually provides. Its pinned ring 0.16.11 has no
# Apple-Silicon assembly, so that artifact is x86_64-only — constrain the app
# arch to the backend's (unless the user explicitly forced DRAGONX_MAC_ARCHS),
# otherwise the arm64 slice fails to link.
if $DO_LITE_BACKEND && [[ -z "${DRAGONX_MAC_ARCHS:-}" && -n "${lb_lib:-}" ]] && command -v lipo &>/dev/null; then
local _backend_archs; _backend_archs=$(lipo -archs "$lb_lib" 2>/dev/null | tr ' ' ';')
if [[ -n "$_backend_archs" && "$_backend_archs" != "$MAC_ARCHS" ]]; then
warn "Lite backend provides only [$_backend_archs] — building the app for that instead of universal."
MAC_ARCHS="$_backend_archs"
fi
fi
if [[ "$MAC_ARCHS" == *";"* || "$MAC_ARCHS" == *","* ]]; then
# Native macOS: build universal binary (arm64 + x86_64)
MAC_ARCH="universal"
else
MAC_ARCH="$MAC_ARCHS"
fi
export MACOSX_DEPLOYMENT_TARGET="11.0"
fi
@@ -998,8 +818,7 @@ TOOLCHAIN
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} \
"${CMAKE_LITE_ARGS[@]}"
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"}
else
# Build libsodium as universal if needed
local need_sodium=false
@@ -1008,7 +827,7 @@ TOOLCHAIN
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 -Eq "arm64.*x86_64|x86_64.*arm64"; then
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
@@ -1019,50 +838,45 @@ TOOLCHAIN
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
fi
info "Configuring (native macOS, arch: $MAC_ARCHS) ..."
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_ARCHITECTURES="$MAC_ARCHS" \
"${CMAKE_LITE_ARGS[@]}"
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
fi
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/${APP_BASENAME}" ]] || { err "macOS build failed"; exit 1; }
[[ -f "bin/ObsidianDragon" ]] || { err "macOS build failed"; exit 1; }
# Strip — use osxcross strip for cross-builds
if $IS_CROSS; then
local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip"
if [[ -x "$STRIP_CMD" ]]; then
info "Stripping (osxcross) ..."
"$STRIP_CMD" "bin/${APP_BASENAME}"
"$STRIP_CMD" bin/ObsidianDragon
else
warn "osxcross strip not found at $STRIP_CMD — skipping"
fi
else
info "Stripping ..."
strip "bin/${APP_BASENAME}"
strip bin/ObsidianDragon
# Verify universal binary
if command -v lipo &>/dev/null; then
info "Architecture info:"
lipo -info "bin/${APP_BASENAME}"
lipo -info bin/ObsidianDragon
fi
fi
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
# ── Create .app bundle ───────────────────────────────────────────────────
rm -rf "$out"
mkdir -p "$out"
# Clean only THIS variant's prior artifacts so full-node and lite releases can
# coexist in release/mac/ (Linux/Windows scope their cleanup the same way). The
# "ObsidianDragon-" glob never matches "ObsidianDragonLite-" (and vice versa),
# and the ".app" names are exact.
rm -rf "$out/${APP_BASENAME}.app" "$out/${APP_BASENAME}-"*.app.zip "$out/${APP_BASENAME}-"*.dmg
local APP="$out/${APP_BASENAME}.app"
local APP="$out/ObsidianDragon.app"
local CONTENTS="$APP/Contents"
local MACOS="$CONTENTS/MacOS"
local RESOURCES="$CONTENTS/Resources"
@@ -1071,13 +885,12 @@ TOOLCHAIN
mkdir -p "$MACOS" "$RESOURCES/res" "$FRAMEWORKS"
# Main binary
cp "bin/${APP_BASENAME}" "$MACOS/"
chmod +x "$MACOS/${APP_BASENAME}"
cp bin/ObsidianDragon "$MACOS/"
chmod +x "$MACOS/ObsidianDragon"
# Resources
cp -r bin/res/* "$RESOURCES/res/" 2>/dev/null || true
if should_bundle_full_node_assets; then
# Daemon binaries (macOS native, from dragonxd-mac/)
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
if [[ -d "$daemon_dir" ]]; then
@@ -1106,12 +919,9 @@ TOOLCHAIN
else
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
fi
else
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
fi
# xmrig binary (from prebuilt-binaries/drg-xmrig/)
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
if [[ -f "$XMRIG_MAC" ]]; then
cp "$XMRIG_MAC" "$MACOS/xmrig"
chmod +x "$MACOS/xmrig"
@@ -1120,13 +930,11 @@ TOOLCHAIN
warn "xmrig not found — mining unavailable in .app"
fi
if should_bundle_full_node_assets; then
# asmap.dat — placed in MacOS/ so the daemon finds it next to its binary
find_asmap 2>/dev/null && {
cp "$ASMAP_DAT" "$MACOS/asmap.dat"
info " Bundled asmap.dat"
}
fi
# Bundle SDL3 dylib
local sdl_dylib=""
@@ -1146,34 +954,26 @@ TOOLCHAIN
# Fix the rpath so the binary finds SDL3 in Frameworks/
if $IS_CROSS; then
local INSTALL_NAME_TOOL="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-install_name_tool"
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/ObsidianDragon" 2>/dev/null || true
else
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/ObsidianDragon" 2>/dev/null || true
fi
info " Bundled $sdl_name"
fi
# Launcher script (ensures working dir + dylib path). Uses ${APP_BASENAME} so the lite
# variant (ObsidianDragonLite) gets a correctly-named launcher + .bin pair.
mv "$MACOS/${APP_BASENAME}" "$MACOS/${APP_BASENAME}.bin"
cat > "$MACOS/${APP_BASENAME}" <<LAUNCH
# Launcher script (ensures working dir + dylib path)
mv "$MACOS/ObsidianDragon" "$MACOS/ObsidianDragon.bin"
cat > "$MACOS/ObsidianDragon" <<'LAUNCH'
#!/bin/bash
DIR="\$(cd "\$(dirname "\$0")" && pwd)"
export DYLD_LIBRARY_PATH="\$DIR/../Frameworks:\$DYLD_LIBRARY_PATH"
export DRAGONX_RES_PATH="\$DIR/../Resources/res"
cd "\$DIR/../Resources"
exec "\$DIR/${APP_BASENAME}.bin" "\$@"
DIR="$(cd "$(dirname "$0")" && pwd)"
export DYLD_LIBRARY_PATH="$DIR/../Frameworks:$DYLD_LIBRARY_PATH"
export DRAGONX_RES_PATH="$DIR/../Resources/res"
cd "$DIR/../Resources"
exec "$DIR/ObsidianDragon.bin" "$@"
LAUNCH
chmod +x "$MACOS/${APP_BASENAME}"
chmod +x "$MACOS/ObsidianDragon"
# Info.plist — display name + bundle id differ per variant so lite and full-node .apps
# can coexist; the executable matches the launcher (${APP_BASENAME}); the icon is shared.
local APP_DISPLAY_NAME="DragonX Wallet"
local APP_BUNDLE_ID="is.hush.dragonx"
if $DO_LITE; then
APP_DISPLAY_NAME="DragonX Wallet Lite"
APP_BUNDLE_ID="is.hush.dragonx.lite"
fi
# Info.plist
cat > "$CONTENTS/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
@@ -1181,17 +981,17 @@ LAUNCH
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>${APP_DISPLAY_NAME}</string>
<string>DragonX Wallet</string>
<key>CFBundleDisplayName</key>
<string>${APP_DISPLAY_NAME}</string>
<string>DragonX Wallet</string>
<key>CFBundleIdentifier</key>
<string>${APP_BUNDLE_ID}</string>
<string>is.hush.dragonx</string>
<key>CFBundleVersion</key>
<string>${VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION}</string>
<key>CFBundleExecutable</key>
<string>${APP_BASENAME}</string>
<string>ObsidianDragon</string>
<key>CFBundleIconFile</key>
<string>ObsidianDragon</string>
<key>CFBundlePackageType</key>
@@ -1253,29 +1053,25 @@ PLIST
info ".app bundle created: $APP"
# ── Zip the .app bundle ──────────────────────────────────────────────────
local APP_ZIP="${APP_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.app.zip"
local APP_ZIP="ObsidianDragon-${VERSION}-macOS-${MAC_ARCH}.app.zip"
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$APP_ZIP" "${APP_BASENAME}.app")
(cd "$out" && zip -r "$APP_ZIP" "ObsidianDragon.app")
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
fi
# ── Create DMG ───────────────────────────────────────────────────────────
# DMG filename matches the app bundle name (ObsidianDragon / ObsidianDragonLite).
# The mounted volume + CFBundleName keep the "DragonX Wallet" display branding
# (APP_DISPLAY_NAME above).
local DMG_BASENAME="${APP_BASENAME}"
local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg"
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"
if command -v create-dmg &>/dev/null; then
# create-dmg (works on macOS; also available on Linux via npm)
info "Creating DMG with create-dmg ..."
create-dmg \
--volname "${APP_DISPLAY_NAME}" \
--volname "DragonX Wallet" \
--volicon "$RESOURCES/ObsidianDragon.icns" \
--window-pos 200 120 \
--window-size 600 400 \
--icon-size 100 \
--icon "${APP_BASENAME}.app" 150 190 \
--icon "ObsidianDragon.app" 150 190 \
--app-drop-link 450 190 \
--no-internet-enable \
"$out/$DMG_NAME" \
@@ -1290,7 +1086,7 @@ PLIST
mkdir -p "$staging"
cp -a "$APP" "$staging/"
ln -s /Applications "$staging/Applications"
hdiutil create -volname "${APP_DISPLAY_NAME}" \
hdiutil create -volname "DragonX Wallet" \
-srcfolder "$staging" \
-ov -format UDZO \
"$out/$DMG_NAME" 2>/dev/null && {
@@ -1306,7 +1102,7 @@ PLIST
cp -a "$APP" "$staging/"
# Can't create a real symlink to /Applications in an ISO, but the .app
# is the important part — users drag it to Applications manually.
genisoimage -V "${APP_DISPLAY_NAME}" \
genisoimage -V "DragonX Wallet" \
-D -R -apple -no-pad \
-o "$out/$DMG_NAME" \
"$staging" 2>/dev/null && {
@@ -1345,9 +1141,3 @@ if $DO_LINUX || $DO_WIN || $DO_MAC; then
[[ -d "$SCRIPT_DIR/release/windows" ]] && echo -e " ${CYAN}windows/${NC} — .exe + .zip"
[[ -d "$SCRIPT_DIR/release/mac" ]] && echo -e " ${CYAN}mac/${NC} — .app + .dmg"
fi
# Reaching here means the build completed (real failures exit 1 at their point of failure).
# Exit 0 explicitly: the final `[[ -d release/mac ]] && echo` above returns non-zero on a
# non-mac build — and since set -e exempts the left side of an &&, that status would otherwise
# become the script's exit code and make a successful build report failure (e.g. to CI).
exit 0

View File

@@ -1,31 +0,0 @@
# patch-libwebp-simd.cmake — portable, idempotent FetchContent patch for libwebp.
#
# libwebp's cmake/cpu.cmake compiles its scalar *reference* DSP files with the
# SSE-disable flags "-mno-sse4.1;-mno-sse2" whenever it can't positively detect
# SSE support. Under a macOS *universal* build (-arch arm64;x86_64) the per-arch
# SSE flag probe fails (a flag valid for x86_64 is invalid for arm64), so those
# disable flags get applied to the x86_64 slice. clang gates the _Float16 type on
# SSE2 for x86_64, and the macOS 15+/26 SDK's <math.h> declares _Float16 math
# functions unconditionally — so any TU including <math.h> fails to compile with
# "_Float16 is not supported on this target".
#
# SSE2 is part of the x86_64 baseline ABI, so disabling it on the reference files
# is unnecessary on every platform we target. Blanking the SSE entries (indices
# must stay aligned with WEBP_SIMD_FLAGS) fixes the universal build and is a no-op
# for single-arch Linux/Windows/x86_64 builds. Idempotent: re-running is a no-op.
if(NOT DEFINED CPU_CMAKE OR NOT EXISTS "${CPU_CMAKE}")
message(FATAL_ERROR "patch-libwebp-simd: cpu.cmake not found at '${CPU_CMAKE}'")
endif()
file(READ "${CPU_CMAKE}" _contents)
string(REPLACE
"set(SIMD_DISABLE_FLAGS \"-mno-sse4.1;-mno-sse2;;-mno-dspr2;;-mno-msa\")"
"set(SIMD_DISABLE_FLAGS \";;;-mno-dspr2;;-mno-msa\")"
_patched "${_contents}")
if(_patched STREQUAL _contents)
message(STATUS "patch-libwebp-simd: no change (already patched or pattern absent)")
else()
file(WRITE "${CPU_CMAKE}" "${_patched}")
message(STATUS "patch-libwebp-simd: neutralized x86 SSE-disable flags in cpu.cmake")
endif()

View File

@@ -0,0 +1,671 @@
# Codebase Cleanup Audit
Current as of 2026-04-27 for ObsidianDragon `1.2.0-rc1`.
## Scope
This audit covers architecture, threading, UI/layout code, build/resource integration, RPC/daemon behavior, and general cleanup opportunities. It is a static maintainability audit, not a runtime performance profile or security penetration test.
Evidence was gathered from source-tree scans, largest-file metrics, targeted code inspection, and focused architecture/UI/build review passes.
## Snapshot
- `src/` contains about 77,639 handwritten C++ source/header lines, excluding generated language outputs.
- Generated language headers previously accounted for about 34,675 source-tree lines under `src/embedded/lang_*.h`; Phase 4 moved those outputs to `build/generated/embedded/`.
- Largest handwritten files are concentrated in UI and application orchestration:
- `src/ui/windows/balance_tab.cpp`: 3,434 lines
- `src/app.cpp`: 3,097 lines
- `src/ui/windows/mining_tab.cpp`: 2,544 lines
- `src/ui/pages/settings_page.cpp`: 2,092 lines
- `src/main.cpp`: 2,014 lines
- `src/app_security.cpp`: 1,918 lines
- `src/app_network.cpp`: 1,912 lines
- `src/ui/windows/console_tab.cpp`: 1,572 lines
- `src/app_wizard.cpp`: 1,378 lines
- `src/daemon/embedded_daemon.cpp`: 1,213 lines
## Executive Summary
The codebase has a solid functional foundation: RPC work is already separated from UI-thread callbacks, the schema-driven UI system is useful, and embedded resource handling is well established. The largest opportunities are mostly structural rather than feature bugs.
The main cleanup theme is ownership. `App` currently coordinates lifecycle, rendering, daemon state, RPC refreshes, wallet security, dialogs, shutdown, mining, and first-run flow. That centralization makes features easy to wire in the short term, but it also pushes thread lifetime, error propagation, UI state, and background polling into a few very large files.
The second theme is consistency. Dialogs mostly use Material overlay helpers, but some still use raw ImGui modals. Many UI dimensions are schema-driven, but dialog/button/icon sizes still have scattered literals. RPC auth exists in more than one place, generated resources live partly in source and partly in build output, and error handling ranges from user-visible reporting to silent `catch (...)` blocks.
## Implementation Status
### Phase 1 Completed On 2026-04-27
- Replaced time-seeded `std::rand()` RPC credential generation in `src/rpc/connection.cpp` with libsodium-backed random generation.
- Replaced direct UI `system()` explorer/about launch calls with `util::Platform::openUrl()`.
- Reworked `util::Platform::openUrl()` and `openFolder()` so macOS/Linux launchers use `posix_spawnp()` arguments instead of shell-built command strings, and folder creation uses `std::filesystem::create_directories()`.
- Added URL scheme validation for platform URL opens.
- Added CMake visibility for missing `xxd` and Python theme expansion dependencies.
- Added `xxd` and Python checks/package coverage to `setup.sh`.
- Added shared `App::sendStopCommandSafely()` logging helper and routed repeated daemon `stop` RPC calls through it.
- Confirmed `libs/incbin.h` is already represented in `THIRD_PARTY_LICENSES`, so no additional license entry was needed.
- Verified with staged builds after logical batches, ending with `cd build && make -j$(nproc)` successfully linking `bin/ObsidianDragon`.
Remaining high-priority follow-ups from this audit include the larger `App`/security/refresh service boundaries and tests around the newly centralized runtime behavior.
### Phase 2 Completed On 2026-04-27
- Added schema-backed dialog layout tokens in `res/themes/ui.toml` and central accessors in `src/ui/layout.h` for common dialog widths, form width, action width/gap, max height ratio, and compact bottom alignment.
- Registered the `dialog` UI schema section in `src/ui/schema/ui_schema.cpp` so those tokens are available through the existing schema cache.
- Extended `material::BeginOverlayDialog()` with an optional ID suffix and added shared overlay action/footer helpers in `src/ui/material/draw_helpers.h`.
- Migrated address book Add/Edit address dialogs in `src/ui/windows/address_book_dialog.cpp` from raw `ImGui::BeginPopupModal()` usage to `material::BeginOverlayDialog()` with one shared form/action renderer.
- Added `src/ui/material/project_icons.h` as the wallet icon registry and moved pickaxe-specific font rendering behind that helper.
- Kept `AddressLabelDialog::drawIconByName()` and `iconGlyphForName()` as compatibility wrappers for existing call sites.
- Verified with `cd build && make -j$(nproc)` successfully linking `bin/ObsidianDragon`; diagnostics were clean on Phase 2 touched files, `git diff --check` passed, and scans found no remaining Add/Edit raw modal usage or local address-label icon arrays.
### Phase 3 Completed On 2026-04-27
- Added `src/util/async_task_manager.h/.cpp` as a named task owner with cancellation tokens, completed-task reaping, and join-on-shutdown behavior.
- Routed App-owned daemon maintenance, wizard daemon stop/check, encryption daemon restart, and decrypt restart/import background work through `AsyncTaskManager` instead of detached App threads.
- Kept `Bootstrap`'s worker joinable so its existing destructor cancellation can join the download/extract thread instead of losing ownership through `detach()`.
- Added `src/daemon/daemon_controller.h/.cpp` as the first daemon ownership boundary; it now owns `EmbeddedDaemon`, syncs settings into the daemon, and centralizes start/stop calls while `App` keeps a non-owning bridge pointer for low-churn follow-up migration.
- Extended `rpc::ConnectionConfig` with auth-source tracking and `use_tls`, parsed `rpctls`/`rpcssl`-style config flags, and centralized `.cookie` auth retry construction in `Connection::buildCookieAuthConfig()`.
- Updated main, fast-lane, temporary stop, wizard stop, and decrypt-import RPC clients to pass the TLS flag to `RPCClient`.
- Added a one-per-session runtime warning and Settings-page warning when a non-localhost RPC host is configured without TLS.
- Verified after each logical batch with `cmake .. && make -j$(nproc)` or `make -j$(nproc)` successfully linking `bin/ObsidianDragon`; diagnostics were clean on Phase 3 touched files, `git diff --check` passed, and scans found no remaining App-owned detached background tasks or App-level manual `.cookie` fallback.
### Phase 4 Completed On 2026-04-27
- Added `src/services/refresh_scheduler.h/.cpp` and moved refresh interval/timer policy out of `App`, while leaving RPC refresh bodies behavior-preserving in the existing app/network methods.
- Replaced App refresh timer fields with `services::RefreshScheduler`, including page-specific refresh policy, wallet mutation refresh marking, transaction-age throttling, OPID polling cadence, price refresh cadence, and fast mining/rescan ticks.
- Moved generated language headers from tracked `src/embedded/lang_*.h` files into `${CMAKE_BINARY_DIR}/generated/embedded/`, keeping the existing `embedded/lang_*.h` include strings working through the generated include directory.
- Added CTest infrastructure and `ObsidianDragonTests` with focused coverage for connection config parsing, cookie fallback and plaintext/TLS checks, payment URI parsing, fixed amount formatting, spendable wallet address filtering, and scheduler behavior.
- Added `src/util/amount_format.h/.cpp` for shared fixed-decimal amount formatting and used it in transaction send payload construction.
- Added pure spendability helpers in `src/data/wallet_state.h/.cpp` and routed Send-tab source address selection through them.
- Split low-risk helpers out of large UI tabs: balance helper formatting/drawing code, mining formatting/estimate/thread helpers, and the console RPC command reference registry.
- Verified focused tests with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`, then verified the UI split batch with `cd build && cmake .. && make -j$(nproc)` successfully linking `bin/ObsidianDragon`.
### Phase 5 Completed On 2026-04-27
- Added `src/services/wallet_security_controller.h/.cpp` and moved deferred wizard encryption/PIN state, connection retry throttling, PIN validation, and secure clearing behind a wallet-security boundary while keeping the existing RPC/UI behavior in `App`.
- Added `src/services/network_refresh_service.h/.cpp` as the fuller refresh boundary around `RefreshScheduler`, named refresh jobs, in-flight job guards, and explicit queue-pressure skips for core, address, transaction, mining, and peer refreshes.
- Extended `DaemonController` with shutdown/external-daemon policy decisions and routed `App::beginShutdown()` through that boundary.
- Grouped Settings-page static UI state into `SettingsPageState` and first-run wizard static UI state into `WizardUiState`, preserving existing file-local behavior while making state ownership explicit.
- Continued low-risk renderer splitting with console layout helpers, balance recent-transaction visual/amount helpers, and mining active-state/thread clamp helpers.
- Expanded focused tests for daemon shutdown policy, wallet security transitions, network refresh job guards, renderer helpers, and generated resource fallback behavior.
- Documented the `src/config/version.h` policy: it remains committed source for editor/release tooling compatibility, and CMake regenerates it from `src/config/version.h.in` during configure.
- Verified Phase 5 batches with repeated `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure` runs.
### Phase 6 Completed On 2026-04-27
- Expanded `WalletSecurityController` with mockable RPC and secure-vault gateways for deferred encryption, unlock, export, import, key classification, import messaging, decrypt export naming, and secure handoff behavior.
- Routed wallet encryption, deferred encryption retry, unlock, and key import/export classification through the wallet-security service boundary while preserving existing App-visible behavior.
- Extended `DaemonController` with lifecycle decisions for manual restart, rescan, blockchain-data deletion, and bootstrap daemon stop sequencing, then routed the corresponding App flows through those decisions.
- Promoted `NetworkRefreshService` to dispatch-ticket ownership with queue-depth telemetry, queue-pressure skips, in-flight skips, completion stats, cancellation, and stale-callback detection for named refresh jobs.
- Continued renderer splitting with stable modules for recent transaction presentation, pool-worker default selection, and console output filtering.
- Removed Settings-page compatibility aliases so the page now uses `SettingsPageState` fields directly.
- Expanded focused integration-style tests with mock wallet-security RPC/vault collaborators, daemon lifecycle policy checks, refresh dispatch telemetry/stale-callback edges, and UI helper coverage.
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 7 completed on 2026-04-27:
- `DaemonController` now owns lifecycle execution ordering for restart, rescan, blockchain-data deletion, and bootstrap stops through mockable `LifecycleRuntime` and `LifecycleTaskContext` collaborators.
- App removed the temporary non-owning `embedded_daemon_` bridge; daemon state/output reads now route through `DaemonController` wrappers or explicit controller access.
- Added `WalletSecurityWorkflow` for decrypt/export/import dialog phase, step, import-active state, and wallet file planning, so the dialog state can be tested without constructing `App`.
- `NetworkRefreshService` now owns enqueue/callback wrapping for named refresh jobs, including queue-depth sampling, queue-pressure skips, stale callback suppression, and UI-thread callback handoff.
- Core, address, transaction, mining, peer, and encryption refresh jobs now use the service enqueue wrapper instead of app-local dispatch-ticket boilerplate.
- Split additional UI helpers into `balance_address_list`, `mining_benchmark`, and `console_input_model`, with focused tests for filtering/sorting, benchmark state estimates, history navigation, and autocomplete.
- Expanded `ObsidianDragonTests` with mock daemon lifecycle execution tests, wallet workflow tests, refresh enqueue/stale-callback tests, and the new UI module coverage.
- Verified after each logical batch with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 8 completed on 2026-04-27:
- Added `WalletSecurityWorkflowExecutor` with mockable RPC, import, file, daemon, and secure-vault cleanup gateways; decrypt wallet unlock/export/backup/restart/import/cleanup sequencing now runs through that executor instead of nested App-local orchestration.
- Completed named refresh enqueue coverage by routing price refresh through `NetworkRefreshService::enqueue(Job::Price)` and connect-time `getinfo`/`getwalletinfo` prefetch through `Job::ConnectionInit`.
- Added daemon lifecycle collaborators for async/immediate task contexts and blockchain-data cleanup, reducing App-specific lifecycle runtime code where the extracted pieces are directly testable.
- Continued large-view reduction with balance address-row layout/USD helpers, mining benchmark transition and pool saved/default helpers, and console command parse/result classification helpers.
- Expanded `ObsidianDragonTests` for workflow executor edges, ConnectionInit enqueue coverage, daemon lifecycle adapters, and the new UI helper/model behavior.
- Verified each logical Phase 8 batch with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 9 completed on 2026-04-27:
- Added typed refresh result models, JSON parsers, and service-owned apply contracts on `NetworkRefreshService` for connection info, wallet encryption, core balance/sync, mining, peers, and price refreshes.
- Routed `App` refresh callbacks through those contracts for connect-time prefetch, warmup info application, core refresh, encryption refresh, mining refresh, peer refresh, and price refresh while preserving the existing RPC call ordering.
- Expanded `ObsidianDragonTests` with focused refresh result parsing/application coverage for balance/sync state, connection metadata, wallet lock state, mining history, peer lists, banned peers, and price history.
- Documented remaining intentional process-wide ImGui state and legacy compatibility wrappers in `docs/ui-static-state.md`, and linked that policy from `docs/codebase-overview.md`.
- Verified the Phase 9 implementation batch with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 10 completed on 2026-04-27:
- Added `NetworkRefreshService::AddressRefreshResult` and routed address snapshot application through `applyAddressRefreshResult()` while preserving App-owned RPC collection and dirty-flag behavior.
- Added service-owned transaction view-cache models plus `TransactionRefreshResult`/`TransactionCacheUpdate` so transaction refresh callbacks now apply `WalletState::transactions`, `last_tx_update`, `last_tx_block_height_`, `viewtx_cache_`, `send_txids_`, and confirmed transaction caches through one cache-update contract.
- Moved z-viewtransaction output parsing and outgoing-send enrichment into `NetworkRefreshService` helpers, leaving only the RPC call order and per-cycle throttling in `App`.
- Evaluated the remaining decrypt workflow orchestration and kept the nested UI step handoffs App-owned because those boundaries still carry progress updates, worker-thread callback ordering, and shutdown/token cancellation semantics.
- Did not touch high-churn tab/dialog statics in this phase; `docs/ui-static-state.md` remains the policy for future behavior-adjacent UI state changes.
- Expanded `ObsidianDragonTests` with address/transaction applicator coverage, view-transaction enrichment/cache-update tests, stale callback cancellation coverage, additional remote/TLS config parsing, and shutdown-cancellation lifecycle coverage.
- Verified the Phase 10 implementation batch with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 11 completed on 2026-04-27:
- Revisited the decrypt restart/import flow and kept the remaining UI handoff choreography App-owned because each boundary still corresponds to an explicit progress step, cancellation check, or background import transition. No partial executor move was made.
- Moved additional address refresh construction into `NetworkRefreshService` helpers for shielded-address validation results, transparent address parsing, and unspent-output balance application while preserving the existing daemon RPC call order in `App::refreshAddressData()`.
- Moved transaction pre-refresh snapshot construction and list parsing helpers into `NetworkRefreshService`, including shielded address snapshots, fully enriched txid snapshots, transparent transaction parsing, shielded receive parsing, and final transaction sorting. `App::refreshTransactionData()` still owns RPC call order and per-cycle `z_viewtransaction` throttling.
- Expanded `ObsidianDragonTests` with focused coverage for the new address/transaction snapshot helpers, worker callback ordering across independent refresh jobs, and reconnect-style stale transaction callbacks.
- Did not migrate additional tab/dialog statics because Phase 11 did not touch those views for behavior changes.
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 12 completed on 2026-04-27:
- Added a shared ordered mock refresh RPC fixture in `ObsidianDragonTests` so service-level refresh collectors can assert exact daemon method ordering and parameters.
- Introduced `NetworkRefreshService::RefreshRpcGateway` plus `collectAddressRefreshResult()` and `collectTransactionRefreshResult()`; address and transaction refresh RPC collection now lives behind the service boundary while preserving the same daemon call order and per-cycle `z_viewtransaction` cap.
- Routed `App::refreshAddressData()` and `App::refreshTransactionData()` through a small `RPCClient` adapter for those collectors, keeping App focused on enqueueing and applying the returned results.
- Expanded refresh lifecycle coverage for ordered callbacks, reconnect-style stale transaction callbacks, and collector ordering around address validation, z-balance fallback, shielded receive polling, cached viewtransaction entries, fresh `z_viewtransaction`, and `gettransaction` enrichment.
- Revisited decrypt restart/import orchestration again and made no extra move because the remaining boundaries are still the explicit progress/cancellation/import handoff points.
- Did not migrate additional tab/dialog statics because Phase 12 did not touch those views for behavior changes.
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 13 completed on 2026-04-27:
- Added service-owned core and peer refresh collectors behind `NetworkRefreshService::RefreshRpcGateway`, moving the fixed `z_gettotalbalance`/`getblockchaininfo` and `getpeerinfo`/`listbanned` RPC bodies out of `App`.
- Extended the ordered mock RPC fixture tests to assert the exact core and peer daemon call order, empty parameter arrays, parsed result values, and partial-failure behavior where the second RPC still runs after the first fails.
- Kept warmup `getinfo`, mining refresh, connection init, and price fetch orchestration App-owned because those paths either have UI status handoffs, cadence-specific behavior, or non-RPC HTTP/callback details that were not part of this small collector move.
- Did not add new reconnect/shutdown lifecycle tests because Phase 13 did not change async cancellation, daemon restart, or worker lifecycle ownership.
- Revisited decrypt restart/import orchestration and high-churn UI state migrations by scope only; no behavior work touched those complete progress/cancellation or tab/dialog seams.
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 14 completed on 2026-04-27:
- Added `NetworkRefreshService::collectMiningRefreshResult()` behind `RefreshRpcGateway`; the service now owns the ordered `getlocalsolps` plus optional `getmininginfo` RPC collection while `App` still owns the fast/slow cadence decision and daemon-memory snapshot.
- Added `NetworkRefreshService::collectConnectionInitResult()` so the initial `getinfo` then `getwalletinfo` prefetch is service-owned while `App::onConnected()` keeps the high-priority enqueue and `encryption_state_prefetched_` lock-screen timing flag.
- Extended ordered mock RPC tests for mining slow ticks, mining fast-only ticks, mining partial failure, connection-init success, and connection-init wallet-info prefetch after `getinfo` failure.
- Did not add reconnect/shutdown lifecycle tests because Phase 14 did not change worker callback ownership, shutdown, reconnect, daemon restart, or cancellation behavior.
- Did not touch decrypt orchestration or high-churn UI state because no complete behavior-preserving step in those areas was part of the batch.
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 15 completed on 2026-04-27:
- Added `NetworkRefreshService::WarmupPollResult` and `collectWarmupPollResult()` behind `RefreshRpcGateway`; the service now owns the single warmup `getinfo` call and preserves either parsed connection info or the raw RPC error string.
- Routed the warmup branch of `App::refreshCoreData()` through the new collector while keeping UI status translation, daemon block-height decoration, `connection_status_`, and the `refreshData()` transition App-owned.
- Extended ordered mock RPC tests for warmup success and warmup failure so the `getinfo` call, empty params, parsed ready state, and error propagation are directly covered.
- Reviewed price HTTP fetching and kept it App-owned because the current worker callback, libcurl setup, HTTP-status handling, logging, and parse/apply handoff are clearer in one place; only JSON response parsing remains service-owned.
- Left command-style RPC actions App-owned because Phase 15 did not include behavior changes for send, address creation, mining toggles, ban operations, or import/export commands.
- Did not add reconnect/shutdown lifecycle tests because worker callback ownership, shutdown, reconnect, daemon restart, and cancellation behavior did not change.
- Did not touch decrypt orchestration or high-churn UI state because no complete behavior-preserving step in those areas was part of the batch.
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 16 completed on 2026-04-27:
- Added `NetworkRefreshService::PriceHttpResponse`, `PriceHttpResult`, and `parsePriceHttpResponse()` so libcurl transport status, HTTP status, parse success, and failure messages are represented as a focused service-owned boundary.
- Kept libcurl initialization, request execution, worker callback ownership, successful price logging, and UI-thread price application in `App::refreshPrice()`.
- Expanded focused tests for successful price HTTP parsing, HTTP non-200 failures, transport failures, and unrecognized response bodies.
- Kept command-style RPC actions App-owned because Phase 16 did not include a complete behavior change for send, address creation, mining toggles, ban operations, or import/export commands.
- Did not add reconnect/shutdown lifecycle tests because callback ownership, cancellation, shutdown, reconnect, and daemon restart behavior did not change.
- Did not touch decrypt orchestration or high-churn UI state because no behavior work touched those workflows.
- Verified with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 17 completed on 2026-04-27:
- Reviewed command-style RPC actions for send, address creation, mining toggles, ban operations, and key import/export; kept them App-owned because no complete behavior change created a focused extraction boundary with tests.
- Reviewed lifecycle, reconnect, shutdown, and daemon restart ownership; no new lifecycle tests were added because callback ownership, cancellation, shutdown, reconnect, and daemon restart behavior did not change.
- Reviewed decrypt and high-churn UI-state seams by scope; no changes were made because Phase 17 did not directly touch those workflows for behavior.
- Kept refresh and price boundaries stable because the current service collectors/result helpers already cover the testable seams introduced in prior phases, and no new smaller behavior seam appeared.
- Verified the stable-boundary decision with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 18 completed on 2026-04-27:
- Re-reviewed the remaining command-style RPC actions (`setgenerate`, `setban`, new address creation, key import, send) and kept them App-owned because Phase 18 did not include a real behavior change that supplied a focused extraction boundary and tests.
- Re-reviewed reconnect, shutdown, daemon restart, and callback ownership; no lifecycle tests were added because those ownership and cancellation behaviors did not change.
- Re-reviewed decrypt and high-churn UI-state seams by scope; no code changes were made because those workflows were not directly touched for behavior.
- Preserved the stable refresh and price boundaries introduced in prior phases because no new behavior made a smaller tested seam useful.
- Verified the feature-driven cleanup decision with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 19 completed on 2026-04-27:
- Treated the audit as feature-scoped maintenance rather than an extraction-only phase and made no source changes because no real behavior change created a new focused test seam.
- Re-reviewed command-style RPC actions (`setgenerate`, `setban`, new address creation, key import, send) and kept them App-owned because their current boundaries are command-specific and tied to immediate UI/app-state updates.
- Re-reviewed lifecycle, reconnect, shutdown, daemon restart, decrypt, and high-churn UI-state seams; no tests or migrations were added because those behaviors did not change directly.
- Preserved the stable refresh and price service boundaries because the existing collectors/result helpers already cover the testable seams created by earlier work.
- Verified the maintenance decision with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 20 completed on 2026-04-27:
- Moved the cleanup audit into maintenance mode and avoided additional extraction-only work because no concrete behavior change created a smaller tested seam.
- Kept command-style RPC actions App-owned until a complete command workflow change supplies focused tests.
- Added no lifecycle, decrypt, or UI-state tests because callback ownership, cancellation, daemon restart, decrypt flow, and UI state behavior did not change directly.
- Preserved stable refresh and price service boundaries because no new behavior exposed a better tested boundary.
- Verified maintenance mode with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 21 completed on 2026-04-27:
- Sustained maintenance mode and avoided extraction-only work because no concrete feature change created a focused tested seam.
- Kept command-style RPC actions App-owned until a complete command workflow change supplies focused tests.
- Added no lifecycle, decrypt, or UI-state coverage because callback ownership, cancellation, daemon restart, decrypt flow, and UI state behavior did not change directly.
- Preserved stable refresh and price service boundaries because no new behavior exposed a smaller tested seam.
- Verified sustained maintenance mode with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Phase 22 completed on 2026-04-27:
- Continued sustained maintenance mode and avoided extraction-only work because no concrete feature change created a focused tested seam.
- Kept command-style RPC actions App-owned until a complete command workflow change supplies focused tests.
- Added no lifecycle, decrypt, or UI-state coverage because callback ownership, cancellation, daemon restart, decrypt flow, and UI state behavior did not change directly.
- Preserved stable refresh and price service boundaries because no new behavior exposed a smaller tested seam.
- Verified the maintenance checkpoint with `cd build && cmake .. && make -j$(nproc) && ctest --output-on-failure`.
Remaining Phase 23 follow-ups:
- Continue sustained maintenance mode and avoid extraction-only phases unless concrete feature work creates a focused tested seam.
- Keep command-style RPC actions App-owned until a complete command workflow change supplies focused tests.
- Add lifecycle, decrypt, or UI-state coverage only when directly changing callback ownership, cancellation, daemon restart, decrypt flow, or UI state behavior.
- Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
## High-Priority Findings
### 1. `App` Is Carrying Too Many Responsibilities
Evidence:
- `src/app.h`, `src/app.cpp`, `src/app_network.cpp`, `src/app_security.cpp`, and `src/app_wizard.cpp` form a broad partial-class style application layer.
- `src/app.cpp` is 3,141 lines, with lifecycle, rendering, daemon/RPC startup, shutdown, modal rendering, and restart flows.
- `src/app_network.cpp` is 1,926 lines and contains many refresh, polling, send, mining, market, peer, and address metadata paths.
- `src/app_security.cpp` is 1,819 lines and combines wallet encryption, PIN handling, secure vault, import/export, and restart flows.
Why it matters:
- Changes in one workflow can easily affect unrelated lifecycle or UI behavior.
- It is hard to unit-test isolated flows without instantiating most of the app.
- Shutdown and async callback ownership are difficult to reason about because many subsystems capture or mutate `App` state directly.
Recommended cleanup:
- Extract a `DaemonController` for embedded/external daemon ownership, restart, port ownership, and shutdown sequencing.
- Extract a `WalletSecurityController` for PIN, encryption, secure vault, and key import/export flow state.
- Extract a `NetworkRefreshService` or `WalletRefreshScheduler` for balance/address/transaction/mining/market polling.
- Keep `App` focused on initialization, top-level navigation, frame dispatch, and service composition.
Suggested first slice:
- Completed in Phase 3: add the first `DaemonController` boundary around `EmbeddedDaemon` ownership, settings sync, and start/stop behavior. Follow-up work should move more restart/shutdown sequencing and external-daemon policy into the controller.
### 2. Background Thread Lifetime Is Not Centrally Owned
Evidence:
- Detached threads appear in `src/app.cpp`, `src/app_security.cpp`, `src/app_wizard.cpp`, `src/util/bootstrap.cpp`, and `src/main.cpp`.
- Several lambdas capture `this` and then call back after waits or daemon operations.
- Managed thread members also exist (`shutdown_thread_`, `daemon_restart_thread_`, `wizard_stop_thread_`, monitor threads), but ownership patterns vary by subsystem.
Why it matters:
- Detached work can outlive the `App` object or UI state it touches.
- Shutdown order is harder to guarantee.
- Cancellation is inconsistent, especially across daemon restart, wallet import, bootstrap download, and PIN unlock flows.
Recommended cleanup:
- Introduce an `AsyncTaskManager` owned by `App`, with named tasks, cancellation flags/tokens, and join-on-shutdown behavior.
- Replace new detached threads with submitted tasks and explicit cancellation.
- Require background callbacks to hop through the existing UI callback queue before touching UI state.
- Add shutdown assertions or logging for tasks that fail to stop promptly.
Suggested first slice:
- Completed in Phase 3: convert App-owned daemon restart/import/wizard background work to `AsyncTaskManager` and keep `Bootstrap`'s worker joinable. Follow-up work should move remaining subsystem monitor/watchdog policies behind explicit owners where useful.
### 3. RPC Credential Generation And Remote RPC Handling Need Hardening
Evidence:
- Before Phase 1, `src/rpc/connection.cpp` generated default `rpcuser`/`rpcpassword` with `std::srand(std::time(nullptr))` and `std::rand()`.
- Phase 1 replaced that path with libsodium-backed random generation.
- Before Phase 3, `src/rpc/rpc_client.cpp` always constructed an `http://host:port/` URL.
- Before Phase 3, `.cookie` auth handling appeared in `src/rpc/connection.cpp` and also as a 401 fallback in `src/app_network.cpp`.
Why it matters:
- `std::rand()` is not appropriate for credentials.
- HTTP is acceptable for strict localhost daemon RPC, but remote hosts can expose credentials unless the UI explicitly warns or supports TLS.
- Duplicate auth fallback paths make it harder to reason about precedence and failures.
Recommended cleanup:
- Completed in Phase 1: generate RPC credentials with libsodium-backed random generation.
- Completed in Phase 3: centralize `.cookie` retry construction in `Connection` after config-password failure.
- Completed in Phase 3: add a `use_tls` transport flag to `ConnectionConfig` and pass it through `RPCClient` connections.
- Completed in Phase 3: warn at runtime and in Settings when a non-localhost RPC host uses plaintext HTTP.
Suggested first slice:
- Completed in Phase 1 and Phase 3: replace `std::rand()` credential generation, centralize cookie fallback, and add remote plaintext/TLS handling.
### 4. Shell Launching Uses `system()` In UI And Platform Paths
Evidence:
- Before Phase 1, `src/util/platform.cpp` used `system()` for URL/folder opening and directory creation on some platforms.
- Before Phase 1, `src/ui/windows/transactions_tab.cpp`, `src/ui/windows/transaction_details_dialog.cpp`, and `src/ui/windows/about_dialog.cpp` called `system()` directly.
- Phase 1 routed these UI calls through `util::Platform::openUrl()` and removed shell-built launcher strings from `Platform` on macOS/Linux.
Why it matters:
- Shell invocation increases quoting and command-injection risk.
- Direct calls bypass the existing platform abstraction.
- Behavior varies by shell and desktop environment.
Recommended cleanup:
- Route all URL/file/folder launch behavior through `Platform` helpers.
- Replace shell strings with platform APIs where possible: `ShellExecute` on Windows, `open` or `xdg-open` via `fork/exec` or `posix_spawn` on Unix-like systems.
- Validate URL schemes before opening external links.
- Remove direct `system()` calls from UI windows.
Suggested first slice:
- Completed in Phase 1: update About, transaction details, and transaction tab actions to use `Platform::openUrl()` and safer platform launcher behavior.
## Medium-Priority Findings
### 5. Silent And Broad Exception Handling Is Common
Evidence:
- Empty or near-empty `catch (...)` blocks appear in `src/app.cpp`, `src/app_network.cpp`, `src/app_security.cpp`, `src/app_wizard.cpp`, `src/rpc/rpc_worker.cpp`, `src/rpc/rpc_client.cpp`, `src/main.cpp`, and several UI files.
- Repeated `try { rpc_->call("stop"); } catch (...) {}` patterns appear in daemon stop flows.
Why it matters:
- Failures vanish from logs and UI, making support and diagnosis difficult.
- Some swallowed failures are acceptable during best-effort shutdown, but the code does not consistently explain that intent.
- Repeated patterns invite copy-paste drift.
Recommended cleanup:
- Add small helpers such as `stopDaemonSafely(context)` and `parseFloatOrDefault(value, fallback, context)`.
- Log best-effort failures with enough context to debug without spamming normal shutdown.
- Prefer typed catches where the thrown type is known.
- For RPC work, propagate categorized failures to the notification system when they affect user-visible state.
Suggested first slice:
- Completed in Phase 1: replace repeated daemon stop catch blocks with a single helper that logs at debug/verbose level.
### 6. UI Dialogs Have Mixed Modal Systems And Repeated Layout Literals
Evidence:
- Most dialogs use `Material::BeginOverlayDialog()`, but `src/ui/windows/address_book_dialog.cpp` still uses raw `ImGui::BeginPopupModal()` for Add/Edit address modals.
- Dialog widths such as `480.0f`, `500.0f`, `620.0f`, and `660.0f` appear across UI files.
- Button widths and padding literals such as `140.0f`, `160.0f`, and `24.0f` are repeated.
- Icon font sizes are hardcoded in `src/ui/material/typography.cpp` as 14, 18, 24, and 40 px families.
Why it matters:
- The schema system already solves this class of problem, but not all dimensions use it yet.
- Modal behavior diverges when some dialogs bypass overlay scrim/input blocking helpers.
- Theme and density changes require hunting through many files.
Recommended cleanup:
- Add schema-backed dialog tokens under `res/themes/ui.toml`, such as `globals.dialog.width-default`, `width-lg`, `min-width`, and `max-height-ratio`.
- Add schema-backed action widths under a shared `button` or `globals.button-sizes` section.
- Move icon size selection into schema or a single icon token helper.
- Migrate address book Add/Edit dialogs to `BeginOverlayDialog()` and extract one shared form renderer.
Suggested first slice:
- Completed in Phase 2: refactor the address book Add/Edit dialog because it had duplicated modal form code and was still outside the overlay helper path.
### 7. UI State Uses Many Static Locals And File-Scoped Globals
Evidence:
- `src/ui/pages/settings_page.cpp` has many `sp_*` static variables for page state.
- `src/app_wizard.cpp` uses local statics for first-run appearance and daemon checks.
- Theme/color/effect modules use static state for current theme and effect initialization.
Why it matters:
- Static state is hard to reset in tests.
- It makes multi-window or future session-reset behavior harder.
- Initialization order and stale state can become subtle bugs after hot reloads or account switching.
Recommended cleanup:
- Introduce `SettingsPageState` and `WizardState` structures owned by `App` or a UI state container.
- Keep global/static state only for immutable constants or explicit process-wide singletons.
- Add reset/apply methods for state structures to support theme reload and test setup.
Suggested first slice:
- Move settings-page statics into a single struct without changing behavior. This is mostly mechanical and improves readability.
### 8. Build And Generated Resource Lifecycle Is Split Across Source, Build, And Scripts
Evidence:
- `src/config/version.h` is generated from `src/config/version.h.in` but the generated file is present in source control.
- CMake generates `src/embedded/lang_*.h` from `res/lang/*.json` with `xxd`.
- `setup.sh` does not appear to check for `xxd`.
- `find_package(Python3 QUIET COMPONENTS Interpreter)` is used for theme expansion; CMake falls back when Python is unavailable.
- Font `OBJECT_DEPENDS` are listed manually in `CMakeLists.txt`.
- `src/resources/embedded_resources.cpp` conditionally includes `embedded_data.h`, which is produced by release/build scripts for some platforms.
Why it matters:
- Generated files under `src/` make the source tree noisier and can cause stale diffs.
- Missing tools fail late or silently reduce build output quality.
- Resource embedding behavior differs by platform without one obvious matrix.
Recommended cleanup:
- Generate language headers under `build/generated/embedded/` and include that directory instead of writing under `src/embedded/`.
- Decide whether `src/config/version.h` should be generated-only or committed, then enforce that decision with `.gitignore` and documentation.
- Add explicit `xxd` checks to `setup.sh` and CMake configure output.
- Make the Python theme-expansion fallback a warning, not a quiet behavior change.
- Replace manual font `OBJECT_DEPENDS` with a generated list or CMake glob configured for the font directory.
- Document the embedded resource matrix by platform and build type.
Suggested first slice:
- Completed in Phase 1: add `xxd` detection and make missing Python theme expansion noisy.
### 9. Polling And Refresh Flow Would Benefit From A Scheduler Boundary
Evidence:
- `src/app_network.cpp` coordinates many refresh paths and timings.
- `src/app.cpp` also contains timed daemon/mining/rescan update logic.
- `src/rpc/rpc_worker.cpp` already provides a worker queue, but refresh orchestration is still spread through app-level methods.
Why it matters:
- Polling order, throttle behavior, and cancellation are difficult to audit.
- Adding one more refresh path can accidentally affect responsiveness or RPC queue pressure.
- Console fast-lane RPC is a good pattern, but regular refresh batches still need a clear scheduler owner.
Recommended cleanup:
- Add a `PollingScheduler` or `RefreshScheduler` that owns timer intervals, dependencies, cancellation, and rate limiting.
- Model refreshes as named jobs with last-run time, minimum interval, and in-flight state.
- Keep `RPCWorker` as execution plumbing while moving scheduling decisions out of `App`.
Suggested first slice:
- Completed in Phase 4: added `RefreshScheduler` for refresh timers, page intervals, transaction age throttling, OPID cadence, price refresh, and fast tick behavior.
- Completed in Phase 7 and Phase 8: `NetworkRefreshService` now owns enqueue/callback wrapping for core, address, transaction, mining, peer, encryption, price, and connection-init jobs, including queue pressure and stale callback suppression.
### 10. Large UI Tabs Need Feature-Sliced Components
Evidence:
- `src/ui/windows/balance_tab.cpp` is 3,562 lines.
- `src/ui/windows/mining_tab.cpp` is 2,842 lines.
- `src/ui/windows/console_tab.cpp` is 1,861 lines.
- Several of these files combine state management, drawing, filtering, formatting, and modal coordination.
Why it matters:
- UI changes become difficult to review because unrelated drawing and state logic share one file.
- Reusable patterns stay local and get reimplemented elsewhere.
- Testing smaller formatting/state helpers is harder when they are buried in render functions.
Recommended cleanup:
- Split large tabs by feature area: panels, table/list renderers, local state structs, formatting helpers, and action handlers.
- Keep ImGui draw calls close to the view but move data preparation and command decisions into smaller helpers.
- Avoid introducing inheritance-heavy UI abstractions; simple namespaces and structs should be enough.
Suggested first slice:
- Completed in Phase 4 through Phase 8: extracted low-risk balance helpers, balance address-list/recent-transaction models, mining benchmark/pool helpers, console command/input/output models, and additional address-row/mining/console execution helpers. Remaining work should continue moving draw-heavy action glue only when it lowers review/test cost.
## Low-Priority And Quick-Win Findings
### 11. Test Infrastructure Is Missing Or Not Obvious
Before Phase 4, no test target was visible in the workspace snapshot. Phase 4 added a small CTest executable covering connection config parsing, `.cookie` fallback priority, plaintext/TLS detection, wallet state filtering, amount formatting, payment URI parsing, and scheduler behavior.
Recommended cleanup:
- Completed in Phase 4: add a small CTest target with a lightweight assertion harness for pure utility/RPC/scheduler helpers.
- Consider migrating to Catch2 or GoogleTest if the test suite grows beyond a few focused files.
- Add mocks for `RPCClient` and daemon process state once service boundaries exist.
### 12. Translation Key Usage Is Inconsistent
`TR()` and `TrId()` coexist in places such as Settings. This is workable, but it increases typo risk and makes missing translations harder to audit.
Recommended cleanup:
- Define constants for high-traffic translation keys.
- Prefer one translation call style inside each module.
- Add a script that compares referenced keys against `res/lang/*.json`.
### 13. Project Icon Handling Needs One Registry
Material icons are referenced directly in many files, while the pickaxe icon uses a special one-glyph font path. The special case is now documented in `docs/codebase-overview.md`, but a code-level registry would make future icon work cleaner.
Recommended cleanup:
- Completed in Phase 2: add `src/ui/material/project_icons.h` for app-specific icon names.
- Completed in Phase 2: encapsulate pickaxe rendering behind one helper, so feature code does not need to know which font supplies it.
### 14. Release/Portability Docs Can Be Closer To The Real Build
`build.sh` contains much more platform-specific behavior than the README currently explains. AppImage dependency bundling, macOS universal build defaults, Windows MinGW assumptions, embedded daemon/resource behavior, and third-party license sync deserve their own build notes.
Recommended cleanup:
- Add a focused `docs/build-and-release.md`.
- Document development build vs release packaging per platform.
- Add a checklist for third-party license updates, including `libs/incbin.h`.
## Suggested Roadmap
### Phase 1: Small High-Value Fixes
- [x] Replace weak RPC credential generation in `src/rpc/connection.cpp`.
- [x] Route direct URL/open actions away from raw `system()` calls.
- [x] Add `xxd` checks and noisy Python theme-expansion warnings.
- [x] Confirm missing third-party license entry for INCBIN is not needed because `THIRD_PARTY_LICENSES` already includes it.
- [x] Extract repeated daemon stop try/catch blocks into a helper with contextual logging.
### Phase 2: UI Consistency Pass
- [x] Move common dialog sizes and action button widths into `res/themes/ui.toml` or existing layout helpers.
- [x] Migrate address book Add/Edit dialogs to `BeginOverlayDialog()`.
- [x] Add a shared dialog content/footer helper.
- [x] Add an icon registry and hide the pickaxe font special case behind it.
### Phase 3: Ownership And Runtime Boundaries
- [x] Introduce `AsyncTaskManager` for background work.
- [x] Extract `DaemonController` from `App`.
- [x] Consolidate RPC auth and connection fallback behavior.
- [x] Add warning/TLS configuration for non-localhost RPC.
### Phase 4: Larger Maintainability Work
- [x] Extract `NetworkRefreshService` or `RefreshScheduler`.
- [x] Split `balance_tab.cpp`, `mining_tab.cpp`, and `console_tab.cpp` into smaller feature files where safe.
- [x] Move generated language headers into `build/generated`.
- [x] Add focused unit tests for connection config, URI parsing, amount formatting, wallet address spendability filtering, and scheduler behavior.
### Phase 5: Service Boundaries And Test Depth
- [x] Move wallet encryption, PIN, secure vault, key import/export, and restart flow state toward a `WalletSecurityController`.
- [x] Move more daemon restart/shutdown policy and external-daemon behavior from `App` into `DaemonController`.
- [x] Evolve `RefreshScheduler` into a fuller network refresh service by modeling named refresh jobs, in-flight state, and RPC queue pressure explicitly.
- [x] Split larger UI feature renderers after the low-risk helper extractions, starting with balance address/recent transaction panels and mining benchmark/pool panels.
- [x] Move settings/wizard static UI state into explicit state structs.
- [x] Add deeper tests around daemon/RPC service boundaries, scheduler edge cases, wallet security state transitions, and generated resource behavior.
- [x] Decide and document whether `src/config/version.h` is committed source or generated-only output.
### Phase 6: Deeper Service Extraction And Integration Tests
- [x] Move wallet-security RPC orchestration, secure-vault handoffs, key import/export state, and daemon restart sequencing behind service interfaces that can be exercised without constructing the full UI `App`.
- [x] Move daemon restart/rescan/bootstrap shutdown sequencing and external-daemon ownership policy into `DaemonController` with mockable collaborators.
- [x] Promote `NetworkRefreshService` from refresh policy/guard owner to dispatcher owner for named RPC jobs, queue-pressure telemetry, and stale-callback handling.
- [x] Split large UI files into stable feature modules, especially balance address-list/recent-transaction rendering, mining benchmark/pool rendering, and console output/input rendering.
- [x] Replace remaining compatibility aliases around extracted UI state with direct state-struct use.
- [x] Add integration tests with mock RPC/daemon collaborators for wallet security, daemon lifecycle, and refresh dispatch edges.
### Phase 7: Service-Owned Execution And UI Module Finish
- [x] Move async daemon lifecycle execution, restart delays, rescan flags, blockchain-data deletion, bootstrap stops, and external-daemon ownership enforcement deeper into `DaemonController` with mock daemon/filesystem/task collaborators.
- [x] Extract wallet decrypt/export/import dialog workflow state and secure-vault restart handoffs into a wallet-security workflow service that can be tested without constructing `App`.
- [x] Promote `NetworkRefreshService` from dispatch-ticket/telemetry owner to the enqueue/callback wrapper for named RPC refresh jobs, including stale callback suppression and queue-pressure reporting at the service boundary.
- [x] Continue splitting balance address-list rendering, mining benchmark controls, and console input/history/autocomplete into stable modules with tests.
- [x] Remove temporary App compatibility bridges around daemon ownership and any remaining extracted UI state once direct service/state use is complete.
### Phase 8: Workflow Executors And Large-View Reduction
- [x] Move wallet decrypt import/restart RPC orchestration out of `App`'s nested lambdas into a workflow executor with mock RPC/daemon/vault/file collaborators.
- [x] Split concrete daemon lifecycle runtime responsibilities into smaller adapters only where it lowers App coupling without adding indirection for its own sake.
- [x] Complete named refresh enqueue coverage for price/market refresh and connect-time one-off polling.
- [x] Continue feature-slicing large ImGui views by extracting address-row, mining benchmark/pool, and console command execution helper/model seams.
- [x] Add focused tests around the new workflow, refresh, daemon lifecycle, and UI helper seams.
### Phase 9: Typed Refresh Results And State Cleanup
- [x] Evolve refresh jobs toward typed result models and service-owned result application contracts for refresh paths that still require complex app-local snapshots.
- [x] Review remaining static UI state and compatibility glue, then move it to explicit state structs or document what is intentionally process-wide.
- [x] Reviewed draw-heavy ImGui bodies and deferred additional slicing to Phase 10 unless adjacent to a required behavior-preserving change.
- [x] Add focused tests around refresh result application, reconnect/stale-callback edges, and any state-struct migrations.
### Phase 10: Transaction Refresh And State Follow-Through
- [x] Evaluate typed address/transaction refresh result models and cache-update applicators for the refresh paths that still build large App-local snapshots.
- [x] Reduce decrypt workflow orchestration in `App` further only where executor-owned operations can preserve progress reporting and cancellation behavior; Phase 10 evaluated the remaining chain and deferred extra movement because the current App handoffs are the progress/cancellation boundary.
- [x] Convert remaining high-churn tab/dialog statics to explicit state structs as those views are touched, following `docs/ui-static-state.md`; Phase 10 did not touch those views for behavior changes.
- [x] Add stronger reconnect/stale-callback, remote/TLS RPC, and shutdown-cancellation tests.
### Phase 11: Workflow And UI State Hardening
- [x] Revisit decrypt restart/import orchestration for one complete executor-owned step if progress and cancellation can stay explicit.
- [x] Continue high-churn tab/dialog state-struct migrations only when those views are touched for real behavior changes.
- [x] Move more address/transaction RPC snapshot construction into testable helpers if it reduces App coupling without duplicating daemon call ordering.
- [x] Keep broadening refresh/RPC lifecycle tests around realistic worker callback ordering and reconnect races.
### Phase 12: Integration Fixtures And Remaining Orchestration Edges
- [x] Add shared mock RPC fixtures only if they let tests assert daemon call ordering directly instead of duplicating production sequencing in test setup.
- [x] Move more refresh body construction out of `App` only where call ordering remains obvious and covered by tests.
- [x] Revisit decrypt restart/import orchestration only if a complete progress/cancellation step can be executor-owned end to end.
- [x] Continue UI state-struct migrations opportunistically when behavior work touches those tabs/dialogs.
### Phase 13: Ordered Collectors And Lifecycle Coverage
- [x] Extend ordered mock RPC coverage to additional refresh collectors only when those collectors move beyond App-owned call bodies.
- [x] Keep refresh call ordering explicit in tests whenever daemon call sequencing moves into a service.
- [x] Broaden reconnect/shutdown lifecycle coverage around async refresh cancellation only when those flows are touched.
- [x] Continue decrypt and UI-state hardening opportunistically at complete, behavior-preserving seams.
### Phase 14: Mining And Connection Edge Review
- [x] Move mining refresh collection only if the fast/slow polling cadence remains explicit and covered by ordered mock RPC tests.
- [x] Move connection-init prefetch only if lock-screen timing and `getinfo`/`getwalletinfo` ordering remain obvious and tested.
- [x] Add lifecycle coverage only when changing worker callback, shutdown, reconnect, or daemon restart ownership.
- [x] Continue decrypt and UI-state work only when a complete behavior-preserving step is touched.
### Phase 15: Remaining App-Owned Edges
- [x] Review warmup status polling only if the UI status/progress handoff can stay explicit and covered.
- [x] Review price HTTP fetching only if network parsing, callback behavior, and failure handling become easier to test without hiding libcurl details.
- [x] Leave command-style RPC actions App-owned unless a complete behavior change creates a focused extraction boundary.
- [x] Add lifecycle, decrypt, or UI-state tests only when touching those behaviors directly.
### Phase 16: Price And Command Boundary Review
- [x] Revisit price HTTP fetching only if libcurl status/error behavior can be represented as a focused testable boundary.
- [x] Keep send, address creation, mining toggle, ban, and import/export command RPC actions App-owned unless a complete behavior change justifies extraction.
- [x] Add reconnect/shutdown lifecycle coverage only when callback ownership or cancellation behavior changes.
- [x] Continue decrypt and UI-state work only when directly touching those workflows for behavior.
### Phase 17: Command And Lifecycle Guardrails
- [x] Review command-style RPC actions only when a complete behavior change creates a focused extraction boundary.
- [x] Add reconnect/shutdown lifecycle coverage only when callback ownership, cancellation, shutdown, reconnect, or daemon restart behavior changes.
- [x] Continue decrypt and UI-state work only when directly touching those workflows for behavior.
- [x] Keep existing refresh and price boundaries stable unless new behavior creates a smaller tested seam.
### Phase 18: Feature-Driven Cleanup Only
- [x] Move command-style RPC actions only when a real behavior change supplies a focused extraction boundary and tests.
- [x] Add lifecycle tests only when callback ownership, cancellation, shutdown, reconnect, or daemon restart behavior changes.
- [x] Continue decrypt and UI-state cleanup only when those workflows are touched for behavior.
- [x] Preserve stable refresh and price boundaries unless new behavior makes a smaller tested seam useful.
### Phase 19: Feature-Scoped Maintenance
- [x] Prefer feature-scoped maintenance over additional extraction-only phases.
- [x] Move command-style RPC actions only with a complete behavior change and focused tests.
- [x] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
- [x] Preserve existing refresh and price service boundaries unless new behavior exposes a smaller tested seam.
### Phase 20: Maintenance Mode
- [x] Convert the cleanup roadmap from extraction phases to maintenance-mode guidance.
- [x] Keep command-style RPC actions App-owned unless a complete feature change supplies focused tests.
- [x] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
- [x] Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
### Phase 21: Sustained Maintenance Mode
- [x] Continue maintenance-mode cleanup only when concrete feature work creates a focused tested seam.
- [x] Keep command-style RPC actions App-owned unless a complete command workflow change supplies focused tests.
- [x] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
- [x] Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
### Phase 22: Maintenance Checkpoint
- [x] Continue sustained maintenance mode and avoid extraction-only phases unless concrete feature work creates a focused tested seam.
- [x] Keep command-style RPC actions App-owned unless a complete command workflow change supplies focused tests.
- [x] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
- [x] Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
### Phase 23: Maintenance Checkpoint
- [ ] Continue sustained maintenance mode and avoid extraction-only phases unless concrete feature work creates a focused tested seam.
- [ ] Keep command-style RPC actions App-owned unless a complete command workflow change supplies focused tests.
- [ ] Add lifecycle/decrypt/UI-state coverage only when those behaviors change directly.
- [ ] Preserve stable refresh and price service boundaries unless new behavior exposes a smaller tested seam.
## Residual Risk
Several findings are maintainability risks rather than confirmed user-facing bugs. Remote/plaintext RPC handling now warns and has a TLS config path, and Phase 10 added focused parsing tests, but real daemon TLS setups still deserve manual validation.
The safest next step is Phase 23: continue sustained maintenance mode, preserving tested boundaries and tying future cleanup to concrete feature work.

76
docs/codebase-overview.md Normal file
View File

@@ -0,0 +1,76 @@
# Codebase Overview
Current as of 2026-04-27 for ObsidianDragon `1.2.0-rc1`.
## Purpose
ObsidianDragon is a Dear ImGui full-node wallet for DragonX (DRGX). It manages an embedded or external `dragonxd`, renders a schema-driven desktop UI, and provides shielded transactions, mining, market data, address management, explorer tools, and bootstrap download support.
## Runtime Architecture
- `src/main.cpp` initializes SDL3, graphics backends, ImGui, and the main loop.
- `src/app.cpp` owns application lifecycle, navigation, rendering, dialogs, and daemon/RPC startup.
- `src/app_network.cpp` contains refresh and transaction flows, including balance, address, transaction, mining, market, peer, and address metadata updates.
- `src/app_security.cpp` covers wallet encryption, PIN unlock, auto-lock, and secure vault integration.
- `src/app_wizard.cpp` handles first-run setup and bootstrap/encryption flow.
The UI thread renders ImGui and drains callback queues. RPC work runs through `RPCWorker`, which posts worker-thread RPC calls and returns UI-thread callbacks. Console commands can use a separate fast-lane RPC client/worker so they do not queue behind regular refresh batches.
`NetworkRefreshService` owns refresh timing, named job enqueue/callback guards, typed refresh result models, and service-owned result applicators for connection, core balance/sync, encryption, mining, peers, price, address snapshots, and transaction cache updates. Warmup polling, connection-init, core, mining, peer, address, and transaction refresh collection now run through a testable `RefreshRpcGateway`, and price HTTP response evaluation has a service-owned status/error/result model. `App` keeps enqueueing, UI timing decisions, cadence decisions, libcurl execution, and application handoff code for those paths.
## Source Map
| Path | Role |
|------|------|
| `src/config/` | Settings JSON persistence and generated `version.h` |
| `src/data/` | Wallet state, address book, exchange info |
| `src/rpc/` | libcurl JSON-RPC client, connection config, worker queue |
| `src/daemon/` | Embedded `dragonxd` manager and xmrig pool miner manager |
| `src/ui/windows/` | Main tabs and modal dialogs |
| `src/ui/pages/` | Page-style screens such as Settings |
| `src/ui/schema/` | TOML schema loader, skin manager, color resolver |
| `src/ui/material/` | Material-style typography, layout, drawing, components |
| `src/ui/effects/` | Acrylic, blur, noise, theme effects, low-spec fallback |
| `src/resources/` | Embedded resource extraction for params, daemon assets, themes, images |
| `src/platform/` | Windows DX11 and backdrop helpers |
| `src/util/` | i18n, logging, platform paths, bootstrap, vault, URI/base64/texture utilities |
## Build And Resources
- CMake uses C++17 and outputs `build/bin/ObsidianDragon`.
- Version comes from `project(... VERSION 1.2.0)` plus `DRAGONX_VERSION_SUFFIX=-rc1`.
- `src/config/version.h` is intentionally committed source for editor/release tooling compatibility, but CMake regenerates it from `src/config/version.h.in` during configure. Version bumps should update the CMake version/suffix and commit the regenerated header with the template if it changes.
- SDL3 is found from the system first, then fetched by CMake if unavailable.
- nlohmann/json and toml++ are fetched with CMake FetchContent.
- libcurl is system-provided on Linux/macOS and fetched statically for Windows.
- libsodium is system-provided on Linux or local under `libs/libsodium/`, `libs/libsodium-win/`, or `libs/libsodium-mac/`.
- Fonts are embedded with INCBIN: Ubuntu, Material Icons, a one-glyph MDI pickaxe subset, and Noto CJK subset.
- `res/themes/ui.toml` is embedded as a fallback and expanded into build themes with `scripts/expand_themes.py`.
- `res/default_banlist.txt` is embedded into `build/generated/default_banlist_embedded.h`.
## RPC And Daemon Notes
- Default RPC port is `21769`.
- `RPCClient` uses local HTTP with Basic auth. TLS is not assumed for localhost daemon RPC.
- DragonX daemon config paths:
- Linux: `~/.hush/DRAGONX/DRAGONX.conf`
- Windows: `%APPDATA%/Hush/DRAGONX/DRAGONX.conf`
- macOS: `~/Library/Application Support/Hush/DRAGONX/DRAGONX.conf`
- `Connection::autoDetectConfig()` creates missing config files, appends `exportdir`, `experimentalfeatures=1`, and `developerencryptwallet=1`, and falls back to `.cookie` auth if no `rpcpassword` is configured.
- The embedded daemon detects an external daemon on the RPC port and connects to it instead of taking ownership.
- Chain args include TLS-only mode, adaptive `-dbcache`, DragonX asset parameters, node seeds `node.dragonx.is` through `node4.dragonx.is`, and optional `-maxconnections=<n>` from Settings.
## UI And Data Notes
- Sidebar navigation is driven by `NavPage`: Overview, Send, Receive, History, Mining, Market, Console, Network, Explorer, Settings.
- Explorer lives in `src/ui/windows/explorer_tab.cpp`; Settings uses `src/ui/pages/settings_page.cpp`.
- Address labels, icons, favorites, hidden state, and manual ordering are persisted in Settings, especially `address_meta`.
- `AddressInfo::has_spending_key` tracks view-only shielded addresses; send flows filter or reject non-spendable z-addresses.
- The pickaxe icon is not a normal `ICON_MD_*` glyph. Use `AddressLabelDialog::drawIconByName()` or `Typography::pickaxeFontForSize()` for that special case.
- Remaining process-wide ImGui state and legacy compatibility wrappers are documented in `docs/ui-static-state.md`.
- Warmup, connection-init, core, mining, peer, address, and transaction refreshes use typed `NetworkRefreshService` result contracts plus gateway-backed collectors. Price refresh keeps libcurl setup/execution and callback ownership in App, while `NetworkRefreshService` owns JSON parsing plus HTTP status/error result evaluation. App still owns warmup status handoffs, mining fast/slow cadence, and command-style RPC actions. Transaction application updates `WalletState`, `viewtx_cache_`, `send_txids_`, the confirmed transaction cache, and block-height markers as one cache-update operation.
## Remaining Work
- Investigate `todo.md`: determine whether DragonX/Komodo wallet storage supports a safe compaction or consolidation workflow for wallets with too many addresses.
- Phase 23 should continue sustained maintenance mode: keep refresh and price boundaries stable, and revisit command/lifecycle/decrypt/UI-state seams only when concrete feature work creates a focused tested boundary.

View File

@@ -1,61 +0,0 @@
**DragonX (DRGX) Lite Desktop Wallet — v1.0.0**
ObsidianDragonLite is a lightweight companion to the ObsidianDragon full-node wallet. It skips the embedded full node entirely — **no multi-gigabyte blockchain download and near-instant startup** — by connecting to a DragonX lite-wallet (lightwalletd-style) server, while keeping the same native ImGui interface and shielded-first workflow.
This is the **first release** of the Lite variant.
---
## Why Lite?
- **No blockchain to download.** Sync in seconds instead of hours; a few MB of state instead of many GB.
- **Same wallet, lighter footprint.** The familiar ObsidianDragon UI, shielded and transparent addresses, and address book — without running a node or miner.
- **Portable.** A single self-contained binary that stores its data in its own `ObsidianDragonLite` folder, so it coexists cleanly alongside the full-node wallet.
---
## Features
- **Fast, node-free operation** — connect to a DragonX lite-wallet server and sync in seconds; minimal disk and RAM.
- **Wallet lifecycle** — create a new wallet, restore from a seed phrase, or open an existing one; wallets auto-open on startup, with a first-run welcome prompt and **guided seed backup** on creation.
- **Shielded + transparent** — send, shield, and receive DRGX; per-address balances computed from unspent notes and UTXOs.
- **Keys & seed** — export your seed phrase and keys, and import keys, from Settings.
- **Passphrase encryption** — encrypt, unlock, lock, and decrypt the wallet; you're prompted to unlock at send time and on startup when the wallet is locked.
- **Encrypted messaging (HushChat)** — built-in **Contacts** and **Chat** with a seed-derived, **SilentDragonXLite-compatible** identity and a seed-encrypted local message store, so you can message other DragonX users end-to-end.
- **Server management** — a built-in **server browser with automatic failover**; switch servers with live reconnect/recovery, and a **"Redownload blocks"** action to rescan from the server.
- **Status & diagnostics** — a **Network tab** showing connection and sync status, plus an **interactive Console** for running backend commands and copyable error output.
- **Multi-language UI** — full internationalization covering 8 languages: German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese.
- **Cross-platform** — native builds for Linux (AppImage + zip), Windows (portable exe + zip), and macOS (DMG + .app), x86-64.
- QR code generation for receiving addresses
- Extensive theme and appearance options
---
## How it differs from the full-node wallet
- **No embedded `dragonxd`, no mining, no local blockchain explorer.** Chain data comes from the lite-wallet server rather than being verified locally by a full node — a lighter footprint in exchange for trusting the server for chain state.
- Everything key-related stays **on your device**: seed, keys, and (when enabled) the encryption passphrase never leave the machine.
---
## Downloads
| File | SHA-256 |
|------|---------|
| ObsidianDragonLite-1.0.0.AppImage | `a1459d6081124a6b8df47aa898b60c0237875e8cbb132e4fd8ae637673055ed4` |
| ObsidianDragonLite-1.0.0-Linux-x64.zip | `573af502a6372334572e4dc88bd26e0cf36ed543b9df1e835fd8e4abd295108d` |
| ObsidianDragonLite-1.0.0.exe | `28659efb770fa573bd1b1d0fcab1c1a3f9c7e46574185c3916cc9f669684852c` |
| ObsidianDragonLite-1.0.0-Windows-x64.zip | `f6434a931e873e5936ebe249b044d6c7a413694767c667374c91ee438259b50c` |
| ObsidianDragonLite-1.0.0-macOS-x86_64.dmg | `10f976f1453e6b1978cb7a85026fd033c85a10c1e66f436475aa628379a298c7` |
| ObsidianDragonLite-1.0.0-macOS-x86_64.app.zip | `3e7259c363f7be2e9027c5282c94b742f4be280c998203752dc0a0551f6ab68b` |
## System Requirements
- **Linux:** x86-64, glibc 2.31+ (Ubuntu 20.04+, Fedora 33+, etc.)
- **Windows:** x86-64, Windows 10 or later
- **macOS:** x86-64 (Intel; runs on Apple Silicon via Rosetta 2), macOS 10.15 Catalina or later
- Network access to a DragonX lite-wallet server.
## License
Released under the **GPLv3**. See [LICENSE](https://git.dragonx.is/DragonX/ObsidianDragon/src/branch/master/LICENSE) for details.

View File

@@ -1,86 +0,0 @@
**DragonX (DRGX) Full-Node Desktop Wallet — v2.0.0**
This is ObsidianDragon, a native ImGui-based wallet for the DragonX network. It ships with an embedded full-node daemon (`dragonxd`) and an integrated CPU miner (DRG-XMRig), giving users a complete, self-contained experience on Linux, Windows, and macOS.
---
## What's New in v2.0.0
v2.0.0 is a major release. It adds **end-to-end encrypted HushChat messaging** and **BIP39 seed-phrase wallets** (with migrate-to-seed for legacy wallets), lets the wallet **update its own node and miner** with each install cryptographically verified, and ships a **security-hardening pass** across the updater, RPC, and secret-storage surfaces — plus full support for the **new multi-threaded `dragonxd`** and a large stability and UX overhaul.
### New Features
- **Encrypted messaging (HushChat)** — new **Contacts** and **Chat** tabs bring DRGX-native, end-to-end encrypted messaging. Your chat identity is derived from your wallet seed, messages are encrypted with libsodium (XChaCha20-Poly1305 / secretstream) and stored in a **seed-encrypted local database**, and the wire format is interoperable with SilentDragonX / SilentDragonXLite.
- **BIP39 seed-phrase wallets & migrate-to-seed** — new wallets are backed by a mnemonic **seed phrase** you can back up from Settings. Existing **legacy wallets can be migrated into a seed wallet** — the wallet mints a new mnemonic wallet, sweeps your funds to it, and adopts it only once the sweep is mined, keeping a timestamped `wallet.dat` backup throughout. (The bundled `dragonxd` supports the required mnemonic RPCs; older nodes degrade gracefully.)
- **In-app daemon updater** — Settings → **Node & Security → Daemon binary → "Check for updates…"** downloads the latest `dragonxd` from the project Gitea, verifies its **SHA-256 and a detached ed25519 signature** before installing, and replaces the binary **atomically while the node keeps running** (the new build takes effect on the next daemon start). A two-pane version picker lets you pin, downgrade, or install any published/pre-release build.
- **In-app miner updater** — an **"Update miner…"** action in the Mining tab fetches, verifies (SHA-256 + ed25519), and installs the latest DRG-XMRig, with the same version picker and a display of current vs. latest.
- **Daemon binary management** — the wallet no longer auto-overwrites a node you've dropped in; a dedicated Settings panel shows the managed binary and gathers all node actions (restart, rescan, repair) onto one toolbar.
- **Repair Wallet** — a one-click `-zapwallettxes=2` recovery in Settings, plus automatic wallet reconciliation after a bootstrap and runtime rescan support for pruned nodes.
- **Explorer search** — fuzzy, live (debounced) filtering of the block list by partial hash or height.
- **History sorting & fast load** — sort transactions by date or amount, a "Loading older history (N%)" indicator during the initial bulk load, and persisted history that surfaces pending sends immediately.
- **Smarter mining** — the thread benchmark now measures **sustained (thermally-throttled) hashrate** instead of an inflated initial burst, with GPU-aware idle mining and corrected idle thread scaling.
### Security & Integrity
- **Mandatory signature verification** — both the daemon and miner updaters **require** a valid ed25519 signature (checked against a key pinned in the wallet) in addition to the SHA-256; an install is refused if the signature is missing or invalid.
- **Hardened secret handling** — RPC credentials are wiped from memory after use, RPC responses are size-capped and scrubbed of secrets in logs, generated node config is written **owner-only (0600)**, and secrets copied to the clipboard **auto-clear**.
- **Safe on-disk writes** — settings, address book, and secret files are written **atomically with owner-only permissions**; the PIN vault fsyncs its secure-delete overwrite; SQLite cache growth is bounded.
- **Recipient validation** — sends validate recipient address checksums (Base58Check + Bech32) before building a transaction.
- **Path-traversal protection** — archive extraction (chain bootstrap and the updaters) rejects any entry that would escape the target directory (zip-slip), and the bootstrap fails closed on a missing checksum rather than trusting an unverified archive.
- **Robustness** — malformed RPC error JSON is guarded and sends are single-flight, preventing duplicate/ill-formed submissions.
### Improvements
- **New multi-threaded daemon support** — live Sapling note-witness rebuild progress, accurate sync-speed display, reliable rescan-completion detection, and RPC polling throttled during sync so block download isn't slowed.
- **Networking** — DragonX DNS seed nodes, `-maxconnections` passed to the daemon, and a peer count that stays current on every tab.
- **Node resilience** — non-blocking warmup so you can connect while the daemon is still initializing, a live daemon-console tail on the startup overlay, fast-failing connect probes, and mid-session disconnect detection that keeps the UI responsive (in-flight RPC calls abort cleanly on disconnect/shutdown).
- **Address list** — modernized with drag-to-transfer, labels, and view-only handling.
- **UI / DPI** — overlay dialogs scale correctly with the font/DPI setting, improved CJK font rendering, native language names in the language picker, and format-incompatible translations are rejected.
- **Settings** — an "Open data folder" button and confirmation modals for rescan and restart-daemon.
### Bug Fixes
Extensive fixes across history (shielded-tx ordering, stuck "refreshing history" banners, unconfirmed-badge stickiness), sends (correct fee passed to `z_sendmany`, fast-lane worker restart on reconnect, note-selection fee-gap workaround), rescan (accurate completion detection, no false "complete", no per-second error flood), RPC (mid-session disconnect handling, stale-refresh invalidation), and UI layout/i18n. See the commit history for the full list.
---
## Features
- **Full-node wallet** — send, receive, and verify DRGX transactions with a bundled `dragonxd` daemon; no external setup required.
- **Self-updating** — verify-and-install the latest node and miner from within the app (SHA-256 + ed25519 signature enforced).
- **Encrypted messaging** — built-in HushChat with a seed-derived identity, end-to-end encryption, and a seed-encrypted local message store, interoperable with SilentDragonX / SilentDragonXLite.
- **Seed-phrase wallets** — BIP39 mnemonic backup, and migrate-to-seed to upgrade a legacy wallet into a seed wallet.
- **Built-in CPU mining** — start/stop DRG-XMRig from the Mining tab with real-time hashrate and pool statistics, mine-when-idle support with configurable delay, and sustained-hashrate benchmarking.
- **Multi-language UI** — full internationalization covering 8 languages: German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese.
- **Cross-platform** — native builds for Linux (AppImage + zip), Windows (portable exe + zip), and macOS (**universal** DMG + .app); Linux/Windows x86-64, macOS Intel + Apple Silicon.
- Shielded (z-address) and transparent (t-address) send/receive
- QR code generation for receiving addresses
- Transaction history with memo support, date/amount sorting, and pending-send tracking
- Blockchain explorer with live block/transaction search
- Blockchain rescan and Sapling witness-rebuild with status-bar progress
- Repair Wallet and daemon-binary management in Settings
- Built-in console
- Extensive theme and appearance options
---
## Downloads
| File | SHA-256 |
|------|---------|
| ObsidianDragon-2.0.0.AppImage | `34c9ea57ec27bf415e59d2890d995ed9069bfce04d979d7d8d58aa18f4570aa1` |
| ObsidianDragon-2.0.0-Linux-x64.zip | `563004b4e45650dbfa9411d62fa1c59a3ca2199bd43a647b1614f2683bbdb5fa` |
| ObsidianDragon-2.0.0.exe | `56100ae59dc3c67445d015cac5412ca40465bf459ba182c5ad2477a3b95ff548` |
| ObsidianDragon-2.0.0-Windows-x64.zip | `ce9d067f36c5296288a473d3c2cceca7fcf807672a93a2084afc53c3b6e780d9` |
| ObsidianDragon-2.0.0-macOS-universal.dmg | `8a01b6c0c2b5bebd64a222ba4a5ad04a1b8851138f28458b5c7f767fbb65db66` |
| ObsidianDragon-2.0.0-macOS-universal.app.zip | `1bce1e5be15d9a2f3c6f42c95b23a0e89df142a73d65895252e4d8f50d19f541` |
## System Requirements
- **Linux:** x86-64, glibc 2.31+ (Ubuntu 20.04+, Fedora 33+, etc.)
- **Windows:** x86-64, Windows 10 or later
- **macOS:** Intel & Apple Silicon (universal binary), macOS 10.15 Catalina or later
## License
Released under the **GPLv3**. See [LICENSE](https://git.dragonx.is/DragonX/ObsidianDragon/src/branch/master/LICENSE) for details.

17
docs/ui-static-state.md Normal file
View File

@@ -0,0 +1,17 @@
# UI Static State Policy
The wallet currently runs one `App` instance inside one ImGui context. Some UI state is still file-static because it represents process-wide tab or modal state that must persist across frames. Phase 9 reviewed these remaining cases and treats them as intentional until the owning view is next refactored.
## Intentional Process-Wide State
- Modal payload state in `src/ui/windows/*_dialog*` files owns one open dialog instance at a time. These statics are acceptable for singleton dialogs, but new async workflow state should live in a service or App-owned controller.
- Tab-local form, filter, and selection state in views such as Send, Receive, Market, Mining, Peers, Transactions, and Explorer persists user input across frames. When those views are touched for behavior changes, prefer an explicit `*TabState` struct over adding more independent statics.
- Material/theme/effect singletons hold process-wide rendering caches or current theme state. They should remain resettable only through their owning theme/effect APIs.
- Compatibility glue remains where external call sites still rely on legacy names: `App::setCurrentTab()`/`getCurrentTab()`, `WalletState` balance aliases, layout `k*` accessors, and icon helper wrappers. Remove these only after all call sites have moved to the newer names.
## Rules For New UI Code
- Add new mutable UI state to an existing explicit state struct when one exists.
- Use file-static state only for singleton UI surfaces that cannot have multiple instances in the current app model.
- Keep long-running workflow progress outside render functions and expose it through services or App-owned controllers.
- Document any new compatibility wrapper with the call sites it protects and delete it when the migration is complete.

View File

@@ -67,8 +67,7 @@
//#define IMGUI_USE_LEGACY_CRC32_ADLER
//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...)
//---- Enabled so chat can render emoji (U+1F300+, above the BMP) — see Typography::loadFont emoji merge (Q12).
#define IMGUI_USE_WCHAR32
//#define IMGUI_USE_WCHAR32
//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version
// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files.

View File

@@ -1,744 +0,0 @@
// dear imgui: FreeType font builder (used as a replacement for the stb_truetype builder)
// (code)
// Get the latest version at https://github.com/ocornut/imgui/tree/master/misc/freetype
// Original code by @vuhdo (Aleksei Skriabin) in 2017, with improvements by @mikesart.
// Maintained since 2019 by @ocornut.
// CHANGELOG
// (minor and older changes stripped away, please see git history for details)
// 2025/06/11: refactored for the new ImFontLoader architecture, and ImGuiBackendFlags_RendererHasTextures support.
// 2024/10/17: added plutosvg support for SVG Fonts (seems faster/better than lunasvg). Enable by using '#define IMGUI_ENABLE_FREETYPE_PLUTOSVG'. (#7927)
// 2023/11/13: added support for ImFontConfig::RasterizationDensity field for scaling render density without scaling metrics.
// 2023/08/01: added support for SVG fonts, enable by using '#define IMGUI_ENABLE_FREETYPE_LUNASVG'. (#6591)
// 2023/01/04: fixed a packing issue which in some occurrences would prevent large amount of glyphs from being packed correctly.
// 2021/08/23: fixed crash when FT_Render_Glyph() fails to render a glyph and returns nullptr.
// 2021/03/05: added ImGuiFreeTypeBuilderFlags_Bitmap to load bitmap glyphs.
// 2021/03/02: set 'atlas->TexPixelsUseColors = true' to help some backends with deciding of a preferred texture format.
// 2021/01/28: added support for color-layered glyphs via ImGuiFreeTypeBuilderFlags_LoadColor (require Freetype 2.10+).
// 2021/01/26: simplified integration by using '#define IMGUI_ENABLE_FREETYPE'. renamed ImGuiFreeType::XXX flags to ImGuiFreeTypeBuilderFlags_XXX for consistency with other API. removed ImGuiFreeType::BuildFontAtlas().
// 2020/06/04: fix for rare case where FT_Get_Char_Index() succeed but FT_Load_Glyph() fails.
// 2019/02/09: added RasterizerFlags::Monochrome flag to disable font anti-aliasing (combine with ::MonoHinting for best results!)
// 2019/01/15: added support for imgui allocators + added FreeType only override function SetAllocatorFunctions().
// 2019/01/10: re-factored to match big update in STB builder. fixed texture height waste. fixed redundant glyphs when merging. support for glyph padding.
// 2018/06/08: added support for ImFontConfig::GlyphMinAdvanceX, GlyphMaxAdvanceX.
// 2018/02/04: moved to main imgui repository (away from http://www.github.com/ocornut/imgui_club)
// 2018/01/22: fix for addition of ImFontAtlas::TexUvscale member.
// 2017/10/22: minor inconsequential change to match change in master (removed an unnecessary statement).
// 2017/09/26: fixes for imgui internal changes.
// 2017/08/26: cleanup, optimizations, support for ImFontConfig::RasterizerFlags, ImFontConfig::RasterizerMultiply.
// 2017/08/16: imported from https://github.com/Vuhdo/imgui_freetype into http://www.github.com/ocornut/imgui_club, updated for latest changes in ImFontAtlas, minor tweaks.
// About Gamma Correct Blending:
// - FreeType assumes blending in linear space rather than gamma space.
// - See https://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#FT_Render_Glyph
// - For correct results you need to be using sRGB and convert to linear space in the pixel shader output.
// - The default dear imgui styles will be impacted by this change (alpha values will need tweaking).
// FIXME: cfg.OversampleH, OversampleV are not supported, but generally not necessary with this rasterizer because Hinting makes everything look better.
#include "imgui.h"
#ifndef IMGUI_DISABLE
#include "imgui_freetype.h"
#include "imgui_internal.h" // ImMin,ImMax,ImFontAtlasBuild*,
#include <stdint.h>
#include <ft2build.h>
#include FT_FREETYPE_H // <freetype/freetype.h>
#include FT_MODULE_H // <freetype/ftmodapi.h>
#include FT_GLYPH_H // <freetype/ftglyph.h>
#include FT_SIZES_H // <freetype/ftsizes.h>
#include FT_SYNTHESIS_H // <freetype/ftsynth.h>
// Handle LunaSVG and PlutoSVG
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) && defined(IMGUI_ENABLE_FREETYPE_PLUTOSVG)
#error "Cannot enable both IMGUI_ENABLE_FREETYPE_LUNASVG and IMGUI_ENABLE_FREETYPE_PLUTOSVG"
#endif
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
#include FT_OTSVG_H // <freetype/otsvg.h>
#include FT_BBOX_H // <freetype/ftbbox.h>
#include <lunasvg.h>
#endif
#ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG
#include <plutosvg.h>
#endif
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) || defined (IMGUI_ENABLE_FREETYPE_PLUTOSVG)
#if !((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12))
#error IMGUI_ENABLE_FREETYPE_PLUTOSVG or IMGUI_ENABLE_FREETYPE_LUNASVG requires FreeType version >= 2.12
#endif
#endif
#ifdef _MSC_VER
#pragma warning (push)
#pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff)
#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3).
#endif
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used
#ifndef __clang__
#pragma GCC diagnostic ignored "-Wsubobject-linkage" // warning: 'xxxx' has a field 'xxxx' whose type uses the anonymous namespace
#endif
#endif
//-------------------------------------------------------------------------
// Data
//-------------------------------------------------------------------------
// Default memory allocators
static void* ImGuiFreeTypeDefaultAllocFunc(size_t size, void* user_data) { IM_UNUSED(user_data); return IM_ALLOC(size); }
static void ImGuiFreeTypeDefaultFreeFunc(void* ptr, void* user_data) { IM_UNUSED(user_data); IM_FREE(ptr); }
// Current memory allocators
static void* (*GImGuiFreeTypeAllocFunc)(size_t size, void* user_data) = ImGuiFreeTypeDefaultAllocFunc;
static void (*GImGuiFreeTypeFreeFunc)(void* ptr, void* user_data) = ImGuiFreeTypeDefaultFreeFunc;
static void* GImGuiFreeTypeAllocatorUserData = nullptr;
// Lunasvg support
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
static FT_Error ImGuiLunasvgPortInit(FT_Pointer* state);
static void ImGuiLunasvgPortFree(FT_Pointer* state);
static FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state);
static FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state);
#endif
//-------------------------------------------------------------------------
// Code
//-------------------------------------------------------------------------
#define FT_CEIL(X) (((X + 63) & -64) / 64) // From SDL_ttf: Handy routines for converting from fixed point
#define FT_SCALEFACTOR 64.0f
// Glyph metrics:
// --------------
//
// xmin xmax
// | |
// |<-------- width -------->|
// | |
// | +-------------------------+----------------- ymax
// | | ggggggggg ggggg | ^ ^
// | | g:::::::::ggg::::g | | |
// | | g:::::::::::::::::g | | |
// | | g::::::ggggg::::::gg | | |
// | | g:::::g g:::::g | | |
// offsetX -|-------->| g:::::g g:::::g | offsetY |
// | | g:::::g g:::::g | | |
// | | g::::::g g:::::g | | |
// | | g:::::::ggggg:::::g | | |
// | | g::::::::::::::::g | | height
// | | gg::::::::::::::g | | |
// baseline ---*---------|---- gggggggg::::::g-----*-------- |
// / | | g:::::g | |
// origin | | gggggg g:::::g | |
// | | g:::::gg gg:::::g | |
// | | g::::::ggg:::::::g | |
// | | gg:::::::::::::g | |
// | | ggg::::::ggg | |
// | | gggggg | v
// | +-------------------------+----------------- ymin
// | |
// |------------- advanceX ----------->|
// Stored in ImFontAtlas::FontLoaderData. ALLOCATED BY US.
struct ImGui_ImplFreeType_Data
{
FT_Library Library;
FT_MemoryRec_ MemoryManager;
ImGui_ImplFreeType_Data() { memset((void*)this, 0, sizeof(*this)); }
};
// Stored in ImFontConfig::FontLoaderData. ALLOCATED BY US.
struct ImGui_ImplFreeType_FontSrcData
{
// Initialize from an external data buffer. Doesn't copy data, and you must ensure it stays valid up to this object lifetime.
bool InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_user_flags);
void CloseFont();
ImGui_ImplFreeType_FontSrcData() { memset((void*)this, 0, sizeof(*this)); }
~ImGui_ImplFreeType_FontSrcData() { CloseFont(); }
// Members
FT_Face FtFace;
ImGuiFreeTypeLoaderFlags UserFlags; // = ImFontConfig::FontLoaderFlags
FT_Int32 LoadFlags;
ImFontBaked* BakedLastActivated;
};
// Stored in ImFontBaked::FontLoaderDatas: pointer to SourcesCount instances of this. ALLOCATED BY CORE.
struct ImGui_ImplFreeType_FontSrcBakedData
{
FT_Size FtSize; // This represent a FT_Face with a given size.
ImGui_ImplFreeType_FontSrcBakedData() { memset((void*)this, 0, sizeof(*this)); }
};
bool ImGui_ImplFreeType_FontSrcData::InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_font_loader_flags)
{
FT_Error error = FT_New_Memory_Face(ft_library, (const FT_Byte*)src->FontData, (FT_Long)src->FontDataSize, (FT_Long)src->FontNo, &FtFace);
if (error != 0)
return false;
error = FT_Select_Charmap(FtFace, FT_ENCODING_UNICODE);
if (error != 0)
return false;
// Convert to FreeType flags (NB: Bold and Oblique are processed separately)
UserFlags = (ImGuiFreeTypeLoaderFlags)(src->FontLoaderFlags | extra_font_loader_flags);
LoadFlags = 0;
if ((UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) == 0)
LoadFlags |= FT_LOAD_NO_BITMAP;
if (UserFlags & ImGuiFreeTypeLoaderFlags_NoHinting)
LoadFlags |= FT_LOAD_NO_HINTING;
if (UserFlags & ImGuiFreeTypeLoaderFlags_NoAutoHint)
LoadFlags |= FT_LOAD_NO_AUTOHINT;
if (UserFlags & ImGuiFreeTypeLoaderFlags_ForceAutoHint)
LoadFlags |= FT_LOAD_FORCE_AUTOHINT;
if (UserFlags & ImGuiFreeTypeLoaderFlags_LightHinting)
LoadFlags |= FT_LOAD_TARGET_LIGHT;
else if (UserFlags & ImGuiFreeTypeLoaderFlags_MonoHinting)
LoadFlags |= FT_LOAD_TARGET_MONO;
else
LoadFlags |= FT_LOAD_TARGET_NORMAL;
if (UserFlags & ImGuiFreeTypeLoaderFlags_LoadColor)
LoadFlags |= FT_LOAD_COLOR;
return true;
}
void ImGui_ImplFreeType_FontSrcData::CloseFont()
{
if (FtFace)
{
FT_Done_Face(FtFace);
FtFace = nullptr;
}
}
static const FT_Glyph_Metrics* ImGui_ImplFreeType_LoadGlyph(ImGui_ImplFreeType_FontSrcData* src_data, uint32_t codepoint)
{
uint32_t glyph_index = FT_Get_Char_Index(src_data->FtFace, codepoint);
if (glyph_index == 0)
return nullptr;
// If this crash for you: FreeType 2.11.0 has a crash bug on some bitmap/colored fonts.
// - https://gitlab.freedesktop.org/freetype/freetype/-/issues/1076
// - https://github.com/ocornut/imgui/issues/4567
// - https://github.com/ocornut/imgui/issues/4566
// You can use FreeType 2.10, or the patched version of 2.11.0 in VcPkg, or probably any upcoming FreeType version.
FT_Error error = FT_Load_Glyph(src_data->FtFace, glyph_index, src_data->LoadFlags);
if (error)
return nullptr;
// Need an outline for this to work
FT_GlyphSlot slot = src_data->FtFace->glyph;
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) || defined(IMGUI_ENABLE_FREETYPE_PLUTOSVG)
IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP || slot->format == FT_GLYPH_FORMAT_SVG);
#else
#if ((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12))
IM_ASSERT(slot->format != FT_GLYPH_FORMAT_SVG && "The font contains SVG glyphs, you'll need to enable IMGUI_ENABLE_FREETYPE_PLUTOSVG or IMGUI_ENABLE_FREETYPE_LUNASVG in imconfig.h and install required libraries in order to use this font");
#endif
IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP);
#endif // IMGUI_ENABLE_FREETYPE_LUNASVG
// Apply convenience transform (this is not picking from real "Bold"/"Italic" fonts! Merely applying FreeType helper transform. Oblique == Slanting)
if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bold)
FT_GlyphSlot_Embolden(slot);
if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Oblique)
{
FT_GlyphSlot_Oblique(slot);
//FT_BBox bbox;
//FT_Outline_Get_BBox(&slot->outline, &bbox);
//slot->metrics.width = bbox.xMax - bbox.xMin;
//slot->metrics.height = bbox.yMax - bbox.yMin;
}
return &slot->metrics;
}
static void ImGui_ImplFreeType_BlitGlyph(const FT_Bitmap* ft_bitmap, uint32_t* dst, uint32_t dst_pitch)
{
IM_ASSERT(ft_bitmap != nullptr);
const uint32_t w = ft_bitmap->width;
const uint32_t h = ft_bitmap->rows;
const uint8_t* src = ft_bitmap->buffer;
const uint32_t src_pitch = ft_bitmap->pitch;
switch (ft_bitmap->pixel_mode)
{
case FT_PIXEL_MODE_GRAY: // Grayscale image, 1 byte per pixel.
{
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
for (uint32_t x = 0; x < w; x++)
dst[x] = IM_COL32(255, 255, 255, src[x]);
break;
}
case FT_PIXEL_MODE_MONO: // Monochrome image, 1 bit per pixel. The bits in each byte are ordered from MSB to LSB.
{
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
{
uint8_t bits = 0;
const uint8_t* bits_ptr = src;
for (uint32_t x = 0; x < w; x++, bits <<= 1)
{
if ((x & 7) == 0)
bits = *bits_ptr++;
dst[x] = IM_COL32(255, 255, 255, (bits & 0x80) ? 255 : 0);
}
}
break;
}
case FT_PIXEL_MODE_BGRA:
{
// FIXME: Converting pre-multiplied alpha to straight. Doesn't smell good.
#define DE_MULTIPLY(color, alpha) ImMin((ImU32)(255.0f * (float)color / (float)(alpha + FLT_MIN) + 0.5f), 255u)
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
for (uint32_t x = 0; x < w; x++)
{
uint8_t r = src[x * 4 + 2], g = src[x * 4 + 1], b = src[x * 4], a = src[x * 4 + 3];
dst[x] = IM_COL32(DE_MULTIPLY(r, a), DE_MULTIPLY(g, a), DE_MULTIPLY(b, a), a);
}
#undef DE_MULTIPLY
break;
}
default:
IM_ASSERT(0 && "FreeTypeFont::BlitGlyph(): Unknown bitmap pixel mode!");
}
}
// FreeType memory allocation callbacks
static void* FreeType_Alloc(FT_Memory /*memory*/, long size)
{
return GImGuiFreeTypeAllocFunc((size_t)size, GImGuiFreeTypeAllocatorUserData);
}
static void FreeType_Free(FT_Memory /*memory*/, void* block)
{
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
}
static void* FreeType_Realloc(FT_Memory /*memory*/, long cur_size, long new_size, void* block)
{
// Implement realloc() as we don't ask user to provide it.
if (block == nullptr)
return GImGuiFreeTypeAllocFunc((size_t)new_size, GImGuiFreeTypeAllocatorUserData);
if (new_size == 0)
{
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
return nullptr;
}
if (new_size > cur_size)
{
void* new_block = GImGuiFreeTypeAllocFunc((size_t)new_size, GImGuiFreeTypeAllocatorUserData);
memcpy(new_block, block, (size_t)cur_size);
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
return new_block;
}
return block;
}
static bool ImGui_ImplFreeType_LoaderInit(ImFontAtlas* atlas)
{
IM_ASSERT(atlas->FontLoaderData == nullptr);
ImGui_ImplFreeType_Data* bd = IM_NEW(ImGui_ImplFreeType_Data)();
// FreeType memory management: https://www.freetype.org/freetype2/docs/design/design-4.html
bd->MemoryManager.user = nullptr;
bd->MemoryManager.alloc = &FreeType_Alloc;
bd->MemoryManager.free = &FreeType_Free;
bd->MemoryManager.realloc = &FreeType_Realloc;
// https://www.freetype.org/freetype2/docs/reference/ft2-module_management.html#FT_New_Library
FT_Error error = FT_New_Library(&bd->MemoryManager, &bd->Library);
if (error != 0)
{
IM_DELETE(bd);
return false;
}
// If you don't call FT_Add_Default_Modules() the rest of code may work, but FreeType won't use our custom allocator.
FT_Add_Default_Modules(bd->Library);
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
// Install svg hooks for FreeType
// https://freetype.org/freetype2/docs/reference/ft2-properties.html#svg-hooks
// https://freetype.org/freetype2/docs/reference/ft2-svg_fonts.html#svg_fonts
SVG_RendererHooks hooks = { ImGuiLunasvgPortInit, ImGuiLunasvgPortFree, ImGuiLunasvgPortRender, ImGuiLunasvgPortPresetSlot };
FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", &hooks);
#endif // IMGUI_ENABLE_FREETYPE_LUNASVG
#ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG
// With plutosvg, use provided hooks
FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", plutosvg_ft_svg_hooks());
#endif // IMGUI_ENABLE_FREETYPE_PLUTOSVG
// Store our data
atlas->FontLoaderData = (void*)bd;
return true;
}
static void ImGui_ImplFreeType_LoaderShutdown(ImFontAtlas* atlas)
{
ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData;
IM_ASSERT(bd != nullptr);
FT_Done_Library(bd->Library);
IM_DELETE(bd);
atlas->FontLoaderData = nullptr;
}
static bool ImGui_ImplFreeType_FontSrcInit(ImFontAtlas* atlas, ImFontConfig* src)
{
ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData;
ImGui_ImplFreeType_FontSrcData* bd_font_data = IM_NEW(ImGui_ImplFreeType_FontSrcData);
IM_ASSERT(src->FontLoaderData == nullptr);
src->FontLoaderData = bd_font_data;
if (!bd_font_data->InitFont(bd->Library, src, (ImGuiFreeTypeLoaderFlags)atlas->FontLoaderFlags))
{
IM_DELETE(bd_font_data);
src->FontLoaderData = nullptr;
return false;
}
return true;
}
static void ImGui_ImplFreeType_FontSrcDestroy(ImFontAtlas* atlas, ImFontConfig* src)
{
IM_UNUSED(atlas);
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
IM_DELETE(bd_font_data);
src->FontLoaderData = nullptr;
}
static bool ImGui_ImplFreeType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src)
{
IM_UNUSED(atlas);
float size = baked->Size;
if (src->MergeMode && src->SizePixels != 0.0f)
size *= (src->SizePixels / baked->OwnerFont->Sources[0]->SizePixels);
size *= src->ExtraSizeScale;
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
bd_font_data->BakedLastActivated = baked;
// We use one FT_Size per (source + baked) combination.
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
IM_ASSERT(bd_baked_data != nullptr);
IM_PLACEMENT_NEW(bd_baked_data) ImGui_ImplFreeType_FontSrcBakedData();
FT_New_Size(bd_font_data->FtFace, &bd_baked_data->FtSize);
FT_Activate_Size(bd_baked_data->FtSize);
// Vuhdo 2017: "I'm not sure how to deal with font sizes properly. As far as I understand, currently ImGui assumes that the 'pixel_height'
// is a maximum height of an any given glyph, i.e. it's the sum of font's ascender and descender. Seems strange to me.
// FT_Set_Pixel_Sizes() doesn't seem to get us the same result."
// (FT_Set_Pixel_Sizes() essentially calls FT_Request_Size() with FT_SIZE_REQUEST_TYPE_NOMINAL)
const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
FT_Size_RequestRec req;
req.type = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) ? FT_SIZE_REQUEST_TYPE_NOMINAL : FT_SIZE_REQUEST_TYPE_REAL_DIM;
req.width = 0;
req.height = (uint32_t)(size * 64 * rasterizer_density);
req.horiResolution = 0;
req.vertResolution = 0;
FT_Request_Size(bd_font_data->FtFace, &req);
// Output
if (src->MergeMode == false)
{
// Read metrics
FT_Size_Metrics metrics = bd_baked_data->FtSize->metrics;
const float scale = 1.0f / (rasterizer_density * src->ExtraSizeScale);
baked->Ascent = (float)FT_CEIL(metrics.ascender) * scale; // The pixel extents above the baseline in pixels (typically positive).
baked->Descent = (float)FT_CEIL(metrics.descender) * scale; // The extents below the baseline in pixels (typically negative).
//LineSpacing = (float)FT_CEIL(metrics.height) * scale; // The baseline-to-baseline distance. Note that it usually is larger than the sum of the ascender and descender taken as absolute values. There is also no guarantee that no glyphs extend above or below subsequent baselines when using this distance. Think of it as a value the designer of the font finds appropriate.
//LineGap = (float)FT_CEIL(metrics.height - metrics.ascender + metrics.descender) * scale; // The spacing in pixels between one row's descent and the next row's ascent.
//MaxAdvanceWidth = (float)FT_CEIL(metrics.max_advance) * scale; // This field gives the maximum horizontal cursor advance for all glyphs in the font.
}
return true;
}
static void ImGui_ImplFreeType_FontBakedDestroy(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src)
{
IM_UNUSED(atlas);
IM_UNUSED(baked);
IM_UNUSED(src);
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
IM_ASSERT(bd_baked_data != nullptr);
FT_Done_Size(bd_baked_data->FtSize);
bd_baked_data->~ImGui_ImplFreeType_FontSrcBakedData(); // ~IM_PLACEMENT_DELETE()
}
static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x)
{
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
uint32_t glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint);
if (glyph_index == 0)
return false;
if (bd_font_data->BakedLastActivated != baked) // <-- could use id
{
// Activate current size
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
FT_Activate_Size(bd_baked_data->FtSize);
bd_font_data->BakedLastActivated = baked;
}
const FT_Glyph_Metrics* metrics = ImGui_ImplFreeType_LoadGlyph(bd_font_data, codepoint);
if (metrics == nullptr)
return false;
FT_Face face = bd_font_data->FtFace;
FT_GlyphSlot slot = face->glyph;
const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
// Load metrics only mode
const float advance_x = (slot->advance.x / FT_SCALEFACTOR) / rasterizer_density;
if (out_advance_x != NULL)
{
IM_ASSERT(out_glyph == NULL);
*out_advance_x = advance_x;
return true;
}
// Render glyph into a bitmap (currently held by FreeType)
FT_Render_Mode render_mode = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Monochrome) ? FT_RENDER_MODE_MONO : FT_RENDER_MODE_NORMAL;
FT_Error error = FT_Render_Glyph(slot, render_mode);
const FT_Bitmap* ft_bitmap = &slot->bitmap;
if (error != 0 || ft_bitmap == nullptr)
return false;
const int w = (int)ft_bitmap->width;
const int h = (int)ft_bitmap->rows;
const bool is_visible = (w != 0 && h != 0);
// Prepare glyph
out_glyph->Codepoint = codepoint;
out_glyph->AdvanceX = advance_x;
// Pack and retrieve position inside texture atlas
if (is_visible)
{
ImFontAtlasRectId pack_id = ImFontAtlasPackAddRect(atlas, w, h);
if (pack_id == ImFontAtlasRectId_Invalid)
{
// Pathological out of memory case (TexMaxWidth/TexMaxHeight set too small?)
IM_ASSERT(pack_id != ImFontAtlasRectId_Invalid && "Out of texture memory.");
return false;
}
ImTextureRect* r = ImFontAtlasPackGetRect(atlas, pack_id);
// Render pixels to our temporary buffer
atlas->Builder->TempBuffer.resize(w * h * 4);
uint32_t* temp_buffer = (uint32_t*)atlas->Builder->TempBuffer.Data;
ImGui_ImplFreeType_BlitGlyph(ft_bitmap, temp_buffer, w);
const float ref_size = baked->OwnerFont->Sources[0]->SizePixels;
const float offsets_scale = (ref_size != 0.0f) ? (baked->Size / ref_size) : 1.0f;
float font_off_x = ImFloor(src->GlyphOffset.x * offsets_scale + 0.5f); // Snap scaled offset.
float font_off_y = ImFloor(src->GlyphOffset.y * offsets_scale + 0.5f) + baked->Ascent;
float recip_h = 1.0f / rasterizer_density;
float recip_v = 1.0f / rasterizer_density;
// Register glyph
float glyph_off_x = (float)face->glyph->bitmap_left;
float glyph_off_y = (float)-face->glyph->bitmap_top;
out_glyph->X0 = glyph_off_x * recip_h + font_off_x;
out_glyph->Y0 = glyph_off_y * recip_v + font_off_y;
out_glyph->X1 = (glyph_off_x + w) * recip_h + font_off_x;
out_glyph->Y1 = (glyph_off_y + h) * recip_v + font_off_y;
out_glyph->Visible = true;
out_glyph->Colored = (ft_bitmap->pixel_mode == FT_PIXEL_MODE_BGRA);
out_glyph->PackId = pack_id;
ImFontAtlasBakedSetFontGlyphBitmap(atlas, baked, src, out_glyph, r, (const unsigned char*)temp_buffer, ImTextureFormat_RGBA32, w * 4);
}
return true;
}
static bool ImGui_ImplFreetype_FontSrcContainsGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImWchar codepoint)
{
IM_UNUSED(atlas);
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
int glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint);
return glyph_index != 0;
}
const ImFontLoader* ImGuiFreeType::GetFontLoader()
{
static ImFontLoader loader;
loader.Name = "FreeType";
loader.LoaderInit = ImGui_ImplFreeType_LoaderInit;
loader.LoaderShutdown = ImGui_ImplFreeType_LoaderShutdown;
loader.FontSrcInit = ImGui_ImplFreeType_FontSrcInit;
loader.FontSrcDestroy = ImGui_ImplFreeType_FontSrcDestroy;
loader.FontSrcContainsGlyph = ImGui_ImplFreetype_FontSrcContainsGlyph;
loader.FontBakedInit = ImGui_ImplFreeType_FontBakedInit;
loader.FontBakedDestroy = ImGui_ImplFreeType_FontBakedDestroy;
loader.FontBakedLoadGlyph = ImGui_ImplFreeType_FontBakedLoadGlyph;
loader.FontBakedSrcLoaderDataSize = sizeof(ImGui_ImplFreeType_FontSrcBakedData);
return &loader;
}
void ImGuiFreeType::SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data)
{
GImGuiFreeTypeAllocFunc = alloc_func;
GImGuiFreeTypeFreeFunc = free_func;
GImGuiFreeTypeAllocatorUserData = user_data;
}
bool ImGuiFreeType::DebugEditFontLoaderFlags(unsigned int* p_font_loader_flags)
{
bool edited = false;
edited |= ImGui::CheckboxFlags("NoHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_NoHinting);
edited |= ImGui::CheckboxFlags("NoAutoHint", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_NoAutoHint);
edited |= ImGui::CheckboxFlags("ForceAutoHint",p_font_loader_flags, ImGuiFreeTypeLoaderFlags_ForceAutoHint);
edited |= ImGui::CheckboxFlags("LightHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_LightHinting);
edited |= ImGui::CheckboxFlags("MonoHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_MonoHinting);
edited |= ImGui::CheckboxFlags("Bold", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Bold);
edited |= ImGui::CheckboxFlags("Oblique", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Oblique);
edited |= ImGui::CheckboxFlags("Monochrome", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Monochrome);
edited |= ImGui::CheckboxFlags("LoadColor", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_LoadColor);
edited |= ImGui::CheckboxFlags("Bitmap", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Bitmap);
return edited;
}
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
// For more details, see https://gitlab.freedesktop.org/freetype/freetype-demos/-/blob/master/src/rsvg-port.c
// The original code from the demo is licensed under CeCILL-C Free Software License Agreement (https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/LICENSE.TXT)
struct LunasvgPortState
{
FT_Error err = FT_Err_Ok;
lunasvg::Matrix matrix;
std::unique_ptr<lunasvg::Document> svg = nullptr;
};
static FT_Error ImGuiLunasvgPortInit(FT_Pointer* _state)
{
*_state = IM_NEW(LunasvgPortState)();
return FT_Err_Ok;
}
static void ImGuiLunasvgPortFree(FT_Pointer* _state)
{
IM_DELETE(*(LunasvgPortState**)_state);
}
static FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state)
{
LunasvgPortState* state = *(LunasvgPortState**)_state;
// If there was an error while loading the svg in ImGuiLunasvgPortPresetSlot(), the renderer hook still get called, so just returns the error.
if (state->err != FT_Err_Ok)
return state->err;
// rows is height, pitch (or stride) equals to width * sizeof(int32)
lunasvg::Bitmap bitmap((uint8_t*)slot->bitmap.buffer, slot->bitmap.width, slot->bitmap.rows, slot->bitmap.pitch);
#if LUNASVG_VERSION_MAJOR >= 3
state->svg->render(bitmap, state->matrix); // state->matrix is already scaled and translated
#else
state->svg->setMatrix(state->svg->matrix().identity()); // Reset the svg matrix to the default value
state->svg->render(bitmap, state->matrix); // state->matrix is already scaled and translated
#endif
state->err = FT_Err_Ok;
return state->err;
}
static FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state)
{
FT_SVG_Document document = (FT_SVG_Document)slot->other;
LunasvgPortState* state = *(LunasvgPortState**)_state;
FT_Size_Metrics& metrics = document->metrics;
// This function is called twice, once in the FT_Load_Glyph() and another right before ImGuiLunasvgPortRender().
// If it's the latter, don't do anything because it's // already done in the former.
if (cache)
return state->err;
state->svg = lunasvg::Document::loadFromData((const char*)document->svg_document, document->svg_document_length);
if (state->svg == nullptr)
{
state->err = FT_Err_Invalid_SVG_Document;
return state->err;
}
#if LUNASVG_VERSION_MAJOR >= 3
lunasvg::Box box = state->svg->boundingBox();
#else
lunasvg::Box box = state->svg->box();
#endif
double scale = std::min(metrics.x_ppem / box.w, metrics.y_ppem / box.h);
double xx = (double)document->transform.xx / (1 << 16);
double xy = -(double)document->transform.xy / (1 << 16);
double yx = -(double)document->transform.yx / (1 << 16);
double yy = (double)document->transform.yy / (1 << 16);
double x0 = (double)document->delta.x / 64 * box.w / metrics.x_ppem;
double y0 = -(double)document->delta.y / 64 * box.h / metrics.y_ppem;
#if LUNASVG_VERSION_MAJOR >= 3
// Scale, transform and pre-translate the matrix for the rendering step
state->matrix = lunasvg::Matrix::translated(-box.x, -box.y);
state->matrix.multiply(lunasvg::Matrix(xx, xy, yx, yy, x0, y0));
state->matrix.scale(scale, scale);
// Apply updated transformation to the bounding box
box.transform(state->matrix);
#else
// Scale and transform, we don't translate the svg yet
state->matrix.identity();
state->matrix.scale(scale, scale);
state->matrix.transform(xx, xy, yx, yy, x0, y0);
state->svg->setMatrix(state->matrix);
// Pre-translate the matrix for the rendering step
state->matrix.translate(-box.x, -box.y);
// Get the box again after the transformation
box = state->svg->box();
#endif
// Calculate the bitmap size
slot->bitmap_left = FT_Int(box.x);
slot->bitmap_top = FT_Int(-box.y);
slot->bitmap.rows = (unsigned int)(ImCeil((float)box.h));
slot->bitmap.width = (unsigned int)(ImCeil((float)box.w));
slot->bitmap.pitch = slot->bitmap.width * 4;
slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA;
// Compute all the bearings and set them correctly. The outline is scaled already, we just need to use the bounding box.
double metrics_width = box.w;
double metrics_height = box.h;
double horiBearingX = box.x;
double horiBearingY = -box.y;
double vertBearingX = slot->metrics.horiBearingX / 64.0 - slot->metrics.horiAdvance / 64.0 / 2.0;
double vertBearingY = (slot->metrics.vertAdvance / 64.0 - slot->metrics.height / 64.0) / 2.0;
slot->metrics.width = FT_Pos(IM_ROUND(metrics_width * 64.0)); // Using IM_ROUND() assume width and height are positive
slot->metrics.height = FT_Pos(IM_ROUND(metrics_height * 64.0));
slot->metrics.horiBearingX = FT_Pos(horiBearingX * 64);
slot->metrics.horiBearingY = FT_Pos(horiBearingY * 64);
slot->metrics.vertBearingX = FT_Pos(vertBearingX * 64);
slot->metrics.vertBearingY = FT_Pos(vertBearingY * 64);
if (slot->metrics.vertAdvance == 0)
slot->metrics.vertAdvance = FT_Pos(metrics_height * 1.2 * 64.0);
state->err = FT_Err_Ok;
return state->err;
}
#endif // #ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
//-----------------------------------------------------------------------------
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
#ifdef _MSC_VER
#pragma warning (pop)
#endif
#endif // #ifndef IMGUI_DISABLE

View File

@@ -1,83 +0,0 @@
// dear imgui: FreeType font builder (used as a replacement for the stb_truetype builder)
// (headers)
#pragma once
#include "imgui.h" // IMGUI_API
#ifndef IMGUI_DISABLE
// Usage:
// - Add '#define IMGUI_ENABLE_FREETYPE' in your imconfig to automatically enable support
// for imgui_freetype in imgui. It is equivalent to selecting the default loader with:
// io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader())
// Optional support for OpenType SVG fonts:
// - Add '#define IMGUI_ENABLE_FREETYPE_PLUTOSVG' to use plutosvg (not provided). See #7927.
// - Add '#define IMGUI_ENABLE_FREETYPE_LUNASVG' to use lunasvg (not provided). See #6591.
// Forward declarations
struct ImFontAtlas;
struct ImFontLoader;
// Hinting greatly impacts visuals (and glyph sizes).
// - By default, hinting is enabled and the font's native hinter is preferred over the auto-hinter.
// - When disabled, FreeType generates blurrier glyphs, more or less matches the stb_truetype.h
// - The Default hinting mode usually looks good, but may distort glyphs in an unusual way.
// - The Light hinting mode generates fuzzier glyphs but better matches Microsoft's rasterizer.
// You can set those flags globally in ImFontAtlas::FontLoaderFlags
// You can set those flags on a per font basis in ImFontConfig::FontLoaderFlags
typedef unsigned int ImGuiFreeTypeLoaderFlags;
enum ImGuiFreeTypeLoaderFlags_
{
ImGuiFreeTypeLoaderFlags_NoHinting = 1 << 0, // Disable hinting. This generally generates 'blurrier' bitmap glyphs when the glyph are rendered in any of the anti-aliased modes.
ImGuiFreeTypeLoaderFlags_NoAutoHint = 1 << 1, // Disable auto-hinter.
ImGuiFreeTypeLoaderFlags_ForceAutoHint = 1 << 2, // Indicates that the auto-hinter is preferred over the font's native hinter.
ImGuiFreeTypeLoaderFlags_LightHinting = 1 << 3, // A lighter hinting algorithm for gray-level modes. Many generated glyphs are fuzzier but better resemble their original shape. This is achieved by snapping glyphs to the pixel grid only vertically (Y-axis), as is done by Microsoft's ClearType and Adobe's proprietary font renderer. This preserves inter-glyph spacing in horizontal text.
ImGuiFreeTypeLoaderFlags_MonoHinting = 1 << 4, // Strong hinting algorithm that should only be used for monochrome output.
ImGuiFreeTypeLoaderFlags_Bold = 1 << 5, // Styling: Should we artificially embolden the font?
ImGuiFreeTypeLoaderFlags_Oblique = 1 << 6, // Styling: Should we slant the font, emulating italic style?
ImGuiFreeTypeLoaderFlags_Monochrome = 1 << 7, // Disable anti-aliasing. Combine this with MonoHinting for best results!
ImGuiFreeTypeLoaderFlags_LoadColor = 1 << 8, // Enable FreeType color-layered glyphs
ImGuiFreeTypeLoaderFlags_Bitmap = 1 << 9, // Enable FreeType bitmap glyphs
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
ImGuiFreeTypeBuilderFlags_NoHinting = ImGuiFreeTypeLoaderFlags_NoHinting,
ImGuiFreeTypeBuilderFlags_NoAutoHint = ImGuiFreeTypeLoaderFlags_NoAutoHint,
ImGuiFreeTypeBuilderFlags_ForceAutoHint = ImGuiFreeTypeLoaderFlags_ForceAutoHint,
ImGuiFreeTypeBuilderFlags_LightHinting = ImGuiFreeTypeLoaderFlags_LightHinting,
ImGuiFreeTypeBuilderFlags_MonoHinting = ImGuiFreeTypeLoaderFlags_MonoHinting,
ImGuiFreeTypeBuilderFlags_Bold = ImGuiFreeTypeLoaderFlags_Bold,
ImGuiFreeTypeBuilderFlags_Oblique = ImGuiFreeTypeLoaderFlags_Oblique,
ImGuiFreeTypeBuilderFlags_Monochrome = ImGuiFreeTypeLoaderFlags_Monochrome,
ImGuiFreeTypeBuilderFlags_LoadColor = ImGuiFreeTypeLoaderFlags_LoadColor,
ImGuiFreeTypeBuilderFlags_Bitmap = ImGuiFreeTypeLoaderFlags_Bitmap,
#endif
};
// Obsolete names (will be removed)
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
typedef ImGuiFreeTypeLoaderFlags_ ImGuiFreeTypeBuilderFlags_;
#endif
namespace ImGuiFreeType
{
// This is automatically assigned when using '#define IMGUI_ENABLE_FREETYPE'.
// If you need to dynamically select between multiple builders:
// - you can manually assign this builder with 'atlas->SetFontLoader(ImGuiFreeType::GetFontLoader())'
// - prefer deep-copying this into your own ImFontLoader instance if you use hot-reloading that messes up static data.
IMGUI_API const ImFontLoader* GetFontLoader();
// Override allocators. By default ImGuiFreeType will use IM_ALLOC()/IM_FREE()
// However, as FreeType does lots of allocations we provide a way for the user to redirect it to a separate memory heap if desired.
IMGUI_API void SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data = nullptr);
// Display UI to edit ImFontAtlas::FontLoaderFlags (shared) or ImFontConfig::FontLoaderFlags (single source)
IMGUI_API bool DebugEditFontLoaderFlags(ImGuiFreeTypeLoaderFlags* p_font_loader_flags);
// Obsolete names (will be removed)
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
//IMGUI_API const ImFontBuilderIO* GetBuilderForFreeType(); // Renamed/changed in 1.92. Change 'io.Fonts->FontBuilderIO = ImGuiFreeType::GetBuilderForFreeType()' to 'io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader())' if you need runtime selection.
//static inline bool BuildFontAtlas(ImFontAtlas* atlas, unsigned int flags = 0) { atlas->FontBuilderIO = GetBuilderForFreeType(); atlas->FontLoaderFlags = flags; return atlas->Build(); } // Prefer using '#define IMGUI_ENABLE_FREETYPE'
#endif
}
#endif // #ifndef IMGUI_DISABLE

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -5,7 +5,7 @@
<assemblyIdentity
type="win32"
name="DragonX.ObsidianDragon.Wallet"
version="@DRAGONX_APP_VERSION_MAJOR@.@DRAGONX_APP_VERSION_MINOR@.@DRAGONX_APP_VERSION_PATCH@.0"
version="@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0"
processorArchitecture="amd64"
/>

View File

@@ -19,8 +19,8 @@
#include <winver.h>
VS_VERSION_INFO VERSIONINFO
FILEVERSION @DRAGONX_APP_VERSION_MAJOR@,@DRAGONX_APP_VERSION_MINOR@,@DRAGONX_APP_VERSION_PATCH@,0
PRODUCTVERSION @DRAGONX_APP_VERSION_MAJOR@,@DRAGONX_APP_VERSION_MINOR@,@DRAGONX_APP_VERSION_PATCH@,0
FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS 0x0L
FILEOS VOS_NT_WINDOWS32
@@ -32,13 +32,13 @@ BEGIN
BLOCK "040904B0" // US-English, Unicode
BEGIN
VALUE "CompanyName", "DragonX Developers\0"
VALUE "FileDescription", "@DRAGONX_APP_NAME@ Wallet\0"
VALUE "FileVersion", "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@\0"
VALUE "InternalName", "@DRAGONX_APP_NAME@\0"
VALUE "FileDescription", "ObsidianDragon Wallet\0"
VALUE "FileVersion", "@PROJECT_VERSION@\0"
VALUE "InternalName", "ObsidianDragon\0"
VALUE "LegalCopyright", "Copyright 2024-2026 DragonX Developers. GPLv3.\0"
VALUE "OriginalFilename", "@DRAGONX_APP_NAME@.exe\0"
VALUE "ProductName", "@DRAGONX_APP_NAME@\0"
VALUE "ProductVersion", "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@\0"
VALUE "OriginalFilename", "ObsidianDragon.exe\0"
VALUE "ProductName", "ObsidianDragon\0"
VALUE "ProductVersion", "@PROJECT_VERSION@\0"
END
END
BLOCK "VarFileInfo"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<style>
.cls-1 {
fill: #fff;
}
.cls-2 {
fill: #d82652;
}
</style>
</defs>
<path class="cls-2" d="M103.98,128s-6.29-24.7-18.73-34.43c-8.53-8.03-15.63-16.49-21.25-24.16-5.62,7.68-12.72,16.17-21.25,24.16-12.4,9.74-18.73,34.43-18.73,34.43-2.38-24.34,7.85-35.82,12.87-41.29,7.82-8.5,15.31-16.56,21.75-25.02-7.64-11.44-11.41-19.72-11.41-19.72-.89-3.27-3.84-6.64-3.84-6.64,6.08-8.1-1.6-16.98-1.6-16.98,5.79-5.62,6.71-10.02,8.32-18.34-1.96,22.35,4.02,39.09,13.93,54.12,9.84-15.03,15.81-31.77,13.86-54.12,1.6,8.35,2.52,12.72,8.32,18.34,0,0-7.68,8.88-1.6,16.98,0,0-2.95,3.38-3.84,6.64,0,0-3.77,8.28-11.37,19.72,6.43,8.45,13.97,16.56,21.75,25.02,4.97,5.47,15.21,16.95,12.83,41.29h0Z"/>
<g>
<path class="cls-1" d="M55.33,61.62c-3.55,4.55-7.39,8.99-11.44,13.47-5.29-4.48-11.23-5.33-11.23-5.33,22.92-7.82,2.81-15.17.28-16.31C9.28,42.78,9.1,14.78,9.1,14.78c11.51,34.15,36.42,32.3,36.42,32.3.35-.21.67-.46.92-.71,1.64,3.27,4.58,8.67,8.88,15.24h0Z"/>
<g>
<path class="cls-1" d="M68.62,40.41c-1.35,2.98-2.91,5.83-4.62,8.63-1.71-2.81-3.23-5.69-4.62-8.63,1.74-3.45,4.62-20.58,4.62-20.58,0,0,2.88,17.13,4.62,20.58Z"/>
<path class="cls-1" d="M76.01,97.93l-3.48,2.34s-.1-4.44-3.52-1.84c-.42.32-2.38,2.21-.03,4.27,0,0-4.05,4.08-4.97,8.21-.92-4.12-4.97-8.21-4.97-8.21,2.34-2.06.39-3.95-.03-4.27-3.41-2.59-3.52,1.84-3.52,1.84l-3.48-2.34c.28-3.55.1-6.68-.46-9.42,4.69-4.94,8.85-9.88,12.47-14.61,3.66,4.72,7.78,9.67,12.47,14.61-.57,2.74-.75,5.86-.46,9.42Z"/>
<path class="cls-1" d="M95.34,69.76s-5.94.85-11.23,5.33c-4.02-4.48-7.89-8.92-11.44-13.47,4.3-6.57,7.25-11.98,8.88-15.24.25.25.57.5.92.71,0,0,24.91,1.84,36.42-32.3,0,0-.18,28-23.84,38.66-2.52,1.14-22.64,8.5.28,16.31h0Z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -19,8 +19,8 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
--on-secondary = "#FFFFFF"
--on-background = "#E8E6F0"
--on-surface = "#E8E6F0"
--on-surface-medium = "rgba(232,230,240,0.85)"
--on-surface-disabled = "rgba(232,230,240,0.58)"
--on-surface-medium = "rgba(232,230,240,0.72)"
--on-surface-disabled = "rgba(232,230,240,0.40)"
--error = "#FF5C72"
--on-error = "#000000"
--success = "#3DE8A0"
@@ -61,7 +61,7 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
--sidebar-badge = "rgba(232,230,240,1.0)"
--sidebar-divider = "rgba(200,190,240,0.05)"
--chart-line = "rgba(124,108,255,0.12)"
--window-control = "rgba(232,230,240,0.85)"
--window-control = "rgba(232,230,240,0.72)"
--window-control-hover = "rgba(124,108,255,0.12)"
--window-close-hover = "rgba(255,92,114,0.75)"
--spinner-track = "rgba(200,190,240,0.08)"

View File

@@ -19,16 +19,16 @@ images = { background_image = "backgrounds/texture/pop-light_bg.png", logo = "lo
--on-secondary = "#FFFFFF"
--on-background = "#1E1E2A"
--on-surface = "#1E1E2A"
--on-surface-medium = "rgba(30,30,42,0.86)"
--on-surface-disabled = "rgba(30,30,42,0.62)"
--on-surface-medium = "rgba(30,30,42,0.72)"
--on-surface-disabled = "rgba(30,30,42,0.38)"
--error = "#E0304A"
--on-error = "#FFFFFF"
--success = "#18A860"
--on-success = "#FFFFFF"
--warning = "#E09020"
--on-warning = "#000000"
--divider = "rgba(30,30,60,0.20)"
--outline = "rgba(30,30,60,0.24)"
--divider = "rgba(30,30,60,0.12)"
--outline = "rgba(30,30,60,0.15)"
--scrim = "rgba(0,0,0,0.45)"
--surface-hover = "rgba(96,64,224,0.05)"
--surface-alt = "rgba(96,64,224,0.02)"
@@ -47,14 +47,14 @@ images = { background_image = "backgrounds/texture/pop-light_bg.png", logo = "lo
--chart-hover-ring = "rgba(96,64,224,0.30)"
--tooltip-bg = "rgba(36,34,52,0.94)"
--tooltip-border = "rgba(96,64,224,0.16)"
--glass-fill = "rgba(255,255,255,0.20)"
--glass-fill = "rgba(255,255,255,0.55)"
--glass-border = "rgba(30,30,60,0.10)"
--glass-noise-tint = "rgba(96,64,224,0.02)"
--tactile-top = "rgba(255,255,255,0.40)"
--tactile-bottom = "rgba(255,255,255,0.05)"
--hover-overlay = "rgba(96,64,224,0.10)"
--hover-overlay = "rgba(96,64,224,0.04)"
--active-overlay = "rgba(96,64,224,0.08)"
--rim-light = "rgba(30,30,42,0.22)"
--rim-light = "rgba(96,64,224,0.06)"
--status-divider = "rgba(30,30,60,0.08)"
--sidebar-hover = "rgba(96,64,224,0.07)"
--sidebar-icon = "rgba(30,30,42,0.50)"

View File

@@ -19,8 +19,8 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
--on-secondary = "#000000"
--on-background = "#D0D0D4"
--on-surface = "#D0D0D4"
--on-surface-medium = "rgba(208,208,212,0.85)"
--on-surface-disabled = "rgba(208,208,212,0.58)"
--on-surface-medium = "rgba(208,208,212,0.75)"
--on-surface-disabled = "rgba(208,208,212,0.45)"
--error = "#B07080"
--on-error = "#000000"
--success = "#7AAE7C"
@@ -61,7 +61,7 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
--sidebar-badge = "rgba(208,208,212,1.0)"
--sidebar-divider = "rgba(220,220,225,0.05)"
--chart-line = "rgba(220,220,225,0.08)"
--window-control = "rgba(208,208,212,0.85)"
--window-control = "rgba(208,208,212,0.75)"
--window-control-hover = "rgba(220,220,225,0.10)"
--window-close-hover = "rgba(200,50,60,0.70)"
--spinner-track = "rgba(220,220,225,0.08)"

View File

@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FDF8F0", --elevation-1 = "#F5EDE0", --elevation-
--on-secondary = "#FFFFFF"
--on-background = "#3A2E22"
--on-surface = "#3A2E22"
--on-surface-medium = "rgba(58,46,34,0.86)"
--on-surface-disabled = "rgba(58,46,34,0.62)"
--on-surface-medium = "rgba(58,46,34,0.68)"
--on-surface-disabled = "rgba(58,46,34,0.38)"
--error = "#A0524A"
--on-error = "#FFFFFF"
--success = "#4E8A42"
--success = "#6A8A5C"
--on-success = "#FFFFFF"
--warning = "#C08840"
--on-warning = "#000000"
--divider = "rgba(140,110,70,0.20)"
--outline = "rgba(140,110,70,0.24)"
--divider = "rgba(140,110,70,0.14)"
--outline = "rgba(140,110,70,0.16)"
--scrim = "rgba(30,20,10,0.45)"
--surface-hover = "rgba(176,120,64,0.06)"
--surface-alt = "rgba(176,120,64,0.03)"
@@ -48,14 +48,14 @@ elevation = { --elevation-0 = "#FDF8F0", --elevation-1 = "#F5EDE0", --elevation-
--chart-hover-ring = "rgba(176,120,64,0.28)"
--tooltip-bg = "rgba(50,38,24,0.94)"
--tooltip-border = "rgba(176,120,64,0.12)"
--glass-fill = "rgba(255,252,245,0.20)"
--glass-fill = "rgba(255,252,245,0.58)"
--glass-border = "rgba(176,120,64,0.14)"
--glass-noise-tint = "rgba(180,140,80,0.03)"
--tactile-top = "rgba(255,255,248,0.50)"
--tactile-bottom = "rgba(255,255,248,0.08)"
--hover-overlay = "rgba(176,120,64,0.10)"
--hover-overlay = "rgba(176,120,64,0.05)"
--active-overlay = "rgba(176,120,64,0.10)"
--rim-light = "rgba(58,46,34,0.22)"
--rim-light = "rgba(212,160,108,0.10)"
--status-divider = "rgba(176,120,64,0.10)"
--sidebar-hover = "rgba(176,120,64,0.08)"
--sidebar-icon = "rgba(58,46,34,0.50)"

View File

@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FDFBFF", --elevation-1 = "#F5F0FA", --elevation-
--on-secondary = "#FFFFFF"
--on-background = "#1C1525"
--on-surface = "#1C1525"
--on-surface-medium = "rgba(28,21,37,0.86)"
--on-surface-disabled = "rgba(28,21,37,0.62)"
--on-surface-medium = "rgba(28,21,37,0.72)"
--on-surface-disabled = "rgba(28,21,37,0.40)"
--error = "#C62828"
--on-error = "#FFFFFF"
--success = "#2E7D32"
--on-success = "#FFFFFF"
--warning = "#E65100"
--on-warning = "#000000"
--divider = "rgba(120,80,160,0.20)"
--outline = "rgba(120,80,160,0.24)"
--divider = "rgba(120,80,160,0.12)"
--outline = "rgba(120,80,160,0.14)"
--scrim = "rgba(20,10,30,0.45)"
--surface-hover = "rgba(140,107,175,0.06)"
--surface-alt = "rgba(140,107,175,0.03)"
@@ -48,14 +48,14 @@ elevation = { --elevation-0 = "#FDFBFF", --elevation-1 = "#F5F0FA", --elevation-
--chart-hover-ring = "rgba(140,107,175,0.28)"
--tooltip-bg = "rgba(32,24,48,0.94)"
--tooltip-border = "rgba(140,107,175,0.12)"
--glass-fill = "rgba(255,255,255,0.20)"
--glass-fill = "rgba(255,255,255,0.55)"
--glass-border = "rgba(140,107,175,0.14)"
--glass-noise-tint = "rgba(180,140,220,0.03)"
--tactile-top = "rgba(255,255,255,0.50)"
--tactile-bottom = "rgba(255,255,255,0.08)"
--hover-overlay = "rgba(140,107,175,0.10)"
--hover-overlay = "rgba(140,107,175,0.05)"
--active-overlay = "rgba(140,107,175,0.10)"
--rim-light = "rgba(28,21,37,0.22)"
--rim-light = "rgba(180,140,255,0.10)"
--status-divider = "rgba(140,107,175,0.10)"
--sidebar-hover = "rgba(140,107,175,0.08)"
--sidebar-icon = "rgba(28,21,37,0.50)"

View File

@@ -1,190 +0,0 @@
[theme]
name = "Jade"
author = "The Hush Developers"
dark = true
elevation = { --elevation-0 = "#071210", --elevation-1 = "#0C1A16", --elevation-2 = "#16261F", --elevation-3 = "#1D3128", --elevation-4 = "#243B30" }
images = { background_image = "backgrounds/texture/jade_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
[theme.palette]
--primary = "#2FA07A"
--primary-variant = "#1E7357"
--primary-light = "#7FD1B5"
--secondary = "#C9A24E"
--secondary-variant = "#A8842F"
--secondary-light = "#E0C583"
--background = "#071210"
--surface = "#0C1A16"
--surface-variant = "#16261F"
--on-primary = "#FFFFFF"
--on-secondary = "#000000"
--on-background = "#DCEDE4"
--on-surface = "#DCEDE4"
--on-surface-medium = "rgba(220,237,228,0.85)"
--on-surface-disabled = "rgba(220,237,228,0.58)"
--error = "#CF6679"
--on-error = "#000000"
--success = "#81C784"
--on-success = "#000000"
--warning = "#FFB74D"
--on-warning = "#000000"
--divider = "rgba(130,205,170,0.14)"
--outline = "rgba(130,205,170,0.16)"
--scrim = "rgba(0,0,0,0.6)"
--surface-hover = "rgba(130,205,170,0.07)"
--surface-alt = "rgba(130,205,170,0.05)"
--surface-active = "rgba(130,205,170,0.10)"
--glass-button = "rgba(130,205,170,0.06)"
--glass-button-hover = "rgba(130,205,170,0.12)"
--card-border = "rgba(130,205,170,0.26)"
--text-shadow = "rgba(0,0,0,0.50)"
--input-overlay-text = "rgba(220,237,228,0.30)"
--slider-text = "rgba(220,237,228,0.85)"
--thumb-fill = "rgba(130,205,170,0.15)"
--thumb-border = "rgba(130,205,170,0.50)"
--disabled-label = "rgba(130,205,170,0.18)"
--chart-grid = "rgba(130,205,170,0.05)"
--chart-crosshair = "rgba(130,205,170,0.15)"
--chart-hover-ring = "rgba(130,205,170,0.30)"
--tooltip-bg = "rgba(9,20,16,0.92)"
--tooltip-border = "rgba(130,205,170,0.12)"
--glass-fill = "rgba(130,205,170,0.08)"
--glass-border = "rgba(47,160,122,0.30)"
--glass-noise-tint = "rgba(130,205,170,0.03)"
--tactile-top = "rgba(130,205,170,0.06)"
--tactile-bottom = "rgba(130,205,170,0.0)"
--hover-overlay = "rgba(130,205,170,0.05)"
--active-overlay = "rgba(130,205,170,0.10)"
--rim-light = "rgba(130,205,170,0.14)"
--status-divider = "rgba(130,205,170,0.08)"
--sidebar-hover = "rgba(130,205,170,0.10)"
--sidebar-icon = "rgba(130,205,170,0.42)"
--sidebar-badge = "rgba(220,237,228,1.0)"
--sidebar-divider = "rgba(130,205,170,0.06)"
--chart-line = "rgba(130,205,170,0.10)"
--window-control = "rgba(220,237,228,0.78)"
--window-control-hover = "rgba(130,205,170,0.12)"
--window-close-hover = "rgba(232,17,35,0.78)"
--spinner-track = "rgba(130,205,170,0.10)"
--spinner-active = "rgba(79,184,154,0.85)"
--shutdown-panel-bg = "rgba(7,18,14,0.90)"
--shutdown-panel-border = "rgba(130,205,170,0.07)"
--ram-bar-app = "#2FA07A"
--ram-bar-system = "rgba(255,255,255,0.18)"
--accent-total = "#7FD1B5"
--accent-shielded = "#4FB89A"
--accent-transparent = "#C9A24E"
--accent-action = "#2FA07A"
--accent-market = "#4FB89A"
--accent-portfolio = "#7FD1B5"
--toast-info-accent = "#2FA07A"
--toast-info-text = "#7FD1B5"
--toast-success-accent = "rgba(50,180,80,1.0)"
--toast-success-text = "rgba(180,255,180,1.0)"
--toast-warning-accent = "rgba(204,166,50,1.0)"
--toast-warning-text = "rgba(255,230,130,1.0)"
--toast-error-accent = "rgba(204,64,64,1.0)"
--toast-error-text = "rgba(255,153,153,1.0)"
--snackbar-bg = "rgba(24,40,34,0.95)"
--snackbar-text = "rgba(220,237,228,0.87)"
--snackbar-action = "rgba(79,184,154,1.0)"
--snackbar-action-hover = "rgba(127,209,181,1.0)"
--switch-track-off = "rgba(130,205,170,0.12)"
--switch-track-on = "rgba(47,160,122,0.50)"
--switch-thumb-off = "#A0C0B4"
--switch-thumb-on = "#DCEDE4"
--control-shadow = "rgba(0,0,0,0.24)"
--checkbox-check = "#000000"
--app-bar-shadow = "rgba(0,0,0,0.25)"
[backdrop]
base-color-top = "rgba(14,32,26,210)"
base-color-bottom = "rgba(6,18,14,210)"
texture-tint-alpha = 120
gradient-top-r = 10
gradient-top-g = 30
gradient-top-b = 22
gradient-top-a = 90
gradient-bottom-r = 5
gradient-bottom-g = 16
gradient-bottom-b = 12
gradient-bottom-a = 70
background-alpha = 0.42
surface-alpha = 0.56
frame-alpha = 0.78
surface-inline-alpha = 0.58
background-inline-alpha = 0.40
# ---------------------------------------------------------------------------
# Theme Visual Effects — Jade (veins of gold shifting through the stone)
# Jade's signature is a slow jade→gold color-shifting border on every glass
# panel + the active nav button — a vein of gold surfacing through nephrite.
# It's drawn via AddRect so it hugs the real rounded corners (no polygonal
# edge-trace). Sparse jade motes drift up the viewport. No other theme turns
# gradient-border-panels on, so the panel-wide vein is Jade's own —
# deliberately NOT Obsidian's specular glare.
# ---------------------------------------------------------------------------
[effects]
hue-cycle-enabled = { size = 0.0 }
rainbow-border-enabled = { size = 0.0 }
# No shimmer sweep — replaced by specular glare
shimmer-enabled = { size = 0.0 }
positional-hue-enabled = { size = 0.0 }
glow-pulse-enabled = { size = 0.0 }
# Edge-trace OFF — its hand-walked perimeter chamfers rounded corners.
# Jade's vein is the gradient-border below (corner-clean via AddRect).
edge-trace-enabled = { size = 0.0 }
edge-trace-speed = { size = 0.16 }
edge-trace-length = { size = 0.34 }
edge-trace-thickness = { size = 1.6 }
edge-trace-alpha = { size = 0.55 }
edge-trace-color = { color = "#C9A24E" }
# Specular glare OFF — that's Obsidian's signature; Jade shouldn't echo it.
specular-glare-enabled = { size = 0.0 }
specular-glare-speed = { size = 0.018 }
specular-glare-intensity = { size = 0.008 }
specular-glare-radius = { size = 0.65 }
specular-glare-count = { size = 1.0 }
specular-glare-color = { color = "rgba(150,220,180,1.0)" }
# HERO — vein of gold: a slow jade→gold color-shifting border on the active
# nav button AND (via gradient-border-panels) every glass panel. Drawn with
# AddRect so it follows the rounded corners exactly. Panels drift at a softer
# alpha and position-phased offset, so a screenful reads like veins at
# different depths rather than one synchronized pulse.
gradient-border-enabled = { size = 1.0 }
gradient-border-panels = { size = 1.0 }
gradient-border-speed = { size = 0.10 }
gradient-border-thickness = { size = 1.5 }
gradient-border-alpha = { size = 0.55 }
gradient-border-color-a = { color = "#7FD1B5" }
gradient-border-color-b = { color = "#C9A24E" }
# Ambient jade motes — sparse, slow, cool green particles drifting up the
# viewport (recolored ember-rise; a different mood from dragonx's fire embers).
ember-rise-enabled = { size = 1.0 }
ember-rise-count = { size = 5.0 }
ember-rise-speed = { size = 0.18 }
ember-rise-particle-size = { size = 1.4 }
ember-rise-alpha = { size = 0.26 }
ember-rise-color = { color = "#7FD1B5" }
# Shader-like viewport overlay — deep green stone atmosphere
viewport-wash-enabled = { size = 1.0 }
viewport-wash-alpha = { size = 0.05 }
viewport-wash-tl = { color = "#12402E" }
viewport-wash-tr = { color = "#0E3828" }
viewport-wash-bl = { color = "#16442E" }
viewport-wash-br = { color = "#1A4A34" }
viewport-wash-rotate = { size = 0.015 }
viewport-wash-pulse = { size = 0.0 }
viewport-wash-pulse-depth = { size = 0.0 }
viewport-vignette-enabled = { size = 1.0 }
viewport-vignette-color = { color = "#04140D" }
viewport-vignette-radius = { size = 0.22 }
viewport-vignette-alpha = { size = 0.15 }

View File

@@ -19,16 +19,16 @@ elevation = { --elevation-0 = "#FAFAFA", --elevation-1 = "#F2F3F5", --elevation-
--on-secondary = "#FFFFFF"
--on-background = "#2A2C30"
--on-surface = "#2A2C30"
--on-surface-medium = "rgba(42,44,48,0.86)"
--on-surface-disabled = "rgba(42,44,48,0.62)"
--on-surface-medium = "rgba(42,44,48,0.68)"
--on-surface-disabled = "rgba(42,44,48,0.38)"
--error = "#8C5A62"
--on-error = "#FFFFFF"
--success = "#3D7A42"
--success = "#5A7E5C"
--on-success = "#FFFFFF"
--warning = "#9A7A2E"
--warning = "#8A7A52"
--on-warning = "#000000"
--divider = "rgba(42,44,48,0.20)"
--outline = "rgba(42,44,48,0.24)"
--divider = "rgba(42,44,48,0.12)"
--outline = "rgba(42,44,48,0.14)"
--scrim = "rgba(0,0,0,0.42)"
--surface-hover = "rgba(42,44,48,0.04)"
--surface-alt = "rgba(42,44,48,0.02)"
@@ -47,14 +47,14 @@ elevation = { --elevation-0 = "#FAFAFA", --elevation-1 = "#F2F3F5", --elevation-
--chart-hover-ring = "rgba(42,44,48,0.24)"
--tooltip-bg = "rgba(50,52,58,0.92)"
--tooltip-border = "rgba(42,44,48,0.10)"
--glass-fill = "rgba(255,255,255,0.20)"
--glass-fill = "rgba(255,255,255,0.55)"
--glass-border = "rgba(42,44,48,0.10)"
--glass-noise-tint = "rgba(42,44,48,0.015)"
--tactile-top = "rgba(255,255,255,0.35)"
--tactile-bottom = "rgba(255,255,255,0.04)"
--hover-overlay = "rgba(42,44,48,0.10)"
--hover-overlay = "rgba(42,44,48,0.04)"
--active-overlay = "rgba(42,44,48,0.08)"
--rim-light = "rgba(42,44,48,0.22)"
--rim-light = "rgba(42,44,48,0.06)"
--status-divider = "rgba(42,44,48,0.08)"
--sidebar-hover = "rgba(42,44,48,0.05)"
--sidebar-icon = "rgba(42,44,48,0.45)"

View File

@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FAFAF8", --elevation-1 = "#F0EEEC", --elevation-
--on-secondary = "#FFFFFF"
--on-background = "#2C2A28"
--on-surface = "#2C2A28"
--on-surface-medium = "rgba(44,42,40,0.86)"
--on-surface-disabled = "rgba(44,42,40,0.62)"
--on-surface-medium = "rgba(44,42,40,0.68)"
--on-surface-disabled = "rgba(44,42,40,0.38)"
--error = "#8C5250"
--on-error = "#FFFFFF"
--success = "#3F7A48"
--success = "#5C7A62"
--on-success = "#FFFFFF"
--warning = "#9A7A2E"
--warning = "#8A7A4C"
--on-warning = "#000000"
--divider = "rgba(80,75,68,0.20)"
--outline = "rgba(80,75,68,0.24)"
--divider = "rgba(80,75,68,0.12)"
--outline = "rgba(80,75,68,0.14)"
--scrim = "rgba(20,18,16,0.42)"
--surface-hover = "rgba(110,117,128,0.05)"
--surface-alt = "rgba(110,117,128,0.025)"
@@ -48,14 +48,14 @@ elevation = { --elevation-0 = "#FAFAF8", --elevation-1 = "#F0EEEC", --elevation-
--chart-hover-ring = "rgba(110,117,128,0.24)"
--tooltip-bg = "rgba(44,42,40,0.94)"
--tooltip-border = "rgba(110,117,128,0.10)"
--glass-fill = "rgba(255,255,254,0.20)"
--glass-fill = "rgba(255,255,254,0.62)"
--glass-border = "rgba(110,117,128,0.10)"
--glass-noise-tint = "rgba(80,75,68,0.02)"
--tactile-top = "rgba(255,255,255,0.45)"
--tactile-bottom = "rgba(255,255,255,0.06)"
--hover-overlay = "rgba(110,117,128,0.10)"
--hover-overlay = "rgba(110,117,128,0.04)"
--active-overlay = "rgba(110,117,128,0.08)"
--rim-light = "rgba(44,42,40,0.22)"
--rim-light = "rgba(180,175,168,0.10)"
--status-divider = "rgba(110,117,128,0.08)"
--sidebar-hover = "rgba(110,117,128,0.06)"
--sidebar-icon = "rgba(44,42,40,0.48)"

View File

@@ -19,8 +19,8 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
--on-secondary = "#000000"
--on-background = "#E8E0F0"
--on-surface = "#E8E0F0"
--on-surface-medium = "rgba(232,224,240,0.85)"
--on-surface-disabled = "rgba(232,224,240,0.58)"
--on-surface-medium = "rgba(232,224,240,0.75)"
--on-surface-disabled = "rgba(232,224,240,0.45)"
--error = "#CF6679"
--on-error = "#000000"
--success = "#81C784"

View File

@@ -37,8 +37,8 @@ images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/l
--on-secondary = "#000000"
--on-background = "#F0E0D8"
--on-surface = "#F0E0D8"
--on-surface-medium = "rgba(240,224,216,0.85)"
--on-surface-disabled = "rgba(240,224,216,0.58)"
--on-surface-medium = "rgba(240,224,216,0.7)"
--on-surface-disabled = "rgba(240,224,216,0.44)"
--error = "#FF5252"
--on-error = "#000000"
--success = "#81C784"
@@ -874,7 +874,7 @@ accent-stripe-inset-ratio = { size = 0.0 }
accent-stripe-left-offset = { size = 0.0 }
accent-stripe-width = { size = 4.0 }
accent-stripe-rounding = { size = 1.5 }
chart-y-axis-min-padding = { size = 54.0 }
chart-y-axis-min-padding = { size = 40.0 }
chart-y-axis-padding = { size = 70.0 }
chart-dot-min-radius = { size = 1.5 }
chart-dot-radius = { size = 2.0 }
@@ -908,7 +908,6 @@ scroll-fade-zone = { size = 24.0 }
[tabs.console]
input-area-padding = 8.0
bg-darken-alpha = { size = 110.0 } # black overlay alpha (0-255) for the terminal-dark output + input
output-line-spacing = 2.0
output = { line-spacing = 2 }
scroll-multiplier = { size = 3.0 }
@@ -931,7 +930,7 @@ scanline-speed = { size = 40.0 }
scanline-height = { size = 36.0 }
scanline-alpha = { size = 8.0 }
scanline-gap = { size = 2.0 }
scanline-line-alpha = { size = 2.0 }
scanline-line-alpha = { size = 4.0 }
scanline-glow-spread = { size = 4.0 }
scanline-glow-intensity = { size = 0.6 }
scanline-glow-color = { size = 255.0 }
@@ -1219,9 +1218,6 @@ notification-progress = { color = "var(--primary)", height = 4, position = 18 }
fill-alpha = { size = 12.0 }
noise-alpha = { size = 14.0 }
[components.overlay-dialog]
confirm-btn-height = { size = 40.0 }
[components.qr-code]
module-scale = { size = 4 }
border-modules = { size = 2 }
@@ -1328,13 +1324,10 @@ wallet-btn-padding = { size = 24.0 }
rpc-label-min-width = { size = 70.0 }
rpc-label-width = { size = 85.0 }
security-combo-width = { size = 120.0 }
node-grid-breakpoint = { size = 900.0 }
port-input-min-width = { size = 60.0 }
port-input-width-ratio = { size = 0.4 }
idle-combo-width = { size = 64.0 }
# Reserved height basis for the About-card logo; the logo is drawn scaled to the
# card's actual height (aspect-preserved) and capped to this * aspect in width.
about-logo-size = { size = 150.0 }
about-logo-size = { size = 64.0 }
[components.main-layout]
app-bar-height = { size = 64.0 }
@@ -1528,6 +1521,7 @@ title = { font = "h5" }
input = { width = 320.0, height = 40.0 }
unlock-button = { width = 320.0, height = 44.0, font = "subtitle1" }
error-text = { font = "caption" }
backdrop-alpha = { opacity = 0.0 }
mode-toggle = { font = "caption" }
# ---------------------------------------------------------------------------

View File

@@ -2100,178 +2100,6 @@ TRANSLATIONS = {
"pt": "EXPLORADOR", "ru": "ОБОЗРЕВАТЕЛЬ", "zh": "浏览器",
"ja": "エクスプローラー", "ko": "탐색기"
},
# --- Wallets dialog: metadata counts + status badges ---
"wallets_col_txs": {
"es": "tx", "de": "Tx", "fr": "tx", "pt": "tx",
"ru": "трз", "zh": "笔交易", "ja": "", "ko": ""
},
"wallets_col_keys": {
"es": "claves", "de": "Schlüssel", "fr": "clés", "pt": "chaves",
"ru": "ключей", "zh": "个密钥", "ja": "個の鍵", "ko": "개 키"
},
"wallets_badge_encrypted": {
"es": "Cifrada (protegida con contraseña)", "de": "Verschlüsselt (passphrasengeschützt)",
"fr": "Chiffré (protégé par phrase secrète)", "pt": "Encriptada (protegida por senha)",
"ru": "Зашифрован (защищён паролем)", "zh": "已加密(密码保护)",
"ja": "暗号化済み(パスフレーズ保護)", "ko": "암호화됨 (암호로 보호됨)"
},
"wallets_badge_seed": {
"es": "Billetera con frase semilla (HD)", "de": "Seed-Phrase-Wallet (HD)",
"fr": "Portefeuille à phrase de récupération (HD)", "pt": "Carteira com frase semente (HD)",
"ru": "Кошелёк с seed-фразой (HD)", "zh": "助记词钱包 (HD)",
"ja": "シードフレーズウォレット (HD)", "ko": "시드 문구 지갑 (HD)"
},
"wallets_badge_legacy": {
"es": "Billetera heredada (sin frase semilla)", "de": "Legacy-Wallet (keine Seed-Phrase)",
"fr": "Portefeuille hérité (sans phrase de récupération)", "pt": "Carteira legada (sem frase semente)",
"ru": "Устаревший кошелёк (без seed-фразы)", "zh": "旧版钱包(无助记词)",
"ja": "レガシーウォレット(シードフレーズなし)", "ko": "레거시 지갑 (시드 문구 없음)"
},
"wallets_badge_unknown": {
"es": "Tipo de billetera no determinado por completo (archivo grande — abra para confirmar)",
"de": "Wallet-Typ nicht vollständig ermittelt (große Datei — zum Bestätigen öffnen)",
"fr": "Type de portefeuille non entièrement déterminé (fichier volumineux — ouvrir pour confirmer)",
"pt": "Tipo de carteira não totalmente determinado (arquivo grande — abra para confirmar)",
"ru": "Тип кошелька определён не полностью (большой файл — откройте для подтверждения)",
"zh": "钱包类型未完全确定(文件较大——打开以确认)",
"ja": "ウォレットの種類を完全に判定できません(大きなファイル — 開いて確認)",
"ko": "지갑 유형을 완전히 확인하지 못함 (큰 파일 — 열어서 확인)"
},
"wallets_badge_seed_short": {
"es": "Frase semilla", "de": "Seed-Phrase", "fr": "Phrase secrète", "pt": "Frase semente",
"ru": "Seed-фраза", "zh": "助记词", "ja": "シードフレーズ", "ko": "시드 문구"
},
"wallets_badge_encrypted_short": {
"es": "Cifrada", "de": "Verschlüsselt", "fr": "Chiffré", "pt": "Encriptada",
"ru": "Зашифрован", "zh": "已加密", "ja": "暗号化", "ko": "암호화됨"
},
"wallets_badge_legacy_short": {
"es": "Heredada", "de": "Legacy", "fr": "Hérité", "pt": "Legada",
"ru": "Устаревший", "zh": "旧版", "ja": "レガシー", "ko": "레거시"
},
"wallets_badge_unknown_short": {
"es": "Desconocido", "de": "Unbekannt", "fr": "Inconnu", "pt": "Desconhecido",
"ru": "Неизвестно", "zh": "未知", "ja": "不明", "ko": "알 수 없음"
},
# --- Wallets dialog: created date + sort control ---
"wallets_created": {
"es": "creada", "de": "erstellt", "fr": "créé", "pt": "criada",
"ru": "создан", "zh": "创建于", "ja": "作成", "ko": "생성"
},
"wallets_sort_by": {
"es": "Ordenar:", "de": "Sortieren:", "fr": "Trier :", "pt": "Ordenar:",
"ru": "Сортировка:", "zh": "排序:", "ja": "並べ替え:", "ko": "정렬:"
},
"wallets_sort_created": {
"es": "Creado", "de": "Erstellt", "fr": "Créé", "pt": "Criado",
"ru": "Создан", "zh": "创建", "ja": "作成", "ko": "생성"
},
"wallets_sort_addresses": {
"es": "Direcciones", "de": "Adressen", "fr": "Adresses", "pt": "Endereços",
"ru": "Адреса", "zh": "地址", "ja": "アドレス", "ko": "주소"
},
"wallets_sort_txs": {
"es": "Txs", "de": "Txs", "fr": "Txs", "pt": "Txs",
"ru": "Транз.", "zh": "交易", "ja": "取引", "ko": "거래"
},
"wallets_sort_size": {
"es": "Tamaño", "de": "Größe", "fr": "Taille", "pt": "Tamanho",
"ru": "Размер", "zh": "大小", "ja": "サイズ", "ko": "크기"
},
"wallets_sort_asc": {
"es": "Ascendente (más antiguo / menos / más pequeño primero)",
"de": "Aufsteigend (ältestes / wenigste / kleinstes zuerst)",
"fr": "Croissant (plus ancien / moins / plus petit d'abord)",
"pt": "Crescente (mais antigo / menos / menor primeiro)",
"ru": "По возрастанию (сначала старые / меньше / меньший)",
"zh": "升序(最早/最少/最小优先)", "ja": "昇順(古い/少ない/小さい順)", "ko": "오름차순 (오래된/적은/작은 순)"
},
"wallets_sort_desc": {
"es": "Descendente (más reciente / más / más grande primero)",
"de": "Absteigend (neuestes / meiste / größtes zuerst)",
"fr": "Décroissant (plus récent / plus / plus grand d'abord)",
"pt": "Decrescente (mais recente / mais / maior primeiro)",
"ru": "По убыванию (сначала новые / больше / больший)",
"zh": "降序(最新/最多/最大优先)", "ja": "降順(新しい/多い/大きい順)", "ko": "내림차순 (최신/많은/큰 순)"
},
"wallets_open_folder": {
"es": "Abrir ubicación de la carpeta", "de": "Ordnerpfad öffnen",
"fr": "Ouvrir l'emplacement du dossier", "pt": "Abrir local da pasta",
"ru": "Открыть расположение папки", "zh": "打开文件夹位置",
"ja": "フォルダーの場所を開く", "ko": "폴더 위치 열기"
},
"wallets_open_inplace_tt": {
"es": "Abre esta cartera donde está, enlazándola al directorio de datos (sin copiar)",
"de": "Öffnet diese Wallet an ihrem Ort — im Datenverzeichnis verlinkt (keine Kopie)",
"fr": "Ouvre ce portefeuille à son emplacement — lié au répertoire de données (sans copie)",
"pt": "Abre esta carteira onde está — vinculada ao diretório de dados (sem cópia)",
"ru": "Открывает этот кошелёк на месте — по ссылке в каталоге данных (без копирования)",
"zh": "在原位置打开此钱包 — 链接到数据目录(不复制)",
"ja": "このウォレットをその場で開きます — データディレクトリにリンク(コピーなし)",
"ko": "이 지갑을 있는 자리에서 엽니다 — 데이터 디렉터리에 링크 (복사 없음)"
},
"wallets_open_failed": {
"es": "No se pudo abrir esta cartera en su ubicación. Probablemente está en una unidad distinta a tu directorio de datos: muévela a la misma unidad (en Windows, activar el Modo de desarrollador también permite enlazar entre unidades).",
"de": "Diese Wallet konnte nicht an ihrem Ort geöffnet werden. Sie liegt vermutlich auf einem anderen Laufwerk als dein Datenverzeichnis — verschiebe sie auf dasselbe Laufwerk (unter Windows erlaubt auch der aktivierte Entwicklermodus laufwerkübergreifende Verknüpfungen).",
"fr": "Impossible d'ouvrir ce portefeuille à son emplacement. Il se trouve probablement sur un lecteur différent de votre répertoire de données — déplacez-le sur le même lecteur (sous Windows, activer le mode développeur permet aussi de créer des liens entre lecteurs).",
"pt": "Não foi possível abrir esta carteira no lugar. Provavelmente está em uma unidade diferente do seu diretório de dados — mova-a para a mesma unidade (no Windows, ativar o Modo de Desenvolvedor também permite vincular entre unidades).",
"ru": "Не удалось открыть этот кошелёк на месте. Вероятно, он на другом диске, чем каталог данных — переместите его на тот же диск (в Windows включённый режим разработчика также позволяет создавать ссылки между дисками).",
"zh": "无法在原位置打开此钱包。它可能与数据目录位于不同的驱动器上 — 请将其移动到同一驱动器(在 Windows 上,启用开发者模式也可跨驱动器链接)。",
"ja": "このウォレットをその場で開けませんでした。データディレクトリとは別のドライブにある可能性があります — 同じドライブに移動してくださいWindows では開発者モードを有効にするとドライブ間のリンクも可能になります)。",
"ko": "이 지갑을 제자리에서 열 수 없습니다. 데이터 디렉터리와 다른 드라이브에 있을 가능성이 높습니다 — 같은 드라이브로 옮기세요 (Windows에서는 개발자 모드를 켜면 드라이브 간 링크도 가능합니다)."
},
"wallets_external_tt": {
"es": "Fuera de tu directorio de datos — Abrir lo enlaza en su lugar (sin copiar).",
"de": "Außerhalb deines Datenverzeichnisses — Öffnen verlinkt sie an ihrem Ort (keine Kopie).",
"fr": "Hors de votre répertoire de données — Ouvrir le lie sur place (sans copie).",
"pt": "Fora do seu diretório de dados — Abrir o vincula no lugar (sem cópia).",
"ru": "Вне каталога данных — «Открыть» создаёт ссылку на месте (без копирования).",
"zh": "在数据目录之外 —「打开」会就地链接(不复制)。",
"ja": "データディレクトリの外 —「開く」はその場でリンクします(コピーなし)。",
"ko": "데이터 디렉터리 밖 — '열기'는 제자리에 링크합니다 (복사 없음)."
},
"wallets_scanned_folders": {
"es": "Carpetas escaneadas:", "de": "Durchsuchte Ordner:",
"fr": "Dossiers analysés :", "pt": "Pastas verificadas:",
"ru": "Просканированные папки:", "zh": "已扫描的文件夹:",
"ja": "スキャン対象フォルダー:", "ko": "스캔한 폴더:"
},
"wallets_remove_folder": {
"es": "Dejar de escanear esta carpeta", "de": "Diesen Ordner nicht mehr durchsuchen",
"fr": "Ne plus analyser ce dossier", "pt": "Parar de verificar esta pasta",
"ru": "Больше не сканировать эту папку", "zh": "停止扫描此文件夹",
"ja": "このフォルダーのスキャンを停止", "ko": "이 폴더 스캔 중지"
},
"wallets_empty_hint": {
"es": "Escanea una carpeta para encontrar más carteras",
"de": "Ordner durchsuchen, um weitere Wallets zu finden",
"fr": "Analysez un dossier pour trouver d'autres portefeuilles",
"pt": "Verifique uma pasta para encontrar mais carteiras",
"ru": "Просканируйте папку, чтобы найти другие кошельки",
"zh": "扫描文件夹以查找更多钱包",
"ja": "フォルダーをスキャンして他のウォレットを探す",
"ko": "폴더를 스캔하여 다른 지갑 찾기"
},
"market_chart_loading": {
"es": "Cargando historial de precios", "de": "Preisverlauf wird geladen",
"fr": "Chargement de l'historique des prix", "pt": "Carregando histórico de preços",
"ru": "Загрузка истории цен", "zh": "正在加载价格历史",
"ja": "価格履歴を読み込み中", "ko": "가격 기록 불러오는 중"
},
"market_style_line": {
"es": "Cambiar a gráfico de líneas", "de": "Zum Liniendiagramm wechseln",
"fr": "Passer au graphique en ligne", "pt": "Mudar para gráfico de linhas",
"ru": "Переключить на линейный график", "zh": "切换到折线图",
"ja": "折れ線チャートに切り替え", "ko": "선형 차트로 전환"
},
"market_style_candle": {
"es": "Cambiar a velas", "de": "Zu Kerzenchart wechseln",
"fr": "Passer aux chandeliers", "pt": "Mudar para velas",
"ru": "Переключить на свечи", "zh": "切换到蜡烛图",
"ja": "ローソク足に切り替え", "ko": "캔들차트로 전환"
},
}
def main():

View File

@@ -1,94 +0,0 @@
#!/usr/bin/env bash
# Cross-build a MINIMAL static FreeType for the mingw-w64 (Windows) target.
#
# Why: the wallet's optional color-emoji rendering needs FreeType (to rasterize the COLR/CPAL Twemoji
# font). Native Linux/macOS pick up the system FreeType via find_package; the Debian/Ubuntu mingw-w64
# cross toolchain ships no FreeType, so we build one here. The Twemoji font is COLRv0 (layered vector),
# which FreeType renders WITHOUT libpng / harfbuzz / brotli / zlib — so this is a dependency-free static
# build (no external libs to also cross-compile), producing a self-contained libfreetype.a.
#
# Output: <prefix>/include/freetype2/... + <prefix>/lib/libfreetype.a (default prefix: third_party/freetype-mingw)
# build.sh --win-release runs this automatically and passes -DDRAGONX_MINGW_FREETYPE_PREFIX to CMake.
set -euo pipefail
FT_VERSION="2.13.3"
FT_SHA256="5c3a8e78f7b24c20b25b54ee575d6daa40007a5f4eea2845861c3409b3021747" # freetype-2.13.3.tar.gz
FT_URL="https://download.savannah.gnu.org/releases/freetype/freetype-${FT_VERSION}.tar.gz"
FT_URL_MIRROR="https://downloads.sourceforge.net/project/freetype/freetype2/${FT_VERSION}/freetype-${FT_VERSION}.tar.gz"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PREFIX="${1:-$SCRIPT_DIR/third_party/freetype-mingw}"
WORK="$SCRIPT_DIR/third_party/.freetype-mingw-build"
# Already built? (libfreetype.a present) → nothing to do.
if [[ -f "$PREFIX/lib/libfreetype.a" && -d "$PREFIX/include/freetype2" ]]; then
echo "FreeType (mingw) already built at: $PREFIX"
exit 0
fi
# Pick the mingw compilers (posix threads variant preferred, matching build.sh).
if command -v x86_64-w64-mingw32-gcc-posix &>/dev/null; then
MINGW_GCC=x86_64-w64-mingw32-gcc-posix; MINGW_GXX=x86_64-w64-mingw32-g++-posix
elif command -v x86_64-w64-mingw32-gcc &>/dev/null; then
MINGW_GCC=x86_64-w64-mingw32-gcc; MINGW_GXX=x86_64-w64-mingw32-g++
else
echo "ERROR: x86_64-w64-mingw32-gcc not found (install mingw-w64)." >&2
exit 1
fi
mkdir -p "$WORK"
cd "$WORK"
TARBALL="freetype-${FT_VERSION}.tar.gz"
if [[ ! -f "$TARBALL" ]]; then
echo "Downloading FreeType ${FT_VERSION} ..."
curl -fsSL -o "$TARBALL" "$FT_URL" || curl -fsSL -o "$TARBALL" "$FT_URL_MIRROR"
fi
echo "Verifying SHA-256 ..."
echo "${FT_SHA256} ${TARBALL}" | sha256sum -c - || {
echo "ERROR: FreeType tarball checksum mismatch (expected ${FT_SHA256})." >&2
echo " got: $(sha256sum "$TARBALL" | cut -d' ' -f1)" >&2
exit 1
}
rm -rf "freetype-${FT_VERSION}"
tar xf "$TARBALL"
SRC="$WORK/freetype-${FT_VERSION}"
# Minimal mingw toolchain for FreeType's own CMake.
cat > "$WORK/ft-mingw-toolchain.cmake" <<TOOLCHAIN
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER ${MINGW_GCC})
set(CMAKE_CXX_COMPILER ${MINGW_GXX})
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
TOOLCHAIN
echo "Configuring FreeType (static, no external deps) ..."
rm -rf "$WORK/build"
cmake -S "$SRC" -B "$WORK/build" \
-DCMAKE_TOOLCHAIN_FILE="$WORK/ft-mingw-toolchain.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$PREFIX" \
-DBUILD_SHARED_LIBS=OFF \
-DFT_DISABLE_ZLIB=ON \
-DFT_DISABLE_BZIP2=ON \
-DFT_DISABLE_PNG=ON \
-DFT_DISABLE_HARFBUZZ=ON \
-DFT_DISABLE_BROTLI=ON
echo "Building + installing FreeType ..."
cmake --build "$WORK/build" -j "$(nproc)"
cmake --install "$WORK/build"
if [[ -f "$PREFIX/lib/libfreetype.a" ]]; then
echo "OK: mingw FreeType -> $PREFIX/lib/libfreetype.a"
else
echo "ERROR: build did not produce libfreetype.a" >&2
exit 1
fi

View File

@@ -1,755 +0,0 @@
#!/usr/bin/env bash
# This script uses bash 4+ features (mapfile, safe empty-array expansion under
# `set -u`). macOS ships bash 3.2, so re-exec under a newer bash when one is
# present (Homebrew), and fail with a clear message otherwise.
if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then
for _newer_bash in /opt/homebrew/bin/bash /usr/local/bin/bash; do
[ -x "$_newer_bash" ] && exec "$_newer_bash" "$0" "$@"
done
echo "ERROR: build-lite-backend-artifact.sh requires bash 4+ (found ${BASH_VERSION:-unknown})." >&2
echo " On macOS: brew install bash" >&2
exit 1
fi
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
ABI_VERSION="sdxl-c-v1"
LINK_MODE="imported"
BACKEND_DIR="$PROJECT_ROOT/third_party/silentdragonxlite/lib"
BACKEND_SOURCE_DIR=""
BUILD_BACKEND_DIR=""
BACKEND_DEPENDENCY_DIR=""
BACKEND_DEPENDENCY_OVERRIDE_REQUESTED=false
OUT_DIR="$PROJECT_ROOT/build/lite-backend"
PLATFORM=""
RUST_TARGET=""
CARGO_TARGET_DIR_VALUE="${CARGO_TARGET_DIR:-}"
ARTIFACT_PATH=""
BUILD_ARTIFACT=true
BUILDER="${DRAGONX_LITE_BACKEND_BUILDER:-local}"
JOBS="${JOBS:-}"
SOURCE_DATE_EPOCH_VALUE="${SOURCE_DATE_EPOCH:-}"
REPRODUCIBLE=false
SIGNATURE_REQUIRED=false
SIGNATURE_FILE=""
SIGNATURE_FORMAT=""
SIGNATURE_VERIFICATION_TOOL=""
SIGNATURE_VERIFICATION_COMMAND=""
SIGNATURE_KEY_FINGERPRINT=""
SIGNATURE_CERTIFICATE_IDENTITY=""
SIGNATURE_CERTIFICATE_ISSUER=""
SIGNATURE_TRANSPARENCY_LOG_URL=""
SIGNATURE_VERIFIED_SHA256=""
SIGNATURE_POLICY_NAME="dragonx-lite-backend-signature-policy-v1"
SIGNATURE_POLICY_DEFINED_MANIFEST_VALUE=true
SIGNATURE_REQUIRED_MANIFEST_VALUE=false
SIGNATURE_METADATA_PROVIDED=false
SIGNATURE_VERIFICATION_PERFORMED=false
SIGNATURE_VERIFICATION_STATUS="not-provided"
SIGNATURE_FILE_SHA256=""
REQUIRED_SYMBOLS=(
litelib_wallet_exists
litelib_initialize_new
litelib_initialize_new_from_phrase
litelib_initialize_existing
litelib_execute
litelib_rust_free_string
litelib_check_server_online
litelib_shutdown
)
EXTRA_CARGO_ARGS=()
EXTRA_REMAP_PATH_PREFIXES=()
usage() {
cat <<EOF
Build or inventory the SDXL-compatible lite backend artifact.
Usage: $0 [options]
Options:
--platform linux|windows|macos Artifact platform. Defaults to host platform.
--rust-target TRIPLE Cargo target triple for cross builds.
--cargo-target-dir PATH Isolated Cargo target directory for clean builds.
--backend-dir PATH SilentDragonXLite/lib source directory.
--silentdragonxlitelib-dir PATH Override the wrapper's silentdragonxlitelib dependency path.
--out-dir PATH Output directory for copied artifact and metadata.
--reproducible Add deterministic Rust path remaps for clean builds.
--remap-path-prefix FROM=TO Extra rustc path remap used with --reproducible.
--builder NAME Redacted builder/provenance label. Default: local.
-j, --jobs N Cargo parallel jobs.
--cargo-arg ARG Extra argument forwarded to cargo build.
-h, --help Show this help.
Outputs:
<out>/<platform>/<artifact>
<out>/<platform>/lite-backend-symbols.txt
<out>/<platform>/lite-backend-artifact-manifest.json
The lite backend is always built from the vendored in-tree source
(third_party/silentdragonxlite), which is the trust root. Prebuilt artifacts
and self-attested signature metadata are NOT accepted (F15-1) — the previous
scheme only recorded an unverified "verified" claim. The script captures the
freshly-built artifact's symbols and checksum, and records build provenance.
It does not load the library, resolve function pointers, call SDXL, sign,
upload, or publish artifacts.
EOF
}
info() { printf '[lite-backend] %s\n' "$*"; }
warn() { printf '[lite-backend] warning: %s\n' "$*" >&2; }
die() { printf '[lite-backend] ERROR: %s\n' "$*" >&2; exit 1; }
absolute_path() {
local path="$1"
if [[ "$path" = /* ]]; then
printf '%s\n' "$path"
else
printf '%s/%s\n' "$PWD" "$path"
fi
}
host_platform() {
case "$(uname -s)" in
Linux) printf 'linux\n' ;;
Darwin) printf 'macos\n' ;;
MINGW*|MSYS*|CYGWIN*) printf 'windows\n' ;;
*) die "unsupported host platform: $(uname -s)" ;;
esac
}
normalize_platform() {
case "$1" in
linux|Linux) printf 'linux\n' ;;
windows|win|Win|Windows) printf 'windows\n' ;;
macos|mac|darwin|Darwin) printf 'macos\n' ;;
"") host_platform ;;
*) die "unsupported platform: $1" ;;
esac
}
while [[ $# -gt 0 ]]; do
case "$1" in
--platform)
[[ $# -ge 2 ]] || die "--platform requires a value"
PLATFORM="$(normalize_platform "$2")"
shift 2
;;
--rust-target)
[[ $# -ge 2 ]] || die "--rust-target requires a value"
RUST_TARGET="$2"
shift 2
;;
--cargo-target-dir)
[[ $# -ge 2 ]] || die "--cargo-target-dir requires a value"
CARGO_TARGET_DIR_VALUE="$(absolute_path "$2")"
shift 2
;;
--backend-dir)
[[ $# -ge 2 ]] || die "--backend-dir requires a value"
BACKEND_DIR="$(absolute_path "$2")"
shift 2
;;
--silentdragonxlitelib-dir)
[[ $# -ge 2 ]] || die "--silentdragonxlitelib-dir requires a value"
BACKEND_DEPENDENCY_DIR="$(absolute_path "$2")"
BACKEND_DEPENDENCY_OVERRIDE_REQUESTED=true
shift 2
;;
--out-dir)
[[ $# -ge 2 ]] || die "--out-dir requires a value"
OUT_DIR="$(absolute_path "$2")"
shift 2
;;
--artifact|--no-build)
die "$1 was removed (F15-1): the lite backend must be built from the vendored in-tree source (third_party/silentdragonxlite); prebuilt artifacts are no longer accepted."
;;
--reproducible)
REPRODUCIBLE=true
shift
;;
--remap-path-prefix)
[[ $# -ge 2 ]] || die "--remap-path-prefix requires FROM=TO"
[[ "$2" == *=* ]] || die "--remap-path-prefix requires FROM=TO"
EXTRA_REMAP_PATH_PREFIXES+=("$2")
shift 2
;;
--builder)
[[ $# -ge 2 ]] || die "--builder requires a value"
BUILDER="$2"
shift 2
;;
--signature-required|--signature-file|--signature-path|--signature-format|\
--signature-verification-tool|--signature-tool|--signature-verification-command|\
--signature-key-fingerprint|--signature-certificate-identity|--signature-certificate-issuer|\
--signature-transparency-log-url|--signature-verified-sha256)
die "signature-attestation flags were removed (F15-1): they recorded a self-attested \"verified\" claim without running any cryptographic verifier. The lite backend is built from the vendored in-tree source, which is the trust root."
;;
-j|--jobs)
[[ $# -ge 2 ]] || die "--jobs requires a value"
JOBS="$2"
shift 2
;;
--cargo-arg)
[[ $# -ge 2 ]] || die "--cargo-arg requires a value"
EXTRA_CARGO_ARGS+=("$2")
shift 2
;;
-h|--help)
usage
exit 0
;;
*) die "unknown option: $1" ;;
esac
done
PLATFORM="$(normalize_platform "$PLATFORM")"
BACKEND_SOURCE_DIR="$BACKEND_DIR"
BUILD_BACKEND_DIR="$BACKEND_SOURCE_DIR"
if [[ "$PLATFORM" == "windows" && -z "$RUST_TARGET" ]]; then
RUST_TARGET="x86_64-pc-windows-gnu"
fi
if [[ "$PLATFORM" == "macos" && -z "$RUST_TARGET" && "$(host_platform)" != "macos" ]]; then
die "macOS artifacts require --rust-target when not running on macOS"
fi
if [[ "$BUILD_ARTIFACT" == false && -z "$ARTIFACT_PATH" ]]; then
die "--no-build requires --artifact"
fi
backend_dependency_path_from_cargo() {
local cargo_toml="$1"
awk '
/^[[:space:]]*silentdragonxlitelib[[:space:]]*=/ {
original = $0
path = $0
sub(/.*path[[:space:]]*=[[:space:]]*"/, "", path)
sub(/".*/, "", path)
if (path != original) print path
exit
}
' "$cargo_toml"
}
canonical_dependency_path() {
local path="$1"
if [[ -d "$path" ]]; then
(cd "$path" && pwd -P)
else
absolute_path "$path"
fi
}
validate_backend_dependency_source() {
[[ -n "$BACKEND_DEPENDENCY_DIR" ]] || return
if [[ ! -f "$BACKEND_DEPENDENCY_DIR/Cargo.toml" ]]; then
if [[ "$BUILD_ARTIFACT" == true || "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
die "Cargo.toml not found in $BACKEND_DEPENDENCY_DIR"
fi
warn "Cargo.toml not found in silentdragonxlitelib source: $BACKEND_DEPENDENCY_DIR"
return
fi
if ! grep -Eq '^[[:space:]]*name[[:space:]]*=[[:space:]]*"silentdragonxlitelib"' "$BACKEND_DEPENDENCY_DIR/Cargo.toml"; then
if [[ "$BUILD_ARTIFACT" == true || "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
die "dependency path does not look like silentdragonxlitelib: $BACKEND_DEPENDENCY_DIR"
fi
warn "dependency path does not look like silentdragonxlitelib: $BACKEND_DEPENDENCY_DIR"
fi
}
# Ensure the Sapling proving params are present in the core crate (rust-embed bakes them in at build
# time). They are the fixed Zcash trusted-setup output — not buildable — so fetch + verify them from
# git.dragonx.is when absent. Override the source with SAPLING_PARAMS_BASE_URL.
SAPLING_PARAMS_BASE_URL="${SAPLING_PARAMS_BASE_URL:-https://git.dragonx.is/DragonX/zcash-params/releases/download/sapling-v1}"
ensure_sapling_params() {
local dir="$1"
[[ -n "$dir" ]] || return 0
mkdir -p "$dir"
local specs=(
"sapling-spend.params:8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"
"sapling-output.params:2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"
)
local spec name want path got
for spec in "${specs[@]}"; do
name="${spec%%:*}"; want="${spec##*:}"; path="$dir/$name"
if [[ -f "$path" ]] && [[ "$(compute_sha256 "$path")" == "$want" ]]; then
info "sapling param present and verified: $name"
continue
fi
info "fetching $name from $SAPLING_PARAMS_BASE_URL"
curl -fsSL "$SAPLING_PARAMS_BASE_URL/$name" -o "$path" || die "failed to download sapling param: $name"
got="$(compute_sha256 "$path")"
[[ "$got" == "$want" ]] || { rm -f "$path"; die "sapling param $name sha256 mismatch (got $got, want $want)"; }
info "downloaded and verified $name"
done
}
prepare_backend_source() {
BUILD_BACKEND_DIR="$BACKEND_SOURCE_DIR"
if [[ "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == false ]]; then
if [[ -f "$BACKEND_SOURCE_DIR/Cargo.toml" ]]; then
local configured_dependency_path
configured_dependency_path="$(backend_dependency_path_from_cargo "$BACKEND_SOURCE_DIR/Cargo.toml")"
if [[ -n "$configured_dependency_path" ]]; then
if [[ "$configured_dependency_path" = /* ]]; then
BACKEND_DEPENDENCY_DIR="$(canonical_dependency_path "$configured_dependency_path")"
warn "backend Cargo.toml uses an absolute silentdragonxlitelib path; use --silentdragonxlitelib-dir for portable builders"
else
BACKEND_DEPENDENCY_DIR="$(canonical_dependency_path "$BACKEND_SOURCE_DIR/$configured_dependency_path")"
info "using relative silentdragonxlitelib dependency at $BACKEND_DEPENDENCY_DIR"
fi
validate_backend_dependency_source
fi
fi
return
fi
[[ -f "$BACKEND_SOURCE_DIR/Cargo.toml" ]] || die "Cargo.toml not found in $BACKEND_SOURCE_DIR"
validate_backend_dependency_source
[[ "$BACKEND_DEPENDENCY_DIR" != *\"* ]] || die "--silentdragonxlitelib-dir path cannot contain a double quote"
local prepared_root="$OUT_DIR/.prepared-backend/$PLATFORM"
[[ "$prepared_root" == */.prepared-backend/* ]] || die "refusing unsafe prepared backend path: $prepared_root"
rm -rf "$prepared_root"
mkdir -p "$prepared_root"
ln -s "$BACKEND_SOURCE_DIR/src" "$prepared_root/src"
[[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock"
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
# Honor the pinned Rust toolchain (rust-toolchain.toml) inside the prepared root too,
# so builds using --silentdragonxlitelib-dir still select rustc 1.63.
[[ -f "$BACKEND_SOURCE_DIR/rust-toolchain.toml" ]] && ln -s "$BACKEND_SOURCE_DIR/rust-toolchain.toml" "$prepared_root/rust-toolchain.toml"
[[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw"
# Vendored crate deps (offline builds): the .cargo/config.toml's vendored-sources directory is
# "vendor" relative to the build root, so expose it inside the prepared root too.
[[ -d "$BACKEND_SOURCE_DIR/vendor" ]] && ln -s "$BACKEND_SOURCE_DIR/vendor" "$prepared_root/vendor"
[[ -f "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" ]] && ln -s "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" "$prepared_root/silentdragonxlitelib.h"
local replacement="silentdragonxlitelib = { path = \"$BACKEND_DEPENDENCY_DIR\" }"
awk -v replacement="$replacement" '
BEGIN { replaced = 0 }
/^[[:space:]]*silentdragonxlitelib[[:space:]]*=/ {
print replacement
replaced = 1
next
}
{ print }
END { if (replaced != 1) exit 42 }
' "$BACKEND_SOURCE_DIR/Cargo.toml" > "$prepared_root/Cargo.toml" \
|| die "failed to prepare backend Cargo.toml with portable silentdragonxlitelib path"
BUILD_BACKEND_DIR="$prepared_root"
info "prepared backend source at $BUILD_BACKEND_DIR with silentdragonxlitelib from $BACKEND_DEPENDENCY_DIR"
}
prepare_backend_source
artifact_kind() {
local name="${1##*/}"
case "$name" in
*.a|*.lib) printf 'static-library\n' ;;
*.so|*.dylib|*.dll) printf 'shared-library\n' ;;
*) printf 'unknown\n' ;;
esac
}
cargo_output_candidates() {
local cargo_target_root="$BUILD_BACKEND_DIR/target"
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
cargo_target_root="$CARGO_TARGET_DIR_VALUE"
fi
local base="$cargo_target_root/release"
if [[ -n "$RUST_TARGET" ]]; then
base="$cargo_target_root/$RUST_TARGET/release"
fi
case "$PLATFORM" in
linux)
printf '%s\n' "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.a" "$base/libsilentdragonxlite.so"
;;
windows)
printf '%s\n' "$base/silentdragonxlite.lib" "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.dll"
;;
macos)
printf '%s\n' "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.a" "$base/libsilentdragonxlite.dylib" "$base/silentdragonxlite.dylib"
;;
esac
}
source_revision_for() {
local dir="$1"
local revision_file
for revision_file in "$dir/DRAGONX_SOURCE_REVISION" "$dir/../DRAGONX_SOURCE_REVISION"; do
if [[ -f "$revision_file" ]]; then
sed -n '1p' "$revision_file"
return
fi
done
if git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git -C "$dir" rev-parse HEAD 2>/dev/null || printf 'unknown'
else
printf 'unknown'
fi
}
default_source_date_epoch() {
if git -C "$PROJECT_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git -C "$PROJECT_ROOT" log -1 --format=%ct 2>/dev/null || printf '0'
else
printf '0'
fi
}
append_rustflag() {
local rustflag="$1"
if [[ -n "${RUSTFLAGS:-}" ]]; then
export RUSTFLAGS="${RUSTFLAGS} ${rustflag}"
else
export RUSTFLAGS="$rustflag"
fi
}
append_rust_path_remap() {
local from_path="$1"
local to_path="$2"
[[ -n "$from_path" && -n "$to_path" ]] || return
append_rustflag "--remap-path-prefix=${from_path}=${to_path}"
}
apply_reproducible_rustflags() {
local cargo_target_root="$BUILD_BACKEND_DIR/target"
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
cargo_target_root="$CARGO_TARGET_DIR_VALUE"
fi
append_rust_path_remap "$PROJECT_ROOT" "/dragonx-project"
append_rust_path_remap "$BACKEND_SOURCE_DIR" "/dragonx-lite-backend"
if [[ "$BUILD_BACKEND_DIR" != "$BACKEND_SOURCE_DIR" ]]; then
append_rust_path_remap "$BUILD_BACKEND_DIR" "/dragonx-lite-backend"
fi
append_rust_path_remap "$BACKEND_DEPENDENCY_DIR" "/dragonx-lite-backend-dependency"
for path_remap in "${EXTRA_REMAP_PATH_PREFIXES[@]}"; do
append_rustflag "--remap-path-prefix=${path_remap}"
done
local cargo_home="${CARGO_HOME:-}"
if [[ -z "$cargo_home" && -n "${HOME:-}" ]]; then
cargo_home="$HOME/.cargo"
fi
if [[ -n "$cargo_home" && -d "$cargo_home" ]]; then
append_rust_path_remap "$cargo_home" "/cargo-home"
fi
append_rust_path_remap "$cargo_target_root" "/dragonx-lite-cargo-target"
}
build_with_cargo() {
command -v cargo >/dev/null 2>&1 || die "cargo was not found"
[[ -f "$BUILD_BACKEND_DIR/Cargo.toml" ]] || die "Cargo.toml not found in $BUILD_BACKEND_DIR"
if [[ -z "$SOURCE_DATE_EPOCH_VALUE" ]]; then
SOURCE_DATE_EPOCH_VALUE="$(default_source_date_epoch)"
fi
export CARGO_INCREMENTAL=0
export SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH_VALUE"
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
export CARGO_TARGET_DIR="$CARGO_TARGET_DIR_VALUE"
fi
if [[ "$REPRODUCIBLE" == true ]]; then
apply_reproducible_rustflags
fi
if [[ "$PLATFORM" == "windows" && -d "$BUILD_BACKEND_DIR/libsodium-mingw" ]]; then
export SODIUM_LIB_DIR="$BUILD_BACKEND_DIR/libsodium-mingw"
fi
[[ -n "$BACKEND_DEPENDENCY_DIR" ]] && ensure_sapling_params "$BACKEND_DEPENDENCY_DIR/zcash-params"
local cargo_cmd=(cargo build --locked --lib --release)
if [[ -n "$RUST_TARGET" ]]; then
cargo_cmd+=(--target "$RUST_TARGET")
fi
if [[ -n "$JOBS" ]]; then
cargo_cmd+=(-j "$JOBS")
fi
cargo_cmd+=("${EXTRA_CARGO_ARGS[@]}")
info "building backend in $BUILD_BACKEND_DIR"
(cd "$BUILD_BACKEND_DIR" && "${cargo_cmd[@]}")
while IFS= read -r candidate; do
if [[ -f "$candidate" ]]; then
ARTIFACT_PATH="$candidate"
return
fi
done < <(cargo_output_candidates)
die "cargo finished, but no expected backend artifact was found under $BUILD_BACKEND_DIR/target"
}
select_nm_tool() {
if [[ "$PLATFORM" == "windows" ]] && command -v x86_64-w64-mingw32-nm >/dev/null 2>&1; then
printf 'x86_64-w64-mingw32-nm\n'
return
fi
if command -v llvm-nm >/dev/null 2>&1; then
printf 'llvm-nm\n'
return
fi
if command -v nm >/dev/null 2>&1; then
printf 'nm\n'
return
fi
die "no symbol inventory tool found; install nm, llvm-nm, or x86_64-w64-mingw32-nm"
}
compute_sha256() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$file" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file" | awk '{print $1}'
else
die "sha256sum or shasum is required"
fi
}
json_escape() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
value="${value//$'\n'/\\n}"
value="${value//$'\r'/}"
value="${value//$'\t'/\\t}"
printf '"%s"' "$value"
}
json_array() {
local first=true
printf '['
for value in "$@"; do
if [[ "$first" == true ]]; then
first=false
else
printf ','
fi
json_escape "$value"
done
printf ']'
}
json_array_from_file() {
local file="$1"
local values=()
if [[ -f "$file" ]]; then
mapfile -t values < "$file"
fi
json_array "${values[@]}"
}
signature_metadata_requested() {
[[ "$SIGNATURE_REQUIRED" == true || \
-n "$SIGNATURE_FILE" || \
-n "$SIGNATURE_FORMAT" || \
-n "$SIGNATURE_VERIFICATION_TOOL" || \
-n "$SIGNATURE_VERIFICATION_COMMAND" || \
-n "$SIGNATURE_KEY_FINGERPRINT" || \
-n "$SIGNATURE_CERTIFICATE_IDENTITY" || \
-n "$SIGNATURE_CERTIFICATE_ISSUER" || \
-n "$SIGNATURE_TRANSPARENCY_LOG_URL" || \
-n "$SIGNATURE_VERIFIED_SHA256" ]]
}
validate_signature_metadata() {
SIGNATURE_REQUIRED_MANIFEST_VALUE=false
if [[ "$SIGNATURE_REQUIRED" == true ]]; then
SIGNATURE_REQUIRED_MANIFEST_VALUE=true
fi
if ! signature_metadata_requested; then
return
fi
[[ -n "$SIGNATURE_FILE" ]] || die "signature metadata requires --signature-file"
[[ -f "$SIGNATURE_FILE" ]] || die "signature file does not exist: $SIGNATURE_FILE"
[[ -n "$SIGNATURE_FORMAT" ]] || die "signature metadata requires --signature-format"
case "$SIGNATURE_FORMAT" in
minisign|gpg|sigstore|external|other) ;;
*) die "unsupported --signature-format: $SIGNATURE_FORMAT" ;;
esac
[[ -n "$SIGNATURE_VERIFICATION_TOOL" ]] || die "signature metadata requires --signature-verification-tool"
[[ -n "$SIGNATURE_VERIFIED_SHA256" ]] || die "signature metadata requires --signature-verified-sha256"
[[ "$SIGNATURE_VERIFIED_SHA256" == "$SHA256_DIGEST" ]] || die "signature verified SHA-256 does not match artifact SHA-256"
if [[ -z "$SIGNATURE_KEY_FINGERPRINT" && -z "$SIGNATURE_CERTIFICATE_IDENTITY" ]]; then
die "signature metadata requires --signature-key-fingerprint or --signature-certificate-identity"
fi
SIGNATURE_METADATA_PROVIDED=true
SIGNATURE_VERIFICATION_PERFORMED=true
SIGNATURE_VERIFICATION_STATUS="verified"
SIGNATURE_FILE_SHA256="$(compute_sha256 "$SIGNATURE_FILE")"
}
if [[ "$BUILD_ARTIFACT" == true ]]; then
build_with_cargo
fi
if [[ -z "$SOURCE_DATE_EPOCH_VALUE" ]]; then
SOURCE_DATE_EPOCH_VALUE="$(default_source_date_epoch)"
fi
[[ -f "$ARTIFACT_PATH" ]] || die "artifact not found: $ARTIFACT_PATH"
KIND="$(artifact_kind "$ARTIFACT_PATH")"
[[ "$KIND" != "unknown" ]] || die "artifact kind is unsupported: $ARTIFACT_PATH"
PLATFORM_OUT_DIR="$OUT_DIR/$PLATFORM"
mkdir -p "$PLATFORM_OUT_DIR"
ARTIFACT_NAME="$(basename "$ARTIFACT_PATH")"
ARTIFACT_OUTPUT="$PLATFORM_OUT_DIR/$ARTIFACT_NAME"
if [[ "$(absolute_path "$ARTIFACT_PATH")" != "$(absolute_path "$ARTIFACT_OUTPUT")" ]]; then
cp -p "$ARTIFACT_PATH" "$ARTIFACT_OUTPUT"
fi
SYMBOLS_FILE="$PLATFORM_OUT_DIR/lite-backend-symbols.txt"
RAW_SYMBOLS_FILE="$PLATFORM_OUT_DIR/lite-backend-symbols.raw.txt"
NM_TOOL="$(select_nm_tool)"
info "capturing exported symbols with $NM_TOOL"
if ! "$NM_TOOL" -g --defined-only "$ARTIFACT_OUTPUT" > "$RAW_SYMBOLS_FILE" 2> "$PLATFORM_OUT_DIR/lite-backend-symbols.err.txt"; then
die "symbol inventory failed; see $PLATFORM_OUT_DIR/lite-backend-symbols.err.txt"
fi
awk '{print $NF}' "$RAW_SYMBOLS_FILE" \
| sed 's/^_//' \
| grep -E '^(litelib_[A-Za-z0-9_]*|blake3_PW)$' \
| sort -u > "$SYMBOLS_FILE" || true
[[ -s "$SYMBOLS_FILE" ]] || die "no SDXL C ABI symbols were found in $ARTIFACT_OUTPUT"
MISSING_SYMBOLS=()
for required in "${REQUIRED_SYMBOLS[@]}"; do
if ! grep -Fxq "$required" "$SYMBOLS_FILE"; then
MISSING_SYMBOLS+=("$required")
fi
done
if [[ ${#MISSING_SYMBOLS[@]} -ne 0 ]]; then
printf '%s\n' "${MISSING_SYMBOLS[@]}" > "$PLATFORM_OUT_DIR/lite-backend-missing-symbols.txt"
die "artifact is missing required symbols; see $PLATFORM_OUT_DIR/lite-backend-missing-symbols.txt"
fi
SHA256_DIGEST="$(compute_sha256 "$ARTIFACT_OUTPUT")"
validate_signature_metadata
ARTIFACT_SIZE_BYTES="$(wc -c < "$ARTIFACT_OUTPUT" | tr -d ' ')"
PROJECT_REVISION="$(source_revision_for "$PROJECT_ROOT")"
BACKEND_REVISION="$(source_revision_for "$BACKEND_SOURCE_DIR")"
BACKEND_DEPENDENCY_REVISION=""
if [[ -n "$BACKEND_DEPENDENCY_DIR" ]]; then
BACKEND_DEPENDENCY_REVISION="$(source_revision_for "$BACKEND_DEPENDENCY_DIR")"
fi
ARTIFACT_SET_ID="$PLATFORM-${SHA256_DIGEST:0:16}"
REPRODUCIBLE_MANIFEST_VALUE=false
if [[ "$BUILD_ARTIFACT" == true && "$REPRODUCIBLE" == true ]]; then
REPRODUCIBLE_MANIFEST_VALUE=true
fi
PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE=false
if [[ "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE=true
fi
FILE_DESCRIPTION="unknown"
if command -v file >/dev/null 2>&1; then
FILE_DESCRIPTION="$(file -b "$ARTIFACT_OUTPUT")"
fi
MANIFEST_FILE="$PLATFORM_OUT_DIR/lite-backend-artifact-manifest.json"
{
printf '{\n'
printf ' "schema": "dragonx.lite.backend-artifact.v1",\n'
printf ' "generated_by": "scripts/build-lite-backend-artifact.sh",\n'
printf ' "read_only_inventory": true,\n'
printf ' "artifact_mutation_requested": false,\n'
printf ' "upload_requested": false,\n'
printf ' "signing_requested": false,\n'
printf ' "publication_requested": false,\n'
printf ' "signature_verification": {\n'
printf ' "policy_name": '; json_escape "$SIGNATURE_POLICY_NAME"; printf ',\n'
printf ' "policy_defined": %s,\n' "$SIGNATURE_POLICY_DEFINED_MANIFEST_VALUE"
printf ' "required_for_release": %s,\n' "$SIGNATURE_REQUIRED_MANIFEST_VALUE"
printf ' "metadata_read_only": true,\n'
printf ' "metadata_provided": %s,\n' "$SIGNATURE_METADATA_PROVIDED"
printf ' "verification_performed": %s,\n' "$SIGNATURE_VERIFICATION_PERFORMED"
printf ' "verification_status": '; json_escape "$SIGNATURE_VERIFICATION_STATUS"; printf ',\n'
printf ' "signature_format": '; json_escape "$SIGNATURE_FORMAT"; printf ',\n'
printf ' "signature_path": '; json_escape "$SIGNATURE_FILE"; printf ',\n'
printf ' "signature_file_sha256": '; json_escape "$SIGNATURE_FILE_SHA256"; printf ',\n'
printf ' "verification_tool": '; json_escape "$SIGNATURE_VERIFICATION_TOOL"; printf ',\n'
printf ' "verification_command": '; json_escape "$SIGNATURE_VERIFICATION_COMMAND"; printf ',\n'
printf ' "key_fingerprint": '; json_escape "$SIGNATURE_KEY_FINGERPRINT"; printf ',\n'
printf ' "certificate_identity": '; json_escape "$SIGNATURE_CERTIFICATE_IDENTITY"; printf ',\n'
printf ' "certificate_issuer": '; json_escape "$SIGNATURE_CERTIFICATE_ISSUER"; printf ',\n'
printf ' "transparency_log_url": '; json_escape "$SIGNATURE_TRANSPARENCY_LOG_URL"; printf ',\n'
printf ' "verified_artifact_sha256": '; json_escape "$SIGNATURE_VERIFIED_SHA256"; printf '\n'
printf ' },\n'
printf ' "abi_version": '; json_escape "$ABI_VERSION"; printf ',\n'
printf ' "link_mode": '; json_escape "$LINK_MODE"; printf ',\n'
printf ' "platform": '; json_escape "$PLATFORM"; printf ',\n'
printf ' "rust_target": '; json_escape "$RUST_TARGET"; printf ',\n'
printf ' "artifact": {\n'
printf ' "path": '; json_escape "$ARTIFACT_OUTPUT"; printf ',\n'
printf ' "kind": '; json_escape "$KIND"; printf ',\n'
printf ' "size_bytes": %s,\n' "$ARTIFACT_SIZE_BYTES"
printf ' "sha256": '; json_escape "$SHA256_DIGEST"; printf ',\n'
printf ' "file_description": '; json_escape "$FILE_DESCRIPTION"; printf '\n'
printf ' },\n'
printf ' "symbol_inventory": {\n'
printf ' "tool": '; json_escape "$NM_TOOL"; printf ',\n'
printf ' "symbols_path": '; json_escape "$SYMBOLS_FILE"; printf ',\n'
printf ' "raw_symbols_path": '; json_escape "$RAW_SYMBOLS_FILE"; printf ',\n'
printf ' "required_symbols": '; json_array "${REQUIRED_SYMBOLS[@]}"; printf ',\n'
printf ' "exported_symbols": '; json_array_from_file "$SYMBOLS_FILE"; printf ',\n'
printf ' "missing_required_symbols": []\n'
printf ' },\n'
printf ' "provenance": {\n'
printf ' "owner_ready": true,\n'
printf ' "built_from_source": true,\n'
printf ' "metadata_provided": true,\n'
printf ' "source": '; json_escape "$BACKEND_SOURCE_DIR"; printf ',\n'
printf ' "cargo_build_source": '; json_escape "$BUILD_BACKEND_DIR"; printf ',\n'
printf ' "portable_dependency_override": %s,\n' "$PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE"
printf ' "silentdragonxlitelib_source": '; json_escape "$BACKEND_DEPENDENCY_DIR"; printf ',\n'
printf ' "builder": '; json_escape "$BUILDER"; printf ',\n'
printf ' "source_revision": '; json_escape "$BACKEND_REVISION"; printf ',\n'
printf ' "silentdragonxlitelib_revision": '; json_escape "$BACKEND_DEPENDENCY_REVISION"; printf ',\n'
printf ' "project_revision": '; json_escape "$PROJECT_REVISION"; printf ',\n'
printf ' "artifact_set_id": '; json_escape "$ARTIFACT_SET_ID"; printf ',\n'
printf ' "source_date_epoch": '; json_escape "$SOURCE_DATE_EPOCH_VALUE"; printf ',\n'
printf ' "reproducible": %s,\n' "$REPRODUCIBLE_MANIFEST_VALUE"
printf ' "redacted": true\n'
printf ' }\n'
printf '}\n'
} > "$MANIFEST_FILE"
info "artifact: $ARTIFACT_OUTPUT"
info "symbols: $SYMBOLS_FILE"
info "manifest: $MANIFEST_FILE"
info "sha256: $SHA256_DIGEST"
cat <<EOF
CMake configure example:
cmake -S "$PROJECT_ROOT" -B "$PROJECT_ROOT/build/lite" \\
-DDRAGONX_BUILD_LITE=ON \\
-DDRAGONX_ENABLE_LITE_BACKEND=ON \\
-DDRAGONX_LITE_BACKEND_LIBRARY="$ARTIFACT_OUTPUT" \\
-DDRAGONX_LITE_BACKEND_SYMBOLS_FILE="$SYMBOLS_FILE" \\
-DDRAGONX_LITE_BACKEND_MANIFEST="$MANIFEST_FILE" \\
-DDRAGONX_LITE_BACKEND_LINK_MODE=$LINK_MODE \\
-DDRAGONX_LITE_BACKEND_ABI=$ABI_VERSION
EOF

View File

@@ -1,87 +0,0 @@
#!/usr/bin/env python3
"""
Build a monochrome Noto Emoji subset for the chat/message UI.
Dear ImGui rasterizes fonts with stb_truetype, which handles only monochrome
(outline `glyf`) fonts — NOT color emoji (CBDT/sbix/COLR). So we use Google's
*monochrome* Noto Emoji (github.com/google/fonts, ofl/notoemoji, OFL-licensed)
and merge it into the text fonts (see Typography::loadFont, the block after the
CJK merge). ImGui renders one glyph per codepoint with no shaping, so ZWJ
sequences / regional-indicator flags won't compose — single-codepoint emoji
(😀 🎉 ❤ 🔥 👍 …) render fine, which covers the overwhelming majority of use.
Source is the variable font pinned to wght=400 → static, then subset to the
emoji planes plus the higher symbol/star ranges the base UI font doesn't cover.
The base Ubuntu font already owns U+260026FF etc.; ImGui's MergeMode gives the
first-loaded glyph precedence, so those stay text-styled and only the codepoints
the base lacks fall through to this font.
Get the source once (OFL, redistributable):
curl -fsSL -o /tmp/NotoEmoji-VF.ttf \
'https://github.com/google/fonts/raw/main/ofl/notoemoji/NotoEmoji%5Bwght%5D.ttf'
Then: python3 scripts/build_emoji_subset.py
Output: res/fonts/NotoEmoji-Subset.ttf (committed; embedded via INCBIN)
"""
import os
from fontTools import ttLib, subset
from fontTools.varLib.instancer import instantiateVariableFont
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SOURCE_VF = '/tmp/NotoEmoji-VF.ttf'
STATIC = '/tmp/NotoEmoji-Static.ttf'
OUTPUT = os.path.join(ROOT, 'res', 'fonts', 'NotoEmoji-Subset.ttf')
# Emoji codepoints to keep. Ranges are inclusive.
RANGES = [
(0x1F000, 0x1FAFF), # all the main emoji planes (emoticons, pictographs, transport, supplement, extended)
(0x2600, 0x27BF), # Miscellaneous Symbols + Dingbats
(0x2B00, 0x2BFF), # stars (⭐ 2B50) and misc arrows
(0xFE00, 0xFE0F), # variation selectors (VS16 emoji-style)
(0x2194, 0x21AA), # arrows used as emoji
(0x231A, 0x231B), # ⌚ ⌛
(0x23E9, 0x23FA), # media-control emoji
(0x25AA, 0x25FE), # small squares
]
SINGLES = [0x200D, 0x2934, 0x2935, 0x3030, 0x303D, 0x3297, 0x3299,
0x00A9, 0x00AE, 0x2122, 0x2139, 0x24C2]
def main():
if not os.path.exists(SOURCE_VF):
raise SystemExit(f"missing source font {SOURCE_VF} — see the header for the curl command")
# 1. Pin the weight axis so stb_truetype rasterizes a clean static instance.
f = ttLib.TTFont(SOURCE_VF)
if 'fvar' in f:
instantiateVariableFont(f, {'wght': 400}, inplace=True)
f.save(STATIC)
unicodes = list(SINGLES)
for lo, hi in RANGES:
unicodes.extend(range(lo, hi + 1))
opts = subset.Options()
opts.layout_features = [] # ImGui does no shaping — drop GSUB/GPOS
opts.name_IDs = []
opts.notdef_outline = True
opts.glyph_names = False
opts.drop_tables = ['GSUB', 'GPOS', 'GDEF', 'morx', 'kern']
font = subset.load_font(STATIC, opts)
ss = subset.Subsetter(options=opts)
ss.populate(unicodes=unicodes)
ss.subset(font)
subset.save_font(font, OUTPUT, opts)
out = ttLib.TTFont(OUTPUT)
cmap = out.getBestCmap()
color = any(t in out.reader.keys() for t in ('CBDT', 'sbix', 'COLR'))
print(f"Output: {OUTPUT}")
print(f"Size: {os.path.getsize(OUTPUT)//1024} KB | glyphs: {len(cmap)} | color tables: {color}")
if color:
raise SystemExit("ERROR: subset has color tables — stb_truetype cannot render it")
if __name__ == '__main__':
main()

View File

@@ -1,56 +0,0 @@
#!/bin/bash
# Source-tree hygiene guard.
#
# Blocks two failure modes that an AI coding session previously introduced in
# src/wallet/ (the lite-wallet "_plan"/"_batch" churn): pathologically long
# filenames (which also break the Windows MAX_PATH 260-char limit during the
# cross-build) and the runaway "receipt/custody/handoff/stewardship" naming
# explosion where each session wrapped the previous artifact in one more layer.
#
# Usage:
# scripts/check-source-hygiene.sh # check working-tree src/
# scripts/check-source-hygiene.sh --staged # check staged files (pre-commit)
#
# Install as a git pre-commit hook:
# ln -sf ../../scripts/check-source-hygiene.sh .git/hooks/pre-commit
# # (the hook invokes it with --staged automatically when named pre-commit)
set -euo pipefail
MAX_LEN=80
# Naming-explosion tokens. Two or more chained in one basename is the smell.
CHURN_RE='receipt|custody|handoff|stewardship|promotion_activation|acceptance_confirmation|archive_handoff|post_closure'
mode="${1:-}"
if [[ "$mode" == "--staged" || "$(basename "$0")" == "pre-commit" ]]; then
mapfile -t files < <(git diff --cached --name-only --diff-filter=AR | grep -E '\.(cpp|h|hpp|cc)$' || true)
else
mapfile -t files < <(git ls-files 'src/**/*.cpp' 'src/**/*.h' 2>/dev/null; \
find src -type f \( -name '*.cpp' -o -name '*.h' \) 2>/dev/null)
# de-dup
mapfile -t files < <(printf '%s\n' "${files[@]}" | sort -u)
fi
fail=0
for f in "${files[@]}"; do
[[ -z "$f" ]] && continue
base="$(basename "$f")"
len=${#base}
if (( len > MAX_LEN )); then
echo "✗ filename too long ($len > $MAX_LEN chars): $f" >&2
fail=1
fi
# count distinct churn tokens in the basename ( || true: grep exits 1 on no match)
n=$(printf '%s' "$base" | grep -oE "$CHURN_RE" | sort -u | wc -l || true)
if (( n >= 2 )); then
echo "✗ runaway naming pattern ($n churn tokens) — refactor in place, don't add a layer: $f" >&2
fail=1
fi
done
if (( fail )); then
echo "" >&2
echo "Source hygiene check failed. See docs in scripts/check-source-hygiene.sh." >&2
exit 1
fi
echo "source hygiene OK (${#files[@]} files checked)"

View File

@@ -24,27 +24,18 @@ if [ ! -f "${BUILD_DIR}/bin/ObsidianDragon" ]; then
exit 1
fi
# Check for appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
# The old "continuous" tag is a moving, unverified network download that runs on the release
# builder; verify it or refuse to package.
APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
# Check for appimagetool
APPIMAGETOOL=""
if command -v appimagetool &> /dev/null; then
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
APPIMAGETOOL="appimagetool"
elif [ -f "${BUILD_DIR}/appimagetool-x86_64.AppImage" ]; then
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
else
AT="${BUILD_DIR}/appimagetool-x86_64.AppImage"
if [ ! -f "$AT" ] || ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
print_status "Downloading appimagetool 1.9.0 (pinned)..."
wget -q -O "$AT" "$APPIMAGETOOL_URL"
if ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
print_error "appimagetool SHA-256 verification failed — refusing to use it"
rm -f "$AT"
exit 1
fi
chmod +x "$AT"
fi
APPIMAGETOOL="$AT"
print_status "Downloading appimagetool..."
wget -q -O "${BUILD_DIR}/appimagetool-x86_64.AppImage" \
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x "${BUILD_DIR}/appimagetool-x86_64.AppImage"
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
fi
print_status "Creating AppDir structure..."

View File

@@ -1,35 +0,0 @@
#!/usr/bin/env bash
# Generate SDXL lite-wallet mainnet checkpoint entries from a fully-synced dragonxd.
# Each entry is (height,"blockhash","serialized_sapling_tree") in checkpoints.rs format.
# Fills the 1,770,000 -> tip gap so wallets reseed close to their birthday on rescan,
# bounding the (divergence-prone) compact-block replay span. Usage:
# scripts/gen-lite-checkpoints.sh [start] [step] > /tmp/new_checkpoints.txt
set -euo pipefail
CLI=${DRAGONX_CLI:-/home/d/dragonx/src/dragonx-cli}
START=${1:-1770000}
STEP=${2:-10000}
tip=$("$CLI" getblockcount)
end=$(( (tip / STEP) * STEP ))
# Sanity: confirm the method reproduces a KNOWN checkpoint tree before trusting it.
ref_hash=$("$CLI" getblockhash 1760000 | tr -d '"[:space:]')
ref_tree=$("$CLI" getblockmerkletree 1760000 | tr -d '"[:space:]')
expect_hash="0000545a45b8d4ee4e4b423cb1ea74d67e3a04c320c6ea2f59ee06c08f91a117"
if [ "$ref_hash" != "$expect_hash" ]; then
echo "ABORT: getblockhash 1760000 = $ref_hash != known $expect_hash" >&2; exit 1
fi
echo "# self-check: 1760000 hash matches; tree len=${#ref_tree}" >&2
n=0
h=$START
while [ "$h" -le "$end" ]; do
hash=$("$CLI" getblockhash "$h" | tr -d '"[:space:]')
tree=$("$CLI" getblockmerkletree "$h" | tr -d '"[:space:]')
if [ -z "$hash" ] || [ -z "$tree" ]; then echo "ABORT: empty hash/tree at $h" >&2; exit 1; fi
printf '\t(%s,"%s",\n\t\t"%s"\n\t),\n' "$h" "$hash" "$tree"
n=$((n+1))
h=$((h+STEP))
done
echo "# generated $n checkpoints from $START to $end (tip=$tip)" >&2

645
scripts/gen_de.py Normal file
View File

@@ -0,0 +1,645 @@
#!/usr/bin/env python3
"""Generate German (de) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "24h Änderung",
"24h_volume": "24h Volumen",
"about": "Über",
"about_block_explorer": "Block-Explorer",
"about_block_height": "Blockhöhe:",
"about_build_date": "Erstellungsdatum:",
"about_build_type": "Build-Typ:",
"about_chain": "Chain:",
"about_connections": "Verbindungen:",
"about_credits": "Danksagungen",
"about_daemon": "Daemon:",
"about_debug": "Debug",
"about_dragonx": "Über ObsidianDragon",
"about_edition": "ImGui Edition",
"about_github": "GitHub",
"about_imgui": "ImGui:",
"about_license": "Lizenz",
"about_license_text": "Diese Software wird unter der GNU General Public License v3 (GPLv3) veröffentlicht. Sie dürfen diese Software gemäß den Lizenzbedingungen frei verwenden, modifizieren und verbreiten.",
"about_peers_count": "%zu Peers",
"about_release": "Release",
"about_title": "Über ObsidianDragon",
"about_version": "Version:",
"about_website": "Webseite",
"acrylic": "Acryl",
"add": "Hinzufügen",
"address": "Adresse",
"address_book_add": "Adresse hinzufügen",
"address_book_add_new": "Neue hinzufügen",
"address_book_added": "Adresse zum Buch hinzugefügt",
"address_book_count": "%zu Adressen gespeichert",
"address_book_deleted": "Eintrag gelöscht",
"address_book_edit": "Adresse bearbeiten",
"address_book_empty": "Keine gespeicherten Adressen. Klicken Sie auf 'Neue hinzufügen', um eine hinzuzufügen.",
"address_book_exists": "Adresse existiert bereits im Buch",
"address_book_title": "Adressbuch",
"address_book_update_failed": "Aktualisierung fehlgeschlagen - Adresse könnte doppelt sein",
"address_book_updated": "Adresse aktualisiert",
"address_copied": "Adresse in Zwischenablage kopiert",
"address_details": "Adressdetails",
"address_label": "Adresse:",
"address_upper": "ADRESSE",
"address_url": "Adress-URL",
"addresses_appear_here": "Ihre Empfangsadressen erscheinen hier, sobald Sie verbunden sind.",
"advanced": "ERWEITERT",
"all_filter": "Alle",
"allow_custom_fees": "Benutzerdefinierte Gebühren erlauben",
"amount": "Betrag",
"amount_details": "BETRAGSDETAILS",
"amount_exceeds_balance": "Betrag übersteigt Guthaben",
"amount_label": "Betrag:",
"appearance": "ERSCHEINUNGSBILD",
"auto_shield": "Mining automatisch abschirmen",
"available": "Verfügbar",
"backup_backing_up": "Sicherung läuft...",
"backup_create": "Sicherung erstellen",
"backup_created": "Wallet-Sicherung erstellt",
"backup_data": "SICHERUNG & DATEN",
"backup_description": "Erstellen Sie eine Sicherung Ihrer wallet.dat-Datei. Diese Datei enthält alle Ihre privaten Schlüssel und den Transaktionsverlauf. Bewahren Sie die Sicherung an einem sicheren Ort auf.",
"backup_destination": "Sicherungsziel:",
"backup_tip_external": "Speichern Sie Sicherungen auf externen Laufwerken oder Cloud-Speicher",
"backup_tip_multiple": "Erstellen Sie mehrere Sicherungen an verschiedenen Orten",
"backup_tip_test": "Testen Sie regelmäßig die Wiederherstellung aus der Sicherung",
"backup_tips": "Tipps:",
"backup_title": "Wallet sichern",
"backup_wallet": "Wallet sichern...",
"backup_wallet_not_found": "Warnung: wallet.dat nicht am erwarteten Speicherort gefunden",
"balance": "Guthaben",
"balance_layout": "Guthaben-Layout",
"ban": "Sperren",
"banned_peers": "Gesperrte Peers",
"block": "Block",
"block_bits": "Bits:",
"block_click_next": "Klicken für nächsten Block",
"block_click_prev": "Klicken für vorherigen Block",
"block_explorer": "Block-Explorer",
"block_get_info": "Block-Info abrufen",
"block_hash": "Block-Hash:",
"block_height": "Blockhöhe:",
"block_info_title": "Block-Informationen",
"block_merkle_root": "Merkle-Root:",
"block_nav_next": "Weiter >>",
"block_nav_prev": "<< Zurück",
"block_next": "Nächster Block:",
"block_previous": "Vorheriger Block:",
"block_size": "Größe:",
"block_timestamp": "Zeitstempel:",
"block_transactions": "Transaktionen:",
"blockchain_syncing": "Blockchain synchronisiert (%.1f%%)... Guthaben könnten ungenau sein.",
"cancel": "Abbrechen",
"characters": "Zeichen",
"clear": "Leeren",
"clear_all_bans": "Alle Sperren aufheben",
"clear_form_confirm": "Alle Formularfelder leeren?",
"clear_request": "Anfrage leeren",
"click_copy_address": "Klicken zum Kopieren der Adresse",
"click_copy_uri": "Klicken zum Kopieren der URI",
"close": "Schließen",
"conf_count": "%d Best.",
"confirm_and_send": "Bestätigen & Senden",
"confirm_send": "Senden bestätigen",
"confirm_transaction": "Transaktion bestätigen",
"confirmations": "Bestätigungen",
"confirmations_display": "%d Bestätigungen | %s",
"confirmed": "Bestätigt",
"connected": "Verbunden",
"connected_peers": "Verbundene Peers",
"connecting": "Verbinde...",
"console": "Konsole",
"console_auto_scroll": "Automatisch scrollen",
"console_available_commands": "Verfügbare Befehle:",
"console_capturing_output": "Erfasse Daemon-Ausgabe...",
"console_clear": "Leeren",
"console_clear_console": "Konsole leeren",
"console_cleared": "Konsole geleert",
"console_click_commands": "Befehle oben klicken zum Einfügen",
"console_click_insert": "Klicken zum Einfügen",
"console_click_insert_params": "Klicken zum Einfügen mit Parametern",
"console_close": "Schließen",
"console_commands": "Befehle",
"console_common_rpc": "Häufige RPC-Befehle:",
"console_completions": "Vervollständigungen:",
"console_connected": "Verbunden mit Daemon",
"console_copy_all": "Alles kopieren",
"console_copy_selected": "Kopieren",
"console_daemon": "Daemon",
"console_daemon_error": "Daemon-Fehler!",
"console_daemon_started": "Daemon gestartet",
"console_daemon_stopped": "Daemon gestoppt",
"console_disconnected": "Vom Daemon getrennt",
"console_errors": "Fehler",
"console_filter_hint": "Ausgabe filtern...",
"console_help_clear": " clear - Konsole leeren",
"console_help_getbalance": " getbalance - Transparentes Guthaben anzeigen",
"console_help_getblockcount": " getblockcount - Aktuelle Blockhöhe anzeigen",
"console_help_getinfo": " getinfo - Knoteninformationen anzeigen",
"console_help_getmininginfo": " getmininginfo - Mining-Status anzeigen",
"console_help_getpeerinfo": " getpeerinfo - Verbundene Peers anzeigen",
"console_help_gettotalbalance": " gettotalbalance - Gesamtguthaben anzeigen",
"console_help_help": " help - Diese Hilfe anzeigen",
"console_help_setgenerate": " setgenerate - Mining steuern",
"console_help_stop": " stop - Daemon stoppen",
"console_line_count": "%zu Zeilen",
"console_new_lines": "%d neue Zeilen",
"console_no_daemon": "Kein Daemon",
"console_not_connected": "Fehler: Nicht mit Daemon verbunden",
"console_rpc_reference": "RPC-Befehlsreferenz",
"console_scanline": "Konsolen-Scanline",
"console_search_commands": "Befehle suchen...",
"console_select_all": "Alles auswählen",
"console_show_daemon_output": "Daemon-Ausgabe anzeigen",
"console_show_errors_only": "Nur Fehler anzeigen",
"console_show_rpc_ref": "RPC-Befehlsreferenz anzeigen",
"console_showing_lines": "Zeige %zu von %zu Zeilen",
"console_starting_node": "Knoten wird gestartet...",
"console_status_error": "Fehler",
"console_status_running": "Läuft",
"console_status_starting": "Startet",
"console_status_stopped": "Gestoppt",
"console_status_stopping": "Stoppt",
"console_status_unknown": "Unbekannt",
"console_tab_completion": "Tab zur Vervollständigung",
"console_type_help": "Geben Sie 'help' ein für verfügbare Befehle",
"console_welcome": "Willkommen bei ObsidianDragon Konsole",
"console_zoom_in": "Vergrößern",
"console_zoom_out": "Verkleinern",
"copy": "Kopieren",
"copy_address": "Vollständige Adresse kopieren",
"copy_error": "Fehler kopieren",
"copy_to_clipboard": "In Zwischenablage kopieren",
"copy_txid": "TxID kopieren",
"copy_uri": "URI kopieren",
"current_price": "Aktueller Preis",
"custom_fees": "Benutzerdefinierte Gebühren",
"dark": "Dunkel",
"date": "Datum",
"date_label": "Datum:",
"delete": "Löschen",
"difficulty": "Schwierigkeit",
"disconnected": "Getrennt",
"dismiss": "Verwerfen",
"display": "Anzeige",
"dragonx_green": "DragonX (Grün)",
"edit": "Bearbeiten",
"error": "Fehler",
"est_time_to_block": "Gesch. Zeit bis Block",
"exit": "Beenden",
"explorer": "EXPLORER",
"export": "Exportieren",
"export_csv": "CSV exportieren",
"export_keys_btn": "Schlüssel exportieren",
"export_keys_danger": "ACHTUNG: Dies exportiert ALLE privaten Schlüssel aus Ihrer Wallet! Jeder mit Zugriff auf diese Datei kann Ihre Gelder stehlen. Sicher aufbewahren und nach Gebrauch löschen.",
"export_keys_include_t": "T-Adressen einschließen (transparent)",
"export_keys_include_z": "Z-Adressen einschließen (abgeschirmt)",
"export_keys_options": "Export-Optionen:",
"export_keys_success": "Schlüssel erfolgreich exportiert",
"export_keys_title": "Alle privaten Schlüssel exportieren",
"export_private_key": "Privaten Schlüssel exportieren",
"export_tx_count": "%zu Transaktionen als CSV exportieren.",
"export_tx_file_fail": "CSV-Datei konnte nicht erstellt werden",
"export_tx_none": "Keine Transaktionen zum Exportieren",
"export_tx_success": "Transaktionen erfolgreich exportiert",
"export_tx_title": "Transaktionen als CSV exportieren",
"export_viewing_key": "Betrachtungsschlüssel exportieren",
"failed_create_shielded": "Abgeschirmte Adresse konnte nicht erstellt werden",
"failed_create_transparent": "Transparente Adresse konnte nicht erstellt werden",
"fee": "Gebühr",
"fee_high": "Hoch",
"fee_label": "Gebühr:",
"fee_low": "Niedrig",
"fee_normal": "Normal",
"fetch_prices": "Preise abrufen",
"file": "Datei",
"file_save_location": "Datei wird gespeichert in: ~/.config/ObsidianDragon/",
"font_scale": "Schriftgröße",
"from": "Von",
"from_upper": "VON",
"full_details": "Alle Details",
"general": "Allgemein",
"go_to_receive": "Zum Empfangen",
"height": "Höhe",
"help": "Hilfe",
"hide": "Ausblenden",
"history": "Verlauf",
"immature_type": "Unreif",
"import": "Importieren",
"import_key_btn": "Schlüssel importieren",
"import_key_formats": "Unterstützte Schlüsselformate:",
"import_key_full_rescan": "(0 = vollständiger Rescan)",
"import_key_label": "Privater Schlüssel:",
"import_key_no_valid": "Keine gültigen Schlüssel in der Eingabe gefunden",
"import_key_rescan": "Blockchain nach Import neu scannen",
"import_key_start_height": "Starthöhe:",
"import_key_success": "Schlüssel erfolgreich importiert",
"import_key_t_format": "T-Adresse WIF private Schlüssel",
"import_key_title": "Privaten Schlüssel importieren",
"import_key_tooltip": "Geben Sie einen oder mehrere private Schlüssel ein, einen pro Zeile.\nUnterstützt sowohl z-Adresse als auch t-Adresse Schlüssel.\nZeilen die mit # beginnen werden als Kommentare behandelt.",
"import_key_warning": "Warnung: Teilen Sie niemals Ihre privaten Schlüssel! Das Importieren von Schlüsseln aus nicht vertrauenswürdigen Quellen kann Ihr Wallet gefährden.",
"import_key_z_format": "Z-Adresse Ausgabeschlüssel (secret-extended-key-...)",
"import_private_key": "Privaten Schlüssel importieren...",
"invalid_address": "Ungültiges Adressformat",
"ip_address": "IP-Adresse",
"keep": "Behalten",
"keep_daemon": "Daemon weiterlaufen lassen",
"key_export_fetching": "Schlüssel wird aus Wallet abgerufen...",
"key_export_private_key": "Privater Schlüssel:",
"key_export_private_warning": "Halten Sie diesen Schlüssel GEHEIM! Jeder mit diesem Schlüssel kann Ihre Gelder ausgeben. Teilen Sie ihn niemals online oder mit nicht vertrauenswürdigen Parteien.",
"key_export_reveal": "Schlüssel anzeigen",
"key_export_viewing_key": "Betrachtungsschlüssel:",
"key_export_viewing_warning": "Dieser Betrachtungsschlüssel ermöglicht es anderen, Ihre eingehenden Transaktionen und Ihr Guthaben zu sehen, aber NICHT Ihre Gelder auszugeben. Teilen Sie ihn nur mit vertrauenswürdigen Parteien.",
"label": "Bezeichnung:",
"language": "Sprache",
"light": "Hell",
"loading": "Laden...",
"loading_addresses": "Adressen werden geladen...",
"local_hashrate": "Lokale Hashrate",
"low_spec_mode": "Energiesparmodus",
"market": "Markt",
"market_12h": "12h",
"market_18h": "18h",
"market_24h": "24h",
"market_24h_volume": "24H VOLUMEN",
"market_6h": "6h",
"market_attribution": "Preisdaten von NonKYC",
"market_btc_price": "BTC PREIS",
"market_cap": "Marktkapitalisierung",
"market_no_history": "Kein Preisverlauf verfügbar",
"market_no_price": "Keine Preisdaten",
"market_now": "Jetzt",
"market_pct_shielded": "%.0f%% Abgeschirmt",
"market_portfolio": "PORTFOLIO",
"market_price_unavailable": "Preisdaten nicht verfügbar",
"market_refresh_price": "Preisdaten aktualisieren",
"market_trade_on": "Handeln auf %s",
"mature": "Reif",
"max": "Max",
"memo": "Memo (optional, verschlüsselt)",
"memo_label": "Memo:",
"memo_optional": "MEMO (OPTIONAL)",
"memo_upper": "MEMO",
"memo_z_only": "Hinweis: Memos sind nur beim Senden an abgeschirmte (z) Adressen verfügbar",
"merge_description": "Mehrere UTXOs zu einer einzelnen abgeschirmten Adresse zusammenführen. Dies kann die Wallet-Größe reduzieren und die Privatsphäre verbessern.",
"merge_funds": "Gelder zusammenführen",
"merge_started": "Zusammenführung gestartet",
"merge_title": "An Adresse zusammenführen",
"mine_when_idle": "Im Leerlauf minen",
"mined": "gemined",
"mined_filter": "Gemined",
"mined_type": "Gemined",
"mined_upper": "GEMINED",
"miner_fee": "Miner-Gebühr",
"mining": "Mining",
"mining_active": "Aktiv",
"mining_address_copied": "Mining-Adresse kopiert",
"mining_all_time": "Gesamt",
"mining_already_saved": "Pool-URL bereits gespeichert",
"mining_block_copied": "Block-Hash kopiert",
"mining_chart_1m_ago": "vor 1m",
"mining_chart_5m_ago": "vor 5m",
"mining_chart_now": "Jetzt",
"mining_click": "Klicken",
"mining_click_copy_address": "Klicken zum Kopieren der Adresse",
"mining_click_copy_block": "Klicken zum Kopieren des Block-Hash",
"mining_click_copy_difficulty": "Klicken zum Kopieren der Schwierigkeit",
"mining_connected": "Verbunden",
"mining_connecting": "Verbinde...",
"mining_control": "Mining-Steuerung",
"mining_difficulty_copied": "Schwierigkeit kopiert",
"mining_est_block": "Gesch. Block",
"mining_est_daily": "Gesch. täglich",
"mining_filter_all": "Alle",
"mining_filter_tip_all": "Alle Einnahmen anzeigen",
"mining_filter_tip_pool": "Nur Pool-Einnahmen anzeigen",
"mining_filter_tip_solo": "Nur Solo-Einnahmen anzeigen",
"mining_idle_off_tooltip": "Leerlauf-Mining aktivieren",
"mining_idle_on_tooltip": "Leerlauf-Mining deaktivieren",
"mining_local_hashrate": "Lokale Hashrate",
"mining_mine": "Minen",
"mining_mining_addr": "Mining-Adr.",
"mining_network": "Netzwerk",
"mining_no_blocks_yet": "Noch keine Blöcke gefunden",
"mining_no_payouts_yet": "Noch keine Pool-Auszahlungen",
"mining_no_saved_addresses": "Keine gespeicherten Adressen",
"mining_no_saved_pools": "Keine gespeicherten Pools",
"mining_off": "Mining ist AUS",
"mining_on": "Mining ist AN",
"mining_open_in_explorer": "Im Explorer öffnen",
"mining_payout_address": "Auszahlungsadresse",
"mining_payout_tooltip": "Adresse für Mining-Belohnungen",
"mining_pool": "Pool",
"mining_pool_hashrate": "Pool-Hashrate",
"mining_pool_url": "Pool-URL",
"mining_recent_blocks": "LETZTE BLÖCKE",
"mining_recent_payouts": "LETZTE POOL-AUSZAHLUNGEN",
"mining_remove": "Entfernen",
"mining_reset_defaults": "Standardwerte zurücksetzen",
"mining_save_payout_address": "Auszahlungsadresse speichern",
"mining_save_pool_url": "Pool-URL speichern",
"mining_saved_addresses": "Gespeicherte Adressen:",
"mining_saved_pools": "Gespeicherte Pools:",
"mining_shares": "Shares",
"mining_show_chart": "Diagramm",
"mining_show_log": "Protokoll",
"mining_solo": "Solo",
"mining_starting": "Startet...",
"mining_starting_tooltip": "Miner startet...",
"mining_statistics": "Mining-Statistiken",
"mining_stop": "Stopp",
"mining_stop_solo_for_pool": "Solo-Mining stoppen bevor Pool-Mining gestartet wird",
"mining_stop_solo_for_pool_settings": "Solo-Mining stoppen um Pool-Einstellungen zu ändern",
"mining_stopping": "Stoppt...",
"mining_stopping_tooltip": "Miner stoppt...",
"mining_syncing_tooltip": "Blockchain synchronisiert...",
"mining_threads": "Mining-Threads",
"mining_to_save": "zum Speichern",
"mining_today": "Heute",
"mining_uptime": "Laufzeit",
"mining_yesterday": "Gestern",
"network": "Netzwerk",
"network_fee": "NETZWERKGEBÜHR",
"network_hashrate": "Netzwerk-Hashrate",
"new": "+ Neu",
"new_shielded_created": "Neue abgeschirmte Adresse erstellt",
"new_t_address": "Neue T-Adresse",
"new_t_transparent": "Neue t-Adresse (Transparent)",
"new_transparent_created": "Neue transparente Adresse erstellt",
"new_z_address": "Neue Z-Adresse",
"new_z_shielded": "Neue z-Adresse (Abgeschirmt)",
"no_addresses": "Keine Adressen gefunden. Erstellen Sie eine mit den Schaltflächen oben.",
"no_addresses_available": "Keine Adressen verfügbar",
"no_addresses_match": "Keine Adressen passen zum Filter",
"no_addresses_with_balance": "Keine Adressen mit Guthaben",
"no_matching": "Keine passenden Transaktionen",
"no_recent_receives": "Keine kürzlichen Empfänge",
"no_recent_sends": "Keine kürzlichen Sendungen",
"no_transactions": "Keine Transaktionen gefunden",
"node": "KNOTEN",
"node_security": "KNOTEN & SICHERHEIT",
"noise": "Rauschen",
"not_connected": "Nicht mit Daemon verbunden...",
"not_connected_to_daemon": "Nicht mit Daemon verbunden",
"notes": "Notizen",
"notes_optional": "Notizen (optional):",
"output_filename": "Ausgabedateiname:",
"overview": "Übersicht",
"paste": "Einfügen",
"paste_from_clipboard": "Aus Zwischenablage einfügen",
"pay_from": "Zahlen von",
"payment_request": "ZAHLUNGSANFRAGE",
"payment_request_copied": "Zahlungsanfrage kopiert",
"payment_uri_copied": "Zahlungs-URI kopiert",
"peers": "Peers",
"peers_avg_ping": "Durchschn. Ping",
"peers_ban_24h": "Peer 24h sperren",
"peers_ban_score": "Sperr-Score: %d",
"peers_banned": "Gesperrt",
"peers_banned_count": "Gesperrt: %d",
"peers_best_block": "Bester Block",
"peers_blockchain": "BLOCKCHAIN",
"peers_blocks": "Blöcke",
"peers_blocks_left": "%d Blöcke übrig",
"peers_clear_all_bans": "Alle Sperren aufheben",
"peers_click_copy": "Klicken zum Kopieren",
"peers_connected": "Verbunden",
"peers_connected_count": "Verbunden: %d",
"peers_copy_ip": "IP kopieren",
"peers_dir_in": "Ein",
"peers_dir_out": "Aus",
"peers_hash_copied": "Hash kopiert",
"peers_hashrate": "Hashrate",
"peers_in_out": "Ein/Aus",
"peers_longest": "Längste",
"peers_longest_chain": "Längste Chain",
"peers_memory": "Speicher",
"peers_no_banned": "Keine gesperrten Peers",
"peers_no_connected": "Keine verbundenen Peers",
"peers_no_tls": "Kein TLS",
"peers_notarized": "Notarisiert",
"peers_p2p_port": "P2P-Port",
"peers_protocol": "Protokoll",
"peers_received": "Empfangen",
"peers_refresh": "Aktualisieren",
"peers_refresh_tooltip": "Peer-Liste aktualisieren",
"peers_refreshing": "Aktualisiere...",
"peers_sent": "Gesendet",
"peers_tt_id": "ID: %d",
"peers_tt_received": "Empfangen: %s",
"peers_tt_sent": "Gesendet: %s",
"peers_tt_services": "Dienste: %s",
"peers_tt_start_height": "Starthöhe: %d",
"peers_tt_synced": "Synchronisiert H/B: %d/%d",
"peers_tt_tls_cipher": "TLS: %s",
"peers_unban": "Entsperren",
"peers_upper": "PEERS",
"peers_version": "Version",
"pending": "Ausstehend",
"ping": "Ping",
"price_chart": "Preisdiagramm",
"qr_code": "QR-Code",
"qr_failed": "QR-Code konnte nicht generiert werden",
"qr_title": "QR-Code",
"qr_unavailable": "QR nicht verfügbar",
"receive": "Empfangen",
"received": "empfangen",
"received_filter": "Empfangen",
"received_label": "Empfangen",
"received_upper": "EMPFANGEN",
"receiving_addresses": "Ihre Empfangsadressen",
"recent_received": "KÜRZLICH EMPFANGEN",
"recent_sends": "KÜRZLICH GESENDET",
"recipient": "EMPFÄNGER",
"recv_type": "Empf.",
"refresh": "Aktualisieren",
"refresh_now": "Jetzt aktualisieren",
"report_bug": "Fehler melden",
"request_amount": "Betrag (optional):",
"request_copy_uri": "URI kopieren",
"request_description": "Erstellen Sie eine Zahlungsanfrage, die andere scannen oder kopieren können. Der QR-Code enthält Ihre Adresse und optionalen Betrag/Memo.",
"request_label": "Bezeichnung (optional):",
"request_memo": "Memo (optional):",
"request_payment": "Zahlung anfordern",
"request_payment_uri": "Zahlungs-URI:",
"request_receive_address": "Empfangsadresse:",
"request_select_address": "Adresse auswählen...",
"request_shielded_addrs": "-- Abgeschirmte Adressen --",
"request_title": "Zahlung anfordern",
"request_transparent_addrs": "-- Transparente Adressen --",
"request_uri_copied": "Zahlungs-URI in Zwischenablage kopiert",
"rescan": "Neu scannen",
"reset_to_defaults": "Standardwerte zurücksetzen",
"review_send": "Senden prüfen",
"rpc_host": "RPC-Host",
"rpc_pass": "Passwort",
"rpc_port": "Port",
"rpc_user": "Benutzername",
"save": "Speichern",
"save_settings": "Einstellungen speichern",
"save_z_transactions": "Z-Tx in Tx-Liste speichern",
"search_placeholder": "Suchen...",
"security": "SICHERHEIT",
"select_address": "Adresse auswählen...",
"select_receiving_address": "Empfangsadresse auswählen...",
"select_source_address": "Quelladresse auswählen...",
"send": "Senden",
"send_amount": "Betrag",
"send_amount_details": "BETRAGSDETAILS",
"send_amount_upper": "BETRAG",
"send_clear_fields": "Alle Formularfelder leeren?",
"send_copy_error": "Fehler kopieren",
"send_dismiss": "Verwerfen",
"send_error_copied": "Fehler in Zwischenablage kopiert",
"send_error_prefix": "Fehler: %s",
"send_exceeds_available": "Übersteigt verfügbar (%.8f)",
"send_fee": "Gebühr",
"send_fee_high": "Hoch",
"send_fee_low": "Niedrig",
"send_fee_normal": "Normal",
"send_form_restored": "Formular wiederhergestellt",
"send_from_this_address": "Von dieser Adresse senden",
"send_go_to_receive": "Zum Empfangen",
"send_keep": "Behalten",
"send_network_fee": "NETZWERKGEBÜHR",
"send_no_balance": "Kein Guthaben",
"send_no_recent": "Keine kürzlichen Sendungen",
"send_recent_sends": "KÜRZLICH GESENDET",
"send_recipient": "EMPFÄNGER",
"send_select_source": "Quelladresse auswählen...",
"send_sending_from": "SENDEN VON",
"send_submitting": "Transaktion wird übermittelt...",
"send_switch_to_receive": "Wechseln Sie zu Empfangen, um Ihre Adresse zu erhalten und Gelder zu empfangen.",
"send_to": "Senden an",
"send_tooltip_enter_amount": "Geben Sie einen Betrag zum Senden ein",
"send_tooltip_exceeds_balance": "Betrag übersteigt verfügbares Guthaben",
"send_tooltip_in_progress": "Transaktion bereits in Bearbeitung",
"send_tooltip_invalid_address": "Geben Sie eine gültige Empfängeradresse ein",
"send_tooltip_not_connected": "Nicht mit Daemon verbunden",
"send_tooltip_select_source": "Wählen Sie zuerst eine Quelladresse",
"send_tooltip_syncing": "Warten Sie auf die Blockchain-Synchronisierung",
"send_total": "Gesamt",
"send_transaction": "Transaktion senden",
"send_tx_failed": "Transaktion fehlgeschlagen",
"send_tx_sent": "Transaktion gesendet!",
"send_tx_success": "Transaktion erfolgreich gesendet!",
"send_txid_copied": "TxID in Zwischenablage kopiert",
"send_txid_label": "TxID: %s",
"send_valid_shielded": "Gültige abgeschirmte Adresse",
"send_valid_transparent": "Gültige transparente Adresse",
"send_wallet_empty": "Ihre Wallet ist leer",
"send_yes_clear": "Ja, leeren",
"sending": "Transaktion wird gesendet",
"sending_from": "SENDEN VON",
"sent": "gesendet",
"sent_filter": "Gesendet",
"sent_type": "Gesendet",
"sent_upper": "GESENDET",
"settings": "Einstellungen",
"setup_wizard": "Einrichtungsassistent",
"share": "Teilen",
"shield_check_status": "Status prüfen",
"shield_completed": "Vorgang erfolgreich abgeschlossen!",
"shield_description": "Schirmen Sie Ihre Mining-Belohnungen ab, indem Sie Coinbase-Ausgaben von transparenten Adressen an eine abgeschirmte Adresse senden. Dies verbessert die Privatsphäre, indem Ihre Mining-Einkünfte verborgen werden.",
"shield_from_address": "Von Adresse:",
"shield_funds": "Gelder abschirmen",
"shield_in_progress": "Vorgang läuft...",
"shield_max_utxos": "Max. UTXOs pro Vorgang",
"shield_merge_done": "Abschirmung/Zusammenführung abgeschlossen!",
"shield_select_z": "z-Adresse auswählen...",
"shield_started": "Abschirmvorgang gestartet",
"shield_title": "Coinbase-Belohnungen abschirmen",
"shield_to_address": "An Adresse (Abgeschirmt):",
"shield_utxo_limit": "UTXO-Limit:",
"shield_wildcard_hint": "Verwenden Sie '*' um von allen transparenten Adressen abzuschirmen",
"shielded": "Abgeschirmt",
"shielded_to": "ABGESCHIRMT AN",
"shielded_type": "Abgeschirmt",
"show": "Anzeigen",
"show_qr_code": "QR-Code anzeigen",
"showing_transactions": "Zeige %d\xe2\x80\x93%d von %d Transaktionen (gesamt: %zu)",
"simple_background": "Einfacher Hintergrund",
"start_mining": "Mining starten",
"status": "Status",
"stop_external": "Externen Daemon stoppen",
"stop_mining": "Mining stoppen",
"submitting_transaction": "Transaktion wird übermittelt...",
"success": "Erfolg",
"summary": "Zusammenfassung",
"syncing": "Synchronisiere...",
"t_addresses": "T-Adressen",
"test_connection": "Testen",
"theme": "Design",
"theme_effects": "Design-Effekte",
"time_days_ago": "vor %d Tagen",
"time_hours_ago": "vor %d Stunden",
"time_minutes_ago": "vor %d Minuten",
"time_seconds_ago": "vor %d Sekunden",
"to": "An",
"to_upper": "AN",
"tools": "WERKZEUGE",
"total": "Gesamt",
"transaction_id": "TRANSAKTIONS-ID",
"transaction_sent": "Transaktion erfolgreich gesendet",
"transaction_sent_msg": "Transaktion gesendet!",
"transaction_url": "Transaktions-URL",
"transactions": "Transaktionen",
"transactions_upper": "TRANSAKTIONEN",
"transparent": "Transparent",
"tx_confirmations": "%d Bestätigungen",
"tx_details_title": "Transaktionsdetails",
"tx_from_address": "Von Adresse:",
"tx_id_label": "Transaktions-ID:",
"tx_immature": "UNREIF",
"tx_mined": "GEMINED",
"tx_received": "EMPFANGEN",
"tx_sent": "GESENDET",
"tx_to_address": "An Adresse:",
"tx_view_explorer": "Im Explorer anzeigen",
"txs_count": "%d Txs",
"type": "Typ",
"ui_opacity": "UI-Transparenz",
"unban": "Entsperren",
"unconfirmed": "Unbestätigt",
"undo_clear": "Leeren rückgängig",
"unknown": "Unbekannt",
"use_embedded_daemon": "Eingebetteten dragonxd verwenden",
"use_tor": "Tor verwenden",
"validate_btn": "Validieren",
"validate_description": "Geben Sie eine DragonX-Adresse ein, um zu prüfen, ob sie gültig ist und ob sie zu dieser Wallet gehört.",
"validate_invalid": "UNGÜLTIG",
"validate_is_mine": "Diese Wallet besitzt diese Adresse",
"validate_not_mine": "Nicht im Besitz dieser Wallet",
"validate_ownership": "Eigentum:",
"validate_results": "Ergebnisse:",
"validate_shielded_type": "Abgeschirmt (z-Adresse)",
"validate_status": "Status:",
"validate_title": "Adresse validieren",
"validate_transparent_type": "Transparent (t-Adresse)",
"validate_type": "Typ:",
"validate_valid": "GÜLTIG",
"validating": "Validiere...",
"verbose_logging": "Ausführliches Logging",
"version": "Version",
"view": "Ansicht",
"view_details": "Details anzeigen",
"view_on_explorer": "Im Explorer anzeigen",
"waiting_for_daemon": "Warte auf Daemon-Verbindung...",
"wallet": "WALLET",
"wallet_empty": "Ihre Wallet ist leer",
"wallet_empty_hint": "Wechseln Sie zu Empfangen, um Ihre Adresse zu erhalten und Gelder zu empfangen.",
"warning": "Warnung",
"warning_upper": "WARNUNG!",
"website": "Webseite",
"window_opacity": "Fenster-Transparenz",
"yes_clear": "Ja, leeren",
"your_addresses": "Ihre Adressen",
"z_addresses": "Z-Adressen",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "de.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} German translations to {os.path.abspath(out)}")

665
scripts/gen_es.py Normal file
View File

@@ -0,0 +1,665 @@
#!/usr/bin/env python3
"""Generate complete Spanish (es.json) translations for ObsidianDragon wallet."""
import json
es = {
# ---- Navigation & Tabs ----
"overview": "Resumen",
"balance": "Saldo",
"send": "Enviar",
"receive": "Recibir",
"transactions": "Transacciones",
"history": "Historial",
"mining": "Minería",
"peers": "Nodos",
"market": "Mercado",
"settings": "Configuración",
"console": "Consola",
"tools": "HERRAMIENTAS",
"advanced": "AVANZADO",
"network": "Red",
# ---- Settings sections ----
"appearance": "APARIENCIA",
"wallet": "CARTERA",
"node_security": "NODO Y SEGURIDAD",
"node": "NODO",
"security": "SEGURIDAD",
"explorer": "EXPLORADOR",
"about": "Acerca de",
"backup_data": "RESPALDO Y DATOS",
"general": "General",
# ---- Settings options ----
"balance_layout": "Diseño de Saldo",
"low_spec_mode": "Modo bajo rendimiento",
"simple_background": "Fondo simple",
"console_scanline": "Líneas de consola",
"theme_effects": "Efectos de tema",
"language": "Idioma",
"save_z_transactions": "Guardar Z-tx en lista",
"allow_custom_fees": "Permitir comisiones personalizadas",
"custom_fees": "Comisiones personalizadas",
"auto_shield": "Auto-proteger minería",
"fetch_prices": "Obtener precios",
"use_tor": "Usar Tor",
"font_scale": "Escala de fuente",
"keep_daemon": "Mantener daemon activo",
"stop_external": "Detener daemon externo",
"mine_when_idle": "Minar en reposo",
"verbose_logging": "Registro detallado",
"acrylic": "Acrílico",
"noise": "Ruido",
"ui_opacity": "Opacidad de UI",
"window_opacity": "Opacidad de ventana",
# ---- Settings buttons ----
"save_settings": "Guardar Configuración",
"reset_to_defaults": "Restablecer Valores",
"report_bug": "Reportar Error",
"website": "Sitio Web",
"setup_wizard": "Asistente de Configuración",
"rescan": "Re-escanear",
"test_connection": "Probar",
# ---- Settings fields ----
"rpc_host": "Host RPC",
"rpc_port": "Puerto",
"rpc_user": "Usuario",
"rpc_pass": "Contraseña",
"transaction_url": "URL de Transacción",
"address_url": "URL de Dirección",
"block_explorer": "Explorador de Bloques",
# ---- Common actions ----
"add": "Agregar",
"edit": "Editar",
"delete": "Eliminar",
"cancel": "Cancelar",
"close": "Cerrar",
"clear": "Limpiar",
"copy": "Copiar",
"paste": "Pegar",
"save": "Guardar",
"refresh": "Actualizar",
"export": "Exportar",
"import": "Importar",
"show": "Mostrar",
"hide": "Ocultar",
"share": "Compartir",
"confirm_and_send": "Confirmar y Enviar",
"confirm_send": "Confirmar Envío",
"confirm_transaction": "Confirmar Transacción",
"review_send": "Revisar Envío",
"copy_address": "Copiar Dirección Completa",
"copy_to_clipboard": "Copiar al Portapapeles",
"paste_from_clipboard": "Pegar del Portapapeles",
"copy_txid": "Copiar TxID",
"copy_uri": "Copiar URI",
"copy_error": "Copiar Error",
"search_placeholder": "Buscar...",
"exit": "Salir",
"help": "Ayuda",
"file": "Archivo",
"display": "Pantalla",
"new": "+ Nuevo",
"dismiss": "Descartar",
"keep": "Mantener",
"yes_clear": "Sí, Limpiar",
"undo_clear": "Deshacer Limpieza",
# ---- Common labels ----
"address": "Dirección",
"address_label": "Dirección:",
"amount": "Cantidad",
"amount_label": "Cantidad:",
"date": "Fecha",
"date_label": "Fecha:",
"fee": "Comisión",
"fee_label": "Comisión:",
"label": "Etiqueta:",
"memo": "Memo (opcional, encriptado)",
"memo_label": "Memo:",
"notes": "Notas",
"notes_optional": "Notas (opcional):",
"total": "Total",
"from": "Desde",
"to_upper": "PARA",
"from_upper": "DESDE",
"max": "Máximo",
"characters": "caracteres",
"ping": "Ping",
"height": "Altura",
"block": "Bloque",
"available": "Disponible",
"unknown": "Desconocido",
"loading": "Cargando...",
"validating": "Validando...",
"warning_upper": "¡ADVERTENCIA!",
"output_filename": "Nombre del archivo:",
"file_save_location": "El archivo se guardará en: ~/.config/ObsidianDragon/",
"light": "Claro",
"dark": "Oscuro",
# ---- Status ----
"connected": "Conectado",
"disconnected": "Desconectado",
"connecting": "Conectando...",
"confirmed": "Confirmada",
"confirmations": "Confirmaciones",
"not_connected_to_daemon": "No conectado al daemon",
"waiting_for_daemon": "Esperando conexión al daemon...",
"blockchain_syncing": "Sincronizando blockchain (%.1f%%)... Los saldos pueden ser inexactos.",
"error": "Error",
"success": "Éxito",
"warning": "Advertencia",
# ---- Time ----
"time_days_ago": "hace %d días",
"time_hours_ago": "hace %d horas",
"time_minutes_ago": "hace %d minutos",
"time_seconds_ago": "hace %d segundos",
# ---- Transaction types/filters ----
"sent_type": "Enviado",
"sent_filter": "Enviado",
"sent_upper": "ENVIADO",
"received_label": "Recibido",
"received_filter": "Recibido",
"received_upper": "RECIBIDO",
"mined_type": "Minado",
"mined_filter": "Minado",
"mined_upper": "MINADO",
"immature_type": "Inmaduro",
"mature": "Maduro",
"recv_type": "Recibido",
"all_filter": "Todos",
"shielded_type": "Protegido",
# ---- Balance / Overview ----
"address_upper": "DIRECCIÓN",
"address_details": "Detalles de Dirección",
"amount_details": "DETALLES DE CANTIDAD",
"transactions_upper": "TRANSACCIONES",
"addresses_appear_here": "Tus direcciones de recepción aparecerán aquí una vez conectado.",
"wallet_empty": "Tu cartera está vacía",
"wallet_empty_hint": "Cambia a Recibir para obtener tu dirección y empezar a recibir fondos.",
"loading_addresses": "Cargando direcciones...",
"no_addresses_match": "No hay direcciones que coincidan con el filtro",
"no_addresses_with_balance": "No hay direcciones con saldo",
"click_copy_address": "Clic para copiar dirección",
"click_copy_uri": "Clic para copiar URI",
"address_copied": "Dirección copiada al portapapeles",
"about_dragonx": "Acerca de DragonX",
"dragonx_green": "DragonX (Verde)",
# ---- Transactions tab ----
"no_transactions": "No se encontraron transacciones",
"no_matching": "No hay transacciones coincidentes",
"showing_transactions": "Mostrando %d\u2013%d de %d transacciones (total: %zu)",
"conf_count": "%d conf",
"confirmations_display": "%d confirmaciones | %s",
"txs_count": "%d txs",
"view_details": "Ver Detalles",
"full_details": "Detalles Completos",
"export_csv": "Exportar CSV",
"transaction_id": "ID DE TRANSACCIÓN",
# ---- Receive tab ----
"select_receiving_address": "Seleccionar dirección de recepción...",
"payment_request": "SOLICITUD DE PAGO",
"recent_received": "RECIBIDOS RECIENTES",
"no_recent_receives": "No hay recepciones recientes",
"qr_unavailable": "QR no disponible",
"clear_request": "Limpiar Solicitud",
"clear_form_confirm": "¿Limpiar todos los campos del formulario?",
"payment_request_copied": "Solicitud de pago copiada",
"payment_uri_copied": "URI de pago copiada",
"failed_create_shielded": "Error al crear dirección protegida",
"failed_create_transparent": "Error al crear dirección transparente",
"new_shielded_created": "Nueva dirección protegida creada",
"new_transparent_created": "Nueva dirección transparente creada",
# ---- Send tab ----
"send_sending_from": "ENVIANDO DESDE",
"send_select_source": "Seleccionar dirección de origen...",
"send_no_balance": "Sin saldo",
"send_recipient": "DESTINATARIO",
"send_amount_upper": "CANTIDAD",
"send_amount": "Cantidad",
"send_amount_details": "DETALLES DE CANTIDAD",
"send_fee": "Comisión",
"send_fee_low": "Baja",
"send_fee_normal": "Normal",
"send_fee_high": "Alta",
"send_network_fee": "COMISIÓN DE RED",
"send_total": "Total",
"send_recent_sends": "ENVÍOS RECIENTES",
"send_no_recent": "No hay envíos recientes",
"send_clear_fields": "¿Limpiar todos los campos del formulario?",
"send_yes_clear": "Sí, Limpiar",
"send_keep": "Mantener",
"send_form_restored": "Formulario restaurado",
"send_valid_shielded": "Dirección protegida válida",
"send_valid_transparent": "Dirección transparente válida",
"send_exceeds_available": "Excede disponible (%.8f)",
"send_submitting": "Enviando transacción...",
"send_tx_sent": "¡Transacción enviada!",
"send_tx_success": "¡Transacción enviada exitosamente!",
"send_tx_failed": "Error en la transacción",
"send_error_prefix": "Error: %s",
"send_error_copied": "Error copiado al portapapeles",
"send_copy_error": "Copiar Error",
"send_dismiss": "Descartar",
"send_txid_copied": "TxID copiado al portapapeles",
"send_txid_label": "TxID: %s",
"send_go_to_receive": "Ir a Recibir",
"send_wallet_empty": "Tu cartera está vacía",
"send_switch_to_receive": "Cambia a Recibir para obtener tu dirección y empezar a recibir fondos.",
"send_tooltip_select_source": "Selecciona una dirección de origen primero",
"send_tooltip_invalid_address": "Ingresa una dirección de destinatario válida",
"send_tooltip_enter_amount": "Ingresa una cantidad a enviar",
"send_tooltip_exceeds_balance": "La cantidad excede el saldo disponible",
"send_tooltip_not_connected": "No conectado al daemon",
"send_tooltip_syncing": "Espera a que se sincronice el blockchain",
"send_tooltip_in_progress": "Transacción ya en progreso",
"sending_from": "ENVIANDO DESDE",
"select_source_address": "Seleccionar dirección de origen...",
"recipient": "DESTINATARIO",
"memo_optional": "MEMO (OPCIONAL)",
"memo_upper": "MEMO",
"network_fee": "COMISIÓN DE RED",
"fee_low": "Baja",
"fee_normal": "Normal",
"fee_high": "Alta",
"recent_sends": "ENVÍOS RECIENTES",
"no_recent_sends": "No hay envíos recientes",
"shielded_to": "PROTEGIDA PARA",
"submitting_transaction": "Enviando transacción...",
"transaction_sent_msg": "¡Transacción enviada!",
"amount_exceeds_balance": "La cantidad excede el saldo",
# ---- Mining tab ----
"mining_solo": "Solo",
"mining_pool": "Pool",
"mining_pool_url": "URL del Pool",
"mining_pool_hashrate": "Hashrate del Pool",
"mining_local_hashrate": "Hashrate Local",
"mining_payout_address": "Dirección de Pago",
"mining_payout_tooltip": "Dirección para recibir recompensas de minería",
"mining_mine": "Minar",
"mining_stop": "Detener",
"mining_starting": "Iniciando...",
"mining_stopping": "Deteniendo...",
"mining_active": "Activo",
"mining_connected": "Conectado",
"mining_connecting": "Conectando...",
"mining_network": "Red",
"mining_shares": "Shares",
"mining_uptime": "Tiempo activo",
"mining_mining_addr": "Dir. Minería",
"mining_est_block": "Bloque Est.",
"mining_est_daily": "Diario Est.",
"mining_today": "Hoy",
"mining_yesterday": "Ayer",
"mining_all_time": "Todo el Tiempo",
"mining_recent_blocks": "BLOQUES RECIENTES",
"mining_recent_payouts": "PAGOS DE POOL RECIENTES",
"mining_no_blocks_yet": "Aún no se han encontrado bloques",
"mining_no_payouts_yet": "Aún no hay pagos del pool",
"mining_show_chart": "Gráfico",
"mining_show_log": "Registro",
"mining_chart_now": "Ahora",
"mining_chart_start": "Inicio",
"mining_chart_1m_ago": "hace 1m",
"mining_chart_5m_ago": "hace 5m",
"mining_save_pool_url": "Guardar URL del pool",
"mining_save_payout_address": "Guardar dirección de pago",
"mining_saved_pools": "Pools Guardados:",
"mining_saved_addresses": "Direcciones Guardadas:",
"mining_no_saved_pools": "No hay pools guardados",
"mining_no_saved_addresses": "No hay direcciones guardadas",
"mining_already_saved": "URL del pool ya guardada",
"mining_remove": "Eliminar",
"mining_reset_defaults": "Restablecer Valores",
"mining_click": "Clic",
"mining_to_save": "para guardar",
"mining_click_copy_address": "Clic para copiar dirección",
"mining_click_copy_block": "Clic para copiar hash de bloque",
"mining_click_copy_difficulty": "Clic para copiar dificultad",
"mining_address_copied": "Dirección de minería copiada",
"mining_block_copied": "Hash de bloque copiado",
"mining_difficulty_copied": "Dificultad copiada",
"mining_open_in_explorer": "Abrir en explorador",
"mining_starting_tooltip": "El minero está iniciando...",
"mining_stopping_tooltip": "El minero está deteniéndose...",
"mining_syncing_tooltip": "El blockchain está sincronizando...",
"mining_idle_on_tooltip": "Desactivar minería en reposo",
"mining_idle_off_tooltip": "Activar minería en reposo",
"mining_stop_solo_for_pool": "Detener minería solo antes de iniciar minería en pool",
"mining_stop_solo_for_pool_settings": "Detener minería solo para cambiar configuración del pool",
"mining_filter_all": "Todos",
"mining_filter_tip_all": "Mostrar todas las ganancias",
"mining_filter_tip_solo": "Mostrar solo ganancias solo",
"mining_filter_tip_pool": "Mostrar solo ganancias del pool",
"local_hashrate": "Tasa Hash Local",
"est_time_to_block": "Tiempo Est. al Bloque",
"difficulty": "Dificultad",
"current_price": "Precio Actual",
"market_cap": "Cap. de Mercado",
# ---- Peers tab ----
"peers_blockchain": "BLOCKCHAIN",
"peers_blocks": "Bloques",
"peers_connected": "Conectados",
"peers_connected_count": "Conectados: %d",
"peers_banned": "Bloqueados",
"peers_banned_count": "Bloqueados: %d",
"peers_upper": "NODOS",
"peers_avg_ping": "Ping Prom.",
"peers_best_block": "Mejor Bloque",
"peers_hashrate": "Hashrate",
"peers_longest": "Más Larga",
"peers_longest_chain": "Cadena Más Larga",
"peers_memory": "Memoria",
"peers_notarized": "Notarizado",
"peers_p2p_port": "Puerto P2P",
"peers_protocol": "Protocolo",
"peers_version": "Versión",
"peers_in_out": "Ent/Sal",
"peers_dir_in": "Ent",
"peers_dir_out": "Sal",
"peers_received": "Recibido",
"peers_sent": "Enviado",
"peers_refresh": "Actualizar",
"peers_refresh_tooltip": "Actualizar lista de nodos",
"peers_refreshing": "Actualizando...",
"peers_no_connected": "No hay nodos conectados",
"peers_no_banned": "No hay nodos bloqueados",
"peers_ban_24h": "Bloquear Nodo 24h",
"peers_unban": "Desbloquear",
"peers_clear_all_bans": "Limpiar Todos los Bloqueos",
"peers_copy_ip": "Copiar IP",
"peers_click_copy": "Clic para copiar",
"peers_hash_copied": "Hash copiado",
"peers_no_tls": "Sin TLS",
"peers_blocks_left": "%d bloques restantes",
"peers_ban_score": "Puntuación: %d",
"peers_tt_id": "ID: %d",
"peers_tt_sent": "Enviado: %s",
"peers_tt_received": "Recibido: %s",
"peers_tt_services": "Servicios: %s",
"peers_tt_start_height": "Altura Inicial: %d",
"peers_tt_synced": "Sinc H/B: %d/%d",
"peers_tt_tls_cipher": "TLS: %s",
"connected_peers": "Nodos Conectados",
"banned_peers": "Nodos Bloqueados",
"ban": "Bloquear",
"clear_all_bans": "Limpiar Todos los Bloqueos",
"ip_address": "Dirección IP",
# ---- Market tab ----
"market_btc_price": "PRECIO BTC",
"market_24h_volume": "VOLUMEN 24H",
"market_portfolio": "PORTAFOLIO",
"market_pct_shielded": "%.0f%% Protegido",
"market_attribution": "Datos de precios de NonKYC",
"market_no_price": "Sin datos de precio",
"market_no_history": "No hay historial de precios disponible",
"market_price_unavailable": "Datos de precio no disponibles",
"market_refresh_price": "Actualizar datos de precio",
"market_trade_on": "Operar en %s",
"market_now": "Ahora",
"market_6h": "6h",
"market_12h": "12h",
"market_18h": "18h",
"market_24h": "24h",
"24h_change": "Cambio 24h",
"24h_volume": "Volumen 24h",
# ---- Console tab ----
"console_welcome": "Bienvenido a la Consola de ObsidianDragon",
"console_type_help": "Escribe 'help' para ver los comandos disponibles",
"console_available_commands": "Comandos disponibles:",
"console_common_rpc": "Comandos RPC comunes:",
"console_rpc_reference": "Referencia de Comandos RPC",
"console_auto_scroll": "Auto-desplazamiento",
"console_clear": "Limpiar",
"console_clear_console": "Limpiar Consola",
"console_cleared": "Consola limpiada",
"console_commands": "Comandos",
"console_completions": "Completaciones:",
"console_tab_completion": "Tab para completar",
"console_connected": "Conectado al daemon",
"console_disconnected": "Desconectado del daemon",
"console_not_connected": "Error: No conectado al daemon",
"console_no_daemon": "Sin daemon",
"console_daemon": "Daemon",
"console_daemon_error": "¡Error del daemon!",
"console_daemon_started": "Daemon iniciado",
"console_daemon_stopped": "Daemon detenido",
"console_errors": "Errores",
"console_filter_hint": "Filtrar salida...",
"console_search_commands": "Buscar comandos...",
"console_copy_all": "Copiar Todo",
"console_copy_selected": "Copiar",
"console_select_all": "Seleccionar Todo",
"console_zoom_in": "Acercar",
"console_zoom_out": "Alejar",
"console_show_daemon_output": "Mostrar salida del daemon",
"console_show_errors_only": "Mostrar solo errores",
"console_show_rpc_ref": "Mostrar referencia de comandos RPC",
"console_capturing_output": "Capturando salida del daemon...",
"console_starting_node": "Iniciando nodo...",
"console_line_count": "%zu líneas",
"console_new_lines": "%d nuevas líneas",
"console_showing_lines": "Mostrando %zu de %zu líneas",
"console_click_commands": "Clic en los comandos de arriba para insertarlos",
"console_click_insert": "Clic para insertar",
"console_click_insert_params": "Clic para insertar con parámetros",
"console_close": "Cerrar",
"console_status_running": "Ejecutando",
"console_status_stopped": "Detenido",
"console_status_starting": "Iniciando",
"console_status_stopping": "Deteniendo",
"console_status_error": "Error",
"console_status_unknown": "Desconocido",
"console_help_help": " help - Mostrar este mensaje de ayuda",
"console_help_getinfo": " getinfo - Mostrar información del nodo",
"console_help_getblockcount": " getblockcount - Mostrar altura actual del bloque",
"console_help_getbalance": " getbalance - Mostrar saldo transparente",
"console_help_gettotalbalance": " gettotalbalance - Mostrar saldo total",
"console_help_getmininginfo": " getmininginfo - Mostrar estado de minería",
"console_help_getpeerinfo": " getpeerinfo - Mostrar nodos conectados",
"console_help_setgenerate": " setgenerate - Controlar minería",
"console_help_stop": " stop - Detener el daemon",
"console_help_clear": " clear - Limpiar la consola",
# ---- About dialog ----
"about_title": "Acerca de ObsidianDragon",
"about_edition": "Edición ImGui",
"about_version": "Versión:",
"about_imgui": "ImGui:",
"about_build_date": "Fecha de Compilación:",
"about_build_type": "Tipo de Compilación:",
"about_debug": "Depuración",
"about_release": "Producción",
"about_daemon": "Daemon:",
"about_chain": "Cadena:",
"about_block_height": "Altura de Bloque:",
"about_connections": "Conexiones:",
"about_peers_count": "%zu nodos",
"about_credits": "Créditos",
"about_license": "Licencia",
"about_license_text": "Este software se distribuye bajo la Licencia Pública General de GNU v3 (GPLv3). Usted es libre de usar, modificar y distribuir este software bajo los términos de la licencia.",
"about_website": "Sitio Web",
"about_github": "GitHub",
"about_block_explorer": "Explorador de Bloques",
# ---- Address Book dialog ----
"address_book_title": "Libreta de Direcciones",
"address_book_add_new": "Agregar Nueva",
"address_book_add": "Agregar Dirección",
"address_book_edit": "Editar Dirección",
"address_book_empty": "No hay direcciones guardadas. Haz clic en 'Agregar Nueva' para añadir una.",
"address_book_count": "%zu direcciones guardadas",
"address_book_deleted": "Entrada eliminada",
"address_book_added": "Dirección agregada a la libreta",
"address_book_exists": "La dirección ya existe en la libreta",
"address_book_updated": "Dirección actualizada",
"address_book_update_failed": "Error al actualizar - la dirección puede estar duplicada",
# ---- Backup dialog ----
"backup_title": "Respaldar Cartera",
"backup_description": "Crea un respaldo de tu archivo wallet.dat. Este archivo contiene todas tus claves privadas e historial de transacciones. Guarda el respaldo en un lugar seguro.",
"backup_destination": "Destino del respaldo:",
"backup_wallet_not_found": "Advertencia: wallet.dat no encontrado en la ubicación esperada",
"backup_create": "Crear Respaldo",
"backup_created": "Respaldo de cartera creado",
"backup_backing_up": "Respaldando...",
"backup_tips": "Consejos:",
"backup_tip_external": "Guarda respaldos en unidades externas o almacenamiento en la nube",
"backup_tip_multiple": "Crea múltiples respaldos en diferentes ubicaciones",
"backup_tip_test": "Prueba restaurar desde el respaldo periódicamente",
"backup_wallet": "Respaldar Cartera...",
# ---- Block Info dialog ----
"block_info_title": "Información del Bloque",
"block_height": "Altura del Bloque:",
"block_get_info": "Obtener Info del Bloque",
"block_hash": "Hash del Bloque:",
"block_timestamp": "Fecha y Hora:",
"block_transactions": "Transacciones:",
"block_size": "Tamaño:",
"block_bits": "Bits:",
"block_merkle_root": "Raíz Merkle:",
"block_previous": "Bloque Anterior:",
"block_next": "Bloque Siguiente:",
"block_click_prev": "Clic para ver bloque anterior",
"block_click_next": "Clic para ver bloque siguiente",
"block_nav_prev": "<< Anterior",
"block_nav_next": "Siguiente >>",
# ---- Export Keys dialog ----
"export_keys_title": "Exportar Todas las Claves Privadas",
"export_keys_danger": "PELIGRO: ¡Esto exportará TODAS las claves privadas de tu cartera! Cualquiera con acceso a este archivo puede robar tus fondos. Guárdalo de forma segura y elimínalo después de usar.",
"export_keys_options": "Opciones de exportación:",
"export_keys_include_z": "Incluir direcciones Z (protegidas)",
"export_keys_include_t": "Incluir direcciones T (transparentes)",
"export_keys_btn": "Exportar Claves",
"export_keys_success": "Claves exportadas exitosamente",
"export_private_key": "Exportar Clave Privada",
"export_viewing_key": "Exportar Clave de Vista",
# ---- Export Transactions dialog ----
"export_tx_title": "Exportar Transacciones a CSV",
"export_tx_count": "Exportar %zu transacciones a archivo CSV.",
"export_tx_none": "No hay transacciones para exportar",
"export_tx_file_fail": "Error al crear archivo CSV",
"export_tx_success": "Transacciones exportadas exitosamente",
# ---- Import Key dialog ----
"import_key_title": "Importar Clave Privada",
"import_key_warning": "Advertencia: ¡Nunca compartas tus claves privadas! Importar claves de fuentes no confiables puede comprometer tu cartera.",
"import_key_label": "Clave(s) Privada(s):",
"import_key_tooltip": "Ingresa una o más claves privadas, una por línea.\nSoporta claves de direcciones z y t.\nLas líneas que empiezan con # se tratan como comentarios.",
"import_key_btn": "Importar Clave(s)",
"import_key_no_valid": "No se encontraron claves válidas en la entrada",
"import_key_success": "Claves importadas exitosamente",
"import_key_rescan": "Re-escanear blockchain después de importar",
"import_key_start_height": "Altura inicial:",
"import_key_full_rescan": "(0 = re-escaneo completo)",
"import_key_formats": "Formatos de clave soportados:",
"import_key_z_format": "Claves de gasto de direcciones Z (secret-extended-key-...)",
"import_key_t_format": "Claves privadas WIF de direcciones T",
"import_private_key": "Importar Clave Privada...",
"invalid_address": "Formato de dirección inválido",
# ---- Key Export dialog ----
"key_export_private_key": "Clave Privada:",
"key_export_viewing_key": "Clave de Vista:",
"key_export_private_warning": "¡Mantén esta clave en SECRETO! Cualquiera con esta clave puede gastar tus fondos. Nunca la compartas en línea ni con personas no confiables.",
"key_export_viewing_warning": "Esta clave de vista permite a otros ver tus transacciones entrantes y saldo, pero NO gastar tus fondos. Comparte solo con personas de confianza.",
"key_export_fetching": "Obteniendo clave de la cartera...",
"key_export_reveal": "Revelar Clave",
# ---- QR dialog ----
"qr_title": "Código QR",
"qr_failed": "Error al generar código QR",
# ---- Request Payment dialog ----
"request_title": "Solicitar Pago",
"request_description": "Genera una solicitud de pago que otros pueden escanear o copiar. El código QR contiene tu dirección y cantidad/memo opcionales.",
"request_receive_address": "Dirección de Recepción:",
"request_select_address": "Seleccionar dirección...",
"request_shielded_addrs": "-- Direcciones Protegidas --",
"request_transparent_addrs": "-- Direcciones Transparentes --",
"request_amount": "Cantidad (opcional):",
"request_label": "Etiqueta (opcional):",
"request_memo": "Memo (opcional):",
"request_payment_uri": "URI de Pago:",
"request_copy_uri": "Copiar URI",
"request_uri_copied": "URI de pago copiada al portapapeles",
# ---- Shield dialog ----
"shield_title": "Proteger Recompensas de Coinbase",
"shield_description": "Protege tus recompensas de minería enviando salidas coinbase de direcciones transparentes a una dirección protegida. Esto mejora la privacidad ocultando tus ingresos de minería.",
"shield_from_address": "Dirección de Origen:",
"shield_wildcard_hint": "Usa '*' para proteger desde todas las direcciones transparentes",
"shield_to_address": "Dirección Destino (Protegida):",
"shield_select_z": "Seleccionar dirección z...",
"shield_utxo_limit": "Límite UTXO:",
"shield_max_utxos": "UTXOs máximos por operación",
"shield_funds": "Proteger Fondos",
"shield_started": "Operación de protección iniciada",
"shield_check_status": "Verificar Estado",
"shield_completed": "¡Operación completada exitosamente!",
"shield_merge_done": "¡Protección/fusión completada!",
"shield_in_progress": "Operación en progreso...",
"merge_title": "Fusionar a Dirección",
"merge_description": "Fusiona múltiples UTXOs en una sola dirección protegida. Esto puede ayudar a reducir el tamaño de la cartera y mejorar la privacidad.",
"merge_funds": "Fusionar Fondos",
"merge_started": "Operación de fusión iniciada",
# ---- Transaction Details dialog ----
"tx_details_title": "Detalles de Transacción",
"tx_received": "RECIBIDO",
"tx_sent": "ENVIADO",
"tx_mined": "MINADO",
"tx_immature": "INMADURO",
"tx_confirmations": "%d confirmaciones",
"tx_id_label": "ID de Transacción:",
"tx_to_address": "Dirección Destino:",
"tx_from_address": "Dirección Origen:",
"tx_view_explorer": "Ver en Explorador",
"pending": "Pendiente",
# ---- Validate Address dialog ----
"validate_title": "Validar Dirección",
"validate_description": "Ingresa una dirección DragonX para verificar si es válida y si pertenece a esta cartera.",
"validate_btn": "Validar",
"validate_results": "Resultados:",
"validate_status": "Estado:",
"validate_valid": "VÁLIDA",
"validate_invalid": "INVÁLIDA",
"validate_type": "Tipo:",
"validate_ownership": "Propiedad:",
"validate_is_mine": "Esta cartera es dueña de esta dirección",
"validate_not_mine": "No es propiedad de esta cartera",
"validate_shielded_type": "Protegida (dirección z)",
"validate_transparent_type": "Transparente (dirección t)",
# ---- Misc ----
"transaction_sent": "Transacción enviada exitosamente",
}
# Load existing to preserve anything we might have missed
import os
existing_path = os.path.join(os.path.dirname(__file__), '..', 'res', 'lang', 'es.json')
out_path = existing_path
with open(out_path, 'w', encoding='utf-8') as f:
json.dump(dict(sorted(es.items())), f, indent=4, ensure_ascii=False)
f.write('\n')
print(f"Wrote {len(es)} Spanish translations to {out_path}")

646
scripts/gen_fr.py Normal file
View File

@@ -0,0 +1,646 @@
#!/usr/bin/env python3
"""Generate French (fr) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "Variation 24h",
"24h_volume": "Volume 24h",
"about": "À propos",
"about_block_explorer": "Explorateur de blocs",
"about_block_height": "Hauteur de bloc :",
"about_build_date": "Date de compilation :",
"about_build_type": "Type de build :",
"about_chain": "Chaîne :",
"about_connections": "Connexions :",
"about_credits": "Crédits",
"about_daemon": "Daemon :",
"about_debug": "Débogage",
"about_dragonx": "À propos d'ObsidianDragon",
"about_edition": "Édition ImGui",
"about_github": "GitHub",
"about_imgui": "ImGui :",
"about_license": "Licence",
"about_license_text": "Ce logiciel est publié sous la licence publique générale GNU v3 (GPLv3). Vous êtes libre d'utiliser, de modifier et de distribuer ce logiciel selon les termes de la licence.",
"about_peers_count": "%zu pairs",
"about_release": "Version",
"about_title": "À propos d'ObsidianDragon",
"about_version": "Version :",
"about_website": "Site web",
"acrylic": "Acrylique",
"add": "Ajouter",
"address": "Adresse",
"address_book_add": "Ajouter une adresse",
"address_book_add_new": "Ajouter",
"address_book_added": "Adresse ajoutée au carnet",
"address_book_count": "%zu adresses enregistrées",
"address_book_deleted": "Entrée supprimée",
"address_book_edit": "Modifier l'adresse",
"address_book_empty": "Aucune adresse enregistrée. Cliquez sur 'Ajouter' pour en créer une.",
"address_book_exists": "L'adresse existe déjà dans le carnet",
"address_book_title": "Carnet d'adresses",
"address_book_update_failed": "Échec de la mise à jour - l'adresse est peut-être en double",
"address_book_updated": "Adresse mise à jour",
"address_copied": "Adresse copiée dans le presse-papiers",
"address_details": "Détails de l'adresse",
"address_label": "Adresse :",
"address_upper": "ADRESSE",
"address_url": "URL de l'adresse",
"addresses_appear_here": "Vos adresses de réception apparaîtront ici une fois connecté.",
"advanced": "AVANCÉ",
"all_filter": "Tout",
"allow_custom_fees": "Autoriser les frais personnalisés",
"amount": "Montant",
"amount_details": "DÉTAILS DU MONTANT",
"amount_exceeds_balance": "Le montant dépasse le solde",
"amount_label": "Montant :",
"appearance": "APPARENCE",
"auto_shield": "Auto-blindage du minage",
"available": "Disponible",
"backup_backing_up": "Sauvegarde en cours...",
"backup_create": "Créer une sauvegarde",
"backup_created": "Sauvegarde du portefeuille créée",
"backup_data": "SAUVEGARDE & DONNÉES",
"backup_description": "Créez une sauvegarde de votre fichier wallet.dat. Ce fichier contient toutes vos clés privées et l'historique des transactions. Conservez la sauvegarde dans un endroit sûr.",
"backup_destination": "Destination de sauvegarde :",
"backup_tip_external": "Stockez les sauvegardes sur des disques externes ou un stockage cloud",
"backup_tip_multiple": "Créez plusieurs sauvegardes à différents endroits",
"backup_tip_test": "Testez périodiquement la restauration à partir de la sauvegarde",
"backup_tips": "Conseils :",
"backup_title": "Sauvegarder le portefeuille",
"backup_wallet": "Sauvegarder le portefeuille...",
"backup_wallet_not_found": "Attention : wallet.dat introuvable à l'emplacement prévu",
"balance": "Solde",
"balance_layout": "Disposition du solde",
"ban": "Bannir",
"banned_peers": "Pairs bannis",
"block": "Bloc",
"block_bits": "Bits :",
"block_click_next": "Cliquez pour voir le bloc suivant",
"block_click_prev": "Cliquez pour voir le bloc précédent",
"block_explorer": "Explorateur de blocs",
"block_get_info": "Obtenir les infos du bloc",
"block_hash": "Hash du bloc :",
"block_height": "Hauteur du bloc :",
"block_info_title": "Informations sur le bloc",
"block_merkle_root": "Racine de Merkle :",
"block_nav_next": "Suivant >>",
"block_nav_prev": "<< Précédent",
"block_next": "Bloc suivant :",
"block_previous": "Bloc précédent :",
"block_size": "Taille :",
"block_timestamp": "Horodatage :",
"block_transactions": "Transactions :",
"blockchain_syncing": "Synchronisation de la blockchain (%.1f%%)... Les soldes peuvent être inexacts.",
"cancel": "Annuler",
"characters": "caractères",
"clear": "Effacer",
"clear_all_bans": "Lever tous les bannissements",
"clear_form_confirm": "Effacer tous les champs du formulaire ?",
"clear_request": "Effacer la demande",
"click_copy_address": "Cliquez pour copier l'adresse",
"click_copy_uri": "Cliquez pour copier l'URI",
"close": "Fermer",
"conf_count": "%d conf.",
"confirm_and_send": "Confirmer & Envoyer",
"confirm_send": "Confirmer l'envoi",
"confirm_transaction": "Confirmer la transaction",
"confirmations": "Confirmations",
"confirmations_display": "%d confirmations | %s",
"confirmed": "Confirmé",
"connected": "Connecté",
"connected_peers": "Pairs connectés",
"connecting": "Connexion...",
"console": "Console",
"console_auto_scroll": "Défilement auto",
"console_available_commands": "Commandes disponibles :",
"console_capturing_output": "Capture de la sortie du daemon...",
"console_clear": "Effacer",
"console_clear_console": "Effacer la console",
"console_cleared": "Console effacée",
"console_click_commands": "Cliquez sur les commandes ci-dessus pour les insérer",
"console_click_insert": "Cliquez pour insérer",
"console_click_insert_params": "Cliquez pour insérer avec paramètres",
"console_close": "Fermer",
"console_commands": "Commandes",
"console_common_rpc": "Commandes RPC courantes :",
"console_completions": "Complétions :",
"console_connected": "Connecté au daemon",
"console_copy_all": "Tout copier",
"console_copy_selected": "Copier",
"console_daemon": "Daemon",
"console_daemon_error": "Erreur du daemon !",
"console_daemon_started": "Daemon démarré",
"console_daemon_stopped": "Daemon arrêté",
"console_disconnected": "Déconnecté du daemon",
"console_errors": "Erreurs",
"console_filter_hint": "Filtrer la sortie...",
"console_help_clear": " clear - Effacer la console",
"console_help_getbalance": " getbalance - Afficher le solde transparent",
"console_help_getblockcount": " getblockcount - Afficher la hauteur de bloc actuelle",
"console_help_getinfo": " getinfo - Afficher les infos du nœud",
"console_help_getmininginfo": " getmininginfo - Afficher le statut du minage",
"console_help_getpeerinfo": " getpeerinfo - Afficher les pairs connectés",
"console_help_gettotalbalance": " gettotalbalance - Afficher le solde total",
"console_help_help": " help - Afficher ce message d'aide",
"console_help_setgenerate": " setgenerate - Contrôler le minage",
"console_help_stop": " stop - Arrêter le daemon",
"console_line_count": "%zu lignes",
"console_new_lines": "%d nouvelles lignes",
"console_no_daemon": "Pas de daemon",
"console_not_connected": "Erreur : Non connecté au daemon",
"console_rpc_reference": "Référence des commandes RPC",
"console_scanline": "Scanline de la console",
"console_search_commands": "Rechercher des commandes...",
"console_select_all": "Tout sélectionner",
"console_show_daemon_output": "Afficher la sortie du daemon",
"console_show_errors_only": "Afficher uniquement les erreurs",
"console_show_rpc_ref": "Afficher la référence des commandes RPC",
"console_showing_lines": "Affichage de %zu sur %zu lignes",
"console_starting_node": "Démarrage du nœud...",
"console_status_error": "Erreur",
"console_status_running": "En cours",
"console_status_starting": "Démarrage",
"console_status_stopped": "Arrêté",
"console_status_stopping": "Arrêt",
"console_status_unknown": "Inconnu",
"console_tab_completion": "Tab pour compléter",
"console_type_help": "Tapez 'help' pour les commandes disponibles",
"console_welcome": "Bienvenue dans la console ObsidianDragon",
"console_zoom_in": "Agrandir",
"console_zoom_out": "Réduire",
"copy": "Copier",
"copy_address": "Copier l'adresse complète",
"copy_error": "Copier l'erreur",
"copy_to_clipboard": "Copier dans le presse-papiers",
"copy_txid": "Copier le TxID",
"copy_uri": "Copier l'URI",
"current_price": "Prix actuel",
"custom_fees": "Frais personnalisés",
"dark": "Sombre",
"date": "Date",
"date_label": "Date :",
"delete": "Supprimer",
"difficulty": "Difficulté",
"disconnected": "Déconnecté",
"dismiss": "Ignorer",
"display": "Affichage",
"dragonx_green": "DragonX (Vert)",
"edit": "Modifier",
"error": "Erreur",
"est_time_to_block": "Temps est. par bloc",
"exit": "Quitter",
"explorer": "EXPLORATEUR",
"export": "Exporter",
"export_csv": "Exporter en CSV",
"export_keys_btn": "Exporter les clés",
"export_keys_danger": "DANGER : Ceci exportera TOUTES les clés privées de votre portefeuille ! Toute personne ayant accès à ce fichier peut voler vos fonds. Conservez-le en sécurité et supprimez-le après utilisation.",
"export_keys_include_t": "Inclure les adresses T (transparentes)",
"export_keys_include_z": "Inclure les adresses Z (blindées)",
"export_keys_options": "Options d'exportation :",
"export_keys_success": "Clés exportées avec succès",
"export_keys_title": "Exporter toutes les clés privées",
"export_private_key": "Exporter la clé privée",
"export_tx_count": "Exporter %zu transactions en fichier CSV.",
"export_tx_file_fail": "Impossible de créer le fichier CSV",
"export_tx_none": "Aucune transaction à exporter",
"export_tx_success": "Transactions exportées avec succès",
"export_tx_title": "Exporter les transactions en CSV",
"export_viewing_key": "Exporter la clé de visualisation",
"failed_create_shielded": "Échec de la création de l'adresse blindée",
"failed_create_transparent": "Échec de la création de l'adresse transparente",
"fee": "Frais",
"fee_high": "Élevés",
"fee_label": "Frais :",
"fee_low": "Faibles",
"fee_normal": "Normal",
"fetch_prices": "Récupérer les prix",
"file": "Fichier",
"file_save_location": "Le fichier sera enregistré dans : ~/.config/ObsidianDragon/",
"font_scale": "Taille de police",
"from": "De",
"from_upper": "DE",
"full_details": "Tous les détails",
"general": "Général",
"go_to_receive": "Aller à Recevoir",
"height": "Hauteur",
"help": "Aide",
"hide": "Masquer",
"history": "Historique",
"immature_type": "Immature",
"import": "Importer",
"import_key_btn": "Importer clé(s)",
"import_key_formats": "Formats de clés pris en charge :",
"import_key_full_rescan": "(0 = rescan complet)",
"import_key_label": "Clé(s) privée(s) :",
"import_key_no_valid": "Aucune clé valide trouvée dans l'entrée",
"import_key_rescan": "Re-scanner la blockchain après l'importation",
"import_key_start_height": "Hauteur de départ :",
"import_key_success": "Clés importées avec succès",
"import_key_t_format": "Clés privées WIF d'adresses T",
"import_key_title": "Importer une clé privée",
"import_key_tooltip": "Entrez une ou plusieurs clés privées, une par ligne.\nPrend en charge les clés z-adresse et t-adresse.\nLes lignes commençant par # sont traitées comme des commentaires.",
"import_key_warning": "Attention : Ne partagez jamais vos clés privées ! L'importation de clés provenant de sources non fiables peut compromettre votre portefeuille.",
"import_key_z_format": "Clés de dépenses z-adresse (secret-extended-key-...)",
"import_private_key": "Importer une clé privée...",
"invalid_address": "Format d'adresse invalide",
"ip_address": "Adresse IP",
"keep": "Conserver",
"keep_daemon": "Garder le daemon en marche",
"key_export_fetching": "Récupération de la clé depuis le portefeuille...",
"key_export_private_key": "Clé privée :",
"key_export_private_warning": "Gardez cette clé SECRÈTE ! Toute personne possédant cette clé peut dépenser vos fonds. Ne la partagez jamais en ligne ou avec des tiers non fiables.",
"key_export_reveal": "Révéler la clé",
"key_export_viewing_key": "Clé de visualisation :",
"key_export_viewing_warning": "Cette clé de visualisation permet à d'autres de voir vos transactions entrantes et votre solde, mais PAS de dépenser vos fonds. Ne la partagez qu'avec des personnes de confiance.",
"label": "Libellé :",
"language": "Langue",
"light": "Clair",
"loading": "Chargement...",
"loading_addresses": "Chargement des adresses...",
"local_hashrate": "Hashrate local",
"low_spec_mode": "Mode économie",
"market": "Marché",
"market_12h": "12h",
"market_18h": "18h",
"market_24h": "24h",
"market_24h_volume": "VOLUME 24H",
"market_6h": "6h",
"market_attribution": "Données de prix de NonKYC",
"market_btc_price": "PRIX BTC",
"market_cap": "Capitalisation",
"market_no_history": "Aucun historique de prix disponible",
"market_no_price": "Pas de données de prix",
"market_now": "Maintenant",
"market_pct_shielded": "%.0f%% Blindé",
"market_portfolio": "PORTEFEUILLE",
"market_price_unavailable": "Données de prix indisponibles",
"market_refresh_price": "Actualiser les données de prix",
"market_trade_on": "Échanger sur %s",
"mature": "Mature",
"max": "Max",
"memo": "Mémo (optionnel, chiffré)",
"memo_label": "Mémo :",
"memo_optional": "MÉMO (OPTIONNEL)",
"memo_upper": "MÉMO",
"memo_z_only": "Note : Les mémos ne sont disponibles que lors de l'envoi vers des adresses blindées (z)",
"merge_description": "Fusionnez plusieurs UTXOs en une seule adresse blindée. Cela peut réduire la taille du portefeuille et améliorer la confidentialité.",
"merge_funds": "Fusionner les fonds",
"merge_started": "Opération de fusion démarrée",
"merge_title": "Fusionner vers une adresse",
"mine_when_idle": "Miner au repos",
"mined": "miné",
"mined_filter": "Miné",
"mined_type": "Miné",
"mined_upper": "MINÉ",
"miner_fee": "Frais de mineur",
"mining": "Minage",
"mining_active": "Actif",
"mining_address_copied": "Adresse de minage copiée",
"mining_all_time": "Tout le temps",
"mining_already_saved": "URL du pool déjà enregistrée",
"mining_block_copied": "Hash du bloc copié",
"mining_chart_1m_ago": "il y a 1m",
"mining_chart_5m_ago": "il y a 5m",
"mining_chart_now": "Maintenant",
"mining_chart_start": "Début",
"mining_click": "Cliquer",
"mining_click_copy_address": "Cliquez pour copier l'adresse",
"mining_click_copy_block": "Cliquez pour copier le hash du bloc",
"mining_click_copy_difficulty": "Cliquez pour copier la difficulté",
"mining_connected": "Connecté",
"mining_connecting": "Connexion...",
"mining_control": "Contrôle du minage",
"mining_difficulty_copied": "Difficulté copiée",
"mining_est_block": "Bloc est.",
"mining_est_daily": "Est. quotidien",
"mining_filter_all": "Tout",
"mining_filter_tip_all": "Afficher tous les gains",
"mining_filter_tip_pool": "Afficher uniquement les gains du pool",
"mining_filter_tip_solo": "Afficher uniquement les gains solo",
"mining_idle_off_tooltip": "Activer le minage au repos",
"mining_idle_on_tooltip": "Désactiver le minage au repos",
"mining_local_hashrate": "Hashrate local",
"mining_mine": "Miner",
"mining_mining_addr": "Adr. minage",
"mining_network": "Réseau",
"mining_no_blocks_yet": "Aucun bloc trouvé pour l'instant",
"mining_no_payouts_yet": "Aucun paiement de pool pour l'instant",
"mining_no_saved_addresses": "Aucune adresse enregistrée",
"mining_no_saved_pools": "Aucun pool enregistré",
"mining_off": "Le minage est DÉSACTIVÉ",
"mining_on": "Le minage est ACTIVÉ",
"mining_open_in_explorer": "Ouvrir dans l'explorateur",
"mining_payout_address": "Adresse de paiement",
"mining_payout_tooltip": "Adresse pour recevoir les récompenses de minage",
"mining_pool": "Pool",
"mining_pool_hashrate": "Hashrate du pool",
"mining_pool_url": "URL du pool",
"mining_recent_blocks": "BLOCS RÉCENTS",
"mining_recent_payouts": "PAIEMENTS DE POOL RÉCENTS",
"mining_remove": "Supprimer",
"mining_reset_defaults": "Réinitialiser les paramètres",
"mining_save_payout_address": "Enregistrer l'adresse de paiement",
"mining_save_pool_url": "Enregistrer l'URL du pool",
"mining_saved_addresses": "Adresses enregistrées :",
"mining_saved_pools": "Pools enregistrés :",
"mining_shares": "Parts",
"mining_show_chart": "Graphique",
"mining_show_log": "Journal",
"mining_solo": "Solo",
"mining_starting": "Démarrage...",
"mining_starting_tooltip": "Le mineur démarre...",
"mining_statistics": "Statistiques de minage",
"mining_stop": "Arrêter",
"mining_stop_solo_for_pool": "Arrêtez le minage solo avant de démarrer le minage en pool",
"mining_stop_solo_for_pool_settings": "Arrêtez le minage solo pour modifier les paramètres du pool",
"mining_stopping": "Arrêt...",
"mining_stopping_tooltip": "Le mineur s'arrête...",
"mining_syncing_tooltip": "La blockchain se synchronise...",
"mining_threads": "Threads de minage",
"mining_to_save": "pour enregistrer",
"mining_today": "Aujourd'hui",
"mining_uptime": "Temps de fonctionnement",
"mining_yesterday": "Hier",
"network": "Réseau",
"network_fee": "FRAIS RÉSEAU",
"network_hashrate": "Hashrate du réseau",
"new": "+ Nouveau",
"new_shielded_created": "Nouvelle adresse blindée créée",
"new_t_address": "Nouvelle adresse T",
"new_t_transparent": "Nouvelle adresse t (Transparente)",
"new_transparent_created": "Nouvelle adresse transparente créée",
"new_z_address": "Nouvelle adresse Z",
"new_z_shielded": "Nouvelle adresse z (Blindée)",
"no_addresses": "Aucune adresse trouvée. Créez-en une avec les boutons ci-dessus.",
"no_addresses_available": "Aucune adresse disponible",
"no_addresses_match": "Aucune adresse ne correspond au filtre",
"no_addresses_with_balance": "Aucune adresse avec solde",
"no_matching": "Aucune transaction correspondante",
"no_recent_receives": "Aucune réception récente",
"no_recent_sends": "Aucun envoi récent",
"no_transactions": "Aucune transaction trouvée",
"node": "NŒUD",
"node_security": "NŒUD & SÉCURITÉ",
"noise": "Bruit",
"not_connected": "Non connecté au daemon...",
"not_connected_to_daemon": "Non connecté au daemon",
"notes": "Notes",
"notes_optional": "Notes (optionnel) :",
"output_filename": "Nom du fichier de sortie :",
"overview": "Aperçu",
"paste": "Coller",
"paste_from_clipboard": "Coller depuis le presse-papiers",
"pay_from": "Payer depuis",
"payment_request": "DEMANDE DE PAIEMENT",
"payment_request_copied": "Demande de paiement copiée",
"payment_uri_copied": "URI de paiement copiée",
"peers": "Pairs",
"peers_avg_ping": "Ping moyen",
"peers_ban_24h": "Bannir le pair 24h",
"peers_ban_score": "Score de ban : %d",
"peers_banned": "Bannis",
"peers_banned_count": "Bannis : %d",
"peers_best_block": "Meilleur bloc",
"peers_blockchain": "BLOCKCHAIN",
"peers_blocks": "Blocs",
"peers_blocks_left": "%d blocs restants",
"peers_clear_all_bans": "Lever tous les bannissements",
"peers_click_copy": "Cliquez pour copier",
"peers_connected": "Connectés",
"peers_connected_count": "Connectés : %d",
"peers_copy_ip": "Copier l'IP",
"peers_dir_in": "Ent.",
"peers_dir_out": "Sort.",
"peers_hash_copied": "Hash copié",
"peers_hashrate": "Hashrate",
"peers_in_out": "Ent./Sort.",
"peers_longest": "Plus longue",
"peers_longest_chain": "Plus longue chaîne",
"peers_memory": "Mémoire",
"peers_no_banned": "Aucun pair banni",
"peers_no_connected": "Aucun pair connecté",
"peers_no_tls": "Pas de TLS",
"peers_notarized": "Notarisé",
"peers_p2p_port": "Port P2P",
"peers_protocol": "Protocole",
"peers_received": "Reçu",
"peers_refresh": "Actualiser",
"peers_refresh_tooltip": "Actualiser la liste des pairs",
"peers_refreshing": "Actualisation...",
"peers_sent": "Envoyé",
"peers_tt_id": "ID : %d",
"peers_tt_received": "Reçu : %s",
"peers_tt_sent": "Envoyé : %s",
"peers_tt_services": "Services : %s",
"peers_tt_start_height": "Hauteur de départ : %d",
"peers_tt_synced": "Synchronisé H/B : %d/%d",
"peers_tt_tls_cipher": "TLS : %s",
"peers_unban": "Débannir",
"peers_upper": "PAIRS",
"peers_version": "Version",
"pending": "En attente",
"ping": "Ping",
"price_chart": "Graphique des prix",
"qr_code": "Code QR",
"qr_failed": "Échec de la génération du code QR",
"qr_title": "Code QR",
"qr_unavailable": "QR indisponible",
"receive": "Recevoir",
"received": "reçu",
"received_filter": "Reçu",
"received_label": "Reçu",
"received_upper": "REÇU",
"receiving_addresses": "Vos adresses de réception",
"recent_received": "REÇUS RÉCENTS",
"recent_sends": "ENVOIS RÉCENTS",
"recipient": "DESTINATAIRE",
"recv_type": "Reçu",
"refresh": "Actualiser",
"refresh_now": "Actualiser maintenant",
"report_bug": "Signaler un bug",
"request_amount": "Montant (optionnel) :",
"request_copy_uri": "Copier l'URI",
"request_description": "Générez une demande de paiement que d'autres peuvent scanner ou copier. Le code QR contient votre adresse et un montant/mémo optionnel.",
"request_label": "Libellé (optionnel) :",
"request_memo": "Mémo (optionnel) :",
"request_payment": "Demander un paiement",
"request_payment_uri": "URI de paiement :",
"request_receive_address": "Adresse de réception :",
"request_select_address": "Sélectionner une adresse...",
"request_shielded_addrs": "-- Adresses blindées --",
"request_title": "Demander un paiement",
"request_transparent_addrs": "-- Adresses transparentes --",
"request_uri_copied": "URI de paiement copiée dans le presse-papiers",
"rescan": "Re-scanner",
"reset_to_defaults": "Réinitialiser les paramètres",
"review_send": "Vérifier l'envoi",
"rpc_host": "Hôte RPC",
"rpc_pass": "Mot de passe",
"rpc_port": "Port",
"rpc_user": "Nom d'utilisateur",
"save": "Enregistrer",
"save_settings": "Enregistrer les paramètres",
"save_z_transactions": "Enregistrer les Z-tx dans la liste",
"search_placeholder": "Rechercher...",
"security": "SÉCURITÉ",
"select_address": "Sélectionner une adresse...",
"select_receiving_address": "Sélectionner une adresse de réception...",
"select_source_address": "Sélectionner une adresse source...",
"send": "Envoyer",
"send_amount": "Montant",
"send_amount_details": "DÉTAILS DU MONTANT",
"send_amount_upper": "MONTANT",
"send_clear_fields": "Effacer tous les champs du formulaire ?",
"send_copy_error": "Copier l'erreur",
"send_dismiss": "Ignorer",
"send_error_copied": "Erreur copiée dans le presse-papiers",
"send_error_prefix": "Erreur : %s",
"send_exceeds_available": "Dépasse le disponible (%.8f)",
"send_fee": "Frais",
"send_fee_high": "Élevés",
"send_fee_low": "Faibles",
"send_fee_normal": "Normal",
"send_form_restored": "Formulaire restauré",
"send_from_this_address": "Envoyer depuis cette adresse",
"send_go_to_receive": "Aller à Recevoir",
"send_keep": "Conserver",
"send_network_fee": "FRAIS RÉSEAU",
"send_no_balance": "Pas de solde",
"send_no_recent": "Aucun envoi récent",
"send_recent_sends": "ENVOIS RÉCENTS",
"send_recipient": "DESTINATAIRE",
"send_select_source": "Sélectionner une adresse source...",
"send_sending_from": "ENVOI DEPUIS",
"send_submitting": "Soumission de la transaction...",
"send_switch_to_receive": "Passez à Recevoir pour obtenir votre adresse et commencer à recevoir des fonds.",
"send_to": "Envoyer à",
"send_tooltip_enter_amount": "Entrez un montant à envoyer",
"send_tooltip_exceeds_balance": "Le montant dépasse le solde disponible",
"send_tooltip_in_progress": "Transaction déjà en cours",
"send_tooltip_invalid_address": "Entrez une adresse de destinataire valide",
"send_tooltip_not_connected": "Non connecté au daemon",
"send_tooltip_select_source": "Sélectionnez d'abord une adresse source",
"send_tooltip_syncing": "Attendez la synchronisation de la blockchain",
"send_total": "Total",
"send_transaction": "Envoyer la transaction",
"send_tx_failed": "Transaction échouée",
"send_tx_sent": "Transaction envoyée !",
"send_tx_success": "Transaction envoyée avec succès !",
"send_txid_copied": "TxID copié dans le presse-papiers",
"send_txid_label": "TxID : %s",
"send_valid_shielded": "Adresse blindée valide",
"send_valid_transparent": "Adresse transparente valide",
"send_wallet_empty": "Votre portefeuille est vide",
"send_yes_clear": "Oui, effacer",
"sending": "Envoi de la transaction",
"sending_from": "ENVOI DEPUIS",
"sent": "envoyé",
"sent_filter": "Envoyé",
"sent_type": "Envoyé",
"sent_upper": "ENVOYÉ",
"settings": "Paramètres",
"setup_wizard": "Assistant de configuration",
"share": "Partager",
"shield_check_status": "Vérifier le statut",
"shield_completed": "Opération terminée avec succès !",
"shield_description": "Blindez vos récompenses de minage en envoyant les sorties coinbase des adresses transparentes vers une adresse blindée. Cela améliore la confidentialité en masquant vos revenus de minage.",
"shield_from_address": "Depuis l'adresse :",
"shield_funds": "Blinder les fonds",
"shield_in_progress": "Opération en cours...",
"shield_max_utxos": "UTXOs max par opération",
"shield_merge_done": "Blindage/fusion terminé !",
"shield_select_z": "Sélectionner une z-adresse...",
"shield_started": "Opération de blindage démarrée",
"shield_title": "Blinder les récompenses coinbase",
"shield_to_address": "Vers l'adresse (blindée) :",
"shield_utxo_limit": "Limite UTXO :",
"shield_wildcard_hint": "Utilisez '*' pour blinder depuis toutes les adresses transparentes",
"shielded": "Blindé",
"shielded_to": "BLINDÉ VERS",
"shielded_type": "Blindé",
"show": "Afficher",
"show_qr_code": "Afficher le code QR",
"showing_transactions": "Affichage %d\xe2\x80\x93%d sur %d transactions (total : %zu)",
"simple_background": "Arrière-plan simple",
"start_mining": "Démarrer le minage",
"status": "Statut",
"stop_external": "Arrêter le daemon externe",
"stop_mining": "Arrêter le minage",
"submitting_transaction": "Soumission de la transaction...",
"success": "Succès",
"summary": "Résumé",
"syncing": "Synchronisation...",
"t_addresses": "Adresses T",
"test_connection": "Tester",
"theme": "Thème",
"theme_effects": "Effets de thème",
"time_days_ago": "il y a %d jours",
"time_hours_ago": "il y a %d heures",
"time_minutes_ago": "il y a %d minutes",
"time_seconds_ago": "il y a %d secondes",
"to": "À",
"to_upper": "À",
"tools": "OUTILS",
"total": "Total",
"transaction_id": "ID DE TRANSACTION",
"transaction_sent": "Transaction envoyée avec succès",
"transaction_sent_msg": "Transaction envoyée !",
"transaction_url": "URL de transaction",
"transactions": "Transactions",
"transactions_upper": "TRANSACTIONS",
"transparent": "Transparent",
"tx_confirmations": "%d confirmations",
"tx_details_title": "Détails de la transaction",
"tx_from_address": "Adresse d'origine :",
"tx_id_label": "ID de transaction :",
"tx_immature": "IMMATURE",
"tx_mined": "MINÉ",
"tx_received": "REÇU",
"tx_sent": "ENVOYÉ",
"tx_to_address": "Adresse de destination :",
"tx_view_explorer": "Voir dans l'explorateur",
"txs_count": "%d txs",
"type": "Type",
"ui_opacity": "Opacité de l'interface",
"unban": "Débannir",
"unconfirmed": "Non confirmé",
"undo_clear": "Annuler l'effacement",
"unknown": "Inconnu",
"use_embedded_daemon": "Utiliser le dragonxd intégré",
"use_tor": "Utiliser Tor",
"validate_btn": "Valider",
"validate_description": "Entrez une adresse DragonX pour vérifier si elle est valide et si elle appartient à ce portefeuille.",
"validate_invalid": "INVALIDE",
"validate_is_mine": "Ce portefeuille possède cette adresse",
"validate_not_mine": "N'appartient pas à ce portefeuille",
"validate_ownership": "Propriété :",
"validate_results": "Résultats :",
"validate_shielded_type": "Blindée (z-adresse)",
"validate_status": "Statut :",
"validate_title": "Valider l'adresse",
"validate_transparent_type": "Transparente (t-adresse)",
"validate_type": "Type :",
"validate_valid": "VALIDE",
"validating": "Validation...",
"verbose_logging": "Journalisation détaillée",
"version": "Version",
"view": "Afficher",
"view_details": "Voir les détails",
"view_on_explorer": "Voir dans l'explorateur",
"waiting_for_daemon": "En attente de la connexion au daemon...",
"wallet": "PORTEFEUILLE",
"wallet_empty": "Votre portefeuille est vide",
"wallet_empty_hint": "Passez à Recevoir pour obtenir votre adresse et commencer à recevoir des fonds.",
"warning": "Attention",
"warning_upper": "ATTENTION !",
"website": "Site web",
"window_opacity": "Opacité de la fenêtre",
"yes_clear": "Oui, effacer",
"your_addresses": "Vos adresses",
"z_addresses": "Adresses Z",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "fr.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} French translations to {os.path.abspath(out)}")

646
scripts/gen_ja.py Normal file
View File

@@ -0,0 +1,646 @@
#!/usr/bin/env python3
"""Generate Japanese (ja) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "24時間変動",
"24h_volume": "24時間出来高",
"about": "概要",
"about_block_explorer": "ブロックエクスプローラー",
"about_block_height": "ブロック高:",
"about_build_date": "ビルド日:",
"about_build_type": "ビルドタイプ:",
"about_chain": "チェーン:",
"about_connections": "接続数:",
"about_credits": "クレジット",
"about_daemon": "デーモン:",
"about_debug": "デバッグ",
"about_dragonx": "ObsidianDragonについて",
"about_edition": "ImGui エディション",
"about_github": "GitHub",
"about_imgui": "ImGui",
"about_license": "ライセンス",
"about_license_text": "本ソフトウェアはGNU General Public License v3 (GPLv3)の下で公開されています。ライセンス条項に従い、自由に使用、変更、配布できます。",
"about_peers_count": "%zu ピア",
"about_release": "リリース",
"about_title": "ObsidianDragonについて",
"about_version": "バージョン:",
"about_website": "ウェブサイト",
"acrylic": "アクリル",
"add": "追加",
"address": "アドレス",
"address_book_add": "アドレスを追加",
"address_book_add_new": "新規追加",
"address_book_added": "アドレスをアドレス帳に追加しました",
"address_book_count": "%zu 件のアドレスを保存済み",
"address_book_deleted": "エントリを削除しました",
"address_book_edit": "アドレスを編集",
"address_book_empty": "保存されたアドレスがありません。「新規追加」をクリックして追加してください。",
"address_book_exists": "アドレスは既にアドレス帳に存在します",
"address_book_title": "アドレス帳",
"address_book_update_failed": "更新に失敗しました — アドレスが重複している可能性があります",
"address_book_updated": "アドレスを更新しました",
"address_copied": "アドレスをクリップボードにコピーしました",
"address_details": "アドレス詳細",
"address_label": "アドレス:",
"address_upper": "アドレス",
"address_url": "アドレスURL",
"addresses_appear_here": "接続後、受信アドレスがここに表示されます。",
"advanced": "詳細設定",
"all_filter": "すべて",
"allow_custom_fees": "カスタム手数料を許可",
"amount": "金額",
"amount_details": "金額の詳細",
"amount_exceeds_balance": "金額が残高を超えています",
"amount_label": "金額:",
"appearance": "外観",
"auto_shield": "マイニング自動シールド",
"available": "利用可能",
"backup_backing_up": "バックアップ中...",
"backup_create": "バックアップを作成",
"backup_created": "ウォレットのバックアップを作成しました",
"backup_data": "バックアップとデータ",
"backup_description": "wallet.datファイルのバックアップを作成します。このファイルにはすべての秘密鍵と取引履歴が含まれています。バックアップは安全な場所に保管してください。",
"backup_destination": "バックアップ先:",
"backup_tip_external": "外部ドライブまたはクラウドストレージにバックアップを保存",
"backup_tip_multiple": "異なる場所に複数のバックアップを作成",
"backup_tip_test": "定期的にバックアップからの復元をテスト",
"backup_tips": "ヒント:",
"backup_title": "ウォレットのバックアップ",
"backup_wallet": "ウォレットをバックアップ...",
"backup_wallet_not_found": "警告予想される場所にwallet.datが見つかりません",
"balance": "残高",
"balance_layout": "残高レイアウト",
"ban": "ブロック",
"banned_peers": "ブロック済みピア",
"block": "ブロック",
"block_bits": "ビット:",
"block_click_next": "クリックして次のブロックを表示",
"block_click_prev": "クリックして前のブロックを表示",
"block_explorer": "ブロックエクスプローラー",
"block_get_info": "ブロック情報を取得",
"block_hash": "ブロックハッシュ:",
"block_height": "ブロック高:",
"block_info_title": "ブロック情報",
"block_merkle_root": "マークルルート:",
"block_nav_next": "次へ >>",
"block_nav_prev": "<< 前へ",
"block_next": "次のブロック:",
"block_previous": "前のブロック:",
"block_size": "サイズ:",
"block_timestamp": "タイムスタンプ:",
"block_transactions": "トランザクション:",
"blockchain_syncing": "ブロックチェーン同期中 (%.1f%%)... 残高が不正確な場合があります。",
"cancel": "キャンセル",
"characters": "文字",
"clear": "クリア",
"clear_all_bans": "すべてのブロックを解除",
"clear_form_confirm": "すべてのフォームフィールドをクリアしますか?",
"clear_request": "リクエストをクリア",
"click_copy_address": "クリックしてアドレスをコピー",
"click_copy_uri": "クリックしてURIをコピー",
"close": "閉じる",
"conf_count": "%d 確認",
"confirm_and_send": "確認して送金",
"confirm_send": "送金を確認",
"confirm_transaction": "取引を確認",
"confirmations": "確認数",
"confirmations_display": "%d 確認 | %s",
"confirmed": "確認済み",
"connected": "接続済み",
"connected_peers": "接続中のピア",
"connecting": "接続中...",
"console": "コンソール",
"console_auto_scroll": "自動スクロール",
"console_available_commands": "利用可能なコマンド:",
"console_capturing_output": "デーモン出力をキャプチャ中...",
"console_clear": "クリア",
"console_clear_console": "コンソールをクリア",
"console_cleared": "コンソールをクリアしました",
"console_click_commands": "上のコマンドをクリックして挿入",
"console_click_insert": "クリックして挿入",
"console_click_insert_params": "クリックしてパラメータ付きで挿入",
"console_close": "閉じる",
"console_commands": "コマンド",
"console_common_rpc": "一般的なRPCコマンド",
"console_completions": "補完:",
"console_connected": "デーモンに接続済み",
"console_copy_all": "すべてコピー",
"console_copy_selected": "コピー",
"console_daemon": "デーモン",
"console_daemon_error": "デーモンエラー!",
"console_daemon_started": "デーモンが起動しました",
"console_daemon_stopped": "デーモンが停止しました",
"console_disconnected": "デーモンから切断されました",
"console_errors": "エラー",
"console_filter_hint": "出力をフィルタ...",
"console_help_clear": " clear - コンソールをクリア",
"console_help_getbalance": " getbalance - 透明残高を表示",
"console_help_getblockcount": " getblockcount - 現在のブロック高を表示",
"console_help_getinfo": " getinfo - ノード情報を表示",
"console_help_getmininginfo": " getmininginfo - マイニング状況を表示",
"console_help_getpeerinfo": " getpeerinfo - 接続中のピアを表示",
"console_help_gettotalbalance": " gettotalbalance - 合計残高を表示",
"console_help_help": " help - このヘルプを表示",
"console_help_setgenerate": " setgenerate - マイニングを制御",
"console_help_stop": " stop - デーモンを停止",
"console_line_count": "%zu 行",
"console_new_lines": "%d 新しい行",
"console_no_daemon": "デーモンなし",
"console_not_connected": "エラー:デーモンに接続されていません",
"console_rpc_reference": "RPCコマンドリファレンス",
"console_scanline": "コンソールスキャンライン",
"console_search_commands": "コマンドを検索...",
"console_select_all": "すべて選択",
"console_show_daemon_output": "デーモン出力を表示",
"console_show_errors_only": "エラーのみ表示",
"console_show_rpc_ref": "RPCコマンドリファレンスを表示",
"console_showing_lines": "%zu / %zu 行を表示中",
"console_starting_node": "ノードを起動中...",
"console_status_error": "エラー",
"console_status_running": "実行中",
"console_status_starting": "起動中",
"console_status_stopped": "停止済み",
"console_status_stopping": "停止中",
"console_status_unknown": "不明",
"console_tab_completion": "Tabで補完",
"console_type_help": "'help'と入力して利用可能なコマンドを表示",
"console_welcome": "ObsidianDragonコンソールへようこそ",
"console_zoom_in": "拡大",
"console_zoom_out": "縮小",
"copy": "コピー",
"copy_address": "完全なアドレスをコピー",
"copy_error": "エラーをコピー",
"copy_to_clipboard": "クリップボードにコピー",
"copy_txid": "TxIDをコピー",
"copy_uri": "URIをコピー",
"current_price": "現在の価格",
"custom_fees": "カスタム手数料",
"dark": "ダーク",
"date": "日付",
"date_label": "日付:",
"delete": "削除",
"difficulty": "難易度",
"disconnected": "切断済み",
"dismiss": "閉じる",
"display": "表示",
"dragonx_green": "DragonXグリーン",
"edit": "編集",
"error": "エラー",
"est_time_to_block": "予測ブロック時間",
"exit": "終了",
"explorer": "エクスプローラー",
"export": "エクスポート",
"export_csv": "CSVエクスポート",
"export_keys_btn": "鍵をエクスポート",
"export_keys_danger": "危険:ウォレットからすべての秘密鍵がエクスポートされます!このファイルにアクセスできる人は誰でもあなたの資金を盗めます。安全に保管し、使用後は削除してください。",
"export_keys_include_t": "Tアドレスを含める透明",
"export_keys_include_z": "Zアドレスを含めるシールド",
"export_keys_options": "エクスポートオプション:",
"export_keys_success": "鍵のエクスポートに成功しました",
"export_keys_title": "すべての秘密鍵をエクスポート",
"export_private_key": "秘密鍵をエクスポート",
"export_tx_count": "%zu件の取引をCSVファイルにエクスポート。",
"export_tx_file_fail": "CSVファイルの作成に失敗しました",
"export_tx_none": "エクスポートする取引がありません",
"export_tx_success": "取引のエクスポートに成功しました",
"export_tx_title": "取引をCSVにエクスポート",
"export_viewing_key": "閲覧鍵をエクスポート",
"failed_create_shielded": "シールドアドレスの作成に失敗しました",
"failed_create_transparent": "透明アドレスの作成に失敗しました",
"fee": "手数料",
"fee_high": "高い",
"fee_label": "手数料:",
"fee_low": "低い",
"fee_normal": "通常",
"fetch_prices": "価格を取得",
"file": "ファイル",
"file_save_location": "ファイルの保存先:~/.config/ObsidianDragon/",
"font_scale": "フォントサイズ",
"from": "送信元",
"from_upper": "送信元",
"full_details": "詳細情報",
"general": "一般",
"go_to_receive": "受信へ移動",
"height": "高さ",
"help": "ヘルプ",
"hide": "非表示",
"history": "履歴",
"immature_type": "未成熟",
"import": "インポート",
"import_key_btn": "鍵をインポート",
"import_key_formats": "サポートされる鍵形式:",
"import_key_full_rescan": "0 = 完全再スキャン)",
"import_key_label": "秘密鍵:",
"import_key_no_valid": "入力に有効な鍵が見つかりません",
"import_key_rescan": "インポート後にブロックチェーンを再スキャン",
"import_key_start_height": "開始高:",
"import_key_success": "鍵のインポートに成功しました",
"import_key_t_format": "TアドレスWIF秘密鍵",
"import_key_title": "秘密鍵をインポート",
"import_key_tooltip": "1行に1つずつ秘密鍵を入力してください。\nzアドレスとtアドレスの鍵の両方に対応しています。\n#で始まる行はコメントとして扱われます。",
"import_key_warning": "警告:秘密鍵を決して共有しないでください!信頼できないソースからの鍵のインポートはウォレットを危険にさらす可能性があります。",
"import_key_z_format": "Zアドレス支出鍵 (secret-extended-key-...)",
"import_private_key": "秘密鍵をインポート...",
"invalid_address": "無効なアドレス形式",
"ip_address": "IPアドレス",
"keep": "保持",
"keep_daemon": "デーモンを実行し続ける",
"key_export_fetching": "ウォレットから鍵を取得中...",
"key_export_private_key": "秘密鍵:",
"key_export_private_warning": "この鍵は秘密にしてください!この鍵を持つ人は誰でもあなたの資金を使えます。オンラインや信頼できない相手と共有しないでください。",
"key_export_reveal": "鍵を表示",
"key_export_viewing_key": "閲覧鍵:",
"key_export_viewing_warning": "この閲覧鍵を使うと、他者があなたの受信取引と残高を見ることができますが、資金を使うことはできません。信頼できる相手とのみ共有してください。",
"label": "ラベル:",
"language": "言語",
"light": "ライト",
"loading": "読み込み中...",
"loading_addresses": "アドレスを読み込み中...",
"local_hashrate": "ローカルハッシュレート",
"low_spec_mode": "省電力モード",
"market": "市場",
"market_12h": "12時間",
"market_18h": "18時間",
"market_24h": "24時間",
"market_24h_volume": "24時間出来高",
"market_6h": "6時間",
"market_attribution": "価格データNonKYC提供",
"market_btc_price": "BTC価格",
"market_cap": "時価総額",
"market_no_history": "価格履歴がありません",
"market_no_price": "価格データなし",
"market_now": "現在",
"market_pct_shielded": "%.0f%% シールド済み",
"market_portfolio": "ポートフォリオ",
"market_price_unavailable": "価格データが利用できません",
"market_refresh_price": "価格データを更新",
"market_trade_on": "%s で取引",
"mature": "成熟済み",
"max": "最大",
"memo": "メモ(任意、暗号化)",
"memo_label": "メモ:",
"memo_optional": "メモ(任意)",
"memo_upper": "メモ",
"memo_z_only": "注:メモはシールド (z) アドレスへの送金時のみ利用可能です",
"merge_description": "複数のUTXOを単一のシールドアドレスに統合します。ウォレットサイズの縮小とプライバシーの向上に役立ちます。",
"merge_funds": "資金を統合",
"merge_started": "統合操作を開始しました",
"merge_title": "アドレスに統合",
"mine_when_idle": "アイドル時にマイニング",
"mined": "採掘済み",
"mined_filter": "採掘済み",
"mined_type": "採掘済み",
"mined_upper": "採掘済み",
"miner_fee": "マイナー手数料",
"mining": "マイニング",
"mining_active": "アクティブ",
"mining_address_copied": "マイニングアドレスをコピーしました",
"mining_all_time": "全期間",
"mining_already_saved": "プールURLは既に保存済みです",
"mining_block_copied": "ブロックハッシュをコピーしました",
"mining_chart_1m_ago": "1分前",
"mining_chart_5m_ago": "5分前",
"mining_chart_now": "現在",
"mining_chart_start": "開始",
"mining_click": "クリック",
"mining_click_copy_address": "クリックしてアドレスをコピー",
"mining_click_copy_block": "クリックしてブロックハッシュをコピー",
"mining_click_copy_difficulty": "クリックして難易度をコピー",
"mining_connected": "接続済み",
"mining_connecting": "接続中...",
"mining_control": "マイニング制御",
"mining_difficulty_copied": "難易度をコピーしました",
"mining_est_block": "予測ブロック",
"mining_est_daily": "予測日収",
"mining_filter_all": "すべて",
"mining_filter_tip_all": "すべての収益を表示",
"mining_filter_tip_pool": "プール収益のみ表示",
"mining_filter_tip_solo": "ソロ収益のみ表示",
"mining_idle_off_tooltip": "アイドルマイニングを有効にする",
"mining_idle_on_tooltip": "アイドルマイニングを無効にする",
"mining_local_hashrate": "ローカルハッシュレート",
"mining_mine": "マイニング",
"mining_mining_addr": "マイニングアドレス",
"mining_network": "ネットワーク",
"mining_no_blocks_yet": "まだブロックが見つかっていません",
"mining_no_payouts_yet": "まだプール支払いがありません",
"mining_no_saved_addresses": "保存されたアドレスがありません",
"mining_no_saved_pools": "保存されたプールがありません",
"mining_off": "マイニングはオフです",
"mining_on": "マイニングはオンです",
"mining_open_in_explorer": "エクスプローラーで開く",
"mining_payout_address": "支払いアドレス",
"mining_payout_tooltip": "マイニング報酬の受取アドレス",
"mining_pool": "プール",
"mining_pool_hashrate": "プールハッシュレート",
"mining_pool_url": "プールURL",
"mining_recent_blocks": "最近のブロック",
"mining_recent_payouts": "最近のプール支払い",
"mining_remove": "削除",
"mining_reset_defaults": "デフォルトにリセット",
"mining_save_payout_address": "支払いアドレスを保存",
"mining_save_pool_url": "プールURLを保存",
"mining_saved_addresses": "保存済みアドレス:",
"mining_saved_pools": "保存済みプール:",
"mining_shares": "シェア",
"mining_show_chart": "チャート",
"mining_show_log": "ログ",
"mining_solo": "ソロ",
"mining_starting": "起動中...",
"mining_starting_tooltip": "マイナーを起動中...",
"mining_statistics": "マイニング統計",
"mining_stop": "停止",
"mining_stop_solo_for_pool": "プールマイニングを開始する前にソロマイニングを停止してください",
"mining_stop_solo_for_pool_settings": "プール設定を変更するにはソロマイニングを停止してください",
"mining_stopping": "停止中...",
"mining_stopping_tooltip": "マイナーを停止中...",
"mining_syncing_tooltip": "ブロックチェーン同期中...",
"mining_threads": "マイニングスレッド",
"mining_to_save": "保存する",
"mining_today": "今日",
"mining_uptime": "稼働時間",
"mining_yesterday": "昨日",
"network": "ネットワーク",
"network_fee": "ネットワーク手数料",
"network_hashrate": "ネットワークハッシュレート",
"new": "+ 新規",
"new_shielded_created": "新しいシールドアドレスを作成しました",
"new_t_address": "新しいTアドレス",
"new_t_transparent": "新しいtアドレス透明",
"new_transparent_created": "新しい透明アドレスを作成しました",
"new_z_address": "新しいZアドレス",
"new_z_shielded": "新しいzアドレスシールド",
"no_addresses": "アドレスが見つかりません。上のボタンを使用して作成してください。",
"no_addresses_available": "利用可能なアドレスがありません",
"no_addresses_match": "フィルタに一致するアドレスがありません",
"no_addresses_with_balance": "残高のあるアドレスがありません",
"no_matching": "一致する取引がありません",
"no_recent_receives": "最近の受信がありません",
"no_recent_sends": "最近の送信がありません",
"no_transactions": "取引が見つかりません",
"node": "ノード",
"node_security": "ノードとセキュリティ",
"noise": "ノイズ",
"not_connected": "デーモンに未接続...",
"not_connected_to_daemon": "デーモンに未接続",
"notes": "メモ",
"notes_optional": "メモ(任意):",
"output_filename": "出力ファイル名:",
"overview": "概要",
"paste": "貼り付け",
"paste_from_clipboard": "クリップボードから貼り付け",
"pay_from": "支払い元",
"payment_request": "支払い請求",
"payment_request_copied": "支払い請求をコピーしました",
"payment_uri_copied": "支払いURIをコピーしました",
"peers": "ピア",
"peers_avg_ping": "平均Ping",
"peers_ban_24h": "ピアを24時間ブロック",
"peers_ban_score": "ブロックスコア:%d",
"peers_banned": "ブロック済み",
"peers_banned_count": "ブロック済み:%d",
"peers_best_block": "最良ブロック",
"peers_blockchain": "ブロックチェーン",
"peers_blocks": "ブロック",
"peers_blocks_left": "残り %d ブロック",
"peers_clear_all_bans": "すべてのブロックを解除",
"peers_click_copy": "クリックしてコピー",
"peers_connected": "接続済み",
"peers_connected_count": "接続済み:%d",
"peers_copy_ip": "IPをコピー",
"peers_dir_in": "",
"peers_dir_out": "",
"peers_hash_copied": "ハッシュをコピーしました",
"peers_hashrate": "ハッシュレート",
"peers_in_out": "入/出",
"peers_longest": "最長",
"peers_longest_chain": "最長チェーン",
"peers_memory": "メモリ",
"peers_no_banned": "ブロック済みピアなし",
"peers_no_connected": "接続済みピアなし",
"peers_no_tls": "TLSなし",
"peers_notarized": "公証済み",
"peers_p2p_port": "P2Pポート",
"peers_protocol": "プロトコル",
"peers_received": "受信",
"peers_refresh": "更新",
"peers_refresh_tooltip": "ピアリストを更新",
"peers_refreshing": "更新中...",
"peers_sent": "送信",
"peers_tt_id": "ID%d",
"peers_tt_received": "受信:%s",
"peers_tt_sent": "送信:%s",
"peers_tt_services": "サービス:%s",
"peers_tt_start_height": "開始高:%d",
"peers_tt_synced": "同期済み H/B%d/%d",
"peers_tt_tls_cipher": "TLS%s",
"peers_unban": "ブロック解除",
"peers_upper": "ピア",
"peers_version": "バージョン",
"pending": "保留中",
"ping": "Ping",
"price_chart": "価格チャート",
"qr_code": "QRコード",
"qr_failed": "QRコードの生成に失敗しました",
"qr_title": "QRコード",
"qr_unavailable": "QR利用不可",
"receive": "受信",
"received": "受信済み",
"received_filter": "受信済み",
"received_label": "受信済み",
"received_upper": "受信済み",
"receiving_addresses": "あなたの受信アドレス",
"recent_received": "最近の受信",
"recent_sends": "最近の送信",
"recipient": "受取人",
"recv_type": "受信",
"refresh": "更新",
"refresh_now": "今すぐ更新",
"report_bug": "バグを報告",
"request_amount": "金額(任意):",
"request_copy_uri": "URIをコピー",
"request_description": "他の人がスキャンまたはコピーできる支払い請求を生成します。QRコードにはアドレスとオプションの金額/メモが含まれます。",
"request_label": "ラベル(任意):",
"request_memo": "メモ(任意):",
"request_payment": "支払いを請求",
"request_payment_uri": "支払いURI",
"request_receive_address": "受信アドレス:",
"request_select_address": "アドレスを選択...",
"request_shielded_addrs": "-- シールドアドレス --",
"request_title": "支払いを請求",
"request_transparent_addrs": "-- 透明アドレス --",
"request_uri_copied": "支払いURIをクリップボードにコピーしました",
"rescan": "再スキャン",
"reset_to_defaults": "デフォルトにリセット",
"review_send": "送金を確認",
"rpc_host": "RPCホスト",
"rpc_pass": "パスワード",
"rpc_port": "ポート",
"rpc_user": "ユーザー名",
"save": "保存",
"save_settings": "設定を保存",
"save_z_transactions": "Z取引を取引リストに保存",
"search_placeholder": "検索...",
"security": "セキュリティ",
"select_address": "アドレスを選択...",
"select_receiving_address": "受信アドレスを選択...",
"select_source_address": "送信元アドレスを選択...",
"send": "送金",
"send_amount": "金額",
"send_amount_details": "金額の詳細",
"send_amount_upper": "金額",
"send_clear_fields": "すべてのフォームフィールドをクリアしますか?",
"send_copy_error": "エラーをコピー",
"send_dismiss": "閉じる",
"send_error_copied": "エラーをクリップボードにコピーしました",
"send_error_prefix": "エラー:%s",
"send_exceeds_available": "利用可能額を超過 (%.8f)",
"send_fee": "手数料",
"send_fee_high": "高い",
"send_fee_low": "低い",
"send_fee_normal": "通常",
"send_form_restored": "フォームが復元されました",
"send_from_this_address": "このアドレスから送金",
"send_go_to_receive": "受信へ移動",
"send_keep": "保持",
"send_network_fee": "ネットワーク手数料",
"send_no_balance": "残高なし",
"send_no_recent": "最近の送信なし",
"send_recent_sends": "最近の送信",
"send_recipient": "受取人",
"send_select_source": "送信元アドレスを選択...",
"send_sending_from": "送信元",
"send_submitting": "取引を送信中...",
"send_switch_to_receive": "受信に切り替えてアドレスを取得し、資金の受け取りを開始してください。",
"send_to": "送金先",
"send_tooltip_enter_amount": "送金額を入力してください",
"send_tooltip_exceeds_balance": "金額が利用可能残高を超えています",
"send_tooltip_in_progress": "取引は既に進行中です",
"send_tooltip_invalid_address": "有効な受取人アドレスを入力してください",
"send_tooltip_not_connected": "デーモンに未接続",
"send_tooltip_select_source": "まず送信元アドレスを選択してください",
"send_tooltip_syncing": "ブロックチェーンの同期をお待ちください",
"send_total": "合計",
"send_transaction": "取引を送信",
"send_tx_failed": "取引に失敗しました",
"send_tx_sent": "取引を送信しました!",
"send_tx_success": "取引の送信に成功しました!",
"send_txid_copied": "TxIDをクリップボードにコピーしました",
"send_txid_label": "TxID%s",
"send_valid_shielded": "有効なシールドアドレス",
"send_valid_transparent": "有効な透明アドレス",
"send_wallet_empty": "ウォレットは空です",
"send_yes_clear": "はい、クリア",
"sending": "取引を送信中",
"sending_from": "送信元",
"sent": "送信済み",
"sent_filter": "送信済み",
"sent_type": "送信済み",
"sent_upper": "送信済み",
"settings": "設定",
"setup_wizard": "セットアップウィザード",
"share": "共有",
"shield_check_status": "ステータスを確認",
"shield_completed": "操作が正常に完了しました!",
"shield_description": "透明アドレスのcoinbase出力をシールドアドレスに送信して、マイニング報酬をシールドします。マイニング収入を隠すことでプライバシーが向上します。",
"shield_from_address": "送信元アドレス:",
"shield_funds": "資金をシールド",
"shield_in_progress": "操作進行中...",
"shield_max_utxos": "1回の操作あたりの最大UTXO数",
"shield_merge_done": "シールド/統合が完了しました!",
"shield_select_z": "zアドレスを選択...",
"shield_started": "シールド操作を開始しました",
"shield_title": "Coinbase報酬をシールド",
"shield_to_address": "送信先アドレス(シールド):",
"shield_utxo_limit": "UTXO制限",
"shield_wildcard_hint": "'*' を使用してすべての透明アドレスからシールド",
"shielded": "シールド",
"shielded_to": "シールド先",
"shielded_type": "シールド",
"show": "表示",
"show_qr_code": "QRコードを表示",
"showing_transactions": "%d\xe2\x80\x93%d / %d 件の取引を表示中(合計:%zu",
"simple_background": "シンプル背景",
"start_mining": "マイニング開始",
"status": "ステータス",
"stop_external": "外部デーモンを停止",
"stop_mining": "マイニング停止",
"submitting_transaction": "取引を送信中...",
"success": "成功",
"summary": "概要",
"syncing": "同期中...",
"t_addresses": "Tアドレス",
"test_connection": "テスト",
"theme": "テーマ",
"theme_effects": "テーマ効果",
"time_days_ago": "%d日前",
"time_hours_ago": "%d時間前",
"time_minutes_ago": "%d分前",
"time_seconds_ago": "%d秒前",
"to": "宛先",
"to_upper": "宛先",
"tools": "ツール",
"total": "合計",
"transaction_id": "取引ID",
"transaction_sent": "取引の送信に成功しました",
"transaction_sent_msg": "取引を送信しました!",
"transaction_url": "取引URL",
"transactions": "取引",
"transactions_upper": "取引",
"transparent": "透明",
"tx_confirmations": "%d 確認",
"tx_details_title": "取引の詳細",
"tx_from_address": "送信元アドレス:",
"tx_id_label": "取引ID",
"tx_immature": "未成熟",
"tx_mined": "採掘済み",
"tx_received": "受信済み",
"tx_sent": "送信済み",
"tx_to_address": "送信先アドレス:",
"tx_view_explorer": "エクスプローラーで表示",
"txs_count": "%d",
"type": "タイプ",
"ui_opacity": "UI透明度",
"unban": "ブロック解除",
"unconfirmed": "未確認",
"undo_clear": "クリアを元に戻す",
"unknown": "不明",
"use_embedded_daemon": "内蔵dragonxdを使用",
"use_tor": "Torを使用",
"validate_btn": "検証",
"validate_description": "DragonXアドレスを入力して、有効かどうか、そしてこのウォレットに属しているかどうかを確認します。",
"validate_invalid": "無効",
"validate_is_mine": "このウォレットがこのアドレスを所有しています",
"validate_not_mine": "このウォレットに属していません",
"validate_ownership": "所有者:",
"validate_results": "結果:",
"validate_shielded_type": "シールドzアドレス",
"validate_status": "ステータス:",
"validate_title": "アドレスを検証",
"validate_transparent_type": "透明tアドレス",
"validate_type": "タイプ:",
"validate_valid": "有効",
"validating": "検証中...",
"verbose_logging": "詳細ログ",
"version": "バージョン",
"view": "表示",
"view_details": "詳細を表示",
"view_on_explorer": "エクスプローラーで表示",
"waiting_for_daemon": "デーモン接続を待機中...",
"wallet": "ウォレット",
"wallet_empty": "ウォレットは空です",
"wallet_empty_hint": "受信に切り替えてアドレスを取得し、資金の受け取りを開始してください。",
"warning": "警告",
"warning_upper": "警告!",
"website": "ウェブサイト",
"window_opacity": "ウィンドウ透明度",
"yes_clear": "はい、クリア",
"your_addresses": "あなたのアドレス",
"z_addresses": "Zアドレス",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "ja.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} Japanese translations to {os.path.abspath(out)}")

646
scripts/gen_ko.py Normal file
View File

@@ -0,0 +1,646 @@
#!/usr/bin/env python3
"""Generate Korean (ko) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "24시간 변동",
"24h_volume": "24시간 거래량",
"about": "정보",
"about_block_explorer": "블록 탐색기",
"about_block_height": "블록 높이:",
"about_build_date": "빌드 날짜:",
"about_build_type": "빌드 유형:",
"about_chain": "체인:",
"about_connections": "연결:",
"about_credits": "크레딧",
"about_daemon": "데몬:",
"about_debug": "디버그",
"about_dragonx": "ObsidianDragon 정보",
"about_edition": "ImGui 에디션",
"about_github": "GitHub",
"about_imgui": "ImGui:",
"about_license": "라이선스",
"about_license_text": "본 소프트웨어는 GNU General Public License v3 (GPLv3) 하에 배포됩니다. 라이선스 조건에 따라 자유롭게 사용, 수정 및 배포할 수 있습니다.",
"about_peers_count": "%zu 피어",
"about_release": "릴리스",
"about_title": "ObsidianDragon 정보",
"about_version": "버전:",
"about_website": "웹사이트",
"acrylic": "아크릴",
"add": "추가",
"address": "주소",
"address_book_add": "주소 추가",
"address_book_add_new": "새로 추가",
"address_book_added": "주소록에 주소를 추가했습니다",
"address_book_count": "저장된 주소 %zu개",
"address_book_deleted": "항목이 삭제되었습니다",
"address_book_edit": "주소 편집",
"address_book_empty": "저장된 주소가 없습니다. '새로 추가'를 클릭하여 추가하세요.",
"address_book_exists": "주소가 이미 주소록에 있습니다",
"address_book_title": "주소록",
"address_book_update_failed": "업데이트 실패 — 주소가 중복될 수 있습니다",
"address_book_updated": "주소가 업데이트되었습니다",
"address_copied": "주소가 클립보드에 복사되었습니다",
"address_details": "주소 상세",
"address_label": "주소:",
"address_upper": "주소",
"address_url": "주소 URL",
"addresses_appear_here": "연결 후 수신 주소가 여기에 표시됩니다.",
"advanced": "고급 설정",
"all_filter": "전체",
"allow_custom_fees": "사용자 정의 수수료 허용",
"amount": "금액",
"amount_details": "금액 상세",
"amount_exceeds_balance": "금액이 잔액을 초과합니다",
"amount_label": "금액:",
"appearance": "외관",
"auto_shield": "채굴 자동 차폐",
"available": "사용 가능",
"backup_backing_up": "백업 중...",
"backup_create": "백업 생성",
"backup_created": "지갑 백업이 생성되었습니다",
"backup_data": "백업 및 데이터",
"backup_description": "wallet.dat 파일의 백업을 생성합니다. 이 파일에는 모든 개인 키와 거래 내역이 포함되어 있습니다. 백업을 안전한 곳에 보관하세요.",
"backup_destination": "백업 위치:",
"backup_tip_external": "외장 드라이브 또는 클라우드 스토리지에 백업 저장",
"backup_tip_multiple": "서로 다른 위치에 여러 백업 생성",
"backup_tip_test": "정기적으로 백업 복원 테스트",
"backup_tips": "팁:",
"backup_title": "지갑 백업",
"backup_wallet": "지갑 백업...",
"backup_wallet_not_found": "경고: 예상 위치에서 wallet.dat를 찾을 수 없습니다",
"balance": "잔액",
"balance_layout": "잔액 레이아웃",
"ban": "차단",
"banned_peers": "차단된 피어",
"block": "블록",
"block_bits": "비트:",
"block_click_next": "클릭하여 다음 블록 보기",
"block_click_prev": "클릭하여 이전 블록 보기",
"block_explorer": "블록 탐색기",
"block_get_info": "블록 정보 조회",
"block_hash": "블록 해시:",
"block_height": "블록 높이:",
"block_info_title": "블록 정보",
"block_merkle_root": "머클 루트:",
"block_nav_next": "다음 >>",
"block_nav_prev": "<< 이전",
"block_next": "다음 블록:",
"block_previous": "이전 블록:",
"block_size": "크기:",
"block_timestamp": "타임스탬프:",
"block_transactions": "트랜잭션:",
"blockchain_syncing": "블록체인 동기화 중 (%.1f%%)... 잔액이 정확하지 않을 수 있습니다.",
"cancel": "취소",
"characters": "문자",
"clear": "지우기",
"clear_all_bans": "모든 차단 해제",
"clear_form_confirm": "모든 양식 필드를 지우시겠습니까?",
"clear_request": "요청 지우기",
"click_copy_address": "클릭하여 주소 복사",
"click_copy_uri": "클릭하여 URI 복사",
"close": "닫기",
"conf_count": "%d 확인",
"confirm_and_send": "확인 후 전송",
"confirm_send": "전송 확인",
"confirm_transaction": "거래 확인",
"confirmations": "확인 수",
"confirmations_display": "%d 확인 | %s",
"confirmed": "확인됨",
"connected": "연결됨",
"connected_peers": "연결된 피어",
"connecting": "연결 중...",
"console": "콘솔",
"console_auto_scroll": "자동 스크롤",
"console_available_commands": "사용 가능한 명령어:",
"console_capturing_output": "데몬 출력 캡처 중...",
"console_clear": "지우기",
"console_clear_console": "콘솔 지우기",
"console_cleared": "콘솔이 지워졌습니다",
"console_click_commands": "위의 명령어를 클릭하여 삽입",
"console_click_insert": "클릭하여 삽입",
"console_click_insert_params": "클릭하여 매개변수와 함께 삽입",
"console_close": "닫기",
"console_commands": "명령어",
"console_common_rpc": "일반 RPC 명령어:",
"console_completions": "자동 완성:",
"console_connected": "데몬에 연결됨",
"console_copy_all": "모두 복사",
"console_copy_selected": "복사",
"console_daemon": "데몬",
"console_daemon_error": "데몬 오류!",
"console_daemon_started": "데몬이 시작되었습니다",
"console_daemon_stopped": "데몬이 중지되었습니다",
"console_disconnected": "데몬 연결이 끊어졌습니다",
"console_errors": "오류",
"console_filter_hint": "출력 필터...",
"console_help_clear": " clear - 콘솔 지우기",
"console_help_getbalance": " getbalance - 투명 잔액 표시",
"console_help_getblockcount": " getblockcount - 현재 블록 높이 표시",
"console_help_getinfo": " getinfo - 노드 정보 표시",
"console_help_getmininginfo": " getmininginfo - 채굴 상태 표시",
"console_help_getpeerinfo": " getpeerinfo - 연결된 피어 표시",
"console_help_gettotalbalance": " gettotalbalance - 총 잔액 표시",
"console_help_help": " help - 도움말 표시",
"console_help_setgenerate": " setgenerate - 채굴 제어",
"console_help_stop": " stop - 데몬 중지",
"console_line_count": "%zu줄",
"console_new_lines": "%d 새 줄",
"console_no_daemon": "데몬 없음",
"console_not_connected": "오류: 데몬에 연결되지 않았습니다",
"console_rpc_reference": "RPC 명령어 참조",
"console_scanline": "콘솔 스캔라인",
"console_search_commands": "명령어 검색...",
"console_select_all": "모두 선택",
"console_show_daemon_output": "데몬 출력 표시",
"console_show_errors_only": "오류만 표시",
"console_show_rpc_ref": "RPC 명령어 참조 표시",
"console_showing_lines": "%zu / %zu줄 표시 중",
"console_starting_node": "노드 시작 중...",
"console_status_error": "오류",
"console_status_running": "실행 중",
"console_status_starting": "시작 중",
"console_status_stopped": "중지됨",
"console_status_stopping": "중지 중",
"console_status_unknown": "알 수 없음",
"console_tab_completion": "Tab으로 자동 완성",
"console_type_help": "'help'를 입력하여 사용 가능한 명령어 보기",
"console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다",
"console_zoom_in": "확대",
"console_zoom_out": "축소",
"copy": "복사",
"copy_address": "전체 주소 복사",
"copy_error": "오류 복사",
"copy_to_clipboard": "클립보드에 복사",
"copy_txid": "TxID 복사",
"copy_uri": "URI 복사",
"current_price": "현재 가격",
"custom_fees": "사용자 정의 수수료",
"dark": "다크",
"date": "날짜",
"date_label": "날짜:",
"delete": "삭제",
"difficulty": "난이도",
"disconnected": "연결 끊김",
"dismiss": "닫기",
"display": "디스플레이",
"dragonx_green": "DragonX(그린)",
"edit": "편집",
"error": "오류",
"est_time_to_block": "예상 블록 시간",
"exit": "종료",
"explorer": "탐색기",
"export": "내보내기",
"export_csv": "CSV 내보내기",
"export_keys_btn": "키 내보내기",
"export_keys_danger": "위험: 지갑의 모든 개인 키가 내보내집니다! 이 파일에 접근할 수 있는 사람은 누구나 자금을 훔칠 수 있습니다. 안전하게 보관하고 사용 후 삭제하세요.",
"export_keys_include_t": "T 주소 포함 (투명)",
"export_keys_include_z": "Z 주소 포함 (차폐)",
"export_keys_options": "내보내기 옵션:",
"export_keys_success": "키 내보내기 성공",
"export_keys_title": "모든 개인 키 내보내기",
"export_private_key": "개인 키 내보내기",
"export_tx_count": "%zu건의 거래를 CSV 파일로 내보냈습니다.",
"export_tx_file_fail": "CSV 파일 생성 실패",
"export_tx_none": "내보낼 거래가 없습니다",
"export_tx_success": "거래 내보내기 성공",
"export_tx_title": "거래를 CSV로 내보내기",
"export_viewing_key": "조회 키 내보내기",
"failed_create_shielded": "차폐 주소 생성 실패",
"failed_create_transparent": "투명 주소 생성 실패",
"fee": "수수료",
"fee_high": "높음",
"fee_label": "수수료:",
"fee_low": "낮음",
"fee_normal": "보통",
"fetch_prices": "가격 조회",
"file": "파일",
"file_save_location": "파일 저장 위치: ~/.config/ObsidianDragon/",
"font_scale": "글꼴 크기",
"from": "보낸 곳",
"from_upper": "보낸 곳",
"full_details": "전체 세부 정보",
"general": "일반",
"go_to_receive": "수신으로 이동",
"height": "높이",
"help": "도움말",
"hide": "숨기기",
"history": "내역",
"immature_type": "미성숙",
"import": "가져오기",
"import_key_btn": "키 가져오기",
"import_key_formats": "지원되는 키 형식:",
"import_key_full_rescan": "(0 = 전체 재스캔)",
"import_key_label": "개인 키:",
"import_key_no_valid": "입력에서 유효한 키를 찾을 수 없습니다",
"import_key_rescan": "가져오기 후 블록체인 재스캔",
"import_key_start_height": "시작 높이:",
"import_key_success": "키 가져오기 성공",
"import_key_t_format": "T 주소 WIF 개인 키",
"import_key_title": "개인 키 가져오기",
"import_key_tooltip": "한 줄에 하나의 개인 키를 입력하세요.\nz 주소와 t 주소 키 모두 지원됩니다.\n#으로 시작하는 줄은 주석으로 처리됩니다.",
"import_key_warning": "경고: 개인 키를 절대 공유하지 마세요! 신뢰할 수 없는 소스의 키를 가져오면 지갑이 위험해질 수 있습니다.",
"import_key_z_format": "Z 주소 지출 키 (secret-extended-key-...)",
"import_private_key": "개인 키 가져오기...",
"invalid_address": "잘못된 주소 형식",
"ip_address": "IP 주소",
"keep": "유지",
"keep_daemon": "데몬 계속 실행",
"key_export_fetching": "지갑에서 키를 가져오는 중...",
"key_export_private_key": "개인 키:",
"key_export_private_warning": "이 키를 비밀로 유지하세요! 이 키를 가진 사람은 누구나 자금을 사용할 수 있습니다. 온라인이나 신뢰할 수 없는 사람과 공유하지 마세요.",
"key_export_reveal": "키 표시",
"key_export_viewing_key": "조회 키:",
"key_export_viewing_warning": "이 조회 키를 사용하면 다른 사람이 수신 거래와 잔액을 볼 수 있지만 자금을 사용할 수는 없습니다. 신뢰할 수 있는 사람에게만 공유하세요.",
"label": "라벨:",
"language": "언어",
"light": "라이트",
"loading": "로딩 중...",
"loading_addresses": "주소 로딩 중...",
"local_hashrate": "로컬 해시레이트",
"low_spec_mode": "저사양 모드",
"market": "시장",
"market_12h": "12시간",
"market_18h": "18시간",
"market_24h": "24시간",
"market_24h_volume": "24시간 거래량",
"market_6h": "6시간",
"market_attribution": "가격 데이터: NonKYC 제공",
"market_btc_price": "BTC 가격",
"market_cap": "시가총액",
"market_no_history": "가격 내역 없음",
"market_no_price": "가격 데이터 없음",
"market_now": "현재",
"market_pct_shielded": "%.0f%% 차폐됨",
"market_portfolio": "포트폴리오",
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
"market_refresh_price": "가격 데이터 새로고침",
"market_trade_on": "%s에서 거래",
"mature": "성숙됨",
"max": "최대",
"memo": "메모 (선택, 암호화)",
"memo_label": "메모:",
"memo_optional": "메모 (선택)",
"memo_upper": "메모",
"memo_z_only": "참고: 메모는 차폐 (z) 주소로 전송할 때만 사용할 수 있습니다",
"merge_description": "여러 UTXO를 단일 차폐 주소로 통합합니다. 지갑 크기를 줄이고 프라이버시를 향상시킵니다.",
"merge_funds": "자금 통합",
"merge_started": "통합 작업이 시작되었습니다",
"merge_title": "주소로 통합",
"mine_when_idle": "유휴 시 채굴",
"mined": "채굴됨",
"mined_filter": "채굴됨",
"mined_type": "채굴됨",
"mined_upper": "채굴됨",
"miner_fee": "채굴 수수료",
"mining": "채굴",
"mining_active": "활성",
"mining_address_copied": "채굴 주소가 복사되었습니다",
"mining_all_time": "전체 기간",
"mining_already_saved": "풀 URL이 이미 저장되어 있습니다",
"mining_block_copied": "블록 해시가 복사되었습니다",
"mining_chart_1m_ago": "1분 전",
"mining_chart_5m_ago": "5분 전",
"mining_chart_now": "현재",
"mining_chart_start": "시작",
"mining_click": "클릭",
"mining_click_copy_address": "클릭하여 주소 복사",
"mining_click_copy_block": "클릭하여 블록 해시 복사",
"mining_click_copy_difficulty": "클릭하여 난이도 복사",
"mining_connected": "연결됨",
"mining_connecting": "연결 중...",
"mining_control": "채굴 제어",
"mining_difficulty_copied": "난이도가 복사되었습니다",
"mining_est_block": "예상 블록",
"mining_est_daily": "예상 일일 수익",
"mining_filter_all": "전체",
"mining_filter_tip_all": "모든 수익 표시",
"mining_filter_tip_pool": "풀 수익만 표시",
"mining_filter_tip_solo": "솔로 수익만 표시",
"mining_idle_off_tooltip": "유휴 채굴 활성화",
"mining_idle_on_tooltip": "유휴 채굴 비활성화",
"mining_local_hashrate": "로컬 해시레이트",
"mining_mine": "채굴",
"mining_mining_addr": "채굴 주소",
"mining_network": "네트워크",
"mining_no_blocks_yet": "아직 블록을 찾지 못했습니다",
"mining_no_payouts_yet": "아직 풀 지급이 없습니다",
"mining_no_saved_addresses": "저장된 주소 없음",
"mining_no_saved_pools": "저장된 풀 없음",
"mining_off": "채굴이 꺼져 있습니다",
"mining_on": "채굴이 켜져 있습니다",
"mining_open_in_explorer": "탐색기에서 열기",
"mining_payout_address": "지급 주소",
"mining_payout_tooltip": "채굴 보상 수신 주소",
"mining_pool": "",
"mining_pool_hashrate": "풀 해시레이트",
"mining_pool_url": "풀 URL",
"mining_recent_blocks": "최근 블록",
"mining_recent_payouts": "최근 풀 지급",
"mining_remove": "제거",
"mining_reset_defaults": "기본값으로 재설정",
"mining_save_payout_address": "지급 주소 저장",
"mining_save_pool_url": "풀 URL 저장",
"mining_saved_addresses": "저장된 주소:",
"mining_saved_pools": "저장된 풀:",
"mining_shares": "셰어",
"mining_show_chart": "차트",
"mining_show_log": "로그",
"mining_solo": "솔로",
"mining_starting": "시작 중...",
"mining_starting_tooltip": "채굴기 시작 중...",
"mining_statistics": "채굴 통계",
"mining_stop": "중지",
"mining_stop_solo_for_pool": "풀 채굴을 시작하려면 솔로 채굴을 먼저 중지하세요",
"mining_stop_solo_for_pool_settings": "풀 설정을 변경하려면 솔로 채굴을 중지하세요",
"mining_stopping": "중지 중...",
"mining_stopping_tooltip": "채굴기 중지 중...",
"mining_syncing_tooltip": "블록체인 동기화 중...",
"mining_threads": "채굴 스레드",
"mining_to_save": "저장하려면",
"mining_today": "오늘",
"mining_uptime": "가동 시간",
"mining_yesterday": "어제",
"network": "네트워크",
"network_fee": "네트워크 수수료",
"network_hashrate": "네트워크 해시레이트",
"new": "+ 새로 만들기",
"new_shielded_created": "새 차폐 주소가 생성되었습니다",
"new_t_address": "새 T 주소",
"new_t_transparent": "새 t 주소 (투명)",
"new_transparent_created": "새 투명 주소가 생성되었습니다",
"new_z_address": "새 Z 주소",
"new_z_shielded": "새 z 주소 (차폐)",
"no_addresses": "주소가 없습니다. 위의 버튼을 사용하여 생성하세요.",
"no_addresses_available": "사용 가능한 주소 없음",
"no_addresses_match": "필터와 일치하는 주소가 없습니다",
"no_addresses_with_balance": "잔액이 있는 주소가 없습니다",
"no_matching": "일치하는 거래가 없습니다",
"no_recent_receives": "최근 수신 내역 없음",
"no_recent_sends": "최근 전송 내역 없음",
"no_transactions": "거래 내역이 없습니다",
"node": "노드",
"node_security": "노드 및 보안",
"noise": "노이즈",
"not_connected": "데몬에 연결되지 않음...",
"not_connected_to_daemon": "데몬에 연결되지 않음",
"notes": "메모",
"notes_optional": "메모 (선택):",
"output_filename": "출력 파일명:",
"overview": "개요",
"paste": "붙여넣기",
"paste_from_clipboard": "클립보드에서 붙여넣기",
"pay_from": "보낼 곳",
"payment_request": "결제 요청",
"payment_request_copied": "결제 요청이 복사되었습니다",
"payment_uri_copied": "결제 URI가 복사되었습니다",
"peers": "피어",
"peers_avg_ping": "평균 Ping",
"peers_ban_24h": "피어 24시간 차단",
"peers_ban_score": "차단 점수: %d",
"peers_banned": "차단됨",
"peers_banned_count": "차단됨: %d",
"peers_best_block": "최고 블록",
"peers_blockchain": "블록체인",
"peers_blocks": "블록",
"peers_blocks_left": "남은 블록: %d",
"peers_clear_all_bans": "모든 차단 해제",
"peers_click_copy": "클릭하여 복사",
"peers_connected": "연결됨",
"peers_connected_count": "연결됨: %d",
"peers_copy_ip": "IP 복사",
"peers_dir_in": "수신",
"peers_dir_out": "송신",
"peers_hash_copied": "해시가 복사되었습니다",
"peers_hashrate": "해시레이트",
"peers_in_out": "수신/송신",
"peers_longest": "최장",
"peers_longest_chain": "최장 체인",
"peers_memory": "메모리",
"peers_no_banned": "차단된 피어 없음",
"peers_no_connected": "연결된 피어 없음",
"peers_no_tls": "TLS 없음",
"peers_notarized": "공증됨",
"peers_p2p_port": "P2P 포트",
"peers_protocol": "프로토콜",
"peers_received": "수신됨",
"peers_refresh": "새로고침",
"peers_refresh_tooltip": "피어 목록 새로고침",
"peers_refreshing": "새로고침 중...",
"peers_sent": "전송됨",
"peers_tt_id": "ID: %d",
"peers_tt_received": "수신: %s",
"peers_tt_sent": "전송: %s",
"peers_tt_services": "서비스: %s",
"peers_tt_start_height": "시작 높이: %d",
"peers_tt_synced": "동기화 H/B: %d/%d",
"peers_tt_tls_cipher": "TLS: %s",
"peers_unban": "차단 해제",
"peers_upper": "피어",
"peers_version": "버전",
"pending": "대기 중",
"ping": "Ping",
"price_chart": "가격 차트",
"qr_code": "QR 코드",
"qr_failed": "QR 코드 생성 실패",
"qr_title": "QR 코드",
"qr_unavailable": "QR 사용 불가",
"receive": "수신",
"received": "수신됨",
"received_filter": "수신됨",
"received_label": "수신됨",
"received_upper": "수신됨",
"receiving_addresses": "수신 주소",
"recent_received": "최근 수신",
"recent_sends": "최근 전송",
"recipient": "수신자",
"recv_type": "수신",
"refresh": "새로고침",
"refresh_now": "지금 새로고침",
"report_bug": "버그 신고",
"request_amount": "금액 (선택):",
"request_copy_uri": "URI 복사",
"request_description": "다른 사람이 스캔하거나 복사할 수 있는 결제 요청을 생성합니다. QR 코드에는 주소와 선택적 금액/메모가 포함됩니다.",
"request_label": "라벨 (선택):",
"request_memo": "메모 (선택):",
"request_payment": "결제 요청",
"request_payment_uri": "결제 URI:",
"request_receive_address": "수신 주소:",
"request_select_address": "주소 선택...",
"request_shielded_addrs": "-- 차폐 주소 --",
"request_title": "결제 요청",
"request_transparent_addrs": "-- 투명 주소 --",
"request_uri_copied": "결제 URI가 클립보드에 복사되었습니다",
"rescan": "재스캔",
"reset_to_defaults": "기본값으로 재설정",
"review_send": "전송 검토",
"rpc_host": "RPC 호스트",
"rpc_pass": "비밀번호",
"rpc_port": "포트",
"rpc_user": "사용자명",
"save": "저장",
"save_settings": "설정 저장",
"save_z_transactions": "Z 거래를 거래 목록에 저장",
"search_placeholder": "검색...",
"security": "보안",
"select_address": "주소 선택...",
"select_receiving_address": "수신 주소 선택...",
"select_source_address": "보낼 주소 선택...",
"send": "전송",
"send_amount": "금액",
"send_amount_details": "금액 상세",
"send_amount_upper": "금액",
"send_clear_fields": "모든 양식 필드를 지우시겠습니까?",
"send_copy_error": "오류 복사",
"send_dismiss": "닫기",
"send_error_copied": "오류가 클립보드에 복사되었습니다",
"send_error_prefix": "오류: %s",
"send_exceeds_available": "사용 가능 금액 초과 (%.8f)",
"send_fee": "수수료",
"send_fee_high": "높음",
"send_fee_low": "낮음",
"send_fee_normal": "보통",
"send_form_restored": "양식이 복원되었습니다",
"send_from_this_address": "이 주소에서 전송",
"send_go_to_receive": "수신으로 이동",
"send_keep": "유지",
"send_network_fee": "네트워크 수수료",
"send_no_balance": "잔액 없음",
"send_no_recent": "최근 전송 없음",
"send_recent_sends": "최근 전송",
"send_recipient": "수신자",
"send_select_source": "보낼 주소 선택...",
"send_sending_from": "보내는 곳",
"send_submitting": "거래 제출 중...",
"send_switch_to_receive": "수신으로 전환하여 주소를 받고 자금 수신을 시작하세요.",
"send_to": "받는 곳",
"send_tooltip_enter_amount": "전송할 금액을 입력하세요",
"send_tooltip_exceeds_balance": "금액이 사용 가능 잔액을 초과합니다",
"send_tooltip_in_progress": "거래가 이미 진행 중입니다",
"send_tooltip_invalid_address": "유효한 수신자 주소를 입력하세요",
"send_tooltip_not_connected": "데몬에 연결되지 않음",
"send_tooltip_select_source": "먼저 보낼 주소를 선택하세요",
"send_tooltip_syncing": "블록체인 동기화를 기다려 주세요",
"send_total": "합계",
"send_transaction": "거래 전송",
"send_tx_failed": "거래 실패",
"send_tx_sent": "거래가 전송되었습니다!",
"send_tx_success": "거래 전송 성공!",
"send_txid_copied": "TxID가 클립보드에 복사되었습니다",
"send_txid_label": "TxID: %s",
"send_valid_shielded": "유효한 차폐 주소",
"send_valid_transparent": "유효한 투명 주소",
"send_wallet_empty": "지갑이 비어 있습니다",
"send_yes_clear": "예, 지우기",
"sending": "거래 전송 중",
"sending_from": "보내는 곳",
"sent": "전송됨",
"sent_filter": "전송됨",
"sent_type": "전송됨",
"sent_upper": "전송됨",
"settings": "설정",
"setup_wizard": "설정 마법사",
"share": "공유",
"shield_check_status": "상태 확인",
"shield_completed": "작업이 성공적으로 완료되었습니다!",
"shield_description": "투명 주소의 코인베이스 출력을 차폐 주소로 전송하여 채굴 보상을 차폐합니다. 채굴 수입을 숨겨 프라이버시가 향상됩니다.",
"shield_from_address": "보내는 주소:",
"shield_funds": "자금 차폐",
"shield_in_progress": "작업 진행 중...",
"shield_max_utxos": "작업당 최대 UTXO 수",
"shield_merge_done": "차폐/통합이 완료되었습니다!",
"shield_select_z": "z 주소 선택...",
"shield_started": "차폐 작업이 시작되었습니다",
"shield_title": "코인베이스 보상 차폐",
"shield_to_address": "받는 주소 (차폐):",
"shield_utxo_limit": "UTXO 제한:",
"shield_wildcard_hint": "'*'를 사용하여 모든 투명 주소에서 차폐",
"shielded": "차폐",
"shielded_to": "차폐 대상",
"shielded_type": "차폐",
"show": "표시",
"show_qr_code": "QR 코드 표시",
"showing_transactions": "%d\xe2\x80\x93%d / %d건의 거래 표시 중 (총: %zu)",
"simple_background": "단순 배경",
"start_mining": "채굴 시작",
"status": "상태",
"stop_external": "외부 데몬 중지",
"stop_mining": "채굴 중지",
"submitting_transaction": "거래 제출 중...",
"success": "성공",
"summary": "요약",
"syncing": "동기화 중...",
"t_addresses": "T 주소",
"test_connection": "테스트",
"theme": "테마",
"theme_effects": "테마 효과",
"time_days_ago": "%d일 전",
"time_hours_ago": "%d시간 전",
"time_minutes_ago": "%d분 전",
"time_seconds_ago": "%d초 전",
"to": "받는 곳",
"to_upper": "받는 곳",
"tools": "도구",
"total": "합계",
"transaction_id": "거래 ID",
"transaction_sent": "거래 전송 성공",
"transaction_sent_msg": "거래가 전송되었습니다!",
"transaction_url": "거래 URL",
"transactions": "거래",
"transactions_upper": "거래",
"transparent": "투명",
"tx_confirmations": "%d 확인",
"tx_details_title": "거래 상세",
"tx_from_address": "보낸 주소:",
"tx_id_label": "거래 ID:",
"tx_immature": "미성숙",
"tx_mined": "채굴됨",
"tx_received": "수신됨",
"tx_sent": "전송됨",
"tx_to_address": "받는 주소:",
"tx_view_explorer": "탐색기에서 보기",
"txs_count": "%d",
"type": "유형",
"ui_opacity": "UI 투명도",
"unban": "차단 해제",
"unconfirmed": "미확인",
"undo_clear": "지우기 취소",
"unknown": "알 수 없음",
"use_embedded_daemon": "내장 dragonxd 사용",
"use_tor": "Tor 사용",
"validate_btn": "검증",
"validate_description": "DragonX 주소를 입력하여 유효한지 그리고 이 지갑에 속하는지 확인합니다.",
"validate_invalid": "유효하지 않음",
"validate_is_mine": "이 지갑이 이 주소를 소유합니다",
"validate_not_mine": "이 지갑에 속하지 않음",
"validate_ownership": "소유자:",
"validate_results": "결과:",
"validate_shielded_type": "차폐 (z 주소)",
"validate_status": "상태:",
"validate_title": "주소 검증",
"validate_transparent_type": "투명 (t 주소)",
"validate_type": "유형:",
"validate_valid": "유효함",
"validating": "검증 중...",
"verbose_logging": "상세 로깅",
"version": "버전",
"view": "보기",
"view_details": "상세 보기",
"view_on_explorer": "탐색기에서 보기",
"waiting_for_daemon": "데몬 연결 대기 중...",
"wallet": "지갑",
"wallet_empty": "지갑이 비어 있습니다",
"wallet_empty_hint": "수신으로 전환하여 주소를 받고 자금 수신을 시작하세요.",
"warning": "경고",
"warning_upper": "경고!",
"website": "웹사이트",
"window_opacity": "창 투명도",
"yes_clear": "예, 지우기",
"your_addresses": "내 주소",
"z_addresses": "Z 주소",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "ko.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} Korean translations to {os.path.abspath(out)}")

646
scripts/gen_pt.py Normal file
View File

@@ -0,0 +1,646 @@
#!/usr/bin/env python3
"""Generate Portuguese (pt) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "Variação 24h",
"24h_volume": "Volume 24h",
"about": "Sobre",
"about_block_explorer": "Explorador de Blocos",
"about_block_height": "Altura do Bloco:",
"about_build_date": "Data de Compilação:",
"about_build_type": "Tipo de Build:",
"about_chain": "Chain:",
"about_connections": "Conexões:",
"about_credits": "Créditos",
"about_daemon": "Daemon:",
"about_debug": "Depuração",
"about_dragonx": "Sobre o ObsidianDragon",
"about_edition": "Edição ImGui",
"about_github": "GitHub",
"about_imgui": "ImGui:",
"about_license": "Licença",
"about_license_text": "Este software é disponibilizado sob a Licença Pública Geral GNU v3 (GPLv3). Você é livre para usar, modificar e distribuir este software sob os termos da licença.",
"about_peers_count": "%zu pares",
"about_release": "Versão",
"about_title": "Sobre o ObsidianDragon",
"about_version": "Versão:",
"about_website": "Website",
"acrylic": "Acrílico",
"add": "Adicionar",
"address": "Endereço",
"address_book_add": "Adicionar Endereço",
"address_book_add_new": "Adicionar Novo",
"address_book_added": "Endereço adicionado ao livro",
"address_book_count": "%zu endereços salvos",
"address_book_deleted": "Entrada excluída",
"address_book_edit": "Editar Endereço",
"address_book_empty": "Nenhum endereço salvo. Clique em 'Adicionar Novo' para criar um.",
"address_book_exists": "Endereço já existe no livro",
"address_book_title": "Livro de Endereços",
"address_book_update_failed": "Falha na atualização - endereço pode ser duplicado",
"address_book_updated": "Endereço atualizado",
"address_copied": "Endereço copiado para a área de transferência",
"address_details": "Detalhes do Endereço",
"address_label": "Endereço:",
"address_upper": "ENDEREÇO",
"address_url": "URL do Endereço",
"addresses_appear_here": "Seus endereços de recebimento aparecerão aqui após a conexão.",
"advanced": "AVANÇADO",
"all_filter": "Todos",
"allow_custom_fees": "Permitir taxas personalizadas",
"amount": "Valor",
"amount_details": "DETALHES DO VALOR",
"amount_exceeds_balance": "Valor excede o saldo",
"amount_label": "Valor:",
"appearance": "APARÊNCIA",
"auto_shield": "Auto-blindar mineração",
"available": "Disponível",
"backup_backing_up": "Fazendo backup...",
"backup_create": "Criar Backup",
"backup_created": "Backup da carteira criado",
"backup_data": "BACKUP & DADOS",
"backup_description": "Crie um backup do seu arquivo wallet.dat. Este arquivo contém todas as suas chaves privadas e histórico de transações. Guarde o backup em um local seguro.",
"backup_destination": "Destino do backup:",
"backup_tip_external": "Armazene backups em unidades externas ou armazenamento em nuvem",
"backup_tip_multiple": "Crie múltiplos backups em diferentes locais",
"backup_tip_test": "Teste a restauração do backup periodicamente",
"backup_tips": "Dicas:",
"backup_title": "Backup da Carteira",
"backup_wallet": "Fazer Backup da Carteira...",
"backup_wallet_not_found": "Aviso: wallet.dat não encontrado no local esperado",
"balance": "Saldo",
"balance_layout": "Layout do Saldo",
"ban": "Banir",
"banned_peers": "Pares Banidos",
"block": "Bloco",
"block_bits": "Bits:",
"block_click_next": "Clique para ver o próximo bloco",
"block_click_prev": "Clique para ver o bloco anterior",
"block_explorer": "Explorador de Blocos",
"block_get_info": "Obter Info do Bloco",
"block_hash": "Hash do Bloco:",
"block_height": "Altura do Bloco:",
"block_info_title": "Informações do Bloco",
"block_merkle_root": "Raiz Merkle:",
"block_nav_next": "Próximo >>",
"block_nav_prev": "<< Anterior",
"block_next": "Próximo Bloco:",
"block_previous": "Bloco Anterior:",
"block_size": "Tamanho:",
"block_timestamp": "Carimbo de Data:",
"block_transactions": "Transações:",
"blockchain_syncing": "Blockchain sincronizando (%.1f%%)... Os saldos podem ser imprecisos.",
"cancel": "Cancelar",
"characters": "caracteres",
"clear": "Limpar",
"clear_all_bans": "Remover Todos os Banimentos",
"clear_form_confirm": "Limpar todos os campos do formulário?",
"clear_request": "Limpar Solicitação",
"click_copy_address": "Clique para copiar o endereço",
"click_copy_uri": "Clique para copiar a URI",
"close": "Fechar",
"conf_count": "%d conf.",
"confirm_and_send": "Confirmar & Enviar",
"confirm_send": "Confirmar Envio",
"confirm_transaction": "Confirmar Transação",
"confirmations": "Confirmações",
"confirmations_display": "%d confirmações | %s",
"confirmed": "Confirmado",
"connected": "Conectado",
"connected_peers": "Pares Conectados",
"connecting": "Conectando...",
"console": "Console",
"console_auto_scroll": "Rolagem automática",
"console_available_commands": "Comandos disponíveis:",
"console_capturing_output": "Capturando saída do daemon...",
"console_clear": "Limpar",
"console_clear_console": "Limpar Console",
"console_cleared": "Console limpo",
"console_click_commands": "Clique nos comandos acima para inseri-los",
"console_click_insert": "Clique para inserir",
"console_click_insert_params": "Clique para inserir com parâmetros",
"console_close": "Fechar",
"console_commands": "Comandos",
"console_common_rpc": "Comandos RPC comuns:",
"console_completions": "Completações:",
"console_connected": "Conectado ao daemon",
"console_copy_all": "Copiar Tudo",
"console_copy_selected": "Copiar",
"console_daemon": "Daemon",
"console_daemon_error": "Erro do daemon!",
"console_daemon_started": "Daemon iniciado",
"console_daemon_stopped": "Daemon parado",
"console_disconnected": "Desconectado do daemon",
"console_errors": "Erros",
"console_filter_hint": "Filtrar saída...",
"console_help_clear": " clear - Limpar o console",
"console_help_getbalance": " getbalance - Mostrar saldo transparente",
"console_help_getblockcount": " getblockcount - Mostrar altura atual do bloco",
"console_help_getinfo": " getinfo - Mostrar informações do nó",
"console_help_getmininginfo": " getmininginfo - Mostrar status da mineração",
"console_help_getpeerinfo": " getpeerinfo - Mostrar pares conectados",
"console_help_gettotalbalance": " gettotalbalance - Mostrar saldo total",
"console_help_help": " help - Mostrar esta mensagem de ajuda",
"console_help_setgenerate": " setgenerate - Controlar mineração",
"console_help_stop": " stop - Parar o daemon",
"console_line_count": "%zu linhas",
"console_new_lines": "%d novas linhas",
"console_no_daemon": "Sem daemon",
"console_not_connected": "Erro: Não conectado ao daemon",
"console_rpc_reference": "Referência de Comandos RPC",
"console_scanline": "Scanline do console",
"console_search_commands": "Pesquisar comandos...",
"console_select_all": "Selecionar Tudo",
"console_show_daemon_output": "Mostrar saída do daemon",
"console_show_errors_only": "Mostrar apenas erros",
"console_show_rpc_ref": "Mostrar referência de comandos RPC",
"console_showing_lines": "Mostrando %zu de %zu linhas",
"console_starting_node": "Iniciando nó...",
"console_status_error": "Erro",
"console_status_running": "Em execução",
"console_status_starting": "Iniciando",
"console_status_stopped": "Parado",
"console_status_stopping": "Parando",
"console_status_unknown": "Desconhecido",
"console_tab_completion": "Tab para completar",
"console_type_help": "Digite 'help' para comandos disponíveis",
"console_welcome": "Bem-vindo ao Console ObsidianDragon",
"console_zoom_in": "Aumentar zoom",
"console_zoom_out": "Diminuir zoom",
"copy": "Copiar",
"copy_address": "Copiar Endereço Completo",
"copy_error": "Copiar Erro",
"copy_to_clipboard": "Copiar para Área de Transferência",
"copy_txid": "Copiar TxID",
"copy_uri": "Copiar URI",
"current_price": "Preço Atual",
"custom_fees": "Taxas personalizadas",
"dark": "Escuro",
"date": "Data",
"date_label": "Data:",
"delete": "Excluir",
"difficulty": "Dificuldade",
"disconnected": "Desconectado",
"dismiss": "Dispensar",
"display": "Exibição",
"dragonx_green": "DragonX (Verde)",
"edit": "Editar",
"error": "Erro",
"est_time_to_block": "Tempo Est. por Bloco",
"exit": "Sair",
"explorer": "EXPLORADOR",
"export": "Exportar",
"export_csv": "Exportar CSV",
"export_keys_btn": "Exportar Chaves",
"export_keys_danger": "PERIGO: Isto exportará TODAS as chaves privadas da sua carteira! Qualquer pessoa com acesso a este arquivo pode roubar seus fundos. Guarde com segurança e exclua após o uso.",
"export_keys_include_t": "Incluir endereços T (transparentes)",
"export_keys_include_z": "Incluir endereços Z (blindados)",
"export_keys_options": "Opções de exportação:",
"export_keys_success": "Chaves exportadas com sucesso",
"export_keys_title": "Exportar Todas as Chaves Privadas",
"export_private_key": "Exportar Chave Privada",
"export_tx_count": "Exportar %zu transações para arquivo CSV.",
"export_tx_file_fail": "Falha ao criar arquivo CSV",
"export_tx_none": "Nenhuma transação para exportar",
"export_tx_success": "Transações exportadas com sucesso",
"export_tx_title": "Exportar Transações para CSV",
"export_viewing_key": "Exportar Chave de Visualização",
"failed_create_shielded": "Falha ao criar endereço blindado",
"failed_create_transparent": "Falha ao criar endereço transparente",
"fee": "Taxa",
"fee_high": "Alta",
"fee_label": "Taxa:",
"fee_low": "Baixa",
"fee_normal": "Normal",
"fetch_prices": "Buscar preços",
"file": "Arquivo",
"file_save_location": "O arquivo será salvo em: ~/.config/ObsidianDragon/",
"font_scale": "Escala da Fonte",
"from": "De",
"from_upper": "DE",
"full_details": "Detalhes Completos",
"general": "Geral",
"go_to_receive": "Ir para Receber",
"height": "Altura",
"help": "Ajuda",
"hide": "Ocultar",
"history": "Histórico",
"immature_type": "Imaturo",
"import": "Importar",
"import_key_btn": "Importar Chave(s)",
"import_key_formats": "Formatos de chave suportados:",
"import_key_full_rescan": "(0 = rescan completo)",
"import_key_label": "Chave(s) Privada(s):",
"import_key_no_valid": "Nenhuma chave válida encontrada na entrada",
"import_key_rescan": "Reescanear blockchain após importação",
"import_key_start_height": "Altura inicial:",
"import_key_success": "Chaves importadas com sucesso",
"import_key_t_format": "Chaves privadas WIF de endereços T",
"import_key_title": "Importar Chave Privada",
"import_key_tooltip": "Digite uma ou mais chaves privadas, uma por linha.\nSuporta chaves de z-endereço e t-endereço.\nLinhas começando com # são tratadas como comentários.",
"import_key_warning": "Aviso: Nunca compartilhe suas chaves privadas! Importar chaves de fontes não confiáveis pode comprometer sua carteira.",
"import_key_z_format": "Chaves de gasto de z-endereço (secret-extended-key-...)",
"import_private_key": "Importar Chave Privada...",
"invalid_address": "Formato de endereço inválido",
"ip_address": "Endereço IP",
"keep": "Manter",
"keep_daemon": "Manter daemon em execução",
"key_export_fetching": "Buscando chave da carteira...",
"key_export_private_key": "Chave Privada:",
"key_export_private_warning": "Mantenha esta chave em SEGREDO! Qualquer pessoa com esta chave pode gastar seus fundos. Nunca a compartilhe online ou com terceiros não confiáveis.",
"key_export_reveal": "Revelar Chave",
"key_export_viewing_key": "Chave de Visualização:",
"key_export_viewing_warning": "Esta chave de visualização permite que outros vejam suas transações recebidas e saldo, mas NÃO gastem seus fundos. Compartilhe apenas com partes confiáveis.",
"label": "Rótulo:",
"language": "Idioma",
"light": "Claro",
"loading": "Carregando...",
"loading_addresses": "Carregando endereços...",
"local_hashrate": "Hashrate Local",
"low_spec_mode": "Modo econômico",
"market": "Mercado",
"market_12h": "12h",
"market_18h": "18h",
"market_24h": "24h",
"market_24h_volume": "VOLUME 24H",
"market_6h": "6h",
"market_attribution": "Dados de preço do NonKYC",
"market_btc_price": "PREÇO BTC",
"market_cap": "Capitalização",
"market_no_history": "Nenhum histórico de preços disponível",
"market_no_price": "Sem dados de preço",
"market_now": "Agora",
"market_pct_shielded": "%.0f%% Blindado",
"market_portfolio": "PORTFÓLIO",
"market_price_unavailable": "Dados de preço indisponíveis",
"market_refresh_price": "Atualizar dados de preço",
"market_trade_on": "Negociar no %s",
"mature": "Maduro",
"max": "Máx",
"memo": "Memo (opcional, criptografado)",
"memo_label": "Memo:",
"memo_optional": "MEMO (OPCIONAL)",
"memo_upper": "MEMO",
"memo_z_only": "Nota: Memos só estão disponíveis ao enviar para endereços blindados (z)",
"merge_description": "Fundir múltiplos UTXOs em um único endereço blindado. Isso pode ajudar a reduzir o tamanho da carteira e melhorar a privacidade.",
"merge_funds": "Fundir Fundos",
"merge_started": "Operação de fusão iniciada",
"merge_title": "Fundir para Endereço",
"mine_when_idle": "Minerar quando ocioso",
"mined": "minerado",
"mined_filter": "Minerado",
"mined_type": "Minerado",
"mined_upper": "MINERADO",
"miner_fee": "Taxa de Minerador",
"mining": "Mineração",
"mining_active": "Ativo",
"mining_address_copied": "Endereço de mineração copiado",
"mining_all_time": "Todo o Tempo",
"mining_already_saved": "URL do pool já salva",
"mining_block_copied": "Hash do bloco copiado",
"mining_chart_1m_ago": "1m atrás",
"mining_chart_5m_ago": "5m atrás",
"mining_chart_now": "Agora",
"mining_chart_start": "Início",
"mining_click": "Clique",
"mining_click_copy_address": "Clique para copiar o endereço",
"mining_click_copy_block": "Clique para copiar o hash do bloco",
"mining_click_copy_difficulty": "Clique para copiar a dificuldade",
"mining_connected": "Conectado",
"mining_connecting": "Conectando...",
"mining_control": "Controle de Mineração",
"mining_difficulty_copied": "Dificuldade copiada",
"mining_est_block": "Bloco Est.",
"mining_est_daily": "Est. Diário",
"mining_filter_all": "Todos",
"mining_filter_tip_all": "Mostrar todos os ganhos",
"mining_filter_tip_pool": "Mostrar apenas ganhos do pool",
"mining_filter_tip_solo": "Mostrar apenas ganhos solo",
"mining_idle_off_tooltip": "Ativar mineração ociosa",
"mining_idle_on_tooltip": "Desativar mineração ociosa",
"mining_local_hashrate": "Hashrate Local",
"mining_mine": "Minerar",
"mining_mining_addr": "End. Mineração",
"mining_network": "Rede",
"mining_no_blocks_yet": "Nenhum bloco encontrado ainda",
"mining_no_payouts_yet": "Nenhum pagamento de pool ainda",
"mining_no_saved_addresses": "Nenhum endereço salvo",
"mining_no_saved_pools": "Nenhum pool salvo",
"mining_off": "Mineração está DESLIGADA",
"mining_on": "Mineração está LIGADA",
"mining_open_in_explorer": "Abrir no explorador",
"mining_payout_address": "Endereço de Pagamento",
"mining_payout_tooltip": "Endereço para receber recompensas de mineração",
"mining_pool": "Pool",
"mining_pool_hashrate": "Hashrate do Pool",
"mining_pool_url": "URL do Pool",
"mining_recent_blocks": "BLOCOS RECENTES",
"mining_recent_payouts": "PAGAMENTOS DE POOL RECENTES",
"mining_remove": "Remover",
"mining_reset_defaults": "Redefinir Padrões",
"mining_save_payout_address": "Salvar endereço de pagamento",
"mining_save_pool_url": "Salvar URL do pool",
"mining_saved_addresses": "Endereços Salvos:",
"mining_saved_pools": "Pools Salvos:",
"mining_shares": "Shares",
"mining_show_chart": "Gráfico",
"mining_show_log": "Log",
"mining_solo": "Solo",
"mining_starting": "Iniciando...",
"mining_starting_tooltip": "Minerador está iniciando...",
"mining_statistics": "Estatísticas de Mineração",
"mining_stop": "Parar",
"mining_stop_solo_for_pool": "Pare a mineração solo antes de iniciar a mineração em pool",
"mining_stop_solo_for_pool_settings": "Pare a mineração solo para alterar as configurações do pool",
"mining_stopping": "Parando...",
"mining_stopping_tooltip": "Minerador está parando...",
"mining_syncing_tooltip": "Blockchain está sincronizando...",
"mining_threads": "Threads de Mineração",
"mining_to_save": "para salvar",
"mining_today": "Hoje",
"mining_uptime": "Tempo Ativo",
"mining_yesterday": "Ontem",
"network": "Rede",
"network_fee": "TAXA DA REDE",
"network_hashrate": "Hashrate da Rede",
"new": "+ Novo",
"new_shielded_created": "Novo endereço blindado criado",
"new_t_address": "Novo Endereço T",
"new_t_transparent": "Novo endereço t (Transparente)",
"new_transparent_created": "Novo endereço transparente criado",
"new_z_address": "Novo Endereço Z",
"new_z_shielded": "Novo endereço z (Blindado)",
"no_addresses": "Nenhum endereço encontrado. Crie um usando os botões acima.",
"no_addresses_available": "Nenhum endereço disponível",
"no_addresses_match": "Nenhum endereço corresponde ao filtro",
"no_addresses_with_balance": "Nenhum endereço com saldo",
"no_matching": "Nenhuma transação correspondente",
"no_recent_receives": "Nenhum recebimento recente",
"no_recent_sends": "Nenhum envio recente",
"no_transactions": "Nenhuma transação encontrada",
"node": "",
"node_security": "NÓ & SEGURANÇA",
"noise": "Ruído",
"not_connected": "Não conectado ao daemon...",
"not_connected_to_daemon": "Não conectado ao daemon",
"notes": "Notas",
"notes_optional": "Notas (opcional):",
"output_filename": "Nome do arquivo de saída:",
"overview": "Visão Geral",
"paste": "Colar",
"paste_from_clipboard": "Colar da Área de Transferência",
"pay_from": "Pagar de",
"payment_request": "SOLICITAÇÃO DE PAGAMENTO",
"payment_request_copied": "Solicitação de pagamento copiada",
"payment_uri_copied": "URI de pagamento copiada",
"peers": "Pares",
"peers_avg_ping": "Ping Médio",
"peers_ban_24h": "Banir Par 24h",
"peers_ban_score": "Score de Ban: %d",
"peers_banned": "Banidos",
"peers_banned_count": "Banidos: %d",
"peers_best_block": "Melhor Bloco",
"peers_blockchain": "BLOCKCHAIN",
"peers_blocks": "Blocos",
"peers_blocks_left": "%d blocos restantes",
"peers_clear_all_bans": "Remover Todos os Banimentos",
"peers_click_copy": "Clique para copiar",
"peers_connected": "Conectados",
"peers_connected_count": "Conectados: %d",
"peers_copy_ip": "Copiar IP",
"peers_dir_in": "Ent.",
"peers_dir_out": "Saí.",
"peers_hash_copied": "Hash copiado",
"peers_hashrate": "Hashrate",
"peers_in_out": "Ent./Saí.",
"peers_longest": "Mais longa",
"peers_longest_chain": "Chain Mais Longa",
"peers_memory": "Memória",
"peers_no_banned": "Nenhum par banido",
"peers_no_connected": "Nenhum par conectado",
"peers_no_tls": "Sem TLS",
"peers_notarized": "Notarizado",
"peers_p2p_port": "Porta P2P",
"peers_protocol": "Protocolo",
"peers_received": "Recebido",
"peers_refresh": "Atualizar",
"peers_refresh_tooltip": "Atualizar lista de pares",
"peers_refreshing": "Atualizando...",
"peers_sent": "Enviado",
"peers_tt_id": "ID: %d",
"peers_tt_received": "Recebido: %s",
"peers_tt_sent": "Enviado: %s",
"peers_tt_services": "Serviços: %s",
"peers_tt_start_height": "Altura Inicial: %d",
"peers_tt_synced": "Sincronizado H/B: %d/%d",
"peers_tt_tls_cipher": "TLS: %s",
"peers_unban": "Desbanir",
"peers_upper": "PARES",
"peers_version": "Versão",
"pending": "Pendente",
"ping": "Ping",
"price_chart": "Gráfico de Preços",
"qr_code": "Código QR",
"qr_failed": "Falha ao gerar código QR",
"qr_title": "Código QR",
"qr_unavailable": "QR indisponível",
"receive": "Receber",
"received": "recebido",
"received_filter": "Recebido",
"received_label": "Recebido",
"received_upper": "RECEBIDO",
"receiving_addresses": "Seus Endereços de Recebimento",
"recent_received": "RECEBIDOS RECENTES",
"recent_sends": "ENVIOS RECENTES",
"recipient": "DESTINATÁRIO",
"recv_type": "Receb.",
"refresh": "Atualizar",
"refresh_now": "Atualizar Agora",
"report_bug": "Reportar Bug",
"request_amount": "Valor (opcional):",
"request_copy_uri": "Copiar URI",
"request_description": "Gere uma solicitação de pagamento que outros podem escanear ou copiar. O código QR contém seu endereço e valor/memo opcionais.",
"request_label": "Rótulo (opcional):",
"request_memo": "Memo (opcional):",
"request_payment": "Solicitar Pagamento",
"request_payment_uri": "URI de Pagamento:",
"request_receive_address": "Endereço de Recebimento:",
"request_select_address": "Selecionar endereço...",
"request_shielded_addrs": "-- Endereços Blindados --",
"request_title": "Solicitar Pagamento",
"request_transparent_addrs": "-- Endereços Transparentes --",
"request_uri_copied": "URI de pagamento copiada para a área de transferência",
"rescan": "Reescanear",
"reset_to_defaults": "Redefinir Padrões",
"review_send": "Revisar Envio",
"rpc_host": "Host RPC",
"rpc_pass": "Senha",
"rpc_port": "Porta",
"rpc_user": "Usuário",
"save": "Salvar",
"save_settings": "Salvar Configurações",
"save_z_transactions": "Salvar Z-tx na lista de tx",
"search_placeholder": "Pesquisar...",
"security": "SEGURANÇA",
"select_address": "Selecionar endereço...",
"select_receiving_address": "Selecionar endereço de recebimento...",
"select_source_address": "Selecionar endereço de origem...",
"send": "Enviar",
"send_amount": "Valor",
"send_amount_details": "DETALHES DO VALOR",
"send_amount_upper": "VALOR",
"send_clear_fields": "Limpar todos os campos do formulário?",
"send_copy_error": "Copiar Erro",
"send_dismiss": "Dispensar",
"send_error_copied": "Erro copiado para a área de transferência",
"send_error_prefix": "Erro: %s",
"send_exceeds_available": "Excede o disponível (%.8f)",
"send_fee": "Taxa",
"send_fee_high": "Alta",
"send_fee_low": "Baixa",
"send_fee_normal": "Normal",
"send_form_restored": "Formulário restaurado",
"send_from_this_address": "Enviar deste endereço",
"send_go_to_receive": "Ir para Receber",
"send_keep": "Manter",
"send_network_fee": "TAXA DA REDE",
"send_no_balance": "Sem saldo",
"send_no_recent": "Nenhum envio recente",
"send_recent_sends": "ENVIOS RECENTES",
"send_recipient": "DESTINATÁRIO",
"send_select_source": "Selecionar endereço de origem...",
"send_sending_from": "ENVIANDO DE",
"send_submitting": "Enviando transação...",
"send_switch_to_receive": "Mude para Receber para obter seu endereço e começar a receber fundos.",
"send_to": "Enviar para",
"send_tooltip_enter_amount": "Digite um valor para enviar",
"send_tooltip_exceeds_balance": "Valor excede o saldo disponível",
"send_tooltip_in_progress": "Transação já em andamento",
"send_tooltip_invalid_address": "Digite um endereço de destinatário válido",
"send_tooltip_not_connected": "Não conectado ao daemon",
"send_tooltip_select_source": "Selecione primeiro um endereço de origem",
"send_tooltip_syncing": "Aguarde a sincronização da blockchain",
"send_total": "Total",
"send_transaction": "Enviar Transação",
"send_tx_failed": "Transação falhou",
"send_tx_sent": "Transação enviada!",
"send_tx_success": "Transação enviada com sucesso!",
"send_txid_copied": "TxID copiado para a área de transferência",
"send_txid_label": "TxID: %s",
"send_valid_shielded": "Endereço blindado válido",
"send_valid_transparent": "Endereço transparente válido",
"send_wallet_empty": "Sua carteira está vazia",
"send_yes_clear": "Sim, Limpar",
"sending": "Enviando transação",
"sending_from": "ENVIANDO DE",
"sent": "enviado",
"sent_filter": "Enviado",
"sent_type": "Enviado",
"sent_upper": "ENVIADO",
"settings": "Configurações",
"setup_wizard": "Assistente de Configuração",
"share": "Compartilhar",
"shield_check_status": "Verificar Status",
"shield_completed": "Operação concluída com sucesso!",
"shield_description": "Blinde suas recompensas de mineração enviando saídas coinbase de endereços transparentes para um endereço blindado. Isso melhora a privacidade ocultando sua renda de mineração.",
"shield_from_address": "Do Endereço:",
"shield_funds": "Blindar Fundos",
"shield_in_progress": "Operação em andamento...",
"shield_max_utxos": "Máx. UTXOs por operação",
"shield_merge_done": "Blindagem/fusão concluída!",
"shield_select_z": "Selecionar z-endereço...",
"shield_started": "Operação de blindagem iniciada",
"shield_title": "Blindar Recompensas Coinbase",
"shield_to_address": "Para Endereço (Blindado):",
"shield_utxo_limit": "Limite UTXO:",
"shield_wildcard_hint": "Use '*' para blindar de todos os endereços transparentes",
"shielded": "Blindado",
"shielded_to": "BLINDADO PARA",
"shielded_type": "Blindado",
"show": "Mostrar",
"show_qr_code": "Mostrar Código QR",
"showing_transactions": "Mostrando %d\xe2\x80\x93%d de %d transações (total: %zu)",
"simple_background": "Fundo simples",
"start_mining": "Iniciar Mineração",
"status": "Status",
"stop_external": "Parar daemon externo",
"stop_mining": "Parar Mineração",
"submitting_transaction": "Enviando transação...",
"success": "Sucesso",
"summary": "Resumo",
"syncing": "Sincronizando...",
"t_addresses": "Endereços T",
"test_connection": "Testar",
"theme": "Tema",
"theme_effects": "Efeitos de tema",
"time_days_ago": "%d dias",
"time_hours_ago": "%d horas",
"time_minutes_ago": "%d minutos",
"time_seconds_ago": "%d segundos",
"to": "Para",
"to_upper": "PARA",
"tools": "FERRAMENTAS",
"total": "Total",
"transaction_id": "ID DA TRANSAÇÃO",
"transaction_sent": "Transação enviada com sucesso",
"transaction_sent_msg": "Transação enviada!",
"transaction_url": "URL da Transação",
"transactions": "Transações",
"transactions_upper": "TRANSAÇÕES",
"transparent": "Transparente",
"tx_confirmations": "%d confirmações",
"tx_details_title": "Detalhes da Transação",
"tx_from_address": "Endereço de Origem:",
"tx_id_label": "ID da Transação:",
"tx_immature": "IMATURO",
"tx_mined": "MINERADO",
"tx_received": "RECEBIDO",
"tx_sent": "ENVIADO",
"tx_to_address": "Endereço de Destino:",
"tx_view_explorer": "Ver no Explorador",
"txs_count": "%d txs",
"type": "Tipo",
"ui_opacity": "Opacidade da Interface",
"unban": "Desbanir",
"unconfirmed": "Não confirmado",
"undo_clear": "Desfazer Limpeza",
"unknown": "Desconhecido",
"use_embedded_daemon": "Usar dragonxd integrado",
"use_tor": "Usar Tor",
"validate_btn": "Validar",
"validate_description": "Digite um endereço DragonX para verificar se é válido e se pertence a esta carteira.",
"validate_invalid": "INVÁLIDO",
"validate_is_mine": "Esta carteira possui este endereço",
"validate_not_mine": "Não pertence a esta carteira",
"validate_ownership": "Propriedade:",
"validate_results": "Resultados:",
"validate_shielded_type": "Blindado (z-endereço)",
"validate_status": "Status:",
"validate_title": "Validar Endereço",
"validate_transparent_type": "Transparente (t-endereço)",
"validate_type": "Tipo:",
"validate_valid": "VÁLIDO",
"validating": "Validando...",
"verbose_logging": "Log detalhado",
"version": "Versão",
"view": "Visualizar",
"view_details": "Ver Detalhes",
"view_on_explorer": "Ver no Explorador",
"waiting_for_daemon": "Aguardando conexão com o daemon...",
"wallet": "CARTEIRA",
"wallet_empty": "Sua carteira está vazia",
"wallet_empty_hint": "Mude para Receber para obter seu endereço e começar a receber fundos.",
"warning": "Aviso",
"warning_upper": "AVISO!",
"website": "Website",
"window_opacity": "Opacidade da Janela",
"yes_clear": "Sim, Limpar",
"your_addresses": "Seus Endereços",
"z_addresses": "Endereços Z",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "pt.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} Portuguese translations to {os.path.abspath(out)}")

646
scripts/gen_ru.py Normal file
View File

@@ -0,0 +1,646 @@
#!/usr/bin/env python3
"""Generate Russian (ru) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "Изменение за 24ч",
"24h_volume": "Объём за 24ч",
"about": "О программе",
"about_block_explorer": "Обозреватель блоков",
"about_block_height": "Высота блока:",
"about_build_date": "Дата сборки:",
"about_build_type": "Тип сборки:",
"about_chain": "Цепочка:",
"about_connections": "Подключения:",
"about_credits": "Благодарности",
"about_daemon": "Daemon:",
"about_debug": "Отладка",
"about_dragonx": "Об ObsidianDragon",
"about_edition": "Редакция ImGui",
"about_github": "GitHub",
"about_imgui": "ImGui:",
"about_license": "Лицензия",
"about_license_text": "Это программное обеспечение выпущено под лицензией GNU General Public License v3 (GPLv3). Вы можете свободно использовать, изменять и распространять это ПО в соответствии с условиями лицензии.",
"about_peers_count": "%zu узлов",
"about_release": "Релиз",
"about_title": "Об ObsidianDragon",
"about_version": "Версия:",
"about_website": "Веб-сайт",
"acrylic": "Акрил",
"add": "Добавить",
"address": "Адрес",
"address_book_add": "Добавить адрес",
"address_book_add_new": "Добавить новый",
"address_book_added": "Адрес добавлен в книгу",
"address_book_count": "%zu адресов сохранено",
"address_book_deleted": "Запись удалена",
"address_book_edit": "Редактировать адрес",
"address_book_empty": "Нет сохранённых адресов. Нажмите 'Добавить новый', чтобы создать.",
"address_book_exists": "Адрес уже существует в книге",
"address_book_title": "Адресная книга",
"address_book_update_failed": "Не удалось обновить — адрес может быть дубликатом",
"address_book_updated": "Адрес обновлён",
"address_copied": "Адрес скопирован в буфер обмена",
"address_details": "Детали адреса",
"address_label": "Адрес:",
"address_upper": "АДРЕС",
"address_url": "URL адреса",
"addresses_appear_here": "Ваши адреса для получения появятся здесь после подключения.",
"advanced": "РАСШИРЕННЫЕ",
"all_filter": "Все",
"allow_custom_fees": "Разрешить пользовательские комиссии",
"amount": "Сумма",
"amount_details": "ДЕТАЛИ СУММЫ",
"amount_exceeds_balance": "Сумма превышает баланс",
"amount_label": "Сумма:",
"appearance": "ВНЕШНИЙ ВИД",
"auto_shield": "Авто-экранирование майнинга",
"available": "Доступно",
"backup_backing_up": "Создание резервной копии...",
"backup_create": "Создать резервную копию",
"backup_created": "Резервная копия кошелька создана",
"backup_data": "РЕЗЕРВНОЕ КОПИРОВАНИЕ И ДАННЫЕ",
"backup_description": "Создайте резервную копию файла wallet.dat. Этот файл содержит все ваши приватные ключи и историю транзакций. Храните копию в безопасном месте.",
"backup_destination": "Место сохранения:",
"backup_tip_external": "Храните резервные копии на внешних дисках или в облаке",
"backup_tip_multiple": "Создавайте несколько копий в разных местах",
"backup_tip_test": "Периодически проверяйте восстановление из резервной копии",
"backup_tips": "Советы:",
"backup_title": "Резервное копирование кошелька",
"backup_wallet": "Создать резервную копию...",
"backup_wallet_not_found": "Предупреждение: wallet.dat не найден в ожидаемом расположении",
"balance": "Баланс",
"balance_layout": "Макет баланса",
"ban": "Заблокировать",
"banned_peers": "Заблокированные узлы",
"block": "Блок",
"block_bits": "Биты:",
"block_click_next": "Нажмите для следующего блока",
"block_click_prev": "Нажмите для предыдущего блока",
"block_explorer": "Обозреватель блоков",
"block_get_info": "Получить информацию о блоке",
"block_hash": "Хэш блока:",
"block_height": "Высота блока:",
"block_info_title": "Информация о блоке",
"block_merkle_root": "Корень Меркла:",
"block_nav_next": "Далее >>",
"block_nav_prev": "<< Назад",
"block_next": "Следующий блок:",
"block_previous": "Предыдущий блок:",
"block_size": "Размер:",
"block_timestamp": "Временная метка:",
"block_transactions": "Транзакции:",
"blockchain_syncing": "Синхронизация блокчейна (%.1f%%)... Балансы могут быть неточными.",
"cancel": "Отмена",
"characters": "символов",
"clear": "Очистить",
"clear_all_bans": "Снять все блокировки",
"clear_form_confirm": "Очистить все поля формы?",
"clear_request": "Очистить запрос",
"click_copy_address": "Нажмите, чтобы скопировать адрес",
"click_copy_uri": "Нажмите, чтобы скопировать URI",
"close": "Закрыть",
"conf_count": "%d подтв.",
"confirm_and_send": "Подтвердить и отправить",
"confirm_send": "Подтвердить отправку",
"confirm_transaction": "Подтвердить транзакцию",
"confirmations": "Подтверждения",
"confirmations_display": "%d подтверждений | %s",
"confirmed": "Подтверждено",
"connected": "Подключено",
"connected_peers": "Подключённые узлы",
"connecting": "Подключение...",
"console": "Консоль",
"console_auto_scroll": "Авто-прокрутка",
"console_available_commands": "Доступные команды:",
"console_capturing_output": "Захват вывода daemon...",
"console_clear": "Очистить",
"console_clear_console": "Очистить консоль",
"console_cleared": "Консоль очищена",
"console_click_commands": "Нажмите на команды выше, чтобы вставить их",
"console_click_insert": "Нажмите для вставки",
"console_click_insert_params": "Нажмите для вставки с параметрами",
"console_close": "Закрыть",
"console_commands": "Команды",
"console_common_rpc": "Частые RPC-команды:",
"console_completions": "Дополнения:",
"console_connected": "Подключено к daemon",
"console_copy_all": "Копировать всё",
"console_copy_selected": "Копировать",
"console_daemon": "Daemon",
"console_daemon_error": "Ошибка daemon!",
"console_daemon_started": "Daemon запущен",
"console_daemon_stopped": "Daemon остановлен",
"console_disconnected": "Отключено от daemon",
"console_errors": "Ошибки",
"console_filter_hint": "Фильтр вывода...",
"console_help_clear": " clear - Очистить консоль",
"console_help_getbalance": " getbalance - Показать прозрачный баланс",
"console_help_getblockcount": " getblockcount - Показать текущую высоту блока",
"console_help_getinfo": " getinfo - Показать информацию об узле",
"console_help_getmininginfo": " getmininginfo - Показать статус майнинга",
"console_help_getpeerinfo": " getpeerinfo - Показать подключённые узлы",
"console_help_gettotalbalance": " gettotalbalance - Показать общий баланс",
"console_help_help": " help - Показать эту справку",
"console_help_setgenerate": " setgenerate - Управление майнингом",
"console_help_stop": " stop - Остановить daemon",
"console_line_count": "%zu строк",
"console_new_lines": "%d новых строк",
"console_no_daemon": "Нет daemon",
"console_not_connected": "Ошибка: Не подключено к daemon",
"console_rpc_reference": "Справочник RPC-команд",
"console_scanline": "Скан-линия консоли",
"console_search_commands": "Поиск команд...",
"console_select_all": "Выбрать всё",
"console_show_daemon_output": "Показать вывод daemon",
"console_show_errors_only": "Показать только ошибки",
"console_show_rpc_ref": "Показать справочник RPC-команд",
"console_showing_lines": "Показано %zu из %zu строк",
"console_starting_node": "Запуск узла...",
"console_status_error": "Ошибка",
"console_status_running": "Работает",
"console_status_starting": "Запуск",
"console_status_stopped": "Остановлен",
"console_status_stopping": "Остановка",
"console_status_unknown": "Неизвестно",
"console_tab_completion": "Tab для дополнения",
"console_type_help": "Введите 'help' для списка команд",
"console_welcome": "Добро пожаловать в консоль ObsidianDragon",
"console_zoom_in": "Увеличить",
"console_zoom_out": "Уменьшить",
"copy": "Копировать",
"copy_address": "Копировать полный адрес",
"copy_error": "Копировать ошибку",
"copy_to_clipboard": "Копировать в буфер обмена",
"copy_txid": "Копировать TxID",
"copy_uri": "Копировать URI",
"current_price": "Текущая цена",
"custom_fees": "Пользовательские комиссии",
"dark": "Тёмная",
"date": "Дата",
"date_label": "Дата:",
"delete": "Удалить",
"difficulty": "Сложность",
"disconnected": "Отключено",
"dismiss": "Отклонить",
"display": "Отображение",
"dragonx_green": "DragonX (Зелёная)",
"edit": "Редактировать",
"error": "Ошибка",
"est_time_to_block": "Расч. время до блока",
"exit": "Выход",
"explorer": "ОБОЗРЕВАТЕЛЬ",
"export": "Экспорт",
"export_csv": "Экспорт в CSV",
"export_keys_btn": "Экспорт ключей",
"export_keys_danger": "ОПАСНОСТЬ: Будут экспортированы ВСЕ приватные ключи из вашего кошелька! Любой, кто получит доступ к этому файлу, сможет украсть ваши средства. Храните его в безопасности и удалите после использования.",
"export_keys_include_t": "Включить T-адреса (прозрачные)",
"export_keys_include_z": "Включить Z-адреса (экранированные)",
"export_keys_options": "Параметры экспорта:",
"export_keys_success": "Ключи успешно экспортированы",
"export_keys_title": "Экспорт всех приватных ключей",
"export_private_key": "Экспорт приватного ключа",
"export_tx_count": "Экспортировать %zu транзакций в файл CSV.",
"export_tx_file_fail": "Не удалось создать файл CSV",
"export_tx_none": "Нет транзакций для экспорта",
"export_tx_success": "Транзакции успешно экспортированы",
"export_tx_title": "Экспорт транзакций в CSV",
"export_viewing_key": "Экспорт ключа просмотра",
"failed_create_shielded": "Не удалось создать экранированный адрес",
"failed_create_transparent": "Не удалось создать прозрачный адрес",
"fee": "Комиссия",
"fee_high": "Высокая",
"fee_label": "Комиссия:",
"fee_low": "Низкая",
"fee_normal": "Обычная",
"fetch_prices": "Получить цены",
"file": "Файл",
"file_save_location": "Файл будет сохранён в: ~/.config/ObsidianDragon/",
"font_scale": "Масштаб шрифта",
"from": "От",
"from_upper": "ОТ",
"full_details": "Полные детали",
"general": "Общие",
"go_to_receive": "Перейти к получению",
"height": "Высота",
"help": "Справка",
"hide": "Скрыть",
"history": "История",
"immature_type": "Незрелая",
"import": "Импорт",
"import_key_btn": "Импорт ключей",
"import_key_formats": "Поддерживаемые форматы ключей:",
"import_key_full_rescan": "(0 = полное сканирование)",
"import_key_label": "Приватный ключ(и):",
"import_key_no_valid": "В введённых данных не найдено действительных ключей",
"import_key_rescan": "Пересканировать блокчейн после импорта",
"import_key_start_height": "Начальная высота:",
"import_key_success": "Ключи успешно импортированы",
"import_key_t_format": "Приватные ключи WIF для T-адресов",
"import_key_title": "Импорт приватного ключа",
"import_key_tooltip": "Введите один или несколько приватных ключей, по одному на строку.\nПоддерживаются ключи z-адресов и t-адресов.\nСтроки, начинающиеся с #, считаются комментариями.",
"import_key_warning": "Предупреждение: Никогда не делитесь своими приватными ключами! Импорт ключей из ненадёжных источников может скомпрометировать ваш кошелёк.",
"import_key_z_format": "Ключи расходования z-адресов (secret-extended-key-...)",
"import_private_key": "Импорт приватного ключа...",
"invalid_address": "Неверный формат адреса",
"ip_address": "IP-адрес",
"keep": "Сохранить",
"keep_daemon": "Оставить daemon работающим",
"key_export_fetching": "Получение ключа из кошелька...",
"key_export_private_key": "Приватный ключ:",
"key_export_private_warning": "Держите этот ключ в ТАЙНЕ! Любой, кто владеет этим ключом, может потратить ваши средства. Никогда не делитесь им в интернете или с ненадёжными лицами.",
"key_export_reveal": "Показать ключ",
"key_export_viewing_key": "Ключ просмотра:",
"key_export_viewing_warning": "Этот ключ просмотра позволяет другим видеть входящие транзакции и баланс, но НЕ тратить ваши средства. Делитесь только с доверенными лицами.",
"label": "Метка:",
"language": "Язык",
"light": "Светлая",
"loading": "Загрузка...",
"loading_addresses": "Загрузка адресов...",
"local_hashrate": "Локальный хешрейт",
"low_spec_mode": "Режим экономии",
"market": "Рынок",
"market_12h": "12ч",
"market_18h": "18ч",
"market_24h": "24ч",
"market_24h_volume": "ОБЪЁМ 24Ч",
"market_6h": "",
"market_attribution": "Данные о ценах с NonKYC",
"market_btc_price": "ЦЕНА BTC",
"market_cap": "Рыночная капитализация",
"market_no_history": "Нет истории цен",
"market_no_price": "Нет данных о ценах",
"market_now": "Сейчас",
"market_pct_shielded": "%.0f%% Экранировано",
"market_portfolio": "ПОРТФЕЛЬ",
"market_price_unavailable": "Данные о ценах недоступны",
"market_refresh_price": "Обновить данные о ценах",
"market_trade_on": "Торговать на %s",
"mature": "Зрелая",
"max": "Макс",
"memo": "Заметка (необязательно, зашифровано)",
"memo_label": "Заметка:",
"memo_optional": "ЗАМЕТКА (НЕОБЯЗАТЕЛЬНО)",
"memo_upper": "ЗАМЕТКА",
"memo_z_only": "Примечание: Заметки доступны только при отправке на экранированные (z) адреса",
"merge_description": "Объедините несколько UTXO в один экранированный адрес. Это может уменьшить размер кошелька и улучшить конфиденциальность.",
"merge_funds": "Объединить средства",
"merge_started": "Операция объединения начата",
"merge_title": "Объединить на адрес",
"mine_when_idle": "Майнить в простое",
"mined": "добыто",
"mined_filter": "Добытые",
"mined_type": "Добытая",
"mined_upper": "ДОБЫТО",
"miner_fee": "Комиссия майнера",
"mining": "Майнинг",
"mining_active": "Активен",
"mining_address_copied": "Адрес майнинга скопирован",
"mining_all_time": "За всё время",
"mining_already_saved": "URL пула уже сохранён",
"mining_block_copied": "Хэш блока скопирован",
"mining_chart_1m_ago": "1м назад",
"mining_chart_5m_ago": "5м назад",
"mining_chart_now": "Сейчас",
"mining_chart_start": "Старт",
"mining_click": "Нажмите",
"mining_click_copy_address": "Нажмите, чтобы скопировать адрес",
"mining_click_copy_block": "Нажмите, чтобы скопировать хэш блока",
"mining_click_copy_difficulty": "Нажмите, чтобы скопировать сложность",
"mining_connected": "Подключено",
"mining_connecting": "Подключение...",
"mining_control": "Управление майнингом",
"mining_difficulty_copied": "Сложность скопирована",
"mining_est_block": "Расч. блок",
"mining_est_daily": "Расч. за день",
"mining_filter_all": "Все",
"mining_filter_tip_all": "Показать все доходы",
"mining_filter_tip_pool": "Показать только доходы пула",
"mining_filter_tip_solo": "Показать только доходы соло",
"mining_idle_off_tooltip": "Включить майнинг в простое",
"mining_idle_on_tooltip": "Отключить майнинг в простое",
"mining_local_hashrate": "Локальный хешрейт",
"mining_mine": "Майнить",
"mining_mining_addr": "Адрес майн.",
"mining_network": "Сеть",
"mining_no_blocks_yet": "Блоки пока не найдены",
"mining_no_payouts_yet": "Выплат пула пока нет",
"mining_no_saved_addresses": "Нет сохранённых адресов",
"mining_no_saved_pools": "Нет сохранённых пулов",
"mining_off": "Майнинг ВЫКЛЮЧЕН",
"mining_on": "Майнинг ВКЛЮЧЁН",
"mining_open_in_explorer": "Открыть в обозревателе",
"mining_payout_address": "Адрес выплат",
"mining_payout_tooltip": "Адрес для получения вознаграждений за майнинг",
"mining_pool": "Пул",
"mining_pool_hashrate": "Хешрейт пула",
"mining_pool_url": "URL пула",
"mining_recent_blocks": "ПОСЛЕДНИЕ БЛОКИ",
"mining_recent_payouts": "ПОСЛЕДНИЕ ВЫПЛАТЫ ПУЛА",
"mining_remove": "Удалить",
"mining_reset_defaults": "Сбросить настройки",
"mining_save_payout_address": "Сохранить адрес выплат",
"mining_save_pool_url": "Сохранить URL пула",
"mining_saved_addresses": "Сохранённые адреса:",
"mining_saved_pools": "Сохранённые пулы:",
"mining_shares": "Шары",
"mining_show_chart": "График",
"mining_show_log": "Журнал",
"mining_solo": "Соло",
"mining_starting": "Запуск...",
"mining_starting_tooltip": "Майнер запускается...",
"mining_statistics": "Статистика майнинга",
"mining_stop": "Стоп",
"mining_stop_solo_for_pool": "Остановите соло-майнинг перед запуском пул-майнинга",
"mining_stop_solo_for_pool_settings": "Остановите соло-майнинг для изменения настроек пула",
"mining_stopping": "Остановка...",
"mining_stopping_tooltip": "Майнер останавливается...",
"mining_syncing_tooltip": "Блокчейн синхронизируется...",
"mining_threads": "Потоки майнинга",
"mining_to_save": "для сохранения",
"mining_today": "Сегодня",
"mining_uptime": "Время работы",
"mining_yesterday": "Вчера",
"network": "Сеть",
"network_fee": "СЕТЕВАЯ КОМИССИЯ",
"network_hashrate": "Хешрейт сети",
"new": "+ Новый",
"new_shielded_created": "Создан новый экранированный адрес",
"new_t_address": "Новый T-адрес",
"new_t_transparent": "Новый t-адрес (Прозрачный)",
"new_transparent_created": "Создан новый прозрачный адрес",
"new_z_address": "Новый Z-адрес",
"new_z_shielded": "Новый z-адрес (Экранированный)",
"no_addresses": "Адреса не найдены. Создайте один, используя кнопки выше.",
"no_addresses_available": "Нет доступных адресов",
"no_addresses_match": "Нет адресов, соответствующих фильтру",
"no_addresses_with_balance": "Нет адресов с балансом",
"no_matching": "Нет подходящих транзакций",
"no_recent_receives": "Нет недавних получений",
"no_recent_sends": "Нет недавних отправлений",
"no_transactions": "Транзакции не найдены",
"node": "УЗЕЛ",
"node_security": "УЗЕЛ И БЕЗОПАСНОСТЬ",
"noise": "Шум",
"not_connected": "Не подключено к daemon...",
"not_connected_to_daemon": "Не подключено к daemon",
"notes": "Заметки",
"notes_optional": "Заметки (необязательно):",
"output_filename": "Имя выходного файла:",
"overview": "Обзор",
"paste": "Вставить",
"paste_from_clipboard": "Вставить из буфера обмена",
"pay_from": "Оплатить с",
"payment_request": "ЗАПРОС НА ОПЛАТУ",
"payment_request_copied": "Запрос на оплату скопирован",
"payment_uri_copied": "URI платежа скопирован",
"peers": "Узлы",
"peers_avg_ping": "Средний пинг",
"peers_ban_24h": "Заблокировать узел на 24ч",
"peers_ban_score": "Очки блокировки: %d",
"peers_banned": "Заблокированные",
"peers_banned_count": "Заблокировано: %d",
"peers_best_block": "Лучший блок",
"peers_blockchain": "БЛОКЧЕЙН",
"peers_blocks": "Блоки",
"peers_blocks_left": "Осталось %d блоков",
"peers_clear_all_bans": "Снять все блокировки",
"peers_click_copy": "Нажмите, чтобы скопировать",
"peers_connected": "Подключено",
"peers_connected_count": "Подключено: %d",
"peers_copy_ip": "Копировать IP",
"peers_dir_in": "Вх.",
"peers_dir_out": "Исх.",
"peers_hash_copied": "Хэш скопирован",
"peers_hashrate": "Хешрейт",
"peers_in_out": "Вх./Исх.",
"peers_longest": "Длиннейшая",
"peers_longest_chain": "Длиннейшая цепь",
"peers_memory": "Память",
"peers_no_banned": "Нет заблокированных узлов",
"peers_no_connected": "Нет подключённых узлов",
"peers_no_tls": "Без TLS",
"peers_notarized": "Нотаризован",
"peers_p2p_port": "P2P-порт",
"peers_protocol": "Протокол",
"peers_received": "Получено",
"peers_refresh": "Обновить",
"peers_refresh_tooltip": "Обновить список узлов",
"peers_refreshing": "Обновление...",
"peers_sent": "Отправлено",
"peers_tt_id": "ID: %d",
"peers_tt_received": "Получено: %s",
"peers_tt_sent": "Отправлено: %s",
"peers_tt_services": "Сервисы: %s",
"peers_tt_start_height": "Начальная высота: %d",
"peers_tt_synced": "Синхронизировано В/Б: %d/%d",
"peers_tt_tls_cipher": "TLS: %s",
"peers_unban": "Разблокировать",
"peers_upper": "УЗЛЫ",
"peers_version": "Версия",
"pending": "Ожидание",
"ping": "Пинг",
"price_chart": "График цен",
"qr_code": "QR-код",
"qr_failed": "Не удалось сгенерировать QR-код",
"qr_title": "QR-код",
"qr_unavailable": "QR недоступен",
"receive": "Получить",
"received": "получено",
"received_filter": "Получено",
"received_label": "Получено",
"received_upper": "ПОЛУЧЕНО",
"receiving_addresses": "Ваши адреса для получения",
"recent_received": "НЕДАВНО ПОЛУЧЕНО",
"recent_sends": "НЕДАВНО ОТПРАВЛЕНО",
"recipient": "ПОЛУЧАТЕЛЬ",
"recv_type": "Получ.",
"refresh": "Обновить",
"refresh_now": "Обновить сейчас",
"report_bug": "Сообщить об ошибке",
"request_amount": "Сумма (необязательно):",
"request_copy_uri": "Копировать URI",
"request_description": "Создайте запрос на оплату, который другие могут отсканировать или скопировать. QR-код содержит ваш адрес и опциональную сумму/заметку.",
"request_label": "Метка (необязательно):",
"request_memo": "Заметка (необязательно):",
"request_payment": "Запрос оплаты",
"request_payment_uri": "URI платежа:",
"request_receive_address": "Адрес получения:",
"request_select_address": "Выбрать адрес...",
"request_shielded_addrs": "-- Экранированные адреса --",
"request_title": "Запрос оплаты",
"request_transparent_addrs": "-- Прозрачные адреса --",
"request_uri_copied": "URI платежа скопирован в буфер обмена",
"rescan": "Пересканировать",
"reset_to_defaults": "Сбросить настройки",
"review_send": "Проверить отправку",
"rpc_host": "RPC-хост",
"rpc_pass": "Пароль",
"rpc_port": "Порт",
"rpc_user": "Имя пользователя",
"save": "Сохранить",
"save_settings": "Сохранить настройки",
"save_z_transactions": "Сохранять Z-tx в списке транзакций",
"search_placeholder": "Поиск...",
"security": "БЕЗОПАСНОСТЬ",
"select_address": "Выбрать адрес...",
"select_receiving_address": "Выбрать адрес получения...",
"select_source_address": "Выбрать адрес-источник...",
"send": "Отправить",
"send_amount": "Сумма",
"send_amount_details": "ДЕТАЛИ СУММЫ",
"send_amount_upper": "СУММА",
"send_clear_fields": "Очистить все поля формы?",
"send_copy_error": "Копировать ошибку",
"send_dismiss": "Отклонить",
"send_error_copied": "Ошибка скопирована в буфер обмена",
"send_error_prefix": "Ошибка: %s",
"send_exceeds_available": "Превышает доступное (%.8f)",
"send_fee": "Комиссия",
"send_fee_high": "Высокая",
"send_fee_low": "Низкая",
"send_fee_normal": "Обычная",
"send_form_restored": "Форма восстановлена",
"send_from_this_address": "Отправить с этого адреса",
"send_go_to_receive": "Перейти к получению",
"send_keep": "Сохранить",
"send_network_fee": "СЕТЕВАЯ КОМИССИЯ",
"send_no_balance": "Нет баланса",
"send_no_recent": "Нет недавних отправлений",
"send_recent_sends": "НЕДАВНО ОТПРАВЛЕНО",
"send_recipient": "ПОЛУЧАТЕЛЬ",
"send_select_source": "Выбрать адрес-источник...",
"send_sending_from": "ОТПРАВКА С",
"send_submitting": "Отправка транзакции...",
"send_switch_to_receive": "Перейдите к получению, чтобы получить свой адрес и начать получать средства.",
"send_to": "Отправить на",
"send_tooltip_enter_amount": "Введите сумму для отправки",
"send_tooltip_exceeds_balance": "Сумма превышает доступный баланс",
"send_tooltip_in_progress": "Транзакция уже выполняется",
"send_tooltip_invalid_address": "Введите действительный адрес получателя",
"send_tooltip_not_connected": "Не подключено к daemon",
"send_tooltip_select_source": "Сначала выберите адрес-источник",
"send_tooltip_syncing": "Дождитесь синхронизации блокчейна",
"send_total": "Итого",
"send_transaction": "Отправить транзакцию",
"send_tx_failed": "Транзакция не удалась",
"send_tx_sent": "Транзакция отправлена!",
"send_tx_success": "Транзакция успешно отправлена!",
"send_txid_copied": "TxID скопирован в буфер обмена",
"send_txid_label": "TxID: %s",
"send_valid_shielded": "Действительный экранированный адрес",
"send_valid_transparent": "Действительный прозрачный адрес",
"send_wallet_empty": "Ваш кошелёк пуст",
"send_yes_clear": "Да, очистить",
"sending": "Отправка транзакции",
"sending_from": "ОТПРАВКА С",
"sent": "отправлено",
"sent_filter": "Отправлено",
"sent_type": "Отправлено",
"sent_upper": "ОТПРАВЛЕНО",
"settings": "Настройки",
"setup_wizard": "Мастер настройки",
"share": "Поделиться",
"shield_check_status": "Проверить статус",
"shield_completed": "Операция успешно завершена!",
"shield_description": "Экранируйте вознаграждения за майнинг, отправив coinbase-выходы с прозрачных адресов на экранированный адрес. Это улучшает конфиденциальность, скрывая ваш доход от майнинга.",
"shield_from_address": "С адреса:",
"shield_funds": "Экранировать средства",
"shield_in_progress": "Операция выполняется...",
"shield_max_utxos": "Макс. UTXO за операцию",
"shield_merge_done": "Экранирование/объединение завершено!",
"shield_select_z": "Выбрать z-адрес...",
"shield_started": "Операция экранирования начата",
"shield_title": "Экранировать вознаграждения coinbase",
"shield_to_address": "На адрес (экранированный):",
"shield_utxo_limit": "Лимит UTXO:",
"shield_wildcard_hint": "Используйте '*' для экранирования со всех прозрачных адресов",
"shielded": "Экранированный",
"shielded_to": "ЭКРАНИРОВАНО НА",
"shielded_type": "Экранированный",
"show": "Показать",
"show_qr_code": "Показать QR-код",
"showing_transactions": "Показано %d\xe2\x80\x93%d из %d транзакций (всего: %zu)",
"simple_background": "Простой фон",
"start_mining": "Начать майнинг",
"status": "Статус",
"stop_external": "Остановить внешний daemon",
"stop_mining": "Остановить майнинг",
"submitting_transaction": "Отправка транзакции...",
"success": "Успешно",
"summary": "Итоги",
"syncing": "Синхронизация...",
"t_addresses": "T-адреса",
"test_connection": "Тест",
"theme": "Тема",
"theme_effects": "Эффекты темы",
"time_days_ago": "%d дней назад",
"time_hours_ago": "%d часов назад",
"time_minutes_ago": "%d минут назад",
"time_seconds_ago": "%d секунд назад",
"to": "Кому",
"to_upper": "КОМУ",
"tools": "ИНСТРУМЕНТЫ",
"total": "Итого",
"transaction_id": "ID ТРАНЗАКЦИИ",
"transaction_sent": "Транзакция успешно отправлена",
"transaction_sent_msg": "Транзакция отправлена!",
"transaction_url": "URL транзакции",
"transactions": "Транзакции",
"transactions_upper": "ТРАНЗАКЦИИ",
"transparent": "Прозрачный",
"tx_confirmations": "%d подтверждений",
"tx_details_title": "Детали транзакции",
"tx_from_address": "Адрес отправителя:",
"tx_id_label": "ID транзакции:",
"tx_immature": "НЕЗРЕЛАЯ",
"tx_mined": "ДОБЫТА",
"tx_received": "ПОЛУЧЕНО",
"tx_sent": "ОТПРАВЛЕНО",
"tx_to_address": "Адрес получателя:",
"tx_view_explorer": "Посмотреть в обозревателе",
"txs_count": "%d тр.",
"type": "Тип",
"ui_opacity": "Прозрачность интерфейса",
"unban": "Разблокировать",
"unconfirmed": "Не подтверждено",
"undo_clear": "Отменить очистку",
"unknown": "Неизвестно",
"use_embedded_daemon": "Использовать встроенный dragonxd",
"use_tor": "Использовать Tor",
"validate_btn": "Проверить",
"validate_description": "Введите адрес DragonX, чтобы проверить его действительность и принадлежность к этому кошельку.",
"validate_invalid": "НЕДЕЙСТВИТЕЛЕН",
"validate_is_mine": "Этот кошелёк владеет этим адресом",
"validate_not_mine": "Не принадлежит этому кошельку",
"validate_ownership": "Принадлежность:",
"validate_results": "Результаты:",
"validate_shielded_type": "Экранированный (z-адрес)",
"validate_status": "Статус:",
"validate_title": "Проверить адрес",
"validate_transparent_type": "Прозрачный (t-адрес)",
"validate_type": "Тип:",
"validate_valid": "ДЕЙСТВИТЕЛЕН",
"validating": "Проверка...",
"verbose_logging": "Подробное логирование",
"version": "Версия",
"view": "Просмотр",
"view_details": "Подробнее",
"view_on_explorer": "Посмотреть в обозревателе",
"waiting_for_daemon": "Ожидание подключения к daemon...",
"wallet": "КОШЕЛЁК",
"wallet_empty": "Ваш кошелёк пуст",
"wallet_empty_hint": "Перейдите к получению, чтобы получить свой адрес и начать получать средства.",
"warning": "Предупреждение",
"warning_upper": "ПРЕДУПРЕЖДЕНИЕ!",
"website": "Веб-сайт",
"window_opacity": "Прозрачность окна",
"yes_clear": "Да, очистить",
"your_addresses": "Ваши адреса",
"z_addresses": "Z-адреса",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "ru.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} Russian translations to {os.path.abspath(out)}")

646
scripts/gen_zh.py Normal file
View File

@@ -0,0 +1,646 @@
#!/usr/bin/env python3
"""Generate Chinese Simplified (zh) translations for ObsidianDragon wallet."""
import json, os
translations = {
"24h_change": "24小时变化",
"24h_volume": "24小时交易量",
"about": "关于",
"about_block_explorer": "区块浏览器",
"about_block_height": "区块高度:",
"about_build_date": "构建日期:",
"about_build_type": "构建类型:",
"about_chain": "链:",
"about_connections": "连接数:",
"about_credits": "致谢",
"about_daemon": "守护进程:",
"about_debug": "调试",
"about_dragonx": "关于 ObsidianDragon",
"about_edition": "ImGui 版本",
"about_github": "GitHub",
"about_imgui": "ImGui",
"about_license": "许可证",
"about_license_text": "本软件根据 GNU 通用公共许可证 v3 (GPLv3) 发布。您可以根据许可证条款自由使用、修改和分发本软件。",
"about_peers_count": "%zu 个节点",
"about_release": "发布版",
"about_title": "关于 ObsidianDragon",
"about_version": "版本:",
"about_website": "网站",
"acrylic": "亚克力",
"add": "添加",
"address": "地址",
"address_book_add": "添加地址",
"address_book_add_new": "添加新地址",
"address_book_added": "地址已添加到通讯录",
"address_book_count": "已保存 %zu 个地址",
"address_book_deleted": "条目已删除",
"address_book_edit": "编辑地址",
"address_book_empty": "没有保存的地址。点击'添加新地址'创建一个。",
"address_book_exists": "地址已存在于通讯录中",
"address_book_title": "地址簿",
"address_book_update_failed": "更新失败——地址可能重复",
"address_book_updated": "地址已更新",
"address_copied": "地址已复制到剪贴板",
"address_details": "地址详情",
"address_label": "地址:",
"address_upper": "地址",
"address_url": "地址 URL",
"addresses_appear_here": "连接后,您的接收地址将显示在此处。",
"advanced": "高级",
"all_filter": "全部",
"allow_custom_fees": "允许自定义手续费",
"amount": "金额",
"amount_details": "金额详情",
"amount_exceeds_balance": "金额超过余额",
"amount_label": "金额:",
"appearance": "外观",
"auto_shield": "自动屏蔽挖矿",
"available": "可用",
"backup_backing_up": "正在备份...",
"backup_create": "创建备份",
"backup_created": "钱包备份已创建",
"backup_data": "备份与数据",
"backup_description": "创建 wallet.dat 文件的备份。此文件包含您所有的私钥和交易历史。请将备份存放在安全的地方。",
"backup_destination": "备份目标:",
"backup_tip_external": "将备份存储在外部驱动器或云存储中",
"backup_tip_multiple": "在不同位置创建多个备份",
"backup_tip_test": "定期测试从备份恢复",
"backup_tips": "提示:",
"backup_title": "备份钱包",
"backup_wallet": "备份钱包...",
"backup_wallet_not_found": "警告:在预期位置未找到 wallet.dat",
"balance": "余额",
"balance_layout": "余额布局",
"ban": "封禁",
"banned_peers": "已封禁节点",
"block": "区块",
"block_bits": "比特:",
"block_click_next": "点击查看下一个区块",
"block_click_prev": "点击查看上一个区块",
"block_explorer": "区块浏览器",
"block_get_info": "获取区块信息",
"block_hash": "区块哈希:",
"block_height": "区块高度:",
"block_info_title": "区块信息",
"block_merkle_root": "默克尔根:",
"block_nav_next": "下一个 >>",
"block_nav_prev": "<< 上一个",
"block_next": "下一个区块:",
"block_previous": "上一个区块:",
"block_size": "大小:",
"block_timestamp": "时间戳:",
"block_transactions": "交易:",
"blockchain_syncing": "区块链同步中 (%.1f%%)... 余额可能不准确。",
"cancel": "取消",
"characters": "字符",
"clear": "清除",
"clear_all_bans": "解除所有封禁",
"clear_form_confirm": "清除所有表单字段?",
"clear_request": "清除请求",
"click_copy_address": "点击复制地址",
"click_copy_uri": "点击复制 URI",
"close": "关闭",
"conf_count": "%d 确认",
"confirm_and_send": "确认并发送",
"confirm_send": "确认发送",
"confirm_transaction": "确认交易",
"confirmations": "确认数",
"confirmations_display": "%d 次确认 | %s",
"confirmed": "已确认",
"connected": "已连接",
"connected_peers": "已连接节点",
"connecting": "连接中...",
"console": "控制台",
"console_auto_scroll": "自动滚动",
"console_available_commands": "可用命令:",
"console_capturing_output": "正在捕获守护进程输出...",
"console_clear": "清除",
"console_clear_console": "清除控制台",
"console_cleared": "控制台已清除",
"console_click_commands": "点击上方命令以插入",
"console_click_insert": "点击插入",
"console_click_insert_params": "点击插入(含参数)",
"console_close": "关闭",
"console_commands": "命令",
"console_common_rpc": "常用 RPC 命令:",
"console_completions": "补全:",
"console_connected": "已连接到守护进程",
"console_copy_all": "全部复制",
"console_copy_selected": "复制",
"console_daemon": "守护进程",
"console_daemon_error": "守护进程错误!",
"console_daemon_started": "守护进程已启动",
"console_daemon_stopped": "守护进程已停止",
"console_disconnected": "已断开与守护进程的连接",
"console_errors": "错误",
"console_filter_hint": "过滤输出...",
"console_help_clear": " clear - 清除控制台",
"console_help_getbalance": " getbalance - 显示透明余额",
"console_help_getblockcount": " getblockcount - 显示当前区块高度",
"console_help_getinfo": " getinfo - 显示节点信息",
"console_help_getmininginfo": " getmininginfo - 显示挖矿状态",
"console_help_getpeerinfo": " getpeerinfo - 显示已连接节点",
"console_help_gettotalbalance": " gettotalbalance - 显示总余额",
"console_help_help": " help - 显示此帮助信息",
"console_help_setgenerate": " setgenerate - 控制挖矿",
"console_help_stop": " stop - 停止守护进程",
"console_line_count": "%zu 行",
"console_new_lines": "%d 新行",
"console_no_daemon": "无守护进程",
"console_not_connected": "错误:未连接到守护进程",
"console_rpc_reference": "RPC 命令参考",
"console_scanline": "控制台扫描线",
"console_search_commands": "搜索命令...",
"console_select_all": "全选",
"console_show_daemon_output": "显示守护进程输出",
"console_show_errors_only": "仅显示错误",
"console_show_rpc_ref": "显示 RPC 命令参考",
"console_showing_lines": "显示 %zu / %zu 行",
"console_starting_node": "正在启动节点...",
"console_status_error": "错误",
"console_status_running": "运行中",
"console_status_starting": "启动中",
"console_status_stopped": "已停止",
"console_status_stopping": "停止中",
"console_status_unknown": "未知",
"console_tab_completion": "Tab 补全",
"console_type_help": "输入 'help' 查看可用命令",
"console_welcome": "欢迎使用 ObsidianDragon 控制台",
"console_zoom_in": "放大",
"console_zoom_out": "缩小",
"copy": "复制",
"copy_address": "复制完整地址",
"copy_error": "复制错误",
"copy_to_clipboard": "复制到剪贴板",
"copy_txid": "复制交易ID",
"copy_uri": "复制 URI",
"current_price": "当前价格",
"custom_fees": "自定义手续费",
"dark": "深色",
"date": "日期",
"date_label": "日期:",
"delete": "删除",
"difficulty": "难度",
"disconnected": "已断开",
"dismiss": "关闭",
"display": "显示",
"dragonx_green": "DragonX绿色",
"edit": "编辑",
"error": "错误",
"est_time_to_block": "预计出块时间",
"exit": "退出",
"explorer": "浏览器",
"export": "导出",
"export_csv": "导出 CSV",
"export_keys_btn": "导出密钥",
"export_keys_danger": "危险:这将导出您钱包中的所有私钥!任何获得此文件的人都可以窃取您的资金。请安全保管并在使用后删除。",
"export_keys_include_t": "包含 T 地址(透明)",
"export_keys_include_z": "包含 Z 地址(屏蔽)",
"export_keys_options": "导出选项:",
"export_keys_success": "密钥导出成功",
"export_keys_title": "导出所有私钥",
"export_private_key": "导出私钥",
"export_tx_count": "导出 %zu 笔交易到 CSV 文件。",
"export_tx_file_fail": "无法创建 CSV 文件",
"export_tx_none": "没有交易可导出",
"export_tx_success": "交易导出成功",
"export_tx_title": "导出交易到 CSV",
"export_viewing_key": "导出查看密钥",
"failed_create_shielded": "无法创建屏蔽地址",
"failed_create_transparent": "无法创建透明地址",
"fee": "手续费",
"fee_high": "",
"fee_label": "手续费:",
"fee_low": "",
"fee_normal": "普通",
"fetch_prices": "获取价格",
"file": "文件",
"file_save_location": "文件将保存至:~/.config/ObsidianDragon/",
"font_scale": "字体大小",
"from": "",
"from_upper": "",
"full_details": "完整详情",
"general": "常规",
"go_to_receive": "前往接收",
"height": "高度",
"help": "帮助",
"hide": "隐藏",
"history": "历史",
"immature_type": "未成熟",
"import": "导入",
"import_key_btn": "导入密钥",
"import_key_formats": "支持的密钥格式:",
"import_key_full_rescan": "0 = 完整重扫)",
"import_key_label": "私钥:",
"import_key_no_valid": "输入中未找到有效密钥",
"import_key_rescan": "导入后重新扫描区块链",
"import_key_start_height": "起始高度:",
"import_key_success": "密钥导入成功",
"import_key_t_format": "T 地址 WIF 私钥",
"import_key_title": "导入私钥",
"import_key_tooltip": "输入一个或多个私钥,每行一个。\n支持 z 地址和 t 地址密钥。\n以 # 开头的行视为注释。",
"import_key_warning": "警告:切勿分享您的私钥!从不可信来源导入密钥可能会危及您的钱包安全。",
"import_key_z_format": "Z 地址花费密钥 (secret-extended-key-...)",
"import_private_key": "导入私钥...",
"invalid_address": "无效的地址格式",
"ip_address": "IP 地址",
"keep": "保留",
"keep_daemon": "保持守护进程运行",
"key_export_fetching": "正在从钱包获取密钥...",
"key_export_private_key": "私钥:",
"key_export_private_warning": "请保密此密钥!任何拥有此密钥的人都可以花费您的资金。切勿在网上或与不可信的人分享。",
"key_export_reveal": "显示密钥",
"key_export_viewing_key": "查看密钥:",
"key_export_viewing_warning": "此查看密钥允许他人查看您的入账交易和余额,但不能花费您的资金。仅与信任的人分享。",
"label": "标签:",
"language": "语言",
"light": "浅色",
"loading": "加载中...",
"loading_addresses": "正在加载地址...",
"local_hashrate": "本地算力",
"low_spec_mode": "低配模式",
"market": "市场",
"market_12h": "12小时",
"market_18h": "18小时",
"market_24h": "24小时",
"market_24h_volume": "24小时交易量",
"market_6h": "6小时",
"market_attribution": "价格数据来自 NonKYC",
"market_btc_price": "BTC 价格",
"market_cap": "市值",
"market_no_history": "无价格历史",
"market_no_price": "无价格数据",
"market_now": "现在",
"market_pct_shielded": "%.0f%% 屏蔽",
"market_portfolio": "投资组合",
"market_price_unavailable": "价格数据不可用",
"market_refresh_price": "刷新价格数据",
"market_trade_on": "%s 交易",
"mature": "已成熟",
"max": "最大",
"memo": "备注(可选,加密)",
"memo_label": "备注:",
"memo_optional": "备注(可选)",
"memo_upper": "备注",
"memo_z_only": "注意:备注仅在发送到屏蔽 (z) 地址时可用",
"merge_description": "将多个 UTXO 合并到一个屏蔽地址。这可以帮助减小钱包大小并提高隐私性。",
"merge_funds": "合并资金",
"merge_started": "合并操作已开始",
"merge_title": "合并到地址",
"mine_when_idle": "空闲时挖矿",
"mined": "已挖得",
"mined_filter": "已挖得",
"mined_type": "已挖得",
"mined_upper": "已挖得",
"miner_fee": "矿工费",
"mining": "挖矿",
"mining_active": "活跃",
"mining_address_copied": "挖矿地址已复制",
"mining_all_time": "所有时间",
"mining_already_saved": "矿池 URL 已保存",
"mining_block_copied": "区块哈希已复制",
"mining_chart_1m_ago": "1分钟前",
"mining_chart_5m_ago": "5分钟前",
"mining_chart_now": "现在",
"mining_chart_start": "开始",
"mining_click": "点击",
"mining_click_copy_address": "点击复制地址",
"mining_click_copy_block": "点击复制区块哈希",
"mining_click_copy_difficulty": "点击复制难度",
"mining_connected": "已连接",
"mining_connecting": "连接中...",
"mining_control": "挖矿控制",
"mining_difficulty_copied": "难度已复制",
"mining_est_block": "预计区块",
"mining_est_daily": "预计日收益",
"mining_filter_all": "全部",
"mining_filter_tip_all": "显示所有收益",
"mining_filter_tip_pool": "仅显示矿池收益",
"mining_filter_tip_solo": "仅显示单人收益",
"mining_idle_off_tooltip": "启用空闲挖矿",
"mining_idle_on_tooltip": "禁用空闲挖矿",
"mining_local_hashrate": "本地算力",
"mining_mine": "挖矿",
"mining_mining_addr": "挖矿地址",
"mining_network": "网络",
"mining_no_blocks_yet": "尚未找到区块",
"mining_no_payouts_yet": "尚无矿池支付",
"mining_no_saved_addresses": "没有保存的地址",
"mining_no_saved_pools": "没有保存的矿池",
"mining_off": "挖矿已关闭",
"mining_on": "挖矿已开启",
"mining_open_in_explorer": "在浏览器中打开",
"mining_payout_address": "支付地址",
"mining_payout_tooltip": "接收挖矿奖励的地址",
"mining_pool": "矿池",
"mining_pool_hashrate": "矿池算力",
"mining_pool_url": "矿池 URL",
"mining_recent_blocks": "最近区块",
"mining_recent_payouts": "最近矿池支付",
"mining_remove": "移除",
"mining_reset_defaults": "重置默认值",
"mining_save_payout_address": "保存支付地址",
"mining_save_pool_url": "保存矿池 URL",
"mining_saved_addresses": "已保存地址:",
"mining_saved_pools": "已保存矿池:",
"mining_shares": "份额",
"mining_show_chart": "图表",
"mining_show_log": "日志",
"mining_solo": "单人",
"mining_starting": "启动中...",
"mining_starting_tooltip": "矿工正在启动...",
"mining_statistics": "挖矿统计",
"mining_stop": "停止",
"mining_stop_solo_for_pool": "启动矿池挖矿前请先停止单人挖矿",
"mining_stop_solo_for_pool_settings": "请停止单人挖矿以更改矿池设置",
"mining_stopping": "停止中...",
"mining_stopping_tooltip": "矿工正在停止...",
"mining_syncing_tooltip": "区块链同步中...",
"mining_threads": "挖矿线程",
"mining_to_save": "保存",
"mining_today": "今天",
"mining_uptime": "运行时间",
"mining_yesterday": "昨天",
"network": "网络",
"network_fee": "网络手续费",
"network_hashrate": "全网算力",
"new": "+ 新建",
"new_shielded_created": "新屏蔽地址已创建",
"new_t_address": "新 T 地址",
"new_t_transparent": "新 t 地址(透明)",
"new_transparent_created": "新透明地址已创建",
"new_z_address": "新 Z 地址",
"new_z_shielded": "新 z 地址(屏蔽)",
"no_addresses": "未找到地址。请使用上方按钮创建一个。",
"no_addresses_available": "无可用地址",
"no_addresses_match": "没有匹配过滤器的地址",
"no_addresses_with_balance": "没有有余额的地址",
"no_matching": "没有匹配的交易",
"no_recent_receives": "没有最近的接收",
"no_recent_sends": "没有最近的发送",
"no_transactions": "未找到交易",
"node": "节点",
"node_security": "节点与安全",
"noise": "噪点",
"not_connected": "未连接到守护进程...",
"not_connected_to_daemon": "未连接到守护进程",
"notes": "备注",
"notes_optional": "备注(可选):",
"output_filename": "输出文件名:",
"overview": "概览",
"paste": "粘贴",
"paste_from_clipboard": "从剪贴板粘贴",
"pay_from": "付款来源",
"payment_request": "付款请求",
"payment_request_copied": "付款请求已复制",
"payment_uri_copied": "付款 URI 已复制",
"peers": "节点",
"peers_avg_ping": "平均延迟",
"peers_ban_24h": "封禁节点 24 小时",
"peers_ban_score": "封禁评分:%d",
"peers_banned": "已封禁",
"peers_banned_count": "已封禁:%d",
"peers_best_block": "最佳区块",
"peers_blockchain": "区块链",
"peers_blocks": "区块",
"peers_blocks_left": "剩余 %d 个区块",
"peers_clear_all_bans": "解除所有封禁",
"peers_click_copy": "点击复制",
"peers_connected": "已连接",
"peers_connected_count": "已连接:%d",
"peers_copy_ip": "复制 IP",
"peers_dir_in": "",
"peers_dir_out": "",
"peers_hash_copied": "哈希已复制",
"peers_hashrate": "算力",
"peers_in_out": "入/出",
"peers_longest": "最长",
"peers_longest_chain": "最长链",
"peers_memory": "内存",
"peers_no_banned": "无已封禁节点",
"peers_no_connected": "无已连接节点",
"peers_no_tls": "无 TLS",
"peers_notarized": "已公证",
"peers_p2p_port": "P2P 端口",
"peers_protocol": "协议",
"peers_received": "已接收",
"peers_refresh": "刷新",
"peers_refresh_tooltip": "刷新节点列表",
"peers_refreshing": "刷新中...",
"peers_sent": "已发送",
"peers_tt_id": "ID%d",
"peers_tt_received": "已接收:%s",
"peers_tt_sent": "已发送:%s",
"peers_tt_services": "服务:%s",
"peers_tt_start_height": "起始高度:%d",
"peers_tt_synced": "已同步 H/B%d/%d",
"peers_tt_tls_cipher": "TLS%s",
"peers_unban": "解除封禁",
"peers_upper": "节点",
"peers_version": "版本",
"pending": "待处理",
"ping": "延迟",
"price_chart": "价格图表",
"qr_code": "二维码",
"qr_failed": "无法生成二维码",
"qr_title": "二维码",
"qr_unavailable": "二维码不可用",
"receive": "接收",
"received": "已接收",
"received_filter": "已接收",
"received_label": "已接收",
"received_upper": "已接收",
"receiving_addresses": "您的接收地址",
"recent_received": "最近接收",
"recent_sends": "最近发送",
"recipient": "收款方",
"recv_type": "接收",
"refresh": "刷新",
"refresh_now": "立即刷新",
"report_bug": "报告错误",
"request_amount": "金额(可选):",
"request_copy_uri": "复制 URI",
"request_description": "生成一个付款请求,他人可以扫描或复制。二维码包含您的地址和可选的金额/备注。",
"request_label": "标签(可选):",
"request_memo": "备注(可选):",
"request_payment": "请求付款",
"request_payment_uri": "付款 URI",
"request_receive_address": "接收地址:",
"request_select_address": "选择地址...",
"request_shielded_addrs": "-- 屏蔽地址 --",
"request_title": "请求付款",
"request_transparent_addrs": "-- 透明地址 --",
"request_uri_copied": "付款 URI 已复制到剪贴板",
"rescan": "重新扫描",
"reset_to_defaults": "重置为默认值",
"review_send": "审核发送",
"rpc_host": "RPC 主机",
"rpc_pass": "密码",
"rpc_port": "端口",
"rpc_user": "用户名",
"save": "保存",
"save_settings": "保存设置",
"save_z_transactions": "将 Z 交易保存到列表",
"search_placeholder": "搜索...",
"security": "安全",
"select_address": "选择地址...",
"select_receiving_address": "选择接收地址...",
"select_source_address": "选择来源地址...",
"send": "发送",
"send_amount": "金额",
"send_amount_details": "金额详情",
"send_amount_upper": "金额",
"send_clear_fields": "清除所有表单字段?",
"send_copy_error": "复制错误",
"send_dismiss": "关闭",
"send_error_copied": "错误已复制到剪贴板",
"send_error_prefix": "错误:%s",
"send_exceeds_available": "超过可用额 (%.8f)",
"send_fee": "手续费",
"send_fee_high": "",
"send_fee_low": "",
"send_fee_normal": "普通",
"send_form_restored": "表单已恢复",
"send_from_this_address": "从此地址发送",
"send_go_to_receive": "前往接收",
"send_keep": "保留",
"send_network_fee": "网络手续费",
"send_no_balance": "无余额",
"send_no_recent": "没有最近的发送",
"send_recent_sends": "最近发送",
"send_recipient": "收款方",
"send_select_source": "选择来源地址...",
"send_sending_from": "发送来源",
"send_submitting": "正在提交交易...",
"send_switch_to_receive": "切换到接收页面获取您的地址并开始接收资金。",
"send_to": "发送至",
"send_tooltip_enter_amount": "请输入发送金额",
"send_tooltip_exceeds_balance": "金额超过可用余额",
"send_tooltip_in_progress": "交易正在进行中",
"send_tooltip_invalid_address": "请输入有效的收款地址",
"send_tooltip_not_connected": "未连接到守护进程",
"send_tooltip_select_source": "请先选择来源地址",
"send_tooltip_syncing": "请等待区块链同步",
"send_total": "合计",
"send_transaction": "发送交易",
"send_tx_failed": "交易失败",
"send_tx_sent": "交易已发送!",
"send_tx_success": "交易发送成功!",
"send_txid_copied": "交易ID 已复制到剪贴板",
"send_txid_label": "TxID%s",
"send_valid_shielded": "有效的屏蔽地址",
"send_valid_transparent": "有效的透明地址",
"send_wallet_empty": "您的钱包是空的",
"send_yes_clear": "是,清除",
"sending": "正在发送交易",
"sending_from": "发送来源",
"sent": "已发送",
"sent_filter": "已发送",
"sent_type": "已发送",
"sent_upper": "已发送",
"settings": "设置",
"setup_wizard": "设置向导",
"share": "分享",
"shield_check_status": "检查状态",
"shield_completed": "操作成功完成!",
"shield_description": "通过将透明地址的 coinbase 输出发送到屏蔽地址来屏蔽您的挖矿奖励。这可以隐藏您的挖矿收入,提高隐私性。",
"shield_from_address": "从地址:",
"shield_funds": "屏蔽资金",
"shield_in_progress": "操作进行中...",
"shield_max_utxos": "每次操作最大 UTXO 数",
"shield_merge_done": "屏蔽/合并完成!",
"shield_select_z": "选择 z 地址...",
"shield_started": "屏蔽操作已开始",
"shield_title": "屏蔽 Coinbase 奖励",
"shield_to_address": "至地址(屏蔽):",
"shield_utxo_limit": "UTXO 限制:",
"shield_wildcard_hint": "使用 '*' 从所有透明地址屏蔽",
"shielded": "屏蔽",
"shielded_to": "屏蔽至",
"shielded_type": "屏蔽",
"show": "显示",
"show_qr_code": "显示二维码",
"showing_transactions": "显示第 %d\xe2\x80\x93%d 笔,共 %d 笔交易(总计:%zu",
"simple_background": "简单背景",
"start_mining": "开始挖矿",
"status": "状态",
"stop_external": "停止外部守护进程",
"stop_mining": "停止挖矿",
"submitting_transaction": "正在提交交易...",
"success": "成功",
"summary": "摘要",
"syncing": "同步中...",
"t_addresses": "T 地址",
"test_connection": "测试",
"theme": "主题",
"theme_effects": "主题效果",
"time_days_ago": "%d 天前",
"time_hours_ago": "%d 小时前",
"time_minutes_ago": "%d 分钟前",
"time_seconds_ago": "%d 秒前",
"to": "",
"to_upper": "",
"tools": "工具",
"total": "合计",
"transaction_id": "交易 ID",
"transaction_sent": "交易发送成功",
"transaction_sent_msg": "交易已发送!",
"transaction_url": "交易 URL",
"transactions": "交易",
"transactions_upper": "交易",
"transparent": "透明",
"tx_confirmations": "%d 次确认",
"tx_details_title": "交易详情",
"tx_from_address": "发送地址:",
"tx_id_label": "交易 ID",
"tx_immature": "未成熟",
"tx_mined": "已挖得",
"tx_received": "已接收",
"tx_sent": "已发送",
"tx_to_address": "接收地址:",
"tx_view_explorer": "在浏览器中查看",
"txs_count": "%d 笔交易",
"type": "类型",
"ui_opacity": "界面透明度",
"unban": "解除封禁",
"unconfirmed": "未确认",
"undo_clear": "撤销清除",
"unknown": "未知",
"use_embedded_daemon": "使用内置 dragonxd",
"use_tor": "使用 Tor",
"validate_btn": "验证",
"validate_description": "输入一个 DragonX 地址来检查它是否有效以及是否属于此钱包。",
"validate_invalid": "无效",
"validate_is_mine": "此钱包拥有该地址",
"validate_not_mine": "不属于此钱包",
"validate_ownership": "所有权:",
"validate_results": "结果:",
"validate_shielded_type": "屏蔽z 地址)",
"validate_status": "状态:",
"validate_title": "验证地址",
"validate_transparent_type": "透明t 地址)",
"validate_type": "类型:",
"validate_valid": "有效",
"validating": "验证中...",
"verbose_logging": "详细日志",
"version": "版本",
"view": "查看",
"view_details": "查看详情",
"view_on_explorer": "在浏览器中查看",
"waiting_for_daemon": "等待守护进程连接...",
"wallet": "钱包",
"wallet_empty": "您的钱包是空的",
"wallet_empty_hint": "切换到接收页面获取您的地址并开始接收资金。",
"warning": "警告",
"warning_upper": "警告!",
"website": "网站",
"window_opacity": "窗口透明度",
"yes_clear": "是,清除",
"your_addresses": "您的地址",
"z_addresses": "Z 地址",
}
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "zh.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
print(f"Wrote {len(translations)} Chinese translations to {os.path.abspath(out)}")

View File

@@ -256,8 +256,8 @@ HEADER_START
echo -e "${YELLOW}Note: Daemon binaries not found in prebuilt-binaries/dragonxd-win/ — wallet only${NC}"
fi
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
if [ -f "$XMRIG_DIR/xmrig.exe" ]; then
cp -f "$XMRIG_DIR/xmrig.exe" "$EMBED_RES_DIR/xmrig.exe"
echo " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"

View File

@@ -1,157 +0,0 @@
#!/usr/bin/env bash
# Package the prebuilt dragonx full-node binaries into per-platform release archives and sign them
# for the wallet's in-app daemon updater (ed25519 over the EXACT archive bytes).
#
# The wallet verifies a detached ed25519 signature over the archive bytes against a public key
# pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is MANDATORY
# (kDaemonRequireSignature = true): an in-app update is refused unless a valid "<archive>.sig" is
# published next to the archive. The wallet also checks each archive's SHA-256 against a markdown
# checksum table in the release body, so `release` prints that table for you to paste in.
#
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl. OpenSSL's ed25519 is PureEdDSA (RFC 8032), the
# same primitive libsodium's crypto_sign_verify_detached checks, so the signatures are compatible.
#
# Usage:
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
# scripts/sign-daemon-release.sh sign <secret.key> <file>... # sign existing files -> <file>.sig
# scripts/sign-daemon-release.sh release <secret.key> <version> [--src DIR] [--out DIR]
# # zip prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx-<version>-{linux-amd64,macos,
# # win64}.zip, sign each, and print the SHA-256 checksum table. Platforms with no dragonxd
# # binary staged are skipped.
#
# Keep the secret key (.ed25519.key) OFFLINE (mode 600). Paste the base64 public key into
# kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
die() { echo "error: $*" >&2; exit 1; }
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
# Raw 32-byte ed25519 public key (base64) from a private key file. The DER SubjectPublicKeyInfo for
# ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key.
pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; }
sha256_of() {
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}';
else shasum -a 256 "$1" | awk '{print $1}'; fi
}
# Detached ed25519 signature over the raw file bytes -> <file>.sig (base64 of the 64-byte sig).
sign_file() {
local key="$1" f="$2" raw
raw="$(mktemp)"
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
openssl base64 -A -in "$raw" > "$f.sig"
printf '\n' >> "$f.sig"
rm -f "$raw"
}
# platform -> (staging dir under prebuilt-binaries, release token, expected daemon binary name)
plat_dir() { case "$1" in linux) echo dragonxd-linux;; mac) echo dragonxd-mac;; win) echo dragonxd-win;; esac; }
plat_token() { case "$1" in linux) echo linux-amd64;; mac) echo macos;; win) echo win64;; esac; }
plat_daemon() { case "$1" in win) echo dragonxd.exe;; *) echo dragonxd;; esac; }
cmd="${1:-}"; shift || true
case "$cmd" in
keygen)
prefix="${1:-dragonx-daemon}"
[ -e "$prefix.ed25519.key" ] && die "$prefix.ed25519.key already exists — refusing to overwrite"
openssl genpkey -algorithm ed25519 -out "$prefix.ed25519.key"
chmod 600 "$prefix.ed25519.key"
pub="$(pubkey_b64 "$prefix.ed25519.key")"
printf '%s\n' "$pub" > "$prefix.ed25519.pub.b64"
echo "secret key : $prefix.ed25519.key (KEEP OFFLINE, mode 600)"
echo "public key : $prefix.ed25519.pub.b64"
echo
echo "Pin this in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64):"
echo " $pub"
;;
pubkey)
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
pubkey_b64 "$1"
;;
sign)
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
key="$1"; shift
[ -f "$key" ] || die "no such key: $key"
for f in "$@"; do
[ -f "$f" ] || die "no such file: $f"
sign_file "$key" "$f"
echo "signed: $f -> $f.sig"
done
echo "Upload each .sig as a release asset next to its archive."
;;
release)
[ $# -ge 2 ] || die "usage: release <secret.key> <version> [--src DIR] [--out DIR]"
key="$1"; version="$2"; shift 2
src="$PROJECT_ROOT/prebuilt-binaries"
out="$PROJECT_ROOT/release/daemon"
while [ $# -gt 0 ]; do
case "$1" in
--src) [ $# -ge 2 ] || die "--src needs a value"; src="$2"; shift 2 ;;
--out) [ $# -ge 2 ] || die "--out needs a value"; out="$2"; shift 2 ;;
*) die "unknown option: $1" ;;
esac
done
[ -f "$key" ] || die "no such key: $key"
[ -d "$src" ] || die "no such source dir: $src"
command -v zip >/dev/null 2>&1 || die "zip not found (install 'zip')"
mkdir -p "$out"
# Sanity: warn if this key does not match the public key pinned in the wallet (the wallet would
# then reject every signature made with it — only expected when deliberately rotating the key).
pinned="$(grep -oE '"[A-Za-z0-9+/]{43}="' "$PROJECT_ROOT/src/util/daemon_updater.h" 2>/dev/null | head -1 | tr -d '"')"
mine="$(pubkey_b64 "$key")"
if [ -n "$pinned" ] && [ "$pinned" != "$mine" ]; then
echo "WARNING: this key's public key does not match the one pinned in daemon_updater.h:" >&2
echo " signing key -> $mine" >&2
echo " pinned key -> $pinned" >&2
echo " The wallet will REJECT these signatures unless you are rotating the pinned key." >&2
echo >&2
fi
made=0
table=""
for plat in linux mac win; do
d="$src/$(plat_dir "$plat")"
daemon="$d/$(plat_daemon "$plat")"
if [ ! -f "$daemon" ]; then
echo "skip $plat: no $(plat_daemon "$plat") staged in $d" >&2
continue
fi
archive="dragonx-$version-$(plat_token "$plat").zip"
apath="$out/$archive"
rm -f "$apath"
# Zip the staged files at the archive root (binaries + sapling params + asmap), excluding
# the .gitkeep placeholder. The updater flattens paths via baseName(), so a flat zip is fine.
files=()
while IFS= read -r fn; do files+=("$fn"); done < <(cd "$d" && ls -A | grep -vx '.gitkeep')
[ "${#files[@]}" -gt 0 ] || { echo "skip $plat: nothing to package in $d" >&2; continue; }
( cd "$d" && zip -q -X "$apath" "${files[@]}" )
sign_file "$key" "$apath"
sum="$(sha256_of "$apath")"
table+="| $archive | \`$sum\` |"$'\n'
echo "packaged + signed: $apath (+ .sig) sha256=$sum"
made=$((made + 1))
done
[ "$made" -gt 0 ] || die "no platform had a staged daemon binary under $src/dragonxd-{linux,mac,win}/"
echo
echo "Checksum table (paste into the release body so the wallet can verify SHA-256):"
echo "| Archive | SHA-256 |"
echo "|---|---|"
printf '%s' "$table"
echo
echo "Upload each .zip AND its .zip.sig as release assets. Wallet enforces the ed25519 signature"
echo "(kDaemonRequireSignature=true) and the SHA-256 from the table above."
;;
*)
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>... | release <secret.key> <version> [--src DIR] [--out DIR]}"
;;
esac

View File

@@ -1,66 +0,0 @@
#!/usr/bin/env bash
# Sign DRG-XMRig release archives for the wallet's in-app updater (opt-in ed25519 signatures).
#
# The wallet verifies a detached ed25519 signature over the EXACT archive bytes against a public
# key pinned in src/util/xmrig_updater.h (kXmrigSignaturePublicKeyBase64). For each archive
# <name>.zip this produces <name>.zip.sig holding the base64 of the raw 64-byte ed25519 signature —
# upload that .sig next to the .zip as a release asset.
#
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl needed. OpenSSL's ed25519 is PureEdDSA (RFC 8032),
# the same primitive libsodium's crypto_sign_verify_detached checks, so signatures are compatible
# (verified by the wallet's unit tests + an interop check).
#
# Usage:
# scripts/sign-xmrig-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
# scripts/sign-xmrig-release.sh pubkey <secret.key> # print the base64 public key to pin
# scripts/sign-xmrig-release.sh sign <secret.key> <file>...# -> <file>.sig per file
#
# Keep the secret key (.ed25519.key) OFFLINE. Paste the base64 public key into
# kXmrigSignaturePublicKeyBase64 in src/util/xmrig_updater.h.
set -euo pipefail
die() { echo "error: $*" >&2; exit 1; }
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
# Raw 32-byte ed25519 public key (base64) from a private key file. The DER SubjectPublicKeyInfo for
# ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key.
pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; }
cmd="${1:-}"; shift || true
case "$cmd" in
keygen)
prefix="${1:-drg-xmrig}"
[ -e "$prefix.ed25519.key" ] && die "$prefix.ed25519.key already exists — refusing to overwrite"
openssl genpkey -algorithm ed25519 -out "$prefix.ed25519.key"
chmod 600 "$prefix.ed25519.key"
pub="$(pubkey_b64 "$prefix.ed25519.key")"
printf '%s\n' "$pub" > "$prefix.ed25519.pub.b64"
echo "secret key : $prefix.ed25519.key (KEEP OFFLINE, mode 600)"
echo "public key : $prefix.ed25519.pub.b64"
echo
echo "Pin this in src/util/xmrig_updater.h (kXmrigSignaturePublicKeyBase64):"
echo " $pub"
;;
pubkey)
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
pubkey_b64 "$1"
;;
sign)
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
key="$1"; shift
[ -f "$key" ] || die "no such key: $key"
for f in "$@"; do
[ -f "$f" ] || die "no such file: $f"
raw="$(mktemp)"
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
openssl base64 -A -in "$raw" > "$f.sig"
printf '\n' >> "$f.sig"
rm -f "$raw"
echo "signed: $f -> $f.sig"
done
echo "Upload each .sig as a release asset next to its archive."
;;
*)
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>...}"
;;
esac

129
setup.sh
View File

@@ -133,7 +133,7 @@ pkgs_core_arch="base-devel cmake git pkg-config
libxkbcommon wayland libsodium curl
autoconf automake libtool wget python xxd"
pkgs_core_macos="bash cmake python xxd"
pkgs_core_macos="cmake python xxd"
# Windows cross-compile (from Linux)
pkgs_win_debian="mingw-w64 zip"
@@ -284,14 +284,6 @@ fi
header "Windows Cross-Compile"
if $SETUP_WIN; then
# Only touch apt / update-alternatives (which need sudo) when the toolchain is missing. If it is
# already installed, skip them so `./setup.sh --win` can run WITHOUT sudo — important because the
# daemon cross-compile that follows should run as the invoking user. Running the whole setup under
# sudo leaves root-owned build artifacts under external/dragonx, which then break `make clean` on
# a later non-sudo build (stale objects get relinked -> the mingw link failure recurs).
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
ok "Windows cross-compile toolchain already present — skipping apt install"
else
win_pkgs="$(get_pkgs win)"
if [[ -n "$win_pkgs" ]]; then
install_pkgs "$win_pkgs" "Windows cross-compile"
@@ -306,7 +298,6 @@ if $SETUP_WIN; then
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
fi
fi
fi
# Fetch libsodium for Windows
if [[ ! -f "$PROJECT_DIR/libs/libsodium-win/lib/libsodium.a" ]]; then
@@ -400,26 +391,11 @@ elif $SETUP_SAPLING; then
SPEND_URL="https://z.cash/downloads/sapling-spend.params"
OUTPUT_URL="https://z.cash/downloads/sapling-output.params"
# Consensus-critical MPC parameters with fixed, well-known SHA-256 (identical across every
# Zcash-family node; also pinned in scripts/build-lite-backend-artifact.sh). z.cash is
# plain HTTPS with no signature, so verify the digest and refuse a tampered/corrupt file.
SPEND_SHA256="8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"
OUTPUT_SHA256="2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"
if curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" \
&& echo "${SPEND_SHA256} $PARAMS_DIR/sapling-spend.params" | sha256sum -c --status; then
ok "Downloaded + verified sapling-spend.params"
else
rm -f "$PARAMS_DIR/sapling-spend.params"
err "sapling-spend.params download or SHA-256 verification failed — not installed"
fi
if curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" \
&& echo "${OUTPUT_SHA256} $PARAMS_DIR/sapling-output.params" | sha256sum -c --status; then
ok "Downloaded + verified sapling-output.params"
else
rm -f "$PARAMS_DIR/sapling-output.params"
err "sapling-output.params download or SHA-256 verification failed — not installed"
fi
curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" && \
ok "Downloaded sapling-spend.params"
curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \
ok "Downloaded sapling-output.params"
fi
else
skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)"
@@ -466,53 +442,6 @@ copy_daemon_data() {
done
}
# ── Stale-daemon guard ───────────────────────────────────────────────────────
# A prebuilt daemon binary is only rebuilt on its platform's flag (--win/--mac),
# and build.sh merely BUNDLES whatever binary already exists — so a daemon left
# over from an old source revision silently ships in the wallet (e.g. the Network
# tab once reported v1.0.1 while the source was v1.0.2). These helpers compare the
# version baked into a prebuilt binary against the dragonx source and flag drift.
STALE_DAEMON=0
# MAJOR.MINOR.REVISION from the checked-out dragonx source (empty if unavailable).
dragonx_source_version() {
local hdr="$DRAGONX_SRC/src/clientversion.h"
[[ -f "$hdr" ]] || return 1
local maj min rev
maj=$(awk '/#define[ \t]+CLIENT_VERSION_MAJOR/{print $3; exit}' "$hdr")
min=$(awk '/#define[ \t]+CLIENT_VERSION_MINOR/{print $3; exit}' "$hdr")
rev=$(awk '/#define[ \t]+CLIENT_VERSION_REVISION/{print $3; exit}' "$hdr")
[[ -n "$maj" && -n "$min" && -n "$rev" ]] || return 1
printf '%s.%s.%s' "$maj" "$min" "$rev"
}
# vX.Y.Z baked into a built daemon binary (the daemon embeds "vX.Y.Z-<githash>").
# Uses grep -a so no `strings`/binutils dependency is required.
dragonx_binary_version() {
local bin="$1"
[[ -f "$bin" ]] || return 1
LC_ALL=C grep -aoE 'v[0-9]+\.[0-9]+\.[0-9]+-[0-9a-f]{6,}' "$bin" 2>/dev/null \
| head -1 | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/'
}
# Compare a prebuilt daemon against the source; warn (and set STALE_DAEMON) on drift.
# $1 = label, $2 = binary path, $3 = rebuild flag(s) (e.g. "--win", "" for Linux)
daemon_version_guard() {
local label="$1" bin="$2" rebuild_hint="$3"
[[ -f "$bin" ]] || return 0
local src bv
src=$(dragonx_source_version) || return 0 # no source checked out → can't compare
bv=$(dragonx_binary_version "$bin")
[[ -n "$bv" ]] || return 0 # couldn't read the binary's version
if [[ "$bv" == "$src" ]]; then
ok " $label daemon is v$bv (matches dragonx source)"
else
warn " $label daemon is v$bv but dragonx source is v$src — STALE"
warn " rebuild so the wallet ships the current daemon: ./setup.sh${rebuild_hint:+ $rebuild_hint}"
STALE_DAEMON=1
fi
}
# ── Linux daemon ─────────────────────────────────────────────────────────────
# Skip Linux daemon build if only cross-compile targets were requested
@@ -532,13 +461,11 @@ fi
if $CHECK_ONLY; then
if [[ -f "$DRAGONXD_LINUX/dragonxd" ]] || [[ -f "$DRAGONXD_LINUX/hushd" ]]; then
ok "dragonxd daemon (Linux) present"
daemon_version_guard "Linux" "$DRAGONXD_LINUX/dragonxd" ""
else
miss "dragonxd daemon (Linux) not built"
fi
elif $SKIP_LINUX_DAEMON; then
skip "dragonxd (Linux) — skipped, binaries already present (cross-compile only)"
daemon_version_guard "Linux" "$DRAGONXD_LINUX/dragonxd" ""
else
clone_dragonx_if_needed
@@ -573,11 +500,9 @@ fi
if ! $SETUP_WIN; then
skip "dragonxd (Windows) — use --win to cross-compile"
daemon_version_guard "Windows" "$DRAGONXD_WIN/dragonxd.exe" "--win"
elif $CHECK_ONLY; then
if [[ -f "$DRAGONXD_WIN/dragonxd.exe" ]] || [[ -f "$DRAGONXD_WIN/hushd.exe" ]]; then
ok "dragonxd daemon (Windows) present"
daemon_version_guard "Windows" "$DRAGONXD_WIN/dragonxd.exe" "--win"
else
miss "dragonxd daemon (Windows) not built"
fi
@@ -633,11 +558,9 @@ fi
if ! $SETUP_MAC; then
skip "dragonxd (macOS) — use --mac to cross-compile"
daemon_version_guard "macOS" "$DRAGONXD_MAC/dragonxd" "--mac"
elif $CHECK_ONLY; then
if [[ -f "$DRAGONXD_MAC/dragonxd" ]] || [[ -f "$DRAGONXD_MAC/hushd" ]]; then
ok "dragonxd daemon (macOS) present"
daemon_version_guard "macOS" "$DRAGONXD_MAC/dragonxd" "--mac"
else
miss "dragonxd daemon (macOS) not built"
fi
@@ -700,21 +623,11 @@ else
fi
fi
# Prominent reminder if any prebuilt daemon drifted from the source — these are bundled verbatim
# by build.sh, so a stale binary ships in the wallet (and shows an old version in the Network tab).
if [[ "$STALE_DAEMON" -eq 1 ]]; then
warn "One or more prebuilt daemons are OLDER than the dragonx source (see above)."
warn "build.sh bundles them as-is, so rebuild the stale platform(s) before releasing:"
warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac"
fi
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
header "xmrig-hac Mining Binary"
# ── 7. drg-xmrig (mining binary) ────────────────────────────────────────────
header "drg-xmrig Mining Binary"
XMRIG_SRC="$PROJECT_DIR/external/drg-xmrig"
# Output dir bundled by build.sh (Linux zip, AppImage, Windows embed, mac .app)
# and scripts/legacy/build-windows.sh — keep this path in sync with those.
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/drg-xmrig"
XMRIG_SRC="$PROJECT_DIR/external/xmrig-hac"
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac"
# Clean previous prebuilt xmrig binaries so we always rebuild
# Only clean the binary for the platform(s) we are actually building,
@@ -726,14 +639,14 @@ if ! $CHECK_ONLY; then
fi
fi
# Helper: clone drg-xmrig if not present
# Helper: clone xmrig-hac if not present
clone_xmrig_if_needed() {
if [[ ! -d "$XMRIG_SRC" ]]; then
info "Cloning drg-xmrig..."
git clone https://git.dragonx.is/DragonX/drg-xmrig.git "$XMRIG_SRC"
info "Cloning xmrig-hac..."
git clone https://git.dragonx.is/dragonx/xmrig-hac.git "$XMRIG_SRC"
else
ok "drg-xmrig source already present"
info "Pulling latest drg-xmrig..."
ok "xmrig-hac source already present"
info "Pulling latest xmrig-hac..."
(cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true)
fi
}
@@ -754,15 +667,15 @@ else
rm -rf "$XMRIG_SRC/build"
# Build dependencies (libuv, hwloc, openssl)
info "Building drg-xmrig dependencies (libuv, hwloc, openssl)..."
info "Building xmrig-hac dependencies (libuv, hwloc, openssl)..."
(
cd "$XMRIG_SRC/scripts"
sh build_deps.sh
)
ok "drg-xmrig dependencies built"
ok "xmrig-hac dependencies built"
# Build xmrig
info "Building drg-xmrig (Linux)..."
info "Building xmrig-hac (Linux)..."
mkdir -p "$XMRIG_SRC/build"
(
cd "$XMRIG_SRC/build"
@@ -779,7 +692,7 @@ else
mkdir -p "$XMRIG_PREBUILT"
if [[ -f "$XMRIG_SRC/build/xmrig" ]]; then
cp "$XMRIG_SRC/build/xmrig" "$XMRIG_LINUX"
ok "xmrig (Linux) built and installed to prebuilt-binaries/drg-xmrig/"
ok "xmrig (Linux) built and installed to prebuilt-binaries/xmrig-hac/"
else
err "xmrig (Linux) build failed — binary not found"
MISSING=$((MISSING + 1))
@@ -803,7 +716,7 @@ else
# Clean previous Windows build
rm -rf "$XMRIG_SRC/build-windows"
info "Building drg-xmrig (Windows cross-compile)..."
info "Building xmrig-hac (Windows cross-compile)..."
(
cd "$XMRIG_SRC/scripts"
bash build_windows.sh
@@ -813,7 +726,7 @@ else
mkdir -p "$XMRIG_PREBUILT"
if [[ -f "$XMRIG_SRC/build-windows/xmrig.exe" ]]; then
cp "$XMRIG_SRC/build-windows/xmrig.exe" "$XMRIG_WIN"
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/drg-xmrig/"
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/xmrig-hac/"
else
err "xmrig.exe (Windows) build failed — binary not found"
MISSING=$((MISSING + 1))
@@ -823,7 +736,7 @@ fi
# ── 8. Binary directories ───────────────────────────────────────────────────
header "Binary Directories"
for platform in dragonxd-linux dragonxd-win dragonxd-mac drg-xmrig; do
for platform in dragonxd-linux dragonxd-win dragonxd-mac xmrig; do
dir="$PROJECT_DIR/prebuilt-binaries/$platform"
if [[ -d "$dir" ]]; then
# Count actual files (not .gitkeep)

File diff suppressed because it is too large Load Diff

703
src/app.h
View File

@@ -9,25 +9,16 @@
#include <functional>
#include <thread>
#include <atomic>
#include <mutex>
#include <chrono>
#include <unordered_map>
#include <unordered_set>
#include <deque>
#include <nlohmann/json_fwd.hpp>
#include "data/transaction_history_cache.h"
#include "data/address_book.h"
#include "data/wallet_index.h"
#include "data/wallet_state.h"
#include "rpc/connection.h"
#include "services/network_refresh_service.h"
#include "services/wallet_security_controller.h"
#include "services/wallet_security_workflow.h"
#include "util/async_task_manager.h"
#include "util/pool_stats_service.h"
#include "wallet/wallet_capabilities.h"
#include "chat/chat_service.h"
#include "chat/chat_database.h"
#include "ui/sidebar.h"
#include "ui/windows/console_tab.h"
#include "imgui.h"
@@ -41,7 +32,6 @@ namespace dragonx {
namespace config { class Settings; }
namespace daemon { class DaemonController; class EmbeddedDaemon; class XmrigManager; }
namespace util { class Bootstrap; class SecureVault; }
namespace wallet { class LiteWalletController; struct LiteWalletAppRefreshModel; }
}
namespace dragonx {
@@ -140,16 +130,6 @@ public:
* @brief Whether we are in the shutdown phase
*/
bool isShuttingDown() const { return shutting_down_; }
// True while the wallet-switch progress modal is open — keeps the frame loop redrawing so its live
// phase/spinner update in real time even when the app is otherwise idle.
bool isWalletSwitchInProgress() const { return wallet_switch_dialog_open_.load(); }
wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); }
bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); }
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
bool supportsSoloMining() const { return wallet::supportsSoloMining(walletCapabilities()); }
bool supportsPoolMining() const { return wallet::supportsPoolMining(walletCapabilities()); }
bool supportsLiteBackend() const { return wallet::supportsLiteBackend(walletCapabilities()); }
/**
* @brief Render the shutdown overlay (called instead of normal UI during shutdown)
@@ -165,61 +145,10 @@ public:
// Accessors for subsystems
rpc::RPCClient* rpc() { return rpc_.get(); }
rpc::RPCWorker* worker() { return worker_.get(); }
// Console backend accessors (fast-lane-preferring, defined in app.cpp where the
// subsystem types are complete) used by the shared console executor.
rpc::RPCClient* consoleRpc();
rpc::RPCWorker* consoleWorker();
daemon::EmbeddedDaemon* consoleDaemon();
daemon::XmrigManager* consoleXmrig();
config::Settings* settings() { return settings_.get(); }
// Request a font-atlas rebuild before the next frame (e.g. after toggling color emoji). Handled in
// preFrame() via Typography::reload — safe to call from UI code mid-frame.
void requestFontRebuild() { font_rebuild_requested_ = true; }
// Lite wallet controller (non-null only in lite builds with a linked backend).
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
// HushChat service (identity + in-memory message store); the Chat tab reads its store.
chat::ChatService& chatService() { return chat_service_; }
// HushChat composing: construct, broadcast (broadcastChatMemos), and locally echo an outgoing
// message (to a conversation whose peer key we know) / a new-conversation contact request.
void sendChatMessage(const std::string& conversationId, const std::string& text);
void startChatConversation(const std::string& peerZaddr, const std::string& text);
// Send a contact request into an existing conversation (used to retry a failed request in place).
void sendContactRequestForCid(const std::string& cid, const std::string& peerZaddr,
const std::string& text);
// Debug/sweep convenience: give the Chat tab a demo identity + a few sample conversations
// (in-memory only, not persisted) so the screenshot sweep captures the populated UI. No-op when
// the chat feature is off.
void seedChatDemoData();
// Reason the lite wallet failed to auto-open this session (empty if none / opened OK).
const std::string& liteOpenError() const { return lite_open_error_; }
// Show the lite send-time unlock modal (called when a spend is attempted on a locked wallet).
void requestLiteUnlock() { lite_unlock_prompt_ = true; }
// Lock the lite wallet AND immediately tear down the chat session (the lite backend `lock`
// doesn't update state_.locked until the next poll, so chat secrets would otherwise linger).
bool lockLiteWallet();
// (Re)build the lite controller from current settings so a changed lite-server selection
// takes effect. No-op on non-lite/unlinked builds; preserves a live wallet (see app.cpp).
void rebuildLiteWallet(bool force = false);
WalletState& state() { return state_; }
const WalletState& state() const { return state_; }
const WalletState& getWalletState() const { return state_; }
// Shared contact store (Contacts tab / Send picker / future Chat roster). App-owned so
// every surface reads one source of truth instead of a per-dialog singleton.
data::AddressBook& addressBook() { return address_book_; }
const data::AddressBook& addressBook() const { return address_book_; }
// Hash of the active wallet's identity (derived from its address list). This is the tx-history
// cache key — it changes when the address set changes, so DON'T scope persistent user data on it.
std::string activeWalletIdentityHash() const;
// Stable per-wallet id ("w:"+hex) for scoping persistent per-wallet data (address-book contacts).
// Generated once and persisted in the wallet index (keyed by wallet file), never recomputed from
// the mutable address set — so creating addresses / locking / disconnecting never changes it.
// Empty only when there is no active wallet file. Establishes + persists the id on first call.
std::string activeWalletScopeId();
data::WalletIndex& walletIndex() { return wallet_index_; }
const data::WalletIndex& walletIndex() const { return wallet_index_; }
// Connection state (convenience wrappers)
bool isConnected() const { return state_.connected; }
@@ -252,21 +181,6 @@ public:
int getXmrigRequestedThreads() const {
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
}
// True while the pool miner process is live — used to refuse replacing the binary under it.
bool isPoolMinerRunning() const {
return xmrig_manager_ && xmrig_manager_->isRunning();
}
// Auto-balance: latest per-pool hashrate snapshot (for the mining tab pool list),
// and a request to refresh it now (Refresh button / switching into Auto mode).
util::PoolStatsService::Snapshot poolStatsSnapshot() const {
return pool_stats_service_.snapshot();
}
void requestPoolBalanceRefresh() { balance_refresh_pending_ = true; }
// Installed miner version (detected from `xmrig --version`, cached; kicks the one-shot
// detection on first call) so the mining tab can show it before mining starts.
std::string poolMiningInstalledVersion();
// Mine-when-idle state query
bool isIdleMiningActive() const { return idle_mining_active_; }
@@ -304,70 +218,28 @@ public:
void setAddressSortOrder(const std::string& addr, int order);
int getNextSortOrder() const;
void swapAddressOrder(const std::string& a, const std::string& b);
// Assign dense sort orders (0..N-1) to the given addresses in the given order and
// persist once. Used by drag-reorder so a drop always takes effect (even from the
// default un-ordered state, where a pairwise swap would be a no-op).
void reorderAddresses(const std::vector<std::string>& orderedAddrs);
bool isMiningAddress(const std::string& addr) const;
void setMiningAddress(const std::string& addr, bool mining);
void invalidateAddressValidationCache();
// Key export/import
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
// callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export.
void exportAllKeys(std::function<void(const std::string&, int, int)> callback);
// callback(success, errorOrEmpty, importedAddress). address is "" on failure or when the RPC
// returns none; the import routes to z_importviewingkey / z_importkey / importprivkey by key type.
// startHeight > 0 rescans from that block (shielded RPCs only; ignored for transparent WIF).
void importPrivateKey(const std::string& key, int startHeight,
std::function<void(bool, const std::string&, const std::string&)> callback);
// Sweep a spending key: import it (rescan) then z_sendmany ALL its funds (balance fee) to a
// destination you own — a freshly generated shielded address when destMode == 0, else destExisting.
// Drives the sweep_step_ / sweep_status_ / sweep_txid_ state; reuses the async-operation tracker.
void sweepPrivateKey(const std::string& key, int startHeight, int destMode,
const std::string& destExisting);
void exportAllKeys(std::function<void(const std::string&)> callback);
void importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback);
// Wallet backup
void backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback);
// Export the wallet's BIP39 seed phrase (z_exportmnemonic). The callback receives
// (ok, noMnemonic, phrase, error): ok+phrase on success; noMnemonic=true when the
// wallet's seed is not mnemonic-derived (legacy wallet). Full-node only; the phrase
// is a secret and is wiped after the callback returns.
void exportSeedPhrase(std::function<void(bool ok, bool noMnemonic,
const std::string& phrase,
const std::string& error)> callback);
// Transaction operations
void sendTransaction(const std::string& from, const std::string& to,
double amount, double fee, const std::string& memo,
std::function<void(bool success, const std::string& result)> callback);
// Register a daemon async operation id (z_shieldcoinbase / z_mergetoaddress /
// auto-shield) with the shared opid poller so its eventual success/failure is
// surfaced and balances/transactions refresh on completion. z_sendmany uses the
// richer pending-send path internally; this is for operations with no optimistic
// transaction row of their own.
void trackOperation(const std::string& opid);
// Force refresh
void refreshNow();
void refreshMiningInfo();
void refreshPeerInfo();
void refreshMarketData();
// Fetch the live exchange/pair list from CoinGecko once per session (venues are
// near-static); populates state.market.exchanges. Safe to call every frame.
void refreshExchanges();
// Fetch historical USD price series (CoinGecko market_chart) that back the portfolio
// sparkline intervals; self-throttled to ~30 min. Safe to call every frame.
void refreshMarketChart();
// Fetch the SELECTED pair's candles from that exchange's own API (data/exchange_candles.h) so the
// Market chart shows the real per-exchange price. Re-fetches on pair change; falls back to the
// CoinGecko aggregate for unmapped venues / failed fetches. Safe to call every frame.
void refreshExchangeChart();
// True while a market price-history fetch is in flight (CoinGecko aggregate OR per-exchange). The
// Market chart shows a loading indicator instead of the empty state during pair switches.
bool isMarketChartLoading() const { return chart_fetch_in_flight_ || exchange_chart_fetch_in_flight_; }
/// @brief Per-category refresh intervals, adjusted by active tab
using RefreshIntervals = services::NetworkRefreshService::Intervals;
@@ -379,40 +251,10 @@ public:
void setCurrentPage(ui::NavPage page);
ui::NavPage getCurrentPage() const { return current_page_; }
// Debug: screenshot sweep — cycles every skin x every enabled tab, one PNG each, into a
// timestamped folder under the config dir. Driven from App::render(); main.cpp polls
// wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls
// onScreenshotCaptured() to advance. Transient — restores the original skin/page when done.
void startScreenshotSweep();
// Full UI sweep: like the tab sweep, but also drives every modal / dialog / multi-step flow /
// state overlay into view (with injected demo data, offline, firing no live ops) and captures
// each under every skin. Output: <config>/screenshots-full/<surface>/<skin>.png + an index.
void startFullUiSweep();
std::string screenshotFullDir() const;
bool isScreenshotSweeping() const { return screenshot_sweep_active_; }
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
void onScreenshotCaptured();
std::string screenshotDir() const; // <config>/screenshots (fixed; sweeps overwrite in place)
// Dialog triggers (used by settings page to open modal dialogs)
void showImportKeyDialog() { import_view_mode_ = false; show_import_key_ = true; } // spending
void showImportViewingKeyDialog() { import_view_mode_ = true; show_import_key_ = true; } // watch-only
void showImportKeyDialog() { show_import_key_ = true; }
void showExportKeyDialog() { show_export_key_ = true; }
void showBackupDialog() { show_backup_ = true; }
void showSeedBackupDialog() { show_seed_backup_ = true; }
void showSeedMigrationDialog(); // opens the migration modal (resumes a pending one at Sweep)
// True when the current full-node wallet is a legacy, pre-seed-phrase wallet (no BIP39 mnemonic)
// that a capable daemon could migrate — the Migrate-to-seed button glows to nudge the user.
bool isPreSeedWallet() const { return wallet_seed_status_ == WalletSeedStatus::NoMnemonic; }
// Authoritative BIP39-mnemonic status of the ACTIVE wallet, decided at runtime by z_exportmnemonic
// (not the offline file probe, which can't tell an HD-but-no-mnemonic wallet from a seed-phrase one).
// 0 = unknown/undecidable, 1 = has a seed phrase, 2 = legacy (no mnemonic).
int activeWalletSeedBadge() const {
if (wallet_seed_status_ == WalletSeedStatus::HasMnemonic) return 1;
if (wallet_seed_status_ == WalletSeedStatus::NoMnemonic) return 2;
return 0;
}
void showAboutDialog() { show_about_ = true; }
// Legacy tab compat — maps int to NavPage
@@ -431,30 +273,10 @@ public:
// Embedded daemon control
bool startEmbeddedDaemon();
void stopEmbeddedDaemon();
// Stop the node specifically for a wallet switch, which MUST free the RPC port to relaunch on
// -wallet=<name>. Unlike stopEmbeddedDaemon() (whose DisconnectOnly policy leaves an adopted/external
// daemon running), this sends a graceful RPC "stop" — using our own connection creds, so only our
// daemon obeys it — even to an adopted daemon, then waits (bounded) for the port to actually release.
// Returns true once the RPC port is free (or we're shutting down).
bool stopDaemonForWalletSwitch();
bool isEmbeddedDaemonRunning() const;
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
void rescanBlockchain(); // restart daemon with -rescan flag (full-history nodes)
// Runtime rescanblockchain RPC starting at a snapshot-available height. Unlike the
// -rescan restart, this works on bootstrapped/pruned nodes (which lack pre-snapshot
// block data), reconciling the wallet's stale spent-state without a daemon restart.
void runtimeRescan(int startHeight);
// Async binary-search probe for the lowest block height the node still has on disk.
// cb(ok, lowestHeight, fullHistory): fullHistory==true when genesis is present (a normal,
// non-bootstrapped node). Runs on the UI thread via the RPC worker callbacks.
void detectLowestAvailableBlockHeight(std::function<void(bool ok, int lowestHeight, bool fullHistory)> cb);
// Flag that a bootstrap just finished so the wallet auto-reconciles spent-state once the
// daemon is back up (consumed in update()).
void markPostBootstrapRescanPending() { post_bootstrap_rescan_pending_ = true; }
bool runtimeRescanActive() const { return runtime_rescan_active_; }
void repairWallet(); // restart daemon with -zapwallettxes=2 (wipe & rebuild wallet tx records)
void reinstallBundledDaemon(); // stop daemon, overwrite installed binary with the bundled one, restart
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
void rescanBlockchain(); // restart daemon with -rescan flag
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
bool isBootstrapDownloading() const { return bootstrap_downloading_; }
@@ -479,10 +301,6 @@ public:
// Coin logo texture accessor (DragonX currency icon for balance tab)
ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; }
// DragonX custom chat emoji (the ":drgx:" shortcode) — the mark recolored to the theme accent (like
// the logo), re-rasterized on theme change. Used by the emoji picker tile + inline in chat bubbles.
ImTextureID getDrgxEmojiTexture() const { return drgx_emoji_tex_; }
/**
* @brief Reload theme images (background gradient + logo) from new paths
* @param bgPath Path to background image override (empty = use default)
@@ -490,10 +308,6 @@ public:
*/
void reloadThemeImages(const std::string& bgPath, const std::string& logoPath);
// Load / recolor-per-theme the DragonX header logo (SVG rasterized to the theme accent). Called at
// the top of render() so the wizard, lock screen, and main header all show it.
void ensureLogoTexture();
// Wizard / first-run
WizardPhase getWizardPhase() const { return wizard_phase_; }
bool isFirstRun() const;
@@ -509,23 +323,9 @@ public:
* Shows "Restarting daemon..." in the loading overlay while the daemon cycles.
*/
void restartDaemon();
// Switch the active wallet: persist the new -wallet=<name>, stop the node, restart on it
// (rescan only if it was never synced in this datadir). Per-wallet data follows automatically
// via the identity-scoped caches (P1). No-op if that wallet is already active.
// stopDaemonConfirmed: skip the "stop the running node?" confirmation (set true when re-entered from
// that dialog). salvage: start the target node with -salvagewallet (repair a corrupt wallet).
void switchToWallet(const std::string& walletFile, bool stopDaemonConfirmed = false, bool salvage = false);
// Main-thread continuation: if a wallet switch's daemon failed to start, revert active_wallet_file.
void processWalletSwitchRevert();
// Wallet encryption helpers
void encryptWalletWithPassphrase(const std::string& passphrase);
// Post-encrypt daemon restart: the daemon shuts itself down after
// encryptwallet, so restart it off the main thread. Shared by the
// immediate and deferred encryption continuations. When
// announceRestartStatus is true, connection_status_ is updated first so
// the loading overlay explains the restart.
void restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus);
void unlockWallet(const std::string& passphrase, int timeout);
void lockWallet();
void changePassphrase(const std::string& oldPass, const std::string& newPass);
@@ -555,27 +355,11 @@ public:
void showPinRemoveDialog() { show_pin_remove_ = true; pin_status_.clear(); }
bool hasPinVault() const;
// Debug-options gate: does revealing the debug dropdown need re-authentication (a PIN vault or
// an encrypted wallet)? And verify the entered PIN/passphrase — cb(ok) is invoked on the main
// thread (PIN via the vault, else the wallet passphrase via RPC).
bool debugGateRequiresAuth() const;
void verifyDebugCredential(const std::string& secret, std::function<void(bool)> cb);
/// @brief Check if RPC worker has queued results waiting to be processed
bool hasPendingRPCResults() const;
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
std::string transactionSendProgressText() const;
std::string transactionRefreshProgressText() const;
// Copy a SECRET (seed phrase, private key) to the clipboard and arm an auto-clear: after a
// short delay the clipboard is wiped IF it still holds this secret (so we don't clobber
// something the user copied afterwards). Only a hash of the secret is retained, never the
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
void copySecretToClipboard(const std::string& secret);
void pumpSecretClipboardClear();
// Immediately clear the clipboard if it still holds the armed secret (ignores the 45s timer).
// Called on app shutdown so a copied key/seed does not outlive the process in the OS clipboard.
void clearSecretClipboardIfArmed();
bool isTransactionRefreshInProgress() const {
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
}
@@ -584,77 +368,21 @@ private:
friend class AppDaemonLifecycleRuntime;
friend class AppDaemonLifecycleTaskContext;
// Global keyboard-shortcut handling, dispatched once per frame from update().
void handleGlobalShortcuts();
bool sendStopCommandSafely(rpc::RPCClient& client, const char* context);
void maybeFinishTransactionSendProgress();
// Shared body of createNewZAddress/createNewTAddress (which are thin public forwarders).
// `shielded` selects the z_getnewaddress/getnewaddress RPC, the "shielded"/"transparent" type
// string, and the z_addresses/t_addresses target (the new AddressInfo is pushed into both that
// list and state_.addresses). Lite builds derive locally via the controller and early-return.
void createNewAddress(bool shielded, std::function<void(const std::string&)> callback);
void upsertPendingSendTransaction(const std::string& opid,
const std::string& from,
const std::string& to,
double amount,
const std::string& memo,
double fee = 0.0);
// Work around a dragonxd note-selection bug: its z_sendmany picks notes to cover the recipient
// total but not the miner fee, so a shielded send whose largest notes sum exactly to the amount
// fails with "Insufficient shielded funds, have H, need H+fee" despite ample balance. When a
// failed opid matches that (H >= the requested amount), re-issue the send once with a tiny
// self-output that lifts the daemon's selection target past the boundary so it grabs another
// note; the recipient still receives the exact amount. Returns true if a retry was issued.
bool maybeRetrySendForFeeGap(const std::string& opid, const std::string& rawMsg);
void resendWithFeeGapWorkaround(const std::string& from, const std::string& to,
double amount, double fee, const std::string& memo,
std::function<void(bool, const std::string&)> callback);
// Shared z_sendmany submit path for sendTransaction (single recipient, markFeeGapRetry=false)
// and resendWithFeeGapWorkaround (recipient + self-output, markFeeGapRetry=true). Owns the
// in-flight increment, the worker post + call, and the result closure (dirty flags, opid
// tracking, pending-send bookkeeping, callback delivery). Callers build `recipients` (and pass
// the recipient `to`/`amount`/`memo`/`fee` used to record the optimistic pending-send row).
// When markFeeGapRetry is set, the returned opid is recorded in send_feegap_retried_opids_ so a
// retry of a retry is reported as a real error.
// background=true (autonomous chat sends / note-buffer splits): keep the single-flight + opid
// accounting but DON'T raise the global "transaction in progress" UI (status is on the chat message).
void submitZSendMany(const std::string& from, const std::string& to, double amount, double fee,
const std::string& memo, const nlohmann::json& recipients,
const char* traceLabel, bool markFeeGapRetry,
std::function<void(bool, const std::string&)> callback,
bool background = false);
const std::string& memo);
void markPendingSendTransactionSucceeded(const std::string& opid,
const std::string& txid);
void removePendingSendTransactions(const std::vector<std::string>& opids,
bool restoreBalances);
// Apply a signed per-address balance delta for a pending send: walk z_addresses then
// t_addresses for `fromAddress` and clamp its balance at >=0. A positive `signedAmount`
// restores a debit; a negative one applies it. When `includeAggregates` is set, adjust the
// private/transparent bucket (chosen by the address's leading 'z') and totalBalance with the
// same clamp. Shared by the three pending-send delta sites so they can't drift.
void applyPendingSendDelta(const std::string& fromAddress, double signedAmount,
bool includeAggregates);
// Deliver a deferred z_sendmany result to its waiting UI callback once the opid
// reaches a terminal status. Returns true if a callback was registered (and fired).
bool invokeSendResultCallback(const std::string& opid, bool ok,
const std::string& result);
void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
std::string transactionHistoryCacheWalletIdentity() const;
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
// Record the active wallet's cached metadata (balance, address count, identity, size) into the
// wallet index (wallets.json). Cheap + throttled: only writes when a value changed. markOpened
// stamps last-opened + syncedHere (call once per connect).
void updateWalletIndexForActiveWallet(bool markOpened);
void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
// Shared main-thread continuations for a wallet unlock attempt, so the passphrase
// and PIN paths cannot drift. applyUnlockFailure applies the escalating lockout
// curve on every failed path (a PIN RPC failure used to skip it).
void applyUnlockSuccess(const std::string& passphrase, int timeout);
void applyUnlockFailure(const std::string& errorMessage);
// Clear all rescan + witness-rebuild progress/accumulator state. Shared by the four
// rescan-completion sites so a new witness field can't be forgotten in one copy.
void resetWitnessRescanProgress();
void loadTransactionHistoryCacheIfAvailable();
void storeTransactionHistoryCacheIfAvailable();
void wipePendingTransactionHistoryCachePassphrase();
@@ -662,11 +390,6 @@ private:
void pruneShieldedHistoryScanProgress();
void invalidateShieldedHistoryScanProgress(bool persistCache);
// Auto-balance pool selection: drive the periodic hashrate refresh and apply a
// freshly-completed snapshot (weighted-random pick + optional miner restart).
void updatePoolAutoBalance();
void applyPoolAutoBalance(const util::PoolStatsService::Snapshot& snap);
// Subsystems
std::unique_ptr<rpc::RPCClient> rpc_;
std::unique_ptr<rpc::RPCWorker> worker_;
@@ -681,176 +404,8 @@ private:
rpc::ConnectionConfig saved_config_;
std::unique_ptr<config::Settings> settings_;
std::unique_ptr<wallet::LiteWalletController> lite_wallet_; // lite builds w/ linked backend
// Pending send_tab callback for an in-flight lite send (delivered in update() once the
// controller's async broadcast result arrives). Only one lite send runs at a time.
std::function<void(bool, const std::string&)> lite_send_callback_;
// One-shot guard: auto-open an existing lite wallet on the first update() tick (kept off
// init() so a slow initialize_existing network call doesn't freeze startup before the window).
bool lite_autoopen_done_ = false;
double lite_open_last_attempt_ = 0.0; // ImGui time of the last async open attempt (retry timer)
// Reason an existing lite wallet failed to auto-open (e.g. server unreachable). Surfaced in
// the UI so a stuck "disconnected" state isn't silent; cleared once a wallet opens.
std::string lite_open_error_;
// HushChat (experimental; gated by DRAGONX_ENABLE_CHAT — inert when OFF). App owns the chat
// service so the transaction-refresh harvest can decrypt incoming memos into threaded messages.
// The identity is derived from the wallet's OWN SDXLite-compatible seed phrase (full-node
// z_exportmnemonic / lite exportSeed → the same KDF), so it is portable across both variants.
chat::ChatService chat_service_;
chat::ChatDatabase chat_db_; // persistent backing (seed-derived encryption at rest)
bool chat_identity_provisioned_ = false; // identity set on the service this session
bool chat_identity_fetch_in_flight_ = false; // a z_exportmnemonic worker job is pending
// Bumped by resetChatSession() on every wallet change; an in-flight identity fetch captures the
// value at post time and its completion callback discards its (previous-wallet) secret if the id
// no longer matches — so wallet A's seed can't be provisioned under wallet B via a stale job.
int chat_session_generation_ = 0;
bool chat_identity_unavailable_ = false; // provisioning failed definitively (e.g. non-mnemonic wallet)
// Per-conversation "last seen" watermark (message timestamp) for unread tracking (Q1). Updated when a
// thread is viewed (markChatConversationSeen); wiped in resetChatSession so unread doesn't leak across
// wallets. In-memory only (resets on app restart).
std::map<std::string, std::int64_t> chat_seen_watermark_;
// ── Chat note buffer (BOTH variants) ────────────────────────────────────────────────────────
// Each chat message is a shielded tx that spends a note; its change needs a few confirmations before
// it's spendable again (lite: backend ANCHOR_OFFSET+1 = 5; full node: z_sendmany minconf = 1), so
// rapid sends run out of verified funds. We keep a buffer of ~kChatBufferTarget small self-notes so a
// burst of messages each spends a separate verified note, refilled from change + background self-
// splits. Chat sends, self-splits and user sends share the wallet's single send channel; inflight_op_
// + (lite) lite_send_callback_ / (full node) send_submissions_in_flight_+pending_opids_ serialize them
// so exactly one send is ever outstanding. Implemented in app_network.cpp; pumped from update().
enum class LiteOpKind { None, ChatSend, ContactRequest, Split, UserSend };
struct LiteInflightOp {
LiteOpKind kind = LiteOpKind::None;
std::string echoLocalId; // ChatSend/ContactRequest: the echo to resolve when it completes
int sessionGen = 0; // chat_session_generation_ snapshot at submit (stale-guard)
double submittedAt = 0.0;
};
struct QueuedChatOp {
LiteOpKind kind = LiteOpKind::ChatSend;
chat::OutgoingChatMemos memos; // kept so a transient-funds retry re-broadcasts, never recomposes
std::string echoLocalId;
int sessionGen = 0;
int retries = 0;
};
LiteInflightOp inflight_op_; // the single chat/split op currently on the send channel
std::deque<QueuedChatOp> chat_send_queue_; // chat/contact sends awaiting a free channel + verified note
// Note-availability estimate between refreshes/scans: reset from a fresh count, decremented on each chat
// submit (the count lags a spend by a cycle, but the wallet still picks a fresh note per send). Zeroed on
// a transient-funds failure so we stop draining until the next refresh/scan restores the truth.
int chat_verified_note_budget_ = 0;
int chat_pipeline_note_count_ = 0; // verified + maturing self-notes (drives shouldSplit)
std::uint64_t chat_verified_shielded_zat_ = 0; // verified shielded balance (split affordability)
bool chat_note_model_seen_ = false; // saw a refresh/scan carrying per-note visibility
// Single-split-in-flight guard: a self-split's OUTPUT notes are invisible until mined (~1 block), far
// longer than any wall-clock cooldown — so we permit only ONE outstanding split and clear the flag when
// the pipeline recovers (outputs mined) or a watchdog expires (a split that never mines mustn't wedge
// refill forever). Prevents runaway splitting that would drain balance into fees.
bool chat_split_outstanding_ = false;
double chat_split_submitted_at_ = 0.0; // ImGui time the outstanding split was submitted (watchdog)
// Full-node only: a coordinator-owned z_listunspent worker scan feeds the per-note counts (the shared
// balance poll discards per-note data). Mirrors chat_fast_scan_in_flight_.
bool chat_note_scan_in_flight_ = false;
double chat_note_scan_last_ = 0.0; // ImGui time of the last note scan (rate limit)
// Most-recent chain tip, cached across refreshes: a lite refresh model that carries spendableOutputs
// may NOT carry sync status that same cycle (tolerated partial refresh), so verifiedSelfNoteCount reads
// this cache rather than requiring the current model to have both — else the budget flickers to 0.
std::int64_t chat_last_chain_height_ = 0;
int chat_fast_scan_last_seen_ = -1; // dedup: last memo-note count logged by the 0-conf scan
// Coordinator helpers (both variants unless noted; see app_network.cpp).
void refreshChatNoteBudget(const wallet::LiteWalletAppRefreshModel& model); // lite: recompute caches on a fresh model
void refreshChatNoteBudgetNode(); // full node: rate-limited z_listunspent worker scan
void pumpChatNoteBuffer(); // per-frame: drain the queue / build the buffer
int verifiedSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
int pipelineSelfNoteCount(const wallet::LiteWalletAppRefreshModel& model); // lite
bool shouldSplitChatBuffer();
bool broadcastSelfSplitLite(int noteCount); // lite: self-send minting noteCount reply-address notes
bool broadcastSelfSplitNode(int noteCount); // full node: z_sendmany self-send minting noteCount notes
void enqueueChatSend(LiteOpKind kind, const chat::OutgoingChatMemos& memos, const std::string& echoLocalId);
void onChatBroadcastResult(const LiteInflightOp& op, bool ok, const std::string& error);
static bool isTransientVerifiedFundsError(const std::string& error);
int chatConfsRequired() const; // verified-note confs threshold: 5 (lite) / 1 (full node)
public:
// Status-bar summary of the chat note buffer (empty when not applicable). Shown while the Chat tab is
// active so the buffer's state (ready / building / sending) is visible.
std::string chatBufferStatusText();
private:
public:
// Total unread incoming chat messages across all conversations (for the sidebar badge). 0 when the
// feature is off / no identity.
int chatUnreadCount() const;
// Mark a conversation read up to latestTs (called by the Chat tab while a thread is displayed).
void markChatConversationSeen(const std::string& cid, std::int64_t latestTs);
private:
// Provision the chat identity once the wallet seed is reachable+unlocked (per-tick, both
// variants); derives via deriveChatIdentityFromSecret and wipes the secret. No-op when the
// feature is off, already provisioned, in flight, or unavailable.
void maybeProvisionChatIdentity();
// Drop the in-memory chat identity + decrypted message store, lock the chat DB, and re-arm
// provisioning. MUST be called whenever the loaded wallet changes (switch / seed-migration adopt)
// so wallet A's private chat can't surface — or be signed with A's keys — under wallet B.
void resetChatSession();
// One-time nudge: on a full-node wallet that has a mnemonic, remind the user (once per
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
void maybeRemindSeedBackup();
// Seed-wallet migration (Phase 1: create a new mnemonic wallet in isolation, no funds moved).
void beginCreateSeedWallet(); // starts the isolated create on a background thread
void pumpSeedMigration(); // main thread: pick up background progress/result each frame
// Phase 2: sweep all legacy funds into the new wallet, then adopt it as the primary wallet.
void refreshSeedMigrationBalance(); // query the legacy total (shown on the Sweep step)
void beginSweepToSeedWallet(); // z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"] -> dest
void pollSweepStatus(); // Confirming step: poll sweep confirmations + legacy balance
void beginAdoptSeedWallet(); // stop daemon -> swap wallet.dat -> restart with -rescan
void provisionChatIdentityFromSecret(std::string secret);
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr — chat IDENTITY (reply-to)
// A spendable z-address that can actually PAY the fee (balance >= fee), preferring the identity
// reply address. The reply-to in the memo stays the identity address, so paying from a different
// funded note is transport-transparent. Empty if no z-address can cover the fee.
std::string chatPayFromZaddr(double fee) const;
std::string generateChatLocalId(const char* prefix, int numBytes) const; // unique echo id / cid
// Broadcast the memos and, when the async op resolves, flip the echo (echoLocalId) to Sent/Failed.
bool broadcastChatMemos(const chat::OutgoingChatMemos& memos, const std::string& echoLocalId); // true if submitted
bool broadcastChatMemosLite(const chat::OutgoingChatMemos& memos); // lite two-recipient send
void ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model); // lite chat receive harvest
// Full-node 0-conf fast path: re-scan just the chat reply address at minconf=0 every refresh cycle
// so incoming messages surface at mempool speed (before a block). Hidden conversations are skipped
// (they still un-hide via the normal confirmed harvest). Self-gated; no-op without a chat identity.
void fastScanChatMemos();
bool chat_fast_scan_in_flight_ = false; // guard against overlapping fast-scan RPCs
float chat_fast_scan_accum_ = 0.0f; // seconds since the last fast-scan (dedicated ~2.5s poll)
bool font_rebuild_requested_ = false; // set by requestFontRebuild(); consumed in preFrame()
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
bool lite_firstrun_dismissed_ = false;
// Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
bool lite_unlock_prompt_ = false;
// One-shot: prompt to unlock on startup once we learn the auto-opened wallet is encrypted+locked.
bool lite_startup_lock_checked_ = false;
std::unique_ptr<daemon::DaemonController> daemon_controller_;
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
// Auto-balance runtime state (pool mining, full-node only). The service fetches
// pool hashrates off-thread; the RNG drives the weighted-random pick.
util::PoolStatsService pool_stats_service_;
std::mt19937 balance_rng_;
long long last_balance_eval_ms_ = 0; // steady-clock ms of the last refresh kick
bool balance_refresh_pending_ = false; // UI asked for an immediate refresh
bool balance_snapshot_seen_ = false; // the current in-flight snapshot was applied
bool exchanges_fetch_started_ = false; // once-per-session CoinGecko tickers fetch
bool chart_fetch_in_flight_ = false; // a market_chart history fetch is on the worker
// Per-exchange candle chart (refreshExchangeChart): which pair the loaded series is for, an
// in-flight guard, and a slow refresh timer (candles move slowly, like the aggregate chart).
std::string exchange_chart_key_; // "<identifier>:<BASE>/<QUOTE>" of the loaded series ("" = none)
bool exchange_chart_fetch_in_flight_ = false;
std::chrono::steady_clock::time_point exchange_chart_last_fetch_{};
// Per-pair candle cache: switching back to a recently-viewed venue loads instantly (no re-fetch)
// instead of overwriting the single active buffer. Keyed like exchange_chart_key_.
struct ExchangeChartCache {
std::vector<std::pair<std::time_t, double>> closeIntraday, closeDaily;
std::vector<data::Candle> ohlcIntraday, ohlcDaily;
std::chrono::steady_clock::time_point fetchedAt{};
};
std::unordered_map<std::string, ExchangeChartCache> exchange_chart_cache_;
util::AsyncTaskManager async_tasks_;
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
@@ -870,41 +425,6 @@ private:
// Daemon restart (e.g. after changing debug log categories)
std::atomic<bool> daemon_restarting_{false};
// Wallet-switch failure recovery: the switch worker sets wallet_switch_failed_ when the new
// wallet's daemon won't start/stay up; the main loop then reverts active_wallet_file to
// wallet_switch_prev_file_ (settings writes stay on the main thread) so a broken wallet isn't
// persisted across restarts. daemon_restarting_ stays set until the revert re-arms the reconnect.
std::atomic<bool> wallet_switch_failed_{false};
// Distinguishes the failure reason for the revert message: true when the switch failed because the
// running node wouldn't release the RPC port in time (not because the target wallet is bad).
std::atomic<bool> switch_stop_failed_{false};
// True from a switch until the new wallet's daemon actually connects (onConnected). If the daemon
// instead crash-wedges (a wallet that fails LATE in init, past the fast start grace), the connect
// loop flags a revert so a broken wallet still can't stick.
std::atomic<bool> wallet_switch_pending_confirm_{false};
std::string wallet_switch_prev_file_;
// Confirm-before-stop for a switch that would stop an ADOPTED (externally-running) node. switchToWallet
// sets these and defers to renderSwitchStopDaemonDialog; confirming re-calls switchToWallet(w, true).
bool show_switch_stop_daemon_confirm_ = false;
std::string pending_switch_wallet_file_;
// Live progress for the switch modal: it stays open from confirm through stop → wait-for-exit → start →
// reconnect and auto-closes when the new node connects (onConnected). Phase is worker-updated;
// dialog_open_ gates rendering — both atomic since onConnected/the worker may run off the main thread.
// error_ is written only on the main thread (processWalletSwitchRevert) and read by the (main-thread) UI.
enum class WalletSwitchPhase : int { None = 0, Stopping, Starting, Reconnecting, Failed };
std::atomic<int> wallet_switch_phase_{0};
std::atomic<bool> wallet_switch_dialog_open_{false};
std::string wallet_switch_error_;
double wallet_switch_started_time_ = 0.0; // ImGui::GetTime() captured on first progress frame (elapsed)
// Set when a failed switch's node output indicates the target wallet is CORRUPT — the Failed modal then
// offers a one-click "-salvagewallet" repair, retrying the switch to wallet_switch_target_file_.
std::atomic<bool> switch_wallet_corrupt_{false};
std::string wallet_switch_target_file_; // the wallet we were switching TO (for a salvage retry)
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
std::atomic<int> pending_delete_result_{-1};
// Encryption state check timeout
float encryption_check_timer_ = 0.0f;
@@ -916,96 +436,20 @@ private:
bool show_import_key_ = false;
bool show_export_key_ = false;
bool show_backup_ = false;
bool show_seed_backup_ = false;
// Seed-phrase backup dialog state. seed_backup_phrase_ holds a SECRET (the revealed
// mnemonic) and is wiped with sodium_memzero when the dialog closes.
std::string seed_backup_phrase_;
std::string seed_backup_status_;
bool seed_backup_fetch_started_ = false;
bool seed_backup_loading_ = false;
bool seed_backup_no_mnemonic_ = false;
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
// Cached mnemonic status of the current wallet, driving the Migrate-to-seed button glow. Probed
// once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a
// capable daemon can migrate; Incapable = the daemon lacks z_exportmnemonic (can't tell / can't
// migrate). Reset to Unknown on wallet switch so it re-probes the new wallet.
enum class WalletSeedStatus { Unknown, HasMnemonic, NoMnemonic, Incapable };
WalletSeedStatus wallet_seed_status_ = WalletSeedStatus::Unknown;
bool wallet_seed_status_in_flight_ = false;
int wallet_seed_status_attempts_ = 0; // give up (Incapable) after a few transient probe failures
void probeWalletSeedStatus(); // one-shot per connect; classifies the wallet's mnemonic status
// --- Seed-wallet migration (Phase 1: create; Phase 2: sweep + adopt) ---
enum class SeedMigrationStep { Intro, Working, ShowSeed, Sweep, Sweeping, Confirming, Adopting, Done, Error };
bool show_seed_migration_ = false;
SeedMigrationStep seed_migration_step_ = SeedMigrationStep::Intro;
// Intro pre-flight: probe the current wallet before offering to create a seed wallet, so we can
// skip a pointless migration (AlreadyMnemonic → offer backup) or explain why it can't run
// (DaemonTooOld). Set from beginSeedMigrationPrecheck()'s async callback (main thread).
enum class SeedMigrationPrecheck { Pending, Legacy, AlreadyMnemonic, DaemonTooOld, CheckFailed };
SeedMigrationPrecheck seed_migration_precheck_ = SeedMigrationPrecheck::Pending;
bool seed_migration_precheck_started_ = false;
bool seed_migration_balance_loaded_ = false; // Sweep step: distinguishes "0 funds" from "not loaded yet"
bool seed_migration_nofunds_confirmed_ = false; // "replace my wallet" gate for the no-funds adopt
// "A newer node is bundled — update?" startup prompt (see maybeOfferDaemonUpdate).
bool show_daemon_update_prompt_ = false;
unsigned long long daemon_update_bundled_size_ = 0; // bundled daemon size the prompt offers
bool seed_migration_in_flight_ = false; // main-thread guard while the bg create task runs
std::string seed_migration_seed_; // SECRET — the revealed phrase, wiped on close
std::string seed_migration_dest_; // new shielded z-address (Phase 2 sweep target)
std::string seed_migration_temp_dir_; // temp datadir root holding the new wallet (kept)
bool seed_migration_backed_up_ = false; // "I've written it down" confirmation
std::string seed_migration_status_; // main-thread display (progress / error text)
double seed_migration_balance_ = 0.0; // legacy total to sweep (shown on the Sweep step)
std::string seed_migration_sweep_txid_; // the z_mergetoaddress sweep transaction id
int seed_migration_sweep_confs_ = 0; // confirmations of the sweep tx (adopt gate: >= 1)
double seed_migration_legacy_remaining_ = -1.0; // legacy balance after sweep (-1 = unknown)
float seed_migration_poll_timer_ = 0.0f; // throttles the Confirming-step poll
bool seed_migration_confirm_in_flight_ = false; // guards the confirm poll
// Cross-thread handoff from the background create task (guarded by seed_migration_mutex_).
std::mutex seed_migration_mutex_;
std::string seed_migration_progress_; // latest progress line
bool seed_migration_done_ = false; // create result ready to consume
bool seed_migration_ok_ = false;
std::string seed_migration_r_seed_, seed_migration_r_dest_, seed_migration_r_tmp_, seed_migration_r_err_;
// Cross-thread handoff from the background adopt task (guarded by seed_migration_mutex_).
bool seed_migration_adopt_done_ = false;
bool seed_migration_adopt_ok_ = false;
std::string seed_migration_adopt_err_;
bool show_address_book_ = false;
// Embedded daemon state
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
bool use_embedded_daemon_ = true;
std::string daemon_status_;
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
// Export/Import state
char export_result_[256] = {0}; // SECRET exported key — fixed buffer so it can be sodium_memzero'd
bool export_in_progress_ = false; // async key fetch running (spinner + disable Export)
bool export_error_ = false; // last export returned no key (locked wallet / failure)
std::string export_result_;
char import_key_input_[512] = {0};
std::string export_address_;
std::string import_status_;
bool import_success_ = false;
bool import_key_reveal_ = false; // show the key in plaintext (default masked)
bool import_in_progress_ = false; // an import + rescan is running (disable/spinner)
std::string import_result_address_; // address imported on success (shown as a copy field)
bool import_view_mode_ = false; // dialog mode: false = spending key, true = viewing key
char import_key_scan_height_[16] = {0}; // optional rescan start height (shielded spend / viewing-key imports)
// --- Sweep: import a spending key then move all its funds to one of your own addresses, instead
// of keeping the key in the wallet (spending-key / non-view mode only). ---
bool import_sweep_mode_ = false; // sweep instead of a plain import
int sweep_dest_mode_ = 0; // destination: 0 = fresh shielded address, 1 = an existing one
char sweep_dest_pick_[128] = {0}; // chosen existing destination (sweep_dest_mode_ == 1)
enum class SweepStep { Idle, Running, Done, Error };
SweepStep sweep_step_ = SweepStep::Idle;
std::string sweep_status_; // progress / error text
std::string sweep_txid_; // sweep transaction id (on success)
std::string sweep_dest_shown_; // the destination address the funds were swept to
std::string backup_status_;
bool backup_success_ = false;
@@ -1013,15 +457,6 @@ private:
std::string connection_status_ = "Disconnected";
bool connection_in_progress_ = false;
bool remote_rpc_plaintext_warning_shown_ = false;
// Startup daemon-launch diagnostics: bound the "RPC port busy, no config" wait before warning,
// and show the embedded-daemon start failure (binary/params/spawn) only once. Reset on connect.
int daemon_wait_attempts_ = 0;
bool daemon_start_error_shown_ = false;
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once
bool refresh_policy_syncing_ = false; // whether the sync-throttle refresh profile is active
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
std::uint64_t clipboard_secret_hash_ = 0;
double clipboard_clear_deadline_ = 0.0;
float loading_timer_ = 0.0f; // spinner animation for loading overlay
// Current page (sidebar navigation)
@@ -1029,55 +464,6 @@ private:
ui::NavPage prev_page_ = ui::NavPage::Overview;
float page_alpha_ = 1.0f; // 0→1 fade on page switch
bool sidebar_collapsed_ = false; // true = icon-only mode
// Debug screenshot sweep state.
bool screenshot_sweep_active_ = false;
bool sweep_capture_this_frame_ = false;
int sweep_skin_idx_ = 0;
int sweep_settle_frames_ = 0; // frames to let a new skin/surface settle before capture
std::vector<std::string> sweep_skins_; // skin ids to cycle
std::string sweep_dir_; // output folder for this sweep
std::string sweep_current_path_; // PNG path for the frame about to be captured
std::string sweep_saved_skin_; // restore on completion
ui::NavPage sweep_saved_page_ = ui::NavPage::Overview;
void updateScreenshotSweep(); // called at the top of render() while active
void applySweepTarget(); // apply current (skin,page/surface), path, arm settle
// --- Full UI sweep: the capture unit is a "surface" (a tab, optionally with a modal / step /
// state forced on top). A tab is a surface with a null setup. ---
struct SweepTarget {
std::string name; // fs-safe id, e.g. "modal-seed-backup" / "overview"
ui::NavPage page = ui::NavPage::Overview; // base tab under the surface
std::function<void(App&)> setup; // reveal the surface (null = plain tab)
std::function<void(App&)> teardown; // clear it (null = nothing to undo)
int settle = 4;// blur overlays override to 8
};
std::vector<SweepTarget> sweep_targets_;
int sweep_target_idx_ = 0;
bool sweep_full_ = false; // full-UI sweep (drives surfaces) vs the legacy tab-only sweep
// capture_mode_: set only during a full sweep. CONTRACT: while true, NO live op may fire — no
// RPC, no auto-lock, no async pump. New async paths that could run mid-sweep must guard on it.
bool capture_mode_ = false;
void startSweepImpl(bool full);
void buildSweepCatalog();
void installDemoWalletData();
void clearDemoWalletData();
void applyHealthyDemoState(); // reset the connection/encryption flags to the healthy demo values
void writeSweepManifest() const;
// Snapshot of the state_ fields the demo installer mutates, restored at sweep end. Kept as a
// plain struct because WalletState has reference-alias members and isn't copy-assignable.
struct SweepStateSnapshot {
bool valid = false;
bool connected=false, warming_up=false, daemon_initializing=false;
bool encrypted=false, locked=false, encryption_state_known=false;
std::string warmup_status, warmup_description;
SyncInfo sync;
double privateBalance=0, transparentBalance=0, totalBalance=0, unconfirmedBalance=0;
std::vector<AddressInfo> addresses, z_addresses, t_addresses;
std::vector<TransactionInfo> transactions;
double market_price_usd=0;
double market_change_24h=0;
} sweep_state_snapshot_;
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
@@ -1091,9 +477,6 @@ private:
int logo_h_ = 0;
bool logo_loaded_ = false;
bool logo_is_dark_variant_ = true; // tracks which variant is currently loaded
ImU32 logo_accent_ = 0; // theme accent the SVG logo was last rasterized with (re-render on change)
ImTextureID drgx_emoji_tex_ = 0; // ":drgx:" custom chat emoji (themed to the accent, like the logo)
int drgx_emoji_w_ = 0, drgx_emoji_h_ = 0;
// Coin logo texture (DragonX currency icon, separate from wallet branding)
ImTextureID coin_logo_tex_ = 0;
@@ -1101,9 +484,8 @@ private:
int coin_logo_h_ = 0;
bool coin_logo_loaded_ = false;
// Console tab + its backend executor (full-node RPC or lite backend), created lazily.
// Console tab
ui::ConsoleTab console_tab_;
std::unique_ptr<ui::ConsoleCommandExecutor> console_exec_;
// Pending payment from URI
bool pending_payment_valid_ = false;
@@ -1119,10 +501,6 @@ private:
// Mining toggle guard (prevents concurrent setgenerate calls)
std::atomic<bool> mining_toggle_in_progress_{false};
// True from a successful startPoolMining() until the miner is confirmed connected/hashing in the
// poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay).
std::atomic<bool> pool_starting_{false};
// Auto-shield guard (prevents concurrent auto-shield operations)
std::atomic<bool> auto_shield_pending_{false};
@@ -1131,10 +509,6 @@ private:
static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh
std::size_t shielded_history_scan_cursor_ = 0;
bool shielded_history_scan_pending_ = false;
// False until the first full shielded-history scan finishes. Drives the History tab's
// "Loading older history…" progress so the user knows transactions are still streaming in
// after the first batch appears; goes quiet for the routine per-block re-scans afterward.
bool initial_history_scan_complete_ = false;
std::unordered_map<std::string, int> shielded_history_scan_heights_;
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for
@@ -1155,33 +529,7 @@ private:
bool transactions_dirty_ = false; // true → force tx refresh regardless of block height
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
bool rescan_status_poll_in_progress_ = false;
// True once we've actually observed the rescan running (daemon restarted into -rescan warmup).
// Gates the "rescan complete" detection so a getrescaninfo poll that hits the still-running
// pre-restart daemon (which reports rescanning=false) can't fire a false "complete" instantly.
bool rescan_confirmed_active_ = false;
// A runtime rescanblockchain RPC is in flight (vs the -rescan daemon restart). While set,
// the per-second mining/rescan-status pollers are suppressed (the daemon holds cs_main for
// the whole scan and would block them); completion is signalled by the rescan RPC callback.
bool runtime_rescan_active_ = false;
// Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan
// that reconciles the preserved wallet.dat against the freshly-imported chain.
bool post_bootstrap_rescan_pending_ = false;
// Largest "blocks remaining" seen during the current witness-rebuild phase. The daemon's
// "Building Witnesses for block" fraction resets every call (it's re-invoked per connected
// block, each walking from its own start height to the tip), so we derive a stable, monotonic
// overall percentage from how far "remaining" has fallen below this peak. Reset per phase.
int witness_rebuild_total_blocks_ = 0;
// The daemon's primary witness signal is "Setting Initial Sapling Witness for tx <hash>, <i>
// of <N>", logged once per wallet tx as its initial witness is set. The <i> is the tx's slot in
// an UNORDERED map, so it bounces wildly (was the cause of the resetting progress). The honest
// monotonic metric is how many DISTINCT txs have been witnessed (the set only grows; it also
// dedups the daemon's occasional double-prints) over the reported total N.
std::unordered_set<std::string> witness_seen_txids_;
int witness_total_txs_ = 0;
bool opid_poll_in_progress_ = false;
// Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
// connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.
int consecutive_core_failures_ = 0;
// Pending z_sendmany operation tracking
bool send_progress_active_ = false;
@@ -1192,17 +540,9 @@ private:
std::string to;
std::string memo;
double amount = 0.0;
double fee = 0.0;
std::int64_t timestamp = 0;
};
std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
// Opids issued as a fee-gap auto-retry (see maybeRetrySendForFeeGap). Tracked so a retry that
// fails again is reported to the user instead of looping.
std::unordered_set<std::string> send_feegap_retried_opids_;
// z_sendmany UI callbacks held until the opid reaches a terminal status, so the
// user isn't told "sent successfully" before the tx is actually built/broadcast.
std::unordered_map<std::string, std::function<void(bool, const std::string&)>>
pending_send_callbacks_;
// Txids from completed z_sendmany operations.
// Ensures shielded sends are discoverable by z_viewtransaction
// even when they don't appear in listtransactions or
@@ -1227,8 +567,6 @@ private:
// PIN vault
std::unique_ptr<util::SecureVault> vault_;
data::TransactionHistoryCache transaction_history_cache_;
data::AddressBook address_book_; // shared contact store; loaded once in init(), self-saves on mutation
data::WalletIndex wallet_index_; // per-wallet metadata cache (wallets.json); populated after each load
std::string pending_transaction_history_cache_passphrase_;
bool transaction_history_cache_loaded_ = false;
@@ -1288,39 +626,22 @@ private:
// Private methods - rendering
void renderStatusBar();
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
void renderAboutDialog();
void renderImportKeyDialog();
void renderExportKeyDialog();
void renderBackupDialog();
void renderSeedBackupDialog(); // full-node "Back up seed phrase" modal (z_exportmnemonic)
void renderSeedMigrationDialog(); // "Migrate to a seed wallet" guided modal (Phase 1: create)
void beginSeedMigrationPrecheck(); // Intro: probe whether the wallet is legacy / already-seeded
void maybeOfferDaemonUpdate(); // at startup, flag the prompt if a newer daemon is bundled
void renderDaemonUpdatePrompt(); // "a newer node is bundled — update the installed daemon?"
void renderFirstRunWizard();
void renderLockScreen();
void renderEncryptWalletDialog();
void renderDecryptWalletDialog();
void renderPinDialogs();
void renderAntivirusHelpDialog();
void renderSwitchStopDaemonDialog(); // confirm before stopping an adopted node to switch wallets
void processDeferredEncryption();
// Private methods - connection
void tryConnect();
void onConnected();
void onDisconnected(const std::string& reason);
// Set the "node is initializing" UI state (status line + overlay description) from the
// embedded/external daemon's launch state and its own console output (current phase + block
// height), so a connect probe that times out while the daemon loads shows WHAT it's doing.
// `reachableButBusy` is true when the probe connected but got no RPC reply (a timeout),
// false when the daemon is merely launching (not bound yet). Returns the status title.
std::string applyDaemonInitStatus(bool reachableButBusy);
// Tear down a connection that died mid-session (daemon crash / restart / dropped
// socket) so update()'s reconnect branch re-enters tryConnect(). Unlike onDisconnected
// alone, this also rpc_->disconnect()s so rpc_->isConnected() actually flips to false.
void handleLostConnection(const std::string& reason);
void applyDefaultBanlist();
// Private methods - data refresh

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,810 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Screenshot sweep: capture the UI under every skin. Two modes share one state machine:
// - startScreenshotSweep() — the legacy tab-only sweep (<config>/screenshots).
// - startFullUiSweep() — ALSO drives every modal / dialog / multi-step flow / state overlay
// into view (offline, with injected demo data, firing no live ops) and captures each under
// every skin (<config>/screenshots-full/<surface>/<skin>.png + index.md).
// The capture unit is a "surface" (SweepTarget): a base tab, optionally with a setup() that forces
// a modal/step/state on top and a teardown() that clears it. main.cpp reads the framebuffer on the
// settled frame and calls onScreenshotCaptured() to advance.
#include "app.h"
#include "config/settings.h"
#include "data/address_book.h"
#include "ui/schema/skin_manager.h"
#include "ui/notifications.h"
#include "ui/sidebar.h"
#include "ui/windows/send_tab.h"
#include "ui/windows/wallets_dialog.h"
#include "ui/windows/export_transactions_dialog.h"
#include "ui/windows/export_all_keys_dialog.h"
#include "ui/windows/bootstrap_download_dialog.h"
#include "ui/windows/daemon_download_dialog.h"
#include "ui/windows/xmrig_download_dialog.h"
#include "ui/windows/qr_popup_dialog.h"
#include "ui/windows/request_payment_dialog.h"
#include "ui/windows/validate_address_dialog.h"
#include "ui/windows/address_label_dialog.h"
#include "ui/windows/shield_dialog.h"
#include "ui/windows/address_transfer_dialog.h"
#include "ui/windows/key_export_dialog.h"
#include "ui/windows/contacts_tab.h"
#include "ui/pages/settings_page.h"
#include "util/platform.h"
#include "wallet/wallet_capabilities.h"
#include "imgui.h"
#include "imgui_internal.h" // ClosePopupToLevel / OpenPopupStack — flush stuck popups after teardown
#include <sodium.h>
#include <cmath>
#include <filesystem>
#include <fstream>
namespace dragonx {
namespace {
namespace fs = std::filesystem;
// The real portfolio, snapshotted while demo groups are shown during a full sweep. Settings are
// forward-declared in app.h, so this can't live in the app.h SweepStateSnapshot struct.
std::vector<config::Settings::PortfolioEntry> g_pfSnapshot;
int g_pfStyleSnapshot = 0;
bool g_pfSnapshotValid = false;
std::vector<double> g_marketHistorySnapshot; // real price history, restored at sweep end
// Filesystem-safe one-word tab name.
const char* sweepPageName(ui::NavPage page)
{
switch (page) {
case ui::NavPage::Overview: return "overview";
case ui::NavPage::Send: return "send";
case ui::NavPage::Receive: return "receive";
case ui::NavPage::History: return "history";
case ui::NavPage::Contacts: return "contacts";
case ui::NavPage::Chat: return "chat";
case ui::NavPage::Mining: return "mining";
case ui::NavPage::Market: return "market";
case ui::NavPage::Console: return "console";
case ui::NavPage::LiteConsole: return "console";
case ui::NavPage::Peers: return "network";
case ui::NavPage::LiteNetwork: return "network";
case ui::NavPage::Explorer: return "explorer";
case ui::NavPage::Settings: return "settings";
default: return "page";
}
}
bool sweepPageEnabled(ui::NavPage page)
{
return wallet::isUiSurfaceAvailable(wallet::currentWalletCapabilities(), ui::NavPageSurface(page));
}
// Deterministic demo secrets/addresses (NON-secret — safe to hold in memory + display).
const char* kDemoMnemonic =
"select milk exit banana type alcohol comic moral drama federal just green "
"elevator render stumble lesson convince organ category caution panther misery pelican immune";
const char* kDemoZAddr =
"zs1demoseedwalletreceiveaddressforuisweepcaptures00000000000000000000000000";
const char* kDemoTxid = "b647077c471fdd2877d35c9d8b70e3c785547fae618458b4068d913ee3d1dc4f";
// Contacts-view sweep: seed a few demo contacts (Z + T types, some global) so the Cards/List/Table
// modes render with data, and restore the real book afterward. Uses sweepSetEntries (no disk write),
// so the user's persisted address book is never touched even if the sweep is interrupted.
std::vector<data::AddressBookEntry> s_contactsSweepBackup;
void seedSweepContacts(App& a)
{
s_contactsSweepBackup = a.addressBook().entries();
data::AddressBookEntry e1("drgx pool payout address", kDemoZAddr, "mining pool payouts"); // Z, global
e1.avatar = "icon:account_balance"; // icon avatar (verifies the icon-badge render path)
data::AddressBookEntry e2("exchange deposit", "t1DemoTransparentAddressForUiSweep00000", ""); // T
// Scope to the active wallet so it's visible (the tab hides contacts not in the active wallet);
// non-empty hash also keeps it non-global -> no globe badge, so the list shows a global/non-global mix.
e2.scope = a.activeWalletIdentityHash();
data::AddressBookEntry e3("cold savings",
"zs1sweepdemocoldsavingsaddressforuicapture0000000000000000000000000000", "long-term storage"); // Z, global
a.addressBook().sweepSetEntries({ e1, e2, e3 });
}
void restoreSweepContacts(App& a)
{
a.addressBook().sweepSetEntries(s_contactsSweepBackup);
s_contactsSweepBackup.clear();
if (a.settings()) a.settings()->setContactsViewMode(0);
}
} // namespace
std::string App::screenshotDir() const
{
return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots").string();
}
std::string App::screenshotFullDir() const
{
return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots-full").string();
}
// ── Demo data (full sweep only): make every data-dependent screen render offline ─────────────
void App::installDemoWalletData()
{
// Snapshot the state_ fields we mutate (WalletState isn't copy-assignable — reference aliases).
auto& s = sweep_state_snapshot_;
s.connected = state_.connected; s.warming_up = state_.warming_up;
s.daemon_initializing = state_.daemon_initializing;
s.encrypted = state_.encrypted; s.locked = state_.locked;
s.encryption_state_known = state_.encryption_state_known;
s.warmup_status = state_.warmup_status; s.warmup_description = state_.warmup_description;
s.sync = state_.sync;
s.privateBalance = state_.privateBalance; s.transparentBalance = state_.transparentBalance;
s.totalBalance = state_.totalBalance; s.unconfirmedBalance = state_.unconfirmedBalance;
s.addresses = state_.addresses; s.z_addresses = state_.z_addresses; s.t_addresses = state_.t_addresses;
s.transactions = state_.transactions;
s.market_price_usd = state_.market.price_usd;
s.market_change_24h = state_.market.change_24h;
s.valid = true;
applyHealthyDemoState();
state_.sync.blocks = state_.sync.headers = 3124322;
state_.sync.verification_progress = 1.0; state_.sync.syncing = false;
// Legacy (pre-seed) wallet so the Settings "Migrate to seed" button glows in the sweep.
wallet_seed_status_ = WalletSeedStatus::NoMnemonic;
state_.privateBalance = 12.50000000; state_.transparentBalance = 3.25000000;
state_.totalBalance = 15.75000000; state_.unconfirmedBalance = 0.50000000;
state_.market.price_usd = 0.01336200;
state_.market.change_24h = 5.24000000;
// Wavy, gently-rising price history so the row sparklines render (restored at sweep end).
g_marketHistorySnapshot = state_.market.price_history;
{
std::vector<double> hist;
for (int i = 0; i < 48; i++) {
double t = static_cast<double>(i);
hist.push_back(0.01250 + 0.0000180 * t
+ 0.00060 * std::sin(t * 0.55) + 0.00025 * std::sin(t * 1.7));
}
state_.market.price_history = hist;
}
auto zaddr = [](const char* a, double bal, const char* label) {
AddressInfo i; i.address = a; i.balance = bal; i.type = "shielded"; i.label = label; return i;
};
auto taddr = [](const char* a, double bal, const char* label) {
AddressInfo i; i.address = a; i.balance = bal; i.type = "transparent"; i.label = label; return i;
};
state_.z_addresses = {
zaddr("zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", 12.0, "Savings"),
zaddr("zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", 0.5, ""),
};
state_.t_addresses = {
taddr("t1DemoTransparentAddressForUiSweep00000", 3.25, "Mining payouts"),
taddr("t1DemoSecondTransparentAddressForUiSw00", 0.0, ""),
};
state_.rebuildAddressList();
auto tx = [](const char* id, const char* type, double amt, int64_t ts, int conf,
const char* addr, const char* memo) {
TransactionInfo t; t.txid = id; t.type = type; t.amount = amt; t.timestamp = ts;
t.confirmations = conf; t.address = addr; t.memo = memo; return t;
};
state_.transactions = {
tx(kDemoTxid, "receive", 15.75000000, 1751286000, 42,
"zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", "welcome to DragonX"),
tx("a1b2c3d4e5f600000000000000000000000000000000000000000000000000000000", "send", 2.50000000,
1751200000, 120, "zs1demopeerreceivingaddressabcdef0000000000000000000000000000000", ""),
tx("c0ffee00000000000000000000000000000000000000000000000000000000000000", "mined", 0.30000000,
1751100000, 300, "t1DemoTransparentAddressForUiSweep00000", ""),
tx("deadbeef000000000000000000000000000000000000000000000000000000000000", "receive", 0.50000000,
1751290000, 0, "zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", "pending"),
};
// Demo portfolio groups so the Market tab's row styles render with real-looking data. Snapshot
// the user's real portfolio first; setPortfolio* only mutate memory (save() is never called
// here), and clearDemoWalletData() restores them at sweep end.
if (settings_) {
g_pfSnapshot = settings_->getPortfolioEntries();
g_pfStyleSnapshot = settings_->getPortfolioStyle();
g_pfSnapshotValid = true;
auto grp = [](const char* label, const char* icon, uint32_t color, const char* addr,
bool drgx, bool value, bool ch, bool spark) {
config::Settings::PortfolioEntry e;
e.label = label; e.icon = icon; e.color = color; e.outlineOpacity = 30;
e.addresses = { addr }; e.priceBasis = 0;
e.showDrgx = drgx; e.showValue = value; e.show24h = ch; e.showSparkline = spark;
return e;
};
settings_->setPortfolioEntries({
grp("Savings", "savings", 0xFFFF9D4Fu,
"zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", true, true, true, true),
grp("Mining rewards", "pickaxe", 0xFF3DB0FFu,
"t1DemoTransparentAddressForUiSweep00000", true, true, true, true),
grp("Cold storage", "diamond", 0xFFFF7BB0u,
"zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", true, true, false, false),
});
settings_->setPortfolioStyle(0);
}
seedChatDemoData();
}
void App::applyHealthyDemoState()
{
state_.connected = true;
state_.warming_up = false;
state_.daemon_initializing = false;
state_.encryption_state_known = true;
state_.encrypted = false;
state_.locked = false;
state_.warmup_status.clear();
state_.warmup_description.clear();
// Dismiss any first-run wizard so the base tabs/modals aren't occluded (the wizard targets set
// wizard_phase_ themselves and restore None on teardown).
wizard_phase_ = WizardPhase::None;
}
void App::clearDemoWalletData()
{
auto& s = sweep_state_snapshot_;
if (!s.valid) return;
wallet_seed_status_ = WalletSeedStatus::Unknown; // re-probe on the next real connect
state_.connected = s.connected; state_.warming_up = s.warming_up;
state_.daemon_initializing = s.daemon_initializing;
state_.encrypted = s.encrypted; state_.locked = s.locked;
state_.encryption_state_known = s.encryption_state_known;
state_.warmup_status = s.warmup_status; state_.warmup_description = s.warmup_description;
state_.sync = s.sync;
state_.privateBalance = s.privateBalance; state_.transparentBalance = s.transparentBalance;
state_.totalBalance = s.totalBalance; state_.unconfirmedBalance = s.unconfirmedBalance;
state_.addresses = s.addresses; state_.z_addresses = s.z_addresses; state_.t_addresses = s.t_addresses;
state_.transactions = s.transactions;
state_.market.price_usd = s.market_price_usd;
state_.market.change_24h = s.market_change_24h;
state_.market.price_history = g_marketHistorySnapshot;
g_marketHistorySnapshot.clear();
s = SweepStateSnapshot{}; // invalidate
// Restore the user's real portfolio (demo groups were in-memory only).
if (g_pfSnapshotValid && settings_) {
settings_->setPortfolioEntries(g_pfSnapshot);
settings_->setPortfolioStyle(g_pfStyleSnapshot);
}
g_pfSnapshot.clear();
g_pfSnapshotValid = false;
}
// ── Catalog ──────────────────────────────────────────────────────────────────────────────────
// Lambdas defined in this member function may touch App's private members through the App& arg.
void App::buildSweepCatalog()
{
sweep_targets_.clear();
// Tabs (both sweeps).
for (int p = 0; p < static_cast<int>(ui::NavPage::Count_); ++p) {
ui::NavPage pg = static_cast<ui::NavPage>(p);
if (sweepPageEnabled(pg)) sweep_targets_.push_back({ sweepPageName(pg), pg, nullptr, nullptr, 4 });
}
if (!sweep_full_) return;
// Full sweep: modals / flows / states. Blur overlays need more settle frames.
const int kOverlaySettle = 8;
auto add = [&](const char* name, ui::NavPage pg, std::function<void(App&)> setup,
std::function<void(App&)> teardown) {
sweep_targets_.push_back({ name, pg, std::move(setup), std::move(teardown), kOverlaySettle });
};
// Simple bool-flag modals.
add("modal-import-key", ui::NavPage::Overview,
[](App& a) { a.import_view_mode_ = false; a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; });
add("modal-import-viewkey", ui::NavPage::Overview,
[](App& a) { a.import_view_mode_ = true; a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; a.import_view_mode_ = false; });
add("modal-export-key", ui::NavPage::Overview,
[](App& a) { a.show_export_key_ = true; }, [](App& a) { a.show_export_key_ = false; });
add("modal-export-transactions", ui::NavPage::Settings,
[](App&) { ui::ExportTransactionsDialog::show(); }, [](App&) { ui::ExportTransactionsDialog::hide(); });
add("modal-export-all-keys", ui::NavPage::Settings,
[](App&) { ui::ExportAllKeysDialog::show(); }, [](App&) { ui::ExportAllKeysDialog::hide(); });
add("modal-bootstrap", ui::NavPage::Settings,
[](App& a) { ui::BootstrapDownloadDialog::show(&a); }, [](App&) { ui::BootstrapDownloadDialog::hide(); });
add("modal-backup", ui::NavPage::Overview,
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
// Encrypt-wallet dialog — the redesigned passphrase-entry phase (never fires the async encrypt).
add("modal-encrypt", ui::NavPage::Settings,
[](App& a) { a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry; a.show_encrypt_dialog_ = true; },
[](App& a) {
a.show_encrypt_dialog_ = false; a.encrypt_dialog_phase_ = EncryptDialogPhase::PassphraseEntry;
a.encrypt_status_.clear();
memset(a.encrypt_pass_buf_, 0, sizeof(a.encrypt_pass_buf_));
memset(a.encrypt_confirm_buf_, 0, sizeof(a.encrypt_confirm_buf_));
});
add("modal-change-passphrase", ui::NavPage::Settings,
[](App& a) { a.show_change_passphrase_ = true; },
[](App& a) {
a.show_change_passphrase_ = false; a.encrypt_status_.clear();
memset(a.change_old_pass_buf_, 0, sizeof(a.change_old_pass_buf_));
memset(a.change_new_pass_buf_, 0, sizeof(a.change_new_pass_buf_));
memset(a.change_confirm_buf_, 0, sizeof(a.change_confirm_buf_));
});
// Remove-encryption dialog — the redesigned passphrase-entry phase (reset() keeps the
// workflow in PassphraseEntry; nothing fires the async unlock/export/restart pyramid).
add("modal-decrypt", ui::NavPage::Settings,
[](App& a) { a.wallet_security_workflow_.reset(); a.show_decrypt_dialog_ = true; },
[](App& a) {
a.show_decrypt_dialog_ = false; a.wallet_security_workflow_.reset();
memset(a.decrypt_pass_buf_, 0, sizeof(a.decrypt_pass_buf_));
});
// PIN setup / change / remove dialogs (never fire the async vault store/verify).
add("modal-pin-setup", ui::NavPage::Settings,
[](App& a) { a.show_pin_setup_ = true; },
[](App& a) {
a.show_pin_setup_ = false; a.pin_status_.clear();
memset(a.pin_passphrase_buf_, 0, sizeof(a.pin_passphrase_buf_));
memset(a.pin_buf_, 0, sizeof(a.pin_buf_));
memset(a.pin_confirm_buf_, 0, sizeof(a.pin_confirm_buf_));
});
add("modal-pin-change", ui::NavPage::Settings,
[](App& a) { a.show_pin_change_ = true; },
[](App& a) {
a.show_pin_change_ = false; a.pin_status_.clear();
memset(a.pin_old_buf_, 0, sizeof(a.pin_old_buf_));
memset(a.pin_buf_, 0, sizeof(a.pin_buf_));
memset(a.pin_confirm_buf_, 0, sizeof(a.pin_confirm_buf_));
});
add("modal-pin-remove", ui::NavPage::Settings,
[](App& a) { a.show_pin_remove_ = true; },
[](App& a) {
a.show_pin_remove_ = false; a.pin_status_.clear();
memset(a.pin_old_buf_, 0, sizeof(a.pin_old_buf_));
});
// Wave-1 standalone dialogs (rendered globally from App::render, so any page works).
add("modal-qr-popup", ui::NavPage::Receive,
[](App&) { ui::QRPopupDialog::show(kDemoZAddr, "Savings"); },
[](App&) { ui::QRPopupDialog::close(); });
add("modal-request-payment", ui::NavPage::Receive,
[](App&) { ui::RequestPaymentDialog::show(kDemoZAddr); },
[](App&) { ui::RequestPaymentDialog::hide(); });
add("modal-validate-address", ui::NavPage::Receive,
[](App&) { ui::ValidateAddressDialog::show(); },
[](App&) { ui::ValidateAddressDialog::hide(); });
add("modal-address-label", ui::NavPage::Overview,
[](App& a) { ui::AddressLabelDialog::show(&a, kDemoZAddr, true); },
[](App&) { ui::AddressLabelDialog::hide(); });
// (No modal-daemon-prompt surface: renderDaemonUpdatePrompt is gated behind !capture_mode_,
// so it can't render during a sweep. Its migration is verified by build + the shared overlay pattern.)
add("modal-antivirus", ui::NavPage::Mining,
[](App& a) { a.pending_antivirus_dialog_ = true; },
[](App& a) { a.pending_antivirus_dialog_ = false; });
add("modal-switch-stopnode", ui::NavPage::Settings,
[](App& a) { a.pending_switch_wallet_file_ = "wallet-savings.dat"; a.show_switch_stop_daemon_confirm_ = true; },
[](App& a) { a.show_switch_stop_daemon_confirm_ = false; a.pending_switch_wallet_file_.clear(); });
add("modal-switch-progress", ui::NavPage::Settings,
[](App& a) { a.wallet_switch_phase_.store(static_cast<int>(App::WalletSwitchPhase::Stopping));
a.wallet_switch_dialog_open_.store(true); },
[](App& a) { a.wallet_switch_dialog_open_.store(false);
a.wallet_switch_phase_.store(static_cast<int>(App::WalletSwitchPhase::None)); });
// Wave-2 fund/secret dialogs (setup never fires the async RPC — no button is clicked).
add("modal-shield", ui::NavPage::Send,
[](App&) { ui::ShieldDialog::showShieldCoinbase(); },
[](App&) { ui::ShieldDialog::hide(); });
add("modal-merge", ui::NavPage::Send,
[](App&) { ui::ShieldDialog::showMerge(); },
[](App&) { ui::ShieldDialog::hide(); });
// z->t transfer so the (converted) deshielding DialogWarningHeader renders.
add("modal-transfer", ui::NavPage::Overview,
[](App& a) {
ui::AddressTransferInfo info;
info.fromAddr = kDemoZAddr;
info.toAddr = "t1DemoTransparentReceiveAddress00000";
info.fromBalance = 12.5; info.toBalance = 3.0;
info.fromIsZ = true; info.toIsZ = false;
ui::AddressTransferDialog::show(&a, info);
},
[](App&) { ui::AddressTransferDialog::close(); });
// Distinct from the settings "modal-export-key" (App::renderExportKeyDialog) — this is the
// per-address KeyExportDialog opened from Overview address rows.
add("modal-key-export", ui::NavPage::Overview,
[](App&) { ui::KeyExportDialog::show(kDemoZAddr, ui::KeyExportDialog::KeyType::Private); },
[](App&) { ui::KeyExportDialog::hide(); });
// Contacts address-list view modes, each seeded with demo contacts (restored after; no disk write).
add("contacts-cards", ui::NavPage::Contacts,
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(0); },
[](App& a) { restoreSweepContacts(a); });
add("contacts-list", ui::NavPage::Contacts,
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1); },
[](App& a) { restoreSweepContacts(a); });
add("contacts-table", ui::NavPage::Contacts,
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(2); },
[](App& a) { restoreSweepContacts(a); });
// Revamped edit dialog: live preview + avatar picker, one surface per avatar mode.
add("contacts-edit-icon", ui::NavPage::Contacts,
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1);
ui::ContactsSweepOpenEditDialog(1); },
[](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
add("contacts-edit-badge", ui::NavPage::Contacts,
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1);
ui::ContactsSweepOpenEditDialog(0); },
[](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
add("contacts-edit-image", ui::NavPage::Contacts,
[](App& a) { seedSweepContacts(a); if (a.settings()) a.settings()->setContactsViewMode(1);
ui::ContactsSweepOpenEditDialog(2); },
[](App& a) { ui::ContactsSweepCloseDialog(); restoreSweepContacts(a); });
add("modal-about", ui::NavPage::Overview,
[](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; });
add("modal-settings", ui::NavPage::Settings,
[](App& a) { a.show_settings_ = true; }, [](App& a) { a.show_settings_ = false; });
// Seed-backup: pre-set fetch + a demo phrase so it renders the word grid without any RPC.
add("modal-seed-backup", ui::NavPage::Overview,
[](App& a) {
a.show_seed_backup_ = true; a.seed_backup_fetch_started_ = true;
a.seed_backup_loading_ = false; a.seed_backup_no_mnemonic_ = false;
a.seed_backup_status_.clear();
if (a.seed_backup_phrase_.empty()) a.seed_backup_phrase_ = kDemoMnemonic;
},
[](App& a) {
a.show_seed_backup_ = false; a.seed_backup_fetch_started_ = false;
if (!a.seed_backup_phrase_.empty())
sodium_memzero(&a.seed_backup_phrase_[0], a.seed_backup_phrase_.size());
a.seed_backup_phrase_.clear();
});
// Migrate-to-seed — one surface per step (full-node only). Setting the step directly never
// fires the async create/sweep/adopt (those are button-triggered).
if (supportsFullNodeLifecycleActions()) {
auto mig = [&](const char* name, SeedMigrationStep step, std::function<void(App&)> extra) {
add(name, ui::NavPage::Settings,
[step, extra](App& a) {
a.show_seed_migration_ = true; a.seed_migration_step_ = step;
a.seed_migration_dest_ = kDemoZAddr;
if (extra) extra(a);
},
[](App& a) {
a.show_seed_migration_ = false;
if (!a.seed_migration_seed_.empty())
sodium_memzero(&a.seed_migration_seed_[0], a.seed_migration_seed_.size());
a.seed_migration_seed_.clear(); a.seed_migration_status_.clear();
});
};
// Intro pre-flight branches: pre-set the probe result so the sweep captures each variant
// (the probe itself is guarded off during capture_mode_).
mig("modal-migrate-intro", SeedMigrationStep::Intro, [](App& a) {
a.seed_migration_precheck_ = SeedMigrationPrecheck::Legacy;
a.seed_migration_precheck_started_ = true;
});
mig("modal-migrate-intro-seeded", SeedMigrationStep::Intro, [](App& a) {
a.seed_migration_precheck_ = SeedMigrationPrecheck::AlreadyMnemonic;
a.seed_migration_precheck_started_ = true;
});
mig("modal-migrate-intro-oldnode", SeedMigrationStep::Intro, [](App& a) {
a.seed_migration_precheck_ = SeedMigrationPrecheck::DaemonTooOld;
a.seed_migration_precheck_started_ = true;
});
mig("modal-migrate-showseed", SeedMigrationStep::ShowSeed,
[](App& a) { a.seed_migration_seed_ = kDemoMnemonic; a.seed_migration_backed_up_ = false; });
mig("modal-migrate-sweep", SeedMigrationStep::Sweep,
[](App& a) { a.seed_migration_balance_ = 15.75000000; a.seed_migration_balance_loaded_ = true; });
mig("modal-migrate-nofunds", SeedMigrationStep::Sweep,
[](App& a) { a.seed_migration_balance_ = 0.0; a.seed_migration_balance_loaded_ = true; });
mig("modal-migrate-confirming", SeedMigrationStep::Confirming, [](App& a) {
a.seed_migration_sweep_confs_ = 1; a.seed_migration_sweep_txid_ = kDemoTxid;
a.seed_migration_legacy_remaining_ = 0.0;
});
mig("modal-migrate-done", SeedMigrationStep::Done, nullptr);
mig("modal-migrate-error", SeedMigrationStep::Error,
[](App& a) { a.seed_migration_status_ = "Example error: the daemon did not respond."; });
// Multi-wallet: the wallet-files list. Drop a couple of demo wallet files in the (throwaway)
// datadir + cache their metadata so the list renders populated.
add("modal-wallets", ui::NavPage::Settings,
[](App& a) {
std::error_code ec;
const std::string dd = util::Platform::getDragonXDataDir();
fs::create_directories(dd, ec);
if (!fs::exists(dd + "/wallet.dat", ec))
std::ofstream(dd + "/wallet.dat", std::ios::binary) << std::string(122880, '\0');
if (!fs::exists(dd + "/wallet-savings.dat", ec))
std::ofstream(dd + "/wallet-savings.dat", std::ios::binary) << std::string(65536, '\0');
data::WalletIndexEntry e1; e1.fileName = "wallet.dat"; e1.displayName = "wallet.dat";
e1.cachedBalance = 15.7526; e1.cachedAddressCount = 4;
e1.lastOpenedEpoch = 1720000000; e1.syncedHere = true;
data::WalletIndexEntry e2; e2.fileName = "wallet-savings.dat"; e2.displayName = "wallet-savings.dat";
e2.cachedBalance = 250.0; e2.cachedAddressCount = 2;
e2.lastOpenedEpoch = 1719400000; e2.syncedHere = true;
a.walletIndex().upsert(e1);
a.walletIndex().upsert(e2);
// An out-of-datadir wallet (in an extra folder) so the Import action shows too.
const std::string extra = util::Platform::getConfigDir() + "extra-wallets";
fs::create_directories(extra, ec);
if (!fs::exists(extra + "/my-wallet.dat", ec))
std::ofstream(extra + "/my-wallet.dat", std::ios::binary) << std::string(80000, '\0');
a.walletIndex().addExtraFolder(extra);
if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
ui::WalletsDialog::show(&a);
},
[](App&) { ui::WalletsDialog::hide(); });
// Many wallets — exercises the viewport-cap path: the card can't fit all rows, so the list
// must scroll internally while the create/scan/footer controls stay pinned (esp. at 150%).
// Teardown removes the extra files it dropped so the plain modal-wallets surface stays a
// clean 3-wallet reference (both surfaces share the one throwaway datadir).
static const char* kManyExtra[] = {"wallet-cold.dat", "wallet-trading.dat", "wallet-mining.dat",
"wallet-donations.dat", "wallet-2023.dat", "wallet-payroll.dat"};
add("modal-wallets-many", ui::NavPage::Settings,
[](App& a) {
std::error_code ec;
const std::string dd = util::Platform::getDragonXDataDir();
fs::create_directories(dd, ec);
const char* names[] = {"wallet.dat", "wallet-savings.dat", "wallet-cold.dat",
"wallet-trading.dat", "wallet-mining.dat", "wallet-donations.dat",
"wallet-2023.dat", "wallet-payroll.dat"};
for (int i = 0; i < 8; ++i) {
if (!fs::exists(dd + "/" + names[i], ec))
std::ofstream(dd + "/" + names[i], std::ios::binary) << std::string(64000 + i * 4096, '\0');
data::WalletIndexEntry e; e.fileName = names[i]; e.displayName = names[i];
e.cachedBalance = 5.0 * (i + 1); e.cachedAddressCount = 2 + i;
e.lastOpenedEpoch = 1720000000 - i * 86400; e.syncedHere = true;
a.walletIndex().upsert(e);
}
if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
ui::WalletsDialog::show(&a);
},
[](App& a) {
ui::WalletsDialog::hide();
std::error_code ec;
const std::string dd = util::Platform::getDragonXDataDir();
for (const char* n : kManyExtra) fs::remove(dd + "/" + n, ec);
});
}
// First-run wizard — one per meaningful phase (full-node only; blocks all other UI while shown).
if (!isLiteBuild()) {
auto wiz = [&](const char* name, WizardPhase phase) {
add(name, ui::NavPage::Overview,
[phase](App& a) { a.wizard_phase_ = phase; },
[](App& a) { a.wizard_phase_ = WizardPhase::None; });
};
wiz("wizard-appearance", WizardPhase::Appearance);
wiz("wizard-bootstrap", WizardPhase::BootstrapOffer);
wiz("wizard-encrypt", WizardPhase::EncryptOffer);
wiz("wizard-pin", WizardPhase::PinSetup);
}
// App-state overlays. Teardown restores the healthy demo flags (full restore at sweep end).
add("overlay-lock", ui::NavPage::Overview,
[](App& a) { a.state_.encrypted = true; a.state_.locked = true; },
[](App& a) { a.applyHealthyDemoState(); });
add("overlay-warmup", ui::NavPage::Overview,
[](App& a) {
a.state_.warming_up = true;
a.state_.warmup_status = "Processing blocks…";
a.state_.warmup_description = "The node is loading the block index.";
},
[](App& a) { a.applyHealthyDemoState(); });
add("overlay-not-ready", ui::NavPage::Overview,
[](App& a) { a.state_.connected = false; a.state_.encryption_state_known = false; },
[](App& a) { a.applyHealthyDemoState(); });
// Send-confirm popup (state lives in send_tab statics → driven via a debug hook).
add("popup-send-confirm", ui::NavPage::Send,
[](App& a) { ui::SweepShowSendConfirm(&a, true); },
[](App& a) { ui::SweepShowSendConfirm(&a, false); });
// Market portfolio row styles — capture all three so the redesign is reviewable per skin.
add("market-rows-compact", ui::NavPage::Market,
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); },
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
add("market-rows-detailed", ui::NavPage::Market,
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(1); },
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
add("market-rows-featured", ui::NavPage::Market,
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(2); },
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
// Console RPC command-reference popup (fixed-height dialog with a fill-height command list).
add("modal-console-commands", ui::NavPage::Console,
[](App& a) { a.console_tab_.sweepSetCommandsPopup(true); },
[](App& a) { a.console_tab_.sweepSetCommandsPopup(false); });
// Debug-options gate: confirmation + warning, with the passphrase re-auth field (encrypted).
add("modal-debug-gate", ui::NavPage::Settings,
[](App& a) { a.state_.encrypted = true; ui::SweepOpenDebugGate(true); },
[](App& a) { ui::SweepOpenDebugGate(false); a.applyHealthyDemoState(); });
// Daemon updater — the two-pane version picker (versions left, selected-version detail right).
// Seeds fake releases so the offline sweep renders it without a network fetch / live updater.
add("modal-daemon-update", ui::NavPage::Settings,
[](App& a) {
auto mk = [](const char* tag, const char* name, const char* date, const char* body, bool pre) {
util::DaemonRelease r; r.ok = true; r.tag = tag; r.name = name; r.body = body;
r.prerelease = pre; r.publishedAt = std::string(date) + "T10:00:00Z";
util::DaemonReleaseAsset as;
as.name = std::string("dragonx-") + tag + "-linux-amd64.zip";
as.downloadUrl = "https://git.dragonx.is/" + as.name; as.size = 43000000;
r.assets.push_back(as);
return r;
};
std::vector<util::DaemonRelease> rels;
rels.push_back(mk("v1.0.4", "DragonX v1.0.4", "2026-06-28",
"## What is DragonX?\n\n"
"DragonX is a privacy-focused cryptocurrency built on zero-knowledge mathematics. "
"It enforces mandatory z2z (shielded-to-shielded) transactions after block 340,000.\n\n"
"## Bug Fixes\n"
"* Fix sapling pool persistence \xE2\x80\x94 pool total no longer resets to 0 on restart\n"
"* Add `subsidy` and `fees` fields to the `getblock` RPC response\n\n"
"## Key Features\n"
"* **RandomX Proof-of-Work** \xE2\x80\x94 CPU-mineable, ASIC-resistant\n"
"* **Sapling zk-SNARKs** \xE2\x80\x94 zero-knowledge proofs for private transactions\n"
"* **Encrypted P2P** \xE2\x80\x94 all connections secured with TLS 1.3 via WolfSSL\n\n"
"## Checksums\n| File | SHA-256 |\n"
"|---|---|\n| dragonx-v1.0.4-linux-amd64.zip | `ab12cd34` |\n", false));
rels.push_back(mk("v1.0.3", "DragonX v1.0.3", "2026-05-14",
"## Notes\n- Stability improvements\n- RPC fixes\n", false));
rels.push_back(mk("v1.0.2", "DragonX v1.0.2", "2026-04-02",
"- First tagged mainnet build\n", false));
rels.push_back(mk("v1.1.0-rc1", "DragonX v1.1.0-rc1", "2026-07-01",
"Release candidate for the 1.1.0 series. Testing only.\n", true));
ui::DaemonUpdateDialog::sweepSeed(&a, rels, util::DaemonUpdater::State::ReleaseList,
"v1.0.3-dc45e7d90");
},
[](App&) { ui::DaemonUpdateDialog::sweepClose(); });
// Miner (xmrig) updater — same two-pane version picker, seeded with fake releases for the sweep.
add("modal-xmrig-update", ui::NavPage::Mining,
[](App& a) {
auto mk = [](const char* tag, const char* name, const char* date, const char* body, bool pre) {
util::XmrigRelease r; r.ok = true; r.tag = tag; r.name = name; r.body = body;
r.prerelease = pre; r.publishedAt = std::string(date) + "T10:00:00Z";
util::XmrigReleaseAsset as;
as.name = std::string("drg-xmrig-") + tag + "-linux-x64.zip";
as.downloadUrl = "https://git.dragonx.is/" + as.name; as.size = 8000000;
r.assets.push_back(as);
return r;
};
std::vector<util::XmrigRelease> rels;
rels.push_back(mk("v6.25.3", "DRG-XMRig v6.25.3", "2026-06-20",
"## What's new\n- Rebased on upstream XMRig 6.25.3\n- **RandomX** JIT speedups on modern CPUs\n"
"- Fix `--cpu-priority` parsing on Windows\n\n## Checksums\n| File | SHA-256 |\n"
"|---|---|\n| drg-xmrig-v6.25.3-linux-x64.zip | `ab12cd34` |\n", false));
rels.push_back(mk("v6.24.0", "DRG-XMRig v6.24.0", "2026-04-30",
"## Notes\n- Pool TLS fixes\n- Lower idle CPU\n", false));
rels.push_back(mk("v6.23.0", "DRG-XMRig v6.23.0", "2026-03-11",
"- First DRG-XMRig build\n", false));
rels.push_back(mk("v6.26.0-rc1", "DRG-XMRig v6.26.0-rc1", "2026-07-02",
"Release candidate. **Testing only.**\n", true));
ui::XmrigDownloadDialog::sweepSeed(&a, rels, util::XmrigUpdater::State::ReleaseList, "v6.24.0");
},
[](App&) { ui::XmrigDownloadDialog::sweepClose(); });
// In-app folder picker (Wallets → Scan another folder). Populate a small demo tree so the
// list shows sub-folders + wallet files, then open the wallets dialog + the picker over it.
add("modal-folder-picker", ui::NavPage::Settings,
[](App& a) {
std::error_code ec;
const std::string demo = util::Platform::getConfigDir() + "picker-demo";
for (const char* sub : {"Documents", "Downloads", "Backups", "wallet-archive"})
fs::create_directories(demo + "/" + sub, ec);
for (const char* f : {"wallet-cold.dat", "wallet-2023.dat"})
if (!fs::exists(demo + "/" + f, ec))
std::ofstream(demo + "/" + f, std::ios::binary) << std::string(66000, '\0');
ui::WalletsDialog::show(&a);
ui::FolderPicker::open(demo, [](const std::string&) {});
},
[](App&) { ui::FolderPicker::close(); ui::WalletsDialog::hide(); });
}
// ── State machine ───────────────────────────────────────────────────────────────────────────
void App::startSweepImpl(bool full)
{
if (screenshot_sweep_active_) return;
sweep_skins_.clear();
for (const auto& sk : ui::schema::SkinManager::instance().available())
if (sk.valid) sweep_skins_.push_back(sk.id);
if (sweep_skins_.empty()) return;
sweep_full_ = full;
if (full) { capture_mode_ = true; installDemoWalletData(); }
buildSweepCatalog();
if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }
sweep_dir_ = full ? screenshotFullDir() : screenshotDir();
std::error_code ec; fs::create_directories(sweep_dir_, ec);
sweep_saved_skin_ = ui::schema::SkinManager::instance().activeSkinId();
sweep_saved_page_ = current_page_;
sweep_skin_idx_ = 0; sweep_target_idx_ = 0;
screenshot_sweep_active_ = true;
sweep_capture_this_frame_ = false;
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[0]);
applySweepTarget();
ui::Notifications::instance().info(full ? "Full UI sweep running…" : "Screenshot sweep running…");
DEBUG_LOGF("[Sweep] %s -> %s (%d skins x %d surfaces)\n", full ? "FULL" : "tabs",
sweep_dir_.c_str(), (int)sweep_skins_.size(), (int)sweep_targets_.size());
}
void App::startScreenshotSweep() { startSweepImpl(false); }
void App::startFullUiSweep() { startSweepImpl(true); }
void App::applySweepTarget()
{
const SweepTarget& t = sweep_targets_[sweep_target_idx_];
current_page_ = t.page;
page_alpha_ = 1.0f; // skip the page-switch fade so the shot isn't captured mid-animation
if (t.setup) t.setup(*this);
// <dir>/<surface>/<skin>.png — one subfolder per surface, one PNG per theme, overwritten in
// place next sweep. (writePng in main.cpp creates the parent subfolder.)
sweep_current_path_ = (fs::path(sweep_dir_) / t.name / (sweep_skins_[sweep_skin_idx_] + ".png")).string();
sweep_settle_frames_ = t.settle;
sweep_capture_this_frame_ = false;
}
void App::updateScreenshotSweep()
{
if (!screenshot_sweep_active_) return;
const SweepTarget& t = sweep_targets_[sweep_target_idx_];
current_page_ = t.page; // keep the surface pinned each frame
page_alpha_ = 1.0f;
// Re-run setup every frame: idempotent for flags/enums, and required for OpenPopup-based popups
// (must re-fire while open) + to keep the surface state fixed against any refresh.
if (t.setup) t.setup(*this);
if (sweep_settle_frames_ > 0) { sweep_settle_frames_--; sweep_capture_this_frame_ = false; }
else sweep_capture_this_frame_ = true; // settled — main.cpp captures this frame
}
void App::onScreenshotCaptured()
{
sweep_capture_this_frame_ = false;
if (!screenshot_sweep_active_) return;
{ const SweepTarget& t = sweep_targets_[sweep_target_idx_]; if (t.teardown) t.teardown(*this); }
// Some surfaces leave an ImGui popup open (e.g. the send-confirm dialog calls OpenPopup but is
// torn down by just clearing its flag). The headless sweep never clicks, so ImGui's normal
// click-to-dismiss cleanup never runs and the popup lingers on the stack — which keeps
// IsPopupOpen(AnyPopup) true forever and disables the smooth-scroll wheel capture on
// Settings/Explorer/Console (draw_helpers::ApplySmoothScroll). Flush any lingering popup here so
// it can't bleed into the next surface's capture or survive past the sweep.
if (ImGuiContext* g = ImGui::GetCurrentContext(); g && g->OpenPopupStack.Size > 0)
ImGui::ClosePopupToLevel(0, false);
if (++sweep_target_idx_ >= static_cast<int>(sweep_targets_.size())) {
sweep_target_idx_ = 0;
if (++sweep_skin_idx_ >= static_cast<int>(sweep_skins_.size())) {
// Done — restore the original skin + page, and (full) the real state.
const bool wasFull = sweep_full_;
if (wasFull) writeSweepManifest();
ui::schema::SkinManager::instance().setActiveSkin(sweep_saved_skin_);
current_page_ = sweep_saved_page_;
page_alpha_ = 1.0f;
if (wasFull) { clearDemoWalletData(); capture_mode_ = false; }
sweep_full_ = false;
screenshot_sweep_active_ = false;
ui::Notifications::instance().success(
(wasFull ? std::string("Full UI screenshots saved to ") : std::string("Screenshots saved to ")) + sweep_dir_);
DEBUG_LOGF("[Sweep] done -> %s\n", sweep_dir_.c_str());
return;
}
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[sweep_skin_idx_]);
}
applySweepTarget();
}
void App::writeSweepManifest() const
{
std::error_code ec; fs::create_directories(sweep_dir_, ec);
std::ofstream out((fs::path(sweep_dir_) / "index.md").string(), std::ios::trunc);
if (!out) return;
out << "# Full UI sweep\n\n";
out << sweep_targets_.size() << " surfaces x " << sweep_skins_.size() << " skins\n\n";
for (const auto& t : sweep_targets_) {
out << "## " << t.name << " (base: " << sweepPageName(t.page) << ")\n\n";
for (const auto& skin : sweep_skins_)
out << "- " << skin << ": `" << t.name << "/" << skin << ".png`\n";
out << "\n";
}
}
} // namespace dragonx

View File

@@ -72,11 +72,6 @@ WizardUiState s_wizardUi;
void App::restartWizard()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Lite wallet lifecycle requests are available from Settings as dry-run readiness checks");
return;
}
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
// Reset crash counter for fresh wizard attempt
@@ -121,21 +116,16 @@ void App::renderFirstRunWizard() {
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0)); // reveal the skin backdrop behind
ImGui::Begin("##FirstRunWizard", nullptr, flags);
ImGui::PopStyleColor();
ImGui::PopStyleVar();
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 winPos = ImGui::GetWindowPos();
ImVec2 winSize = ImGui::GetWindowSize();
// The app's skin backdrop (marble / gradient / acrylic) is already painted behind every window by
// drawWindowBackdrop(); reveal it here instead of a flat Surface() slab, under a gentle theme-tinted
// scrim so the wizard cards and text keep their contrast on busy skins.
ImU32 bgCol = ui::material::Surface(); // still used by the completed / not-reached card overlays
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y),
ui::material::WithAlpha(ui::material::Background(), 120));
// Background fill
ImU32 bgCol = ui::material::Surface();
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), bgCol);
// --- Determine which of the 3 masonry sections is focused ---
// 0 = Appearance, 1 = Bootstrap, 2 = Encrypt + PIN
@@ -189,18 +179,11 @@ void App::renderFirstRunWizard() {
headerCy += logoSize + 8.0f * dp;
{
const char* welcomeTitle = TR("wiz_welcome_title");
const char* welcomeTitle = "Welcome to ObsidianDragon!";
ImVec2 wts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, welcomeTitle);
dl->AddText(titleFont, titleFont->LegacySize,
ImVec2(winPos.x + (winSize.x - wts.x) * 0.5f, headerCy), textCol, welcomeTitle);
headerCy += wts.y + 6.0f * dp;
// Warmer, less-sparse header: a one-line subtitle under the welcome (dimmed body).
const char* welcomeSub = TR("wiz_welcome_sub");
ImVec2 sts = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, 0, welcomeSub);
dl->AddText(bodyFont, bodyFont->LegacySize,
ImVec2(winPos.x + (winSize.x - sts.x) * 0.5f, headerCy), dimCol, welcomeSub);
headerCy += sts.y + 16.0f * dp;
headerCy += wts.y + 16.0f * dp;
}
// --- Masonry: 2 columns ---
@@ -235,8 +218,11 @@ void App::renderFirstRunWizard() {
// Background (channel 0)
dl->ChannelsSetCurrent(0);
if (state == 1) {
// Focused card lifts off the backdrop with the app's uniform card shadow (not a hard offset).
ui::material::DrawCardDropShadow(dl, cMin, cMax, cardRound);
// Focused card: subtle drop shadow
float shadowOff = 3.0f * dp;
dl->AddRectFilled(
ImVec2(cMin.x + shadowOff, cMin.y + shadowOff), ImVec2(cMax.x + shadowOff, cMax.y + shadowOff),
IM_COL32(0, 0, 0, 35), cardRound);
}
// Use DrawGlassPanel for proper acrylic/opacity/noise/theme effects
ui::material::GlassPanelSpec glass;
@@ -246,14 +232,14 @@ void App::renderFirstRunWizard() {
// Overlays & borders (channel 2)
dl->ChannelsSetCurrent(2);
if (state == 1) {
// Focused: soft accent ring
dl->AddRect(cMin, cMax, ui::material::Primary(), cardRound, 0, 1.5f * dp);
// Focused: accent border
dl->AddRect(cMin, cMax, ui::material::Primary(), cardRound, 0, 2.0f * dp);
} else if (state == 2) {
// Completed: a light veil — reads as "done", not disabled.
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 70), cardRound);
// Completed: dim overlay (preserves color)
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 110), cardRound);
} else {
// Upcoming: a gentle veil — reads as "waiting", not greyed-out.
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 115), cardRound);
// Not reached: heavy overlay (creates greyscale look)
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 165), cardRound);
}
dl->ChannelsSetCurrent(1);
@@ -274,13 +260,13 @@ void App::renderFirstRunWizard() {
{
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step1"));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 1");
cy += captionFont->LegacySize + 6.0f * dp;
}
// Title
{
const char* t = TR("wiz_appearance");
const char* t = "Appearance";
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
cy += titleFont->LegacySize + 10.0f * dp;
}
@@ -336,14 +322,14 @@ void App::renderFirstRunWizard() {
for (const auto& skin : skins) {
if (skin.id == skinMgr.activeSkinId()) { activePreview = skin.name; break; }
}
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, TR("theme"));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Theme");
float comboX = cx + 110.0f * dp;
float comboW = contentW - 110.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(comboX, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##wiz_theme", activePreview.c_str())) {
ImGui::TextDisabled(TR("wiz_theme_builtin"));
ImGui::TextDisabled("Built-in");
ImGui::Separator();
for (const auto& skin : skins) {
if (!skin.bundled) continue;
@@ -359,7 +345,7 @@ void App::renderFirstRunWizard() {
for (const auto& skin : skins) { if (!skin.bundled) { hasCustom = true; break; } }
if (hasCustom) {
ImGui::Spacing();
ImGui::TextDisabled(TR("wiz_theme_custom"));
ImGui::TextDisabled("Custom");
ImGui::Separator();
for (const auto& skin : skins) {
if (skin.bundled) continue;
@@ -367,7 +353,7 @@ void App::renderFirstRunWizard() {
if (!skin.valid) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1,0.3f,0.3f,1));
ImGui::BeginDisabled(true);
ImGui::Selectable((skin.name + TR("wiz_theme_invalid")).c_str(), false);
ImGui::Selectable((skin.name + " (invalid)").c_str(), false);
ImGui::EndDisabled();
ImGui::PopStyleColor();
} else {
@@ -395,7 +381,7 @@ void App::renderFirstRunWizard() {
for (const auto& l : layouts) {
if (l.id == wiz_balance_layout) { balPreview = l.name; break; }
}
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, TR("balance_layout"));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Balance Layout");
float comboX = cx + 110.0f * dp;
float comboW = contentW - 110.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(comboX, cy));
@@ -426,7 +412,7 @@ void App::renderFirstRunWizard() {
langNames.reserve(languages.size());
for (const auto& lang : languages) langNames.push_back(lang.second.c_str());
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, TR("language"));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Language");
float comboX = cx + 110.0f * dp;
float comboW = contentW - 110.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(comboX, cy));
@@ -497,10 +483,10 @@ void App::renderFirstRunWizard() {
ImGui::SameLine();
dl->AddText(bodyFont, bodyFont->LegacySize,
ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol,
TR("low_spec_mode"));
"Low-spec mode");
cy += bodyFont->LegacySize + 6.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + 28.0f * dp, cy), dimCol, TR("wiz_lowspec_desc"));
ImVec2(cx + 28.0f * dp, cy), dimCol, "Disable all heavy visual effects");
cy += captionFont->LegacySize + 16.0f * dp;
ImGui::BeginDisabled(wiz_low_spec);
@@ -508,15 +494,15 @@ void App::renderFirstRunWizard() {
// Acrylic blur slider
dl->AddText(bodyFont, bodyFont->LegacySize,
ImVec2(cx, cy + 2.0f * dp), textCol,
TR("wiz_acrylic"));
"Acrylic glass effects");
cy += bodyFont->LegacySize + 4.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx, cy), dimCol, TR("wiz_acrylic_desc"));
ImVec2(cx, cy), dimCol, "Translucent blur on panels (Off disables)");
cy += captionFont->LegacySize + 10.0f * dp;
{
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + 4.0f * dp, cy), textCol, TR("wiz_level"));
ImVec2(cx + 4.0f * dp, cy), textCol, "Level:");
ImGui::SetCursorScreenPos(ImVec2(cx + 72.0f * dp, cy - 2.0f * dp));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
float sliderW = contentW - 72.0f * dp;
@@ -524,7 +510,7 @@ void App::renderFirstRunWizard() {
{
char blur_fmt[16];
if (wiz_blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), TR("wiz_off"));
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
else
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", wiz_blur_amount * 25.0f);
if (ImGui::SliderFloat("##wiz_blur", &wiz_blur_amount, 0.0f, 4.0f, blur_fmt,
@@ -558,19 +544,19 @@ void App::renderFirstRunWizard() {
ImGui::SameLine();
dl->AddText(bodyFont, bodyFont->LegacySize,
ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol,
TR("wiz_theme_effects"));
"Theme visual effects");
cy += bodyFont->LegacySize + 6.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + 28.0f * dp, cy), dimCol, TR("wiz_theme_effects_desc"));
ImVec2(cx + 28.0f * dp, cy), dimCol, "Animated borders, color wash");
cy += captionFont->LegacySize + 16.0f * dp;
// UI Opacity slider
dl->AddText(bodyFont, bodyFont->LegacySize,
ImVec2(cx, cy + 2.0f * dp), textCol,
TR("ui_opacity"));
"UI Opacity");
cy += bodyFont->LegacySize + 4.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx, cy), dimCol, TR("wiz_ui_opacity_desc"));
ImVec2(cx, cy), dimCol, "Card & sidebar transparency (1.0 = solid)");
cy += captionFont->LegacySize + 10.0f * dp;
{
ImGui::SetCursorScreenPos(ImVec2(cx, cy - 2.0f * dp));
@@ -599,10 +585,10 @@ void App::renderFirstRunWizard() {
ImGui::SameLine();
dl->AddText(bodyFont, bodyFont->LegacySize,
ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol,
TR("console_scanline"));
"Console scanline");
cy += bodyFont->LegacySize + 6.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + 28.0f * dp, cy), dimCol, TR("wiz_scanline_desc"));
ImVec2(cx + 28.0f * dp, cy), dimCol, "CRT scanline effect in console");
cy += captionFont->LegacySize + 24.0f * dp;
ImGui::EndDisabled(); // low-spec
@@ -620,7 +606,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton(TR("wiz_continue"), ImVec2(btnW, btnH))) {
if (ImGui::Button("Continue##app", ImVec2(btnW, btnH))) {
// Save appearance choices, advance to Bootstrap
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
settings_->setAcrylicQuality(wiz_blur_amount > 0.001f
@@ -667,23 +653,23 @@ void App::renderFirstRunWizard() {
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
float labelX = cx + iconW + 4.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(labelX, cy), dimCol, TR("wiz_step2"));
float step2W = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, TR("wiz_step2")).x;
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(labelX, cy), dimCol, "Step 2");
float step2W = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, "Step 2").x;
float titleX = labelX + step2W + 12.0f * dp;
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(titleX, cy), dimCol, TR("wiz_bootstrap"));
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(titleX, cy), dimCol, "Bootstrap");
cy += captionFont->LegacySize + 4.0f * dp;
} else {
// Step indicator
{
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step2"));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 2");
cy += captionFont->LegacySize + 4.0f * dp;
}
// Title
{
const char* t = TR("wiz_bootstrap");
const char* t = "Bootstrap";
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
cy += titleFont->LegacySize + 6.0f * dp;
}
@@ -706,11 +692,11 @@ void App::renderFirstRunWizard() {
const char* statusTitle;
if (prog.state == util::Bootstrap::State::Downloading)
statusTitle = TR("bootstrap_downloading");
statusTitle = "Downloading bootstrap...";
else if (prog.state == util::Bootstrap::State::Verifying)
statusTitle = TR("bootstrap_verifying");
statusTitle = "Verifying checksums...";
else
statusTitle = TR("bootstrap_extracting");
statusTitle = "Extracting blockchain data...";
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, statusTitle);
cy += bodyFont->LegacySize + 12.0f * dp;
@@ -739,7 +725,7 @@ void App::renderFirstRunWizard() {
if (prog.state == util::Bootstrap::State::Extracting) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
dimCol, TR("bootstrap_wallet_protected"));
dimCol, "(wallet.dat is protected)");
cy += captionFont->LegacySize + 6.0f * dp;
}
@@ -755,9 +741,9 @@ void App::renderFirstRunWizard() {
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
dotR, dotCol);
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
? TR("bootstrap_daemon_stopping")
: TR("bootstrap_daemon_running"))
: TR("bootstrap_daemon_stopped");
? "Daemon stopping..."
: "Daemon running")
: "Daemon stopped";
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
@@ -772,7 +758,7 @@ void App::renderFirstRunWizard() {
float cancelBX = rightX + (colW - cancelW) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton(TR("cancel"), ImVec2(cancelW, cancelH))) {
if (ImGui::Button("Cancel##bs", ImVec2(cancelW, cancelH))) {
bootstrap_->cancel();
}
ImGui::PopStyleVar();
@@ -783,8 +769,6 @@ void App::renderFirstRunWizard() {
auto finalProg = bootstrap_->getProgress();
if (finalProg.state == util::Bootstrap::State::Completed) {
bootstrap_.reset();
// Reconcile the preserved wallet.dat against the new chain once the daemon is up.
markPostBootstrapRescanPending();
wizard_phase_ = WizardPhase::EncryptOffer;
} else {
wizard_phase_ = WizardPhase::BootstrapFailed;
@@ -799,10 +783,10 @@ void App::renderFirstRunWizard() {
errMsg = bootstrap_->getProgress().error;
bootstrap_.reset();
}
if (errMsg.empty()) errMsg = TR("wiz_bootstrap_failed");
if (errMsg.empty()) errMsg = "Bootstrap failed";
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), TR("wiz_download_failed"));
ui::material::Error(), "Download Failed");
cy += bodyFont->LegacySize + 8.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol,
@@ -820,8 +804,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton(TR("retry"), ImVec2(btnW2, btnH2))) {
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
@@ -829,13 +812,12 @@ void App::renderFirstRunWizard() {
bootstrap_->start(dataDir);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(btnW2, btnH2))) {
if (ImGui::Button("Skip##bsfail", ImVec2(btnW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -878,11 +860,11 @@ void App::renderFirstRunWizard() {
{
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, TR("wiz_ext_daemon_running"));
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, "External daemon running");
}
cy += bodyFont->LegacySize + 4.0f * dp;
{
const char* warnBody = TR("wiz_ext_daemon_warning");
const char* warnBody = "It must be stopped before downloading a bootstrap, otherwise chain data could be corrupted.";
ImVec2 ws = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, contentW, warnBody);
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol, warnBody, nullptr, contentW);
cy += ws.y + 12.0f * dp;
@@ -905,9 +887,9 @@ void App::renderFirstRunWizard() {
IM_COL32(220, 60, 60, 255)));
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 255));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton(TR("wiz_stop_daemon"), ImVec2(stopW, btnH2))) {
if (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
wizard_stopping_external_ = true;
wizard_stop_status_ = TR("wiz_daemon_sending_stop");
wizard_stop_status_ = "Sending stop command...";
async_tasks_.submit("wizard-stop-external-daemon", [this](const util::AsyncTaskManager::Token& token) {
auto config = rpc::Connection::autoDetectConfig();
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
@@ -919,17 +901,17 @@ void App::renderFirstRunWizard() {
tmp_rpc->disconnect();
}
}
wizard_stop_status_ = TR("wiz_daemon_waiting_stop");
wizard_stop_status_ = "Waiting for daemon to shut down...";
for (int i = 0; i < 60 && !token.cancelled(); i++) {
std::this_thread::sleep_for(std::chrono::seconds(1));
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
wizard_stop_status_ = TR("wiz_daemon_stopped_ok");
wizard_stop_status_ = "Daemon stopped.";
wizard_stopping_external_ = false;
return;
}
}
if (token.cancelled()) return;
wizard_stop_status_ = TR("wiz_daemon_stop_failed");
wizard_stop_status_ = "Daemon did not stop — try manually.";
wizard_stopping_external_ = false;
});
}
@@ -938,7 +920,7 @@ void App::renderFirstRunWizard() {
ImGui::SetCursorScreenPos(ImVec2(bx + stopW + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(skipW2, btnH2))) {
if (ImGui::Button("Skip##extd", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -947,7 +929,7 @@ void App::renderFirstRunWizard() {
} else {
// --- Normal bootstrap offer ---
{
const char* bsText = TR("wiz_bootstrap_desc");
const char* bsText = "Download a blockchain bootstrap to dramatically speed up initial sync.\n\nYour existing wallet.dat will NOT be modified or replaced.";
ImVec2 bsSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, bsText);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, bsText, nullptr, contentW);
cy += bsSize.y + 8.0f * dp;
@@ -960,7 +942,7 @@ void App::renderFirstRunWizard() {
ImU32 warnCol = (textCol & 0x00FFFFFF) | ((ImU32)(255 * warnOpacity) << 24);
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
const char* twText = TR("bootstrap_trust_warning");
const char* twText = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node.";
float twWrap = contentW - iw - 4.0f * dp;
ImVec2 twSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, twWrap, twText);
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, twText, nullptr, twWrap);
@@ -979,9 +961,9 @@ void App::renderFirstRunWizard() {
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
dotR, dotCol);
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
? TR("bootstrap_daemon_stopping")
: TR("bootstrap_daemon_running"))
: TR("bootstrap_daemon_stopped");
? "Daemon stopping..."
: "Daemon running")
: "Daemon stopped";
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
@@ -1003,8 +985,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton(TR("download"), ImVec2(dlBtnW, btnH2))) {
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
@@ -1012,7 +993,6 @@ void App::renderFirstRunWizard() {
bootstrap_->start(dataDir);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
@@ -1022,8 +1002,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton(TR("bootstrap_mirror"), ImVec2(mirrorW, btnH2))) {
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
@@ -1031,9 +1010,8 @@ void App::renderFirstRunWizard() {
bootstrap_->start(dataDir, mirrorUrl);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered()) {
ui::material::Tooltip(TR("bootstrap_mirror_tooltip"));
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
@@ -1041,7 +1019,7 @@ void App::renderFirstRunWizard() {
// --- Skip button ---
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(skipW2, btnH2))) {
if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -1093,14 +1071,14 @@ void App::renderFirstRunWizard() {
{
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step3"));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 3");
cy += captionFont->LegacySize + 4.0f * dp;
}
// Title (changes for PinSetup sub-state)
{
const char* t = (isFocused && wizard_phase_ == WizardPhase::PinSetup)
? TR("wiz_pin_title") : TR("wiz_encryption");
? "Quick-Unlock PIN" : "Encryption";
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
cy += titleFont->LegacySize + 6.0f * dp;
}
@@ -1117,11 +1095,11 @@ void App::renderFirstRunWizard() {
ImU32 okCol = ui::material::Secondary();
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_VERIFIED_USER).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), okCol, ICON_MD_VERIFIED_USER);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 6.0f * dp, cy), okCol, TR("wiz_already_encrypted"));
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 6.0f * dp, cy), okCol, "Wallet is already encrypted");
cy += bodyFont->LegacySize + 12.0f * dp;
}
{
const char* desc = TR("wiz_already_encrypted_desc");
const char* desc = "Your wallet is protected with a passphrase. No further action is needed.";
ImVec2 ds = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, desc);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, desc, nullptr, contentW);
cy += ds.y + 20.0f * dp;
@@ -1136,7 +1114,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton(TR("wiz_continue"), ImVec2(btnW2, btnH2))) {
if (ImGui::Button("Continue##encok", ImVec2(btnW2, btnH2))) {
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
@@ -1148,7 +1126,7 @@ void App::renderFirstRunWizard() {
} else if (isFocused) {
// ---- Encryption offer + optional PIN (combined) ----
{
const char* encDesc = TR("wiz_encrypt_desc");
const char* encDesc = "Encrypt your wallet to protect private keys with a passphrase.";
ImVec2 edSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, encDesc);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, encDesc, nullptr, contentW);
cy += edSize.y + 6.0f * dp;
@@ -1157,7 +1135,7 @@ void App::renderFirstRunWizard() {
ImU32 warnCol2 = ui::material::Warning();
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol2, ICON_MD_WARNING);
const char* warnLoss = TR("wiz_encrypt_warning");
const char* warnLoss = "If you lose your passphrase, you lose access to your funds.";
float wlWrap = contentW - iw - 4.0f * dp;
ImVec2 wlSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, wlWrap, warnLoss);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol2, warnLoss, nullptr, wlWrap);
@@ -1165,7 +1143,7 @@ void App::renderFirstRunWizard() {
}
// Passphrase input
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, TR("wiz_passphrase"));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Passphrase:");
cy += captionFont->LegacySize + 4.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
@@ -1177,7 +1155,7 @@ void App::renderFirstRunWizard() {
ImGui::PopItemWidth();
cy += 36.0f * dp + 6.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, TR("wiz_confirm"));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Confirm:");
cy += captionFont->LegacySize + 4.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
@@ -1192,16 +1170,16 @@ void App::renderFirstRunWizard() {
// Strength meter
{
size_t len = strlen(encrypt_pass_buf_);
const char* strengthLabel = TR("wiz_strength_weak");
const char* strengthLabel = "Weak";
ImU32 strengthCol = ui::material::Error();
float strengthPct = 0.25f;
if (len >= 16) {
strengthLabel = TR("wiz_strength_strong"); strengthCol = ui::material::Secondary(); strengthPct = 1.0f;
strengthLabel = "Strong"; strengthCol = ui::material::Secondary(); strengthPct = 1.0f;
} else if (len >= 12) {
strengthLabel = TR("wiz_strength_good"); strengthCol = ui::material::Secondary(); strengthPct = 0.75f;
strengthLabel = "Good"; strengthCol = ui::material::Secondary(); strengthPct = 0.75f;
} else if (len >= 8) {
strengthLabel = TR("wiz_strength_fair"); strengthCol = ui::material::Warning(); strengthPct = 0.5f;
strengthLabel = "Fair"; strengthCol = ui::material::Warning(); strengthPct = 0.5f;
}
float sBarH = 4.0f * dp, sBarR = 2.0f * dp;
@@ -1214,7 +1192,7 @@ void App::renderFirstRunWizard() {
cy += sBarH + 4.0f * dp;
char slabel[64];
snprintf(slabel, sizeof(slabel), TR("wiz_strength"), strengthLabel);
snprintf(slabel, sizeof(slabel), "Strength: %s", strengthLabel);
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, slabel);
cy += captionFont->LegacySize + 10.0f * dp;
}
@@ -1224,14 +1202,14 @@ void App::renderFirstRunWizard() {
size_t pLen = strlen(encrypt_pass_buf_);
if (pLen > 0 && pLen < 8) {
char fb[80];
snprintf(fb, sizeof(fb), TR("wiz_pass_too_short"), pLen);
snprintf(fb, sizeof(fb), "Passphrase must be at least 8 characters (%zu/8)", pLen);
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), fb);
cy += captionFont->LegacySize + 6.0f * dp;
} else if (pLen >= 8 && strlen(encrypt_confirm_buf_) > 0 &&
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) != 0) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), TR("wiz_pass_mismatch"));
ui::material::Error(), "Passphrases do not match");
cy += captionFont->LegacySize + 6.0f * dp;
}
}
@@ -1243,12 +1221,12 @@ void App::renderFirstRunWizard() {
cy += 8.0f * dp;
{
const char* pinTitle = TR("wiz_pin_optional");
const char* pinTitle = "Quick-Unlock PIN (optional)";
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol, pinTitle);
cy += captionFont->LegacySize + 4.0f * dp;
}
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, TR("wiz_pin_label"));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "PIN (4-8 digits):");
cy += captionFont->LegacySize + 4.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
@@ -1260,7 +1238,7 @@ void App::renderFirstRunWizard() {
ImGui::PopItemWidth();
cy += 36.0f * dp + 6.0f * dp;
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, TR("wiz_pin_confirm"));
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Confirm PIN:");
cy += captionFont->LegacySize + 4.0f * dp;
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
@@ -1277,12 +1255,12 @@ void App::renderFirstRunWizard() {
std::string pinStr(wizard_pin_buf_);
if (!pinStr.empty() && !util::SecureVault::isValidPin(pinStr)) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), TR("wiz_pin_invalid"));
ui::material::Error(), "PIN must be 4-8 digits");
cy += captionFont->LegacySize + 6.0f * dp;
} else if (!pinStr.empty() && strlen(wizard_pin_confirm_buf_) > 0 &&
pinStr != std::string(wizard_pin_confirm_buf_)) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), TR("wiz_pin_mismatch"));
ui::material::Error(), "PINs do not match");
cy += captionFont->LegacySize + 6.0f * dp;
}
}
@@ -1294,24 +1272,10 @@ void App::renderFirstRunWizard() {
cy += captionFont->LegacySize + 6.0f * dp;
}
// Warn + block if the passphrase has leading/trailing whitespace. Silently trimming it would
// change the passphrase the user believes they set and lock them out on the next unlock.
bool passEdgeSpace = false;
if (size_t pl = strlen(encrypt_pass_buf_)) {
char a = encrypt_pass_buf_[0], b = encrypt_pass_buf_[pl - 1];
passEdgeSpace = (a == ' ' || a == '\t' || b == ' ' || b == '\t');
}
if (passEdgeSpace) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), TR("wiz_pass_spaces"));
cy += captionFont->LegacySize + 6.0f * dp;
}
// Buttons
{
bool passValid = strlen(encrypt_pass_buf_) >= 8 &&
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0 &&
!passEdgeSpace;
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0;
// PIN is optional: if entered, must be valid + confirmed
std::string pinStr(wizard_pin_buf_);
bool pinEntered = !pinStr.empty();
@@ -1333,7 +1297,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!canEncrypt);
if (ui::material::TactileButton(TR("wiz_encrypt_continue"), ImVec2(encBtnW, btnH2))) {
if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
// Save passphrase + optional PIN for background processing
wallet_security_.beginDeferredEncryption(
std::string(encrypt_pass_buf_),
@@ -1356,7 +1320,7 @@ void App::renderFirstRunWizard() {
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
ui::Notifications::instance().info(TR("wiz_encrypt_bg"));
ui::Notifications::instance().info("Encryption will complete in the background");
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
@@ -1364,14 +1328,7 @@ void App::renderFirstRunWizard() {
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(skipW2, btnH2))) {
static bool s_skipEncConfirm = false;
if (!s_skipEncConfirm) {
// Skipping stores private keys UNENCRYPTED — require a confirming second click.
s_skipEncConfirm = true;
encrypt_status_ = TR("wiz_skip_confirm");
} else {
s_skipEncConfirm = false;
if (ImGui::Button("Skip##enc", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
@@ -1383,13 +1340,12 @@ void App::renderFirstRunWizard() {
}
tryConnect();
}
}
ImGui::PopStyleVar();
cy += btnH2;
}
} else {
// ---- Not focused: show static description ----
const char* encDesc = TR("wiz_encrypt_desc");
const char* encDesc = "Encrypt your wallet to protect private keys with a passphrase.";
ImVec2 edSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, encDesc);
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), dimCol, encDesc, nullptr, contentW);
cy += edSize.y + 6.0f * dp;

View File

@@ -1,173 +0,0 @@
// DragonX Wallet - HushChat crypto primitives (implementation).
#include "chat_crypto.h"
#include <sodium.h>
#include <cstring>
#include <vector>
namespace dragonx::chat {
namespace {
// Local constants tied to the libsodium primitive. (The dev-only chat_fixture_tooling.h
// declares equivalents, but that header is not linked into the app.)
constexpr std::size_t kStreamHeaderBytes = 24; // crypto_secretstream_xchacha20poly1305_HEADERBYTES
constexpr std::size_t kStreamABytes = 17; // crypto_secretstream_xchacha20poly1305_ABYTES
static_assert(kChatKeyBytes == crypto_kx_PUBLICKEYBYTES, "kx public key size mismatch");
static_assert(kChatKeyBytes == crypto_kx_SECRETKEYBYTES, "kx secret key size mismatch");
// Decode exactly outLen bytes from a lowercase/uppercase hex string; reject any other length.
bool hexToFixed(const std::string& hex, unsigned char* out, std::size_t outLen) {
if (hex.size() != outLen * 2) return false;
std::size_t binLen = 0;
if (sodium_hex2bin(out, outLen, hex.data(), hex.size(), nullptr, &binLen, nullptr) != 0) {
return false;
}
return binLen == outLen;
}
// Decode a variable-length hex string into bytes.
bool hexToBytes(const std::string& hex, std::vector<unsigned char>& out) {
if (hex.empty() || (hex.size() % 2) != 0) return false;
out.resize(hex.size() / 2);
std::size_t binLen = 0;
if (sodium_hex2bin(out.data(), out.size(), hex.data(), hex.size(), nullptr, &binLen, nullptr) != 0) {
return false;
}
out.resize(binLen);
return true;
}
std::string bytesToHex(const unsigned char* bytes, std::size_t n) {
std::string hex(n * 2 + 1, '\0');
sodium_bin2hex(&hex[0], hex.size(), bytes, n);
hex.resize(n * 2); // drop the NUL sodium_bin2hex appends
return hex;
}
} // namespace
const char* chatCryptoStatusName(ChatCryptoStatus status) {
switch (status) {
case ChatCryptoStatus::Ok: return "Ok";
case ChatCryptoStatus::SodiumInitFailed: return "SodiumInitFailed";
case ChatCryptoStatus::BadPeerKey: return "BadPeerKey";
case ChatCryptoStatus::BadHeaderHex: return "BadHeaderHex";
case ChatCryptoStatus::BadCiphertextHex: return "BadCiphertextHex";
case ChatCryptoStatus::CiphertextTooShort: return "CiphertextTooShort";
case ChatCryptoStatus::SessionKeyFailed: return "SessionKeyFailed";
case ChatCryptoStatus::EncryptFailed: return "EncryptFailed";
case ChatCryptoStatus::DecryptFailed: return "DecryptFailed";
}
return "Unknown";
}
void wipeChatKeyPair(ChatKeyPair& keys) {
sodium_memzero(keys.public_key.data(), keys.public_key.size());
sodium_memzero(keys.secret_key.data(), keys.secret_key.size());
}
ChatCryptoStatus encryptOutgoing(const ChatKeyPair& mine,
const std::string& peerPublicKeyHex,
const std::string& plaintext,
std::string& outStreamHeaderHex,
std::string& outCiphertextHex) {
static_assert(kStreamHeaderBytes == crypto_secretstream_xchacha20poly1305_HEADERBYTES, "");
static_assert(kStreamABytes == crypto_secretstream_xchacha20poly1305_ABYTES, "");
if (sodium_init() < 0) return ChatCryptoStatus::SodiumInitFailed;
unsigned char peerPk[crypto_kx_PUBLICKEYBYTES];
if (!hexToFixed(peerPublicKeyHex, peerPk, sizeof peerPk)) return ChatCryptoStatus::BadPeerKey;
unsigned char rx[crypto_kx_SESSIONKEYBYTES];
unsigned char tx[crypto_kx_SESSIONKEYBYTES];
if (crypto_kx_server_session_keys(rx, tx, mine.public_key.data(), mine.secret_key.data(), peerPk) != 0) {
sodium_memzero(rx, sizeof rx);
sodium_memzero(tx, sizeof tx);
return ChatCryptoStatus::SessionKeyFailed;
}
crypto_secretstream_xchacha20poly1305_state state;
unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES];
ChatCryptoStatus result = ChatCryptoStatus::EncryptFailed;
if (crypto_secretstream_xchacha20poly1305_init_push(&state, header, tx) == 0) {
std::vector<unsigned char> ciphertext(plaintext.size() + crypto_secretstream_xchacha20poly1305_ABYTES);
unsigned long long ctLen = 0;
// Only report Ok if the push actually succeeded — otherwise ctLen stays 0 and we would
// ship a valid header with an empty ciphertext.
if (crypto_secretstream_xchacha20poly1305_push(
&state, ciphertext.data(), &ctLen,
reinterpret_cast<const unsigned char*>(plaintext.data()), plaintext.size(),
nullptr, 0, crypto_secretstream_xchacha20poly1305_TAG_FINAL) == 0) {
outStreamHeaderHex = bytesToHex(header, sizeof header);
outCiphertextHex = bytesToHex(ciphertext.data(), static_cast<std::size_t>(ctLen));
result = ChatCryptoStatus::Ok;
}
}
sodium_memzero(rx, sizeof rx);
sodium_memzero(tx, sizeof tx);
sodium_memzero(&state, sizeof state);
return result;
}
ChatCryptoStatus decryptIncoming(const ChatKeyPair& mine,
const std::string& peerPublicKeyHex,
const std::string& streamHeaderHex,
const std::string& ciphertextHex,
std::string& outPlaintext) {
if (sodium_init() < 0) return ChatCryptoStatus::SodiumInitFailed;
unsigned char peerPk[crypto_kx_PUBLICKEYBYTES];
if (!hexToFixed(peerPublicKeyHex, peerPk, sizeof peerPk)) return ChatCryptoStatus::BadPeerKey;
unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES];
if (!hexToFixed(streamHeaderHex, header, sizeof header)) return ChatCryptoStatus::BadHeaderHex;
std::vector<unsigned char> ciphertext;
if (!hexToBytes(ciphertextHex, ciphertext)) return ChatCryptoStatus::BadCiphertextHex;
// Guard the size_t subtraction below (a ciphertext shorter than the auth tag can't be
// authentic). Exactly ABYTES is the valid empty-plaintext case, so it round-trips
// symmetrically with encryptOutgoing (message-content policy belongs to the caller).
if (ciphertext.size() < crypto_secretstream_xchacha20poly1305_ABYTES) {
return ChatCryptoStatus::CiphertextTooShort;
}
unsigned char rx[crypto_kx_SESSIONKEYBYTES];
unsigned char tx[crypto_kx_SESSIONKEYBYTES];
if (crypto_kx_client_session_keys(rx, tx, mine.public_key.data(), mine.secret_key.data(), peerPk) != 0) {
sodium_memzero(rx, sizeof rx);
sodium_memzero(tx, sizeof tx);
return ChatCryptoStatus::SessionKeyFailed;
}
crypto_secretstream_xchacha20poly1305_state state;
ChatCryptoStatus result = ChatCryptoStatus::DecryptFailed;
if (crypto_secretstream_xchacha20poly1305_init_pull(&state, header, rx) == 0) {
std::vector<unsigned char> plain(ciphertext.size() - crypto_secretstream_xchacha20poly1305_ABYTES);
unsigned long long plainLen = 0;
unsigned char tag = 0;
if (crypto_secretstream_xchacha20poly1305_pull(
&state, plain.data(), &plainLen, &tag,
ciphertext.data(), ciphertext.size(), nullptr, 0) == 0 &&
tag == crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
outPlaintext.assign(reinterpret_cast<const char*>(plain.data()),
static_cast<std::size_t>(plainLen));
result = ChatCryptoStatus::Ok;
}
sodium_memzero(plain.data(), plain.size()); // wipe the decrypted scratch
}
sodium_memzero(rx, sizeof rx);
sodium_memzero(tx, sizeof tx);
sodium_memzero(&state, sizeof state);
return result;
}
} // namespace dragonx::chat

View File

@@ -1,65 +0,0 @@
#pragma once
// DragonX Wallet - HushChat crypto primitives.
//
// crypto_kx (X25519) session-key agreement + crypto_secretstream_xchacha20poly1305
// message encryption, byte-exact per the HushChat wire format so DragonX interoperates
// with SilentDragonXLite. See docs/_archive/contacts-chat-phase1-detail-2026-07-05.md
// (Appendix A.3/A.4). Pure crypto — no gating, no I/O; the feature gate lives at the
// service layer. NEVER logs plaintext, ciphertext, keys, or session material.
#include <array>
#include <cstddef>
#include <string>
namespace dragonx::chat {
// crypto_kx key sizes (== crypto_kx_PUBLICKEYBYTES / SECRETKEYBYTES == 32).
constexpr std::size_t kChatKeyBytes = 32;
using ChatPublicKey = std::array<unsigned char, kChatKeyBytes>;
using ChatSecretKey = std::array<unsigned char, kChatKeyBytes>;
struct ChatKeyPair {
ChatPublicKey public_key{};
ChatSecretKey secret_key{};
};
enum class ChatCryptoStatus {
Ok,
SodiumInitFailed,
BadPeerKey, // peer public-key hex missing / wrong length / not hex
BadHeaderHex, // secretstream header hex missing / wrong length / not hex
BadCiphertextHex, // ciphertext hex malformed
CiphertextTooShort, // ciphertext shorter than the auth tag
SessionKeyFailed, // crypto_kx_*_session_keys rejected the peer key
EncryptFailed,
DecryptFailed // init_pull / pull / tag mismatch — the single neutral auth failure
};
const char* chatCryptoStatusName(ChatCryptoStatus status);
// Encrypt `plaintext` addressed to peer `peerPublicKeyHex` (64 lowercase hex chars).
// Sender takes the crypto_kx "server" role (server_tx), matching SDXL's send path.
// Outputs the secretstream header hex (the memo "e" field) and the ciphertext hex
// (the payload memo). Returns Ok on success.
ChatCryptoStatus encryptOutgoing(const ChatKeyPair& mine,
const std::string& peerPublicKeyHex,
const std::string& plaintext,
std::string& outStreamHeaderHex,
std::string& outCiphertextHex);
// Decrypt an incoming message addressed to us from peer `peerPublicKeyHex`.
// Receiver takes the crypto_kx "client" role (client_rx), matching SDXL's receive path.
// Requires the memo "e" (streamHeaderHex) and the payload ciphertext hex. Enforces the
// Poly1305 auth tag AND that the stream tag is TAG_FINAL. Returns Ok + fills outPlaintext.
ChatCryptoStatus decryptIncoming(const ChatKeyPair& mine,
const std::string& peerPublicKeyHex,
const std::string& streamHeaderHex,
const std::string& ciphertextHex,
std::string& outPlaintext);
// Zero both key arrays (call when discarding an identity's keys).
void wipeChatKeyPair(ChatKeyPair& keys);
} // namespace dragonx::chat

View File

@@ -1,373 +0,0 @@
// DragonX Wallet - HushChat persistent message store (implementation).
#include "chat_database.h"
#include "../util/logger.h"
#include "../util/platform.h"
#include <nlohmann/json.hpp>
#include <sodium.h>
#include <sqlite3.h>
#include <cstdint>
#include <filesystem>
#include <utility>
namespace fs = std::filesystem;
namespace dragonx::chat {
namespace {
// Domain-separated KDF contexts (used as the keyed-BLAKE2b key, like chat_identity). Both lengths
// sit inside crypto_generichash's key-length bounds. Bumping a context rotates that derivation.
constexpr char kStorageKeyContext[] = "DragonX-HushChat-Storage-v1";
constexpr char kWalletTagContext[] = "DragonX-HushChat-WalletId-v1";
constexpr std::size_t kStorageKeyContextLen = sizeof(kStorageKeyContext) - 1;
constexpr std::size_t kWalletTagContextLen = sizeof(kWalletTagContext) - 1;
std::string toHex(const unsigned char* data, std::size_t len)
{
static const char* kHex = "0123456789abcdef";
std::string out;
out.reserve(len * 2);
for (std::size_t i = 0; i < len; ++i) {
out.push_back(kHex[data[i] >> 4]);
out.push_back(kHex[data[i] & 0x0F]);
}
return out;
}
// keyed-BLAKE2b: out = generichash(in=secret, key=context). Deterministic, so the same seed always
// derives the same storage key + wallet tag across sessions.
bool deriveKeyed(const std::string& secret, const char* context, std::size_t contextLen,
unsigned char* out, std::size_t outLen)
{
return crypto_generichash(out, outLen,
reinterpret_cast<const unsigned char*>(secret.data()), secret.size(),
reinterpret_cast<const unsigned char*>(context), contextLen) == 0;
}
std::string associatedData(const std::string& walletTag)
{
return std::string("obsidian-dragon-hushchat-v1:") + walletTag;
}
} // namespace
ChatDatabase::ChatDatabase() : database_path_(defaultDatabasePath()) {}
ChatDatabase::ChatDatabase(std::string databasePath) : database_path_(std::move(databasePath)) {}
ChatDatabase::~ChatDatabase()
{
lock();
close();
}
std::string ChatDatabase::defaultDatabasePath()
{
return (fs::path(util::Platform::getConfigDir()) / "chat_messages.sqlite").string();
}
bool ChatDatabase::unlockWithSecret(const std::string& secret)
{
if (sodium_init() < 0) return false;
if (!deriveKeyed(secret, kStorageKeyContext, kStorageKeyContextLen, key_.data(), key_.size()))
return false;
unsigned char tag[32];
if (!deriveKeyed(secret, kWalletTagContext, kWalletTagContextLen, tag, sizeof(tag))) {
sodium_memzero(key_.data(), key_.size());
return false;
}
wallet_tag_ = toHex(tag, sizeof(tag));
sodium_memzero(tag, sizeof(tag));
key_ready_ = true;
if (!ensureOpen()) {
lock();
return false;
}
return true;
}
void ChatDatabase::lock()
{
sodium_memzero(key_.data(), key_.size());
key_ready_ = false;
wallet_tag_.clear();
}
bool ChatDatabase::append(const ChatMessage& message)
{
if (!key_ready_ || !ensureOpen()) return false;
std::vector<unsigned char> nonce;
std::vector<unsigned char> cipher;
std::string plain = serialize(message); // full plaintext (decrypted body + metadata)
const bool encrypted = encrypt(plain, nonce, cipher);
if (!plain.empty()) sodium_memzero(&plain[0], plain.size()); // don't leave it on the heap
if (!encrypted) return false;
const std::string dedup = dedupHash(message.txid, message.payload_position);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_,
"INSERT OR IGNORE INTO chat_messages (wallet_tag, dedup_hash, nonce, payload) "
"VALUES (?, ?, ?, ?)",
-1, &stmt, nullptr) != SQLITE_OK) {
return false;
}
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, dedup.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 3, nonce.data(), static_cast<int>(nonce.size()), SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 4, cipher.data(), static_cast<int>(cipher.size()), SQLITE_TRANSIENT);
const bool done = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
if (!done) return false;
return sqlite3_changes(db_) > 0;
}
bool ChatDatabase::upsert(const ChatMessage& message)
{
if (!key_ready_ || !ensureOpen()) return false;
std::vector<unsigned char> nonce;
std::vector<unsigned char> cipher;
std::string plain = serialize(message);
const bool encrypted = encrypt(plain, nonce, cipher);
if (!plain.empty()) sodium_memzero(&plain[0], plain.size());
if (!encrypted) return false;
const std::string dedup = dedupHash(message.txid, message.payload_position);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_,
"INSERT INTO chat_messages (wallet_tag, dedup_hash, nonce, payload) VALUES (?, ?, ?, ?) "
"ON CONFLICT(wallet_tag, dedup_hash) DO UPDATE SET nonce=excluded.nonce, payload=excluded.payload",
-1, &stmt, nullptr) != SQLITE_OK) {
return false;
}
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, dedup.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 3, nonce.data(), static_cast<int>(nonce.size()), SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 4, cipher.data(), static_cast<int>(cipher.size()), SQLITE_TRANSIENT);
const bool done = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return done;
}
std::vector<ChatMessage> ChatDatabase::load()
{
std::vector<ChatMessage> out;
if (!key_ready_ || !ensureOpen()) return out;
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_,
"SELECT nonce, payload FROM chat_messages WHERE wallet_tag = ? ORDER BY rowid",
-1, &stmt, nullptr) != SQLITE_OK) {
return out;
}
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
while (sqlite3_step(stmt) == SQLITE_ROW) {
const auto* noncePtr = static_cast<const unsigned char*>(sqlite3_column_blob(stmt, 0));
const int nonceLen = sqlite3_column_bytes(stmt, 0);
const auto* cipherPtr = static_cast<const unsigned char*>(sqlite3_column_blob(stmt, 1));
const int cipherLen = sqlite3_column_bytes(stmt, 1);
if (!noncePtr || !cipherPtr) continue;
std::vector<unsigned char> nonce(noncePtr, noncePtr + nonceLen);
std::vector<unsigned char> cipher(cipherPtr, cipherPtr + cipherLen);
std::string plain;
if (!decrypt(nonce, cipher, plain)) continue; // wrong wallet / tampered — skip
ChatMessage message;
if (deserialize(plain, message)) out.push_back(std::move(message));
sodium_memzero(&plain[0], plain.size());
}
sqlite3_finalize(stmt);
return out;
}
void ChatDatabase::clearWallet()
{
if (wallet_tag_.empty() || !ensureOpen()) return;
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_, "DELETE FROM chat_messages WHERE wallet_tag = ?", -1, &stmt, nullptr)
!= SQLITE_OK) {
return;
}
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
bool ChatDatabase::ensureOpen()
{
if (db_) return true;
try {
fs::path path(database_path_);
if (!path.parent_path().empty()) fs::create_directories(path.parent_path());
} catch (const std::exception& exception) {
DEBUG_LOGF("Failed to create chat database directory: %s\n", exception.what());
return false;
}
sqlite3* openedDb = nullptr;
if (sqlite3_open(database_path_.c_str(), &openedDb) != SQLITE_OK) {
DEBUG_LOGF("Failed to open chat database: %s\n",
openedDb ? sqlite3_errmsg(openedDb) : "unknown error");
if (openedDb) sqlite3_close(openedDb);
return false;
}
db_ = openedDb;
sqlite3_busy_timeout(db_, 2000);
exec("PRAGMA journal_mode=WAL");
exec("PRAGMA synchronous=NORMAL");
// C3-1: restrict the chat DB and its WAL/SHM sidecars to owner-only. sqlite creates them with
// umask-derived permissions (often world/group-readable); they hold per-row nonces + AEAD
// ciphertext of the user's messages. Best-effort (errors swallowed; a no-op-ish on Windows).
{
std::error_code perr;
const auto ownerOnly = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write;
std::filesystem::permissions(database_path_, ownerOnly, std::filesystem::perm_options::replace, perr);
std::filesystem::permissions(database_path_ + "-wal", ownerOnly, std::filesystem::perm_options::replace, perr);
std::filesystem::permissions(database_path_ + "-shm", ownerOnly, std::filesystem::perm_options::replace, perr);
}
if (!createSchema()) {
close();
return false;
}
return true;
}
bool ChatDatabase::exec(const char* sql)
{
if (!db_) return false;
char* error = nullptr;
if (sqlite3_exec(db_, sql, nullptr, nullptr, &error) != SQLITE_OK) {
DEBUG_LOGF("Chat database SQL error: %s\n", error ? error : sqlite3_errmsg(db_));
if (error) sqlite3_free(error);
return false;
}
return true;
}
bool ChatDatabase::createSchema()
{
return exec("CREATE TABLE IF NOT EXISTS chat_messages ("
"wallet_tag TEXT NOT NULL, "
"dedup_hash TEXT NOT NULL, "
"nonce BLOB NOT NULL, "
"payload BLOB NOT NULL, "
"PRIMARY KEY (wallet_tag, dedup_hash))");
}
std::string ChatDatabase::dedupHash(const std::string& txid, std::size_t position) const
{
const std::string input = txid + ":" + std::to_string(position);
unsigned char hash[32];
crypto_generichash(hash, sizeof(hash),
reinterpret_cast<const unsigned char*>(input.data()), input.size(),
key_.data(), key_.size()); // keyed by the storage key → txid stays private
return toHex(hash, sizeof(hash));
}
std::string ChatDatabase::serialize(const ChatMessage& message) const
{
nlohmann::json json;
json["d"] = static_cast<int>(message.direction);
json["k"] = static_cast<int>(message.kind);
json["txid"] = message.txid;
json["cid"] = message.conversation_id;
json["z"] = message.peer_zaddr;
json["p"] = message.peer_public_key_hex;
json["b"] = message.body;
json["ts"] = message.timestamp;
json["pos"] = static_cast<std::uint64_t>(message.payload_position);
json["dl"] = static_cast<int>(message.delivery);
return json.dump();
}
bool ChatDatabase::deserialize(const std::string& json, ChatMessage& out) const
{
try {
const auto parsed = nlohmann::json::parse(json);
out.direction = static_cast<ChatDirection>(parsed.value("d", 0));
out.kind = static_cast<ChatMessageKind>(parsed.value("k", 0));
out.txid = parsed.value("txid", std::string());
out.conversation_id = parsed.value("cid", std::string());
out.peer_zaddr = parsed.value("z", std::string());
out.peer_public_key_hex = parsed.value("p", std::string());
out.body = parsed.value("b", std::string());
out.timestamp = parsed.value("ts", static_cast<std::int64_t>(0));
out.payload_position = static_cast<std::size_t>(parsed.value("pos", static_cast<std::uint64_t>(0)));
out.delivery = static_cast<ChatDelivery>(parsed.value("dl", 0)); // old rows → Sent (0)
// A persisted "Sending" means we crashed mid-broadcast; the outcome is unknown. Resolve it
// optimistically to Sent on load so it can't show a stuck spinner forever.
if (out.delivery == ChatDelivery::Sending) out.delivery = ChatDelivery::Sent;
return true;
} catch (const std::exception&) {
return false;
}
}
bool ChatDatabase::encrypt(const std::string& plain,
std::vector<unsigned char>& nonce,
std::vector<unsigned char>& cipher) const
{
if (!key_ready_) return false;
nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
randombytes_buf(nonce.data(), nonce.size());
const std::string ad = associatedData(wallet_tag_);
cipher.resize(plain.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES);
unsigned long long cipherLen = 0;
if (crypto_aead_xchacha20poly1305_ietf_encrypt(
cipher.data(), &cipherLen,
reinterpret_cast<const unsigned char*>(plain.data()), plain.size(),
reinterpret_cast<const unsigned char*>(ad.data()), ad.size(),
nullptr, nonce.data(), key_.data()) != 0) {
return false;
}
cipher.resize(static_cast<std::size_t>(cipherLen));
return true;
}
bool ChatDatabase::decrypt(const std::vector<unsigned char>& nonce,
const std::vector<unsigned char>& cipher,
std::string& plain) const
{
if (!key_ready_) return false;
if (nonce.size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES) return false;
if (cipher.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) return false;
const std::string ad = associatedData(wallet_tag_);
std::vector<unsigned char> out(cipher.size());
unsigned long long outLen = 0;
if (crypto_aead_xchacha20poly1305_ietf_decrypt(
out.data(), &outLen, nullptr,
cipher.data(), cipher.size(),
reinterpret_cast<const unsigned char*>(ad.data()), ad.size(),
nonce.data(), key_.data()) != 0) {
return false;
}
plain.assign(reinterpret_cast<const char*>(out.data()), static_cast<std::size_t>(outLen));
sodium_memzero(out.data(), out.size());
return true;
}
void ChatDatabase::close()
{
if (db_) {
sqlite3_close(db_);
db_ = nullptr;
}
}
} // namespace dragonx::chat

View File

@@ -1,79 +0,0 @@
#pragma once
// DragonX Wallet - HushChat persistent message store (Phase 2).
//
// Sqlite-backed, encrypted at rest with a SEED-DERIVED key (no wallet passphrase). Every record —
// message bodies, peer z-addresses, threading (conversation id), and timestamps — is AEAD-encrypted
// under a key derived from the wallet's own seed secret (the same secret used for the chat
// identity), and even the per-message dedup key is a KEYED hash of the txid — so the database
// reveals nothing about your conversations to disk-level access without the seed. Rows are
// partitioned by a seed-derived wallet tag so one file can hold several wallets, each readable only
// with its own seed. Not thread-safe — drive from the main thread. Mirrors the lifecycle of
// data::TransactionHistoryCache.
#include "chat_message.h"
#include <array>
#include <cstddef>
#include <string>
#include <vector>
struct sqlite3;
namespace dragonx::chat {
class ChatDatabase {
public:
ChatDatabase();
explicit ChatDatabase(std::string databasePath);
~ChatDatabase();
ChatDatabase(const ChatDatabase&) = delete;
ChatDatabase& operator=(const ChatDatabase&) = delete;
static std::string defaultDatabasePath();
// Derive the storage key + wallet tag from the wallet's seed secret and open the DB. The caller
// still owns and must wipe `secret`. Returns false on sodium/db failure (DB then stays locked).
bool unlockWithSecret(const std::string& secret);
void lock(); // wipe the key material (DB handle stays open); load()/append() then no-op
bool hasKey() const { return key_ready_; }
// Persist one message (INSERT OR IGNORE, deduped by a keyed hash of txid+payload_position).
// Returns true if newly inserted; false on duplicate or while locked.
bool append(const ChatMessage& message);
// Persist-or-overwrite one message by its (txid+position) dedup key. Unlike append(), this
// updates an existing row's payload — used for outgoing echoes whose delivery status changes
// (Sending → Sent/Failed). Returns true on success; false while locked / on error.
bool upsert(const ChatMessage& message);
// Decrypt and return every stored message for the unlocked wallet, in insertion order. Empty
// while locked or if none. Rows that fail to decrypt/parse are skipped.
std::vector<ChatMessage> load();
void clearWallet(); // delete the unlocked wallet's rows
private:
bool ensureOpen();
bool exec(const char* sql);
bool createSchema();
std::string dedupHash(const std::string& txid, std::size_t position) const;
std::string serialize(const ChatMessage& message) const;
bool deserialize(const std::string& json, ChatMessage& out) const;
bool encrypt(const std::string& plain,
std::vector<unsigned char>& nonce,
std::vector<unsigned char>& cipher) const;
bool decrypt(const std::vector<unsigned char>& nonce,
const std::vector<unsigned char>& cipher,
std::string& plain) const;
void close();
sqlite3* db_ = nullptr;
std::string database_path_;
std::array<unsigned char, 32> key_{}; // AEAD storage key (seed-derived)
std::string wallet_tag_; // seed-derived row partition (a keyed hash, hex)
bool key_ready_ = false;
};
} // namespace dragonx::chat

File diff suppressed because it is too large Load Diff

View File

@@ -1,505 +0,0 @@
#pragma once
#include "chat_protocol.h"
#include <cstddef>
#include <string>
#include <vector>
// HushChat compatibility-fixture / capture-manifest / seed-projection tooling.
//
// Everything in this header is DEAD AT RUNTIME in the shipping app and test
// binaries — its only caller is the standalone dev CLI
// tools/hushchat_fixture_check.cpp. It is deliberately compiled ONLY into the
// HushChatFixtureCheck target so this validation scaffolding (and its libsodium
// seed-projection path) never reaches the wallet binary. Keep it that way: do
// not add chat_fixture_tooling.cpp to APP_SOURCES or the test target.
//
// It builds on the runtime types declared in chat_protocol.h.
namespace dragonx::chat {
enum class HushChatDecryptPreflightError {
None,
FeatureDisabled,
NonMessageHeader,
InvalidHeaderNumber,
UnsupportedVersion,
MissingReplyAddress,
MissingConversationId,
InvalidSecretstreamHeader,
InvalidPublicKey,
EmptyCiphertext,
OversizedCiphertext,
OddLengthCiphertext,
InvalidCiphertextHex,
TruncatedCiphertext
};
struct HushChatDecryptPreflightInput {
HushChatHeader header;
std::string ciphertext_hex;
};
struct HushChatDecryptPreflightResult {
bool ok = false;
bool feature_enabled = false;
HushChatDecryptPreflightError error = HushChatDecryptPreflightError::None;
const char* error_name = "None";
std::size_t ciphertext_size = 0;
};
enum class HushChatHexDecodeError {
None,
Empty,
OddLength,
InvalidHex,
UnexpectedByteLength
};
struct HushChatHexDecodeResult {
bool ok = false;
HushChatHexDecodeError error = HushChatHexDecodeError::None;
const char* error_name = "None";
std::vector<unsigned char> bytes;
};
enum class HushChatDecryptDirection {
Incoming,
Outgoing
};
enum class HushChatSessionKeySelection {
ClientRx,
ServerTx
};
enum class HushChatDecryptInputError {
None,
FeatureDisabled,
InvalidStoredChatKey,
DecryptPreflightFailed,
InvalidPeerPublicKey,
InvalidStreamHeader,
InvalidCiphertext
};
struct HushChatDecryptInputMaterial {
std::string stored_chat_key_hex;
HushChatHeader header;
std::string ciphertext_hex;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
std::string peer_public_key_hex;
};
struct HushChatPreparedDecryptInput {
std::vector<unsigned char> stored_chat_key_bytes;
std::vector<unsigned char> seed_bytes;
std::vector<unsigned char> peer_public_key_bytes;
std::vector<unsigned char> stream_header_bytes;
std::vector<unsigned char> ciphertext_bytes;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
std::size_t plaintext_capacity = 0;
};
struct HushChatDecryptInputPreparationResult {
bool ok = false;
bool feature_enabled = false;
HushChatDecryptInputError error = HushChatDecryptInputError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
HushChatPreparedDecryptInput prepared;
};
struct HushChatDecryptFixtureReadinessResult {
bool ready = false;
std::size_t stored_chat_key_size = 0;
std::size_t seed_size = 0;
std::size_t peer_public_key_size = 0;
std::size_t stream_header_size = 0;
std::size_t ciphertext_size = 0;
std::size_t plaintext_capacity = 0;
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
};
enum class HushChatCompatibilityFixtureError {
None,
FeatureDisabled,
MissingFixtureId,
InvalidLocalPublicKey,
InvalidPeerPublicKey,
InvalidHeaderMemo,
InvalidMemoPair,
NonMemoHeader,
HeaderPublicKeyMismatch,
DecryptInputFailed,
NotFixtureReady,
ExpectedStoredChatKeyLengthMismatch,
ExpectedSeedLengthMismatch,
ExpectedLocalPublicKeyLengthMismatch,
ExpectedPeerPublicKeyLengthMismatch,
ExpectedStreamHeaderLengthMismatch,
ExpectedCiphertextLengthMismatch,
ExpectedPlaintextLengthMismatch,
ExpectedRoleMismatch,
InvalidPlaintextHash
};
struct HushChatCompatibilityFixture {
std::string fixture_id;
std::string stored_chat_key_hex;
std::string local_public_key_hex;
std::string peer_public_key_hex;
std::string header_memo;
std::string ciphertext_memo;
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
HushChatSessionKeySelection expected_session_key_selection = HushChatSessionKeySelection::ClientRx;
std::size_t expected_stored_chat_key_size = 32;
std::size_t expected_seed_size = 32;
std::size_t expected_local_public_key_size = 32;
std::size_t expected_peer_public_key_size = 32;
std::size_t expected_stream_header_size = 24;
std::size_t expected_ciphertext_size = 0;
std::size_t expected_plaintext_size = 0;
std::string expected_plaintext_hash_hex;
};
struct HushChatCompatibilityFixtureVerificationResult {
bool ok = false;
bool feature_enabled = false;
HushChatCompatibilityFixtureError error = HushChatCompatibilityFixtureError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
HushChatDecryptInputError decrypt_input_error = HushChatDecryptInputError::None;
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
HushChatHeader header;
HushChatDecryptInputPreparationResult preparation;
HushChatDecryptFixtureReadinessResult readiness;
std::size_t local_public_key_size = 0;
std::size_t peer_public_key_size = 0;
std::size_t plaintext_hash_size = 0;
};
enum class HushChatCompatibilityFixtureKind {
IncomingMemo,
OutgoingMemo,
SeedPublicKeyProjection,
CorruptedAuthFailure,
ContactExclusion
};
enum class HushChatCompatibilityFixtureFileStatus {
Pending,
Ready
};
enum class HushChatCompatibilityFixtureFileError {
None,
FeatureDisabled,
InvalidJson,
JsonNotObject,
InvalidSchema,
MissingKind,
UnknownKind,
MissingStatus,
UnknownStatus,
MissingFixtureId,
MissingPendingReason,
MissingFixtureObject,
InvalidFixtureField,
FixtureVerificationFailed,
ContactFixtureNotExcluded,
FileReadFailed
};
struct HushChatCompatibilityFixtureFile {
std::string schema;
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureFileStatus status = HushChatCompatibilityFixtureFileStatus::Pending;
std::string fixture_id;
std::string pending_reason;
HushChatCompatibilityFixture fixture;
};
struct HushChatCompatibilityFixtureFileParseResult {
bool ok = false;
bool feature_enabled = false;
bool pending = false;
bool verified = false;
bool excluded_from_decrypt = false;
HushChatCompatibilityFixtureFileError error = HushChatCompatibilityFixtureFileError::None;
const char* error_name = "None";
HushChatCompatibilityFixtureFile file;
HushChatCompatibilityFixtureVerificationResult verification;
};
enum class HushChatSeedPublicKeyProjectionError {
None,
FeatureDisabled,
MissingFixtureId,
InvalidStoredChatKey,
InvalidLocalPublicKey,
ExpectedStoredChatKeyLengthMismatch,
ExpectedSeedLengthMismatch,
ExpectedLocalPublicKeyLengthMismatch,
SodiumInitializationFailed,
KeypairProjectionFailed,
ProjectedPublicKeyMismatch
};
struct HushChatSeedPublicKeyProjectionResult {
bool ok = false;
bool feature_enabled = false;
HushChatSeedPublicKeyProjectionError error = HushChatSeedPublicKeyProjectionError::None;
const char* error_name = "None";
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
std::size_t stored_chat_key_size = 0;
std::size_t seed_size = 0;
std::size_t local_public_key_size = 0;
std::size_t projected_public_key_size = 0;
};
enum class HushChatCorruptedAuthFailureReadinessError {
None,
FeatureDisabled,
FixturePending,
WrongFixtureKind,
FixtureNotVerified,
SeedProjectionNotVerified
};
struct HushChatCorruptedAuthFailureReadinessResult {
bool ok = false;
bool feature_enabled = false;
bool structurally_ready_for_future_auth_check = false;
bool requires_future_secretstream_auth_failure = false;
bool decrypted = false;
bool authenticated = false;
HushChatCorruptedAuthFailureReadinessError error = HushChatCorruptedAuthFailureReadinessError::None;
const char* error_name = "None";
};
enum class HushChatCompatibilityFixtureImportError {
None,
FeatureDisabled,
MissingRequiredKind,
DuplicateKind,
FixtureLoadFailed,
FixtureKindMismatch,
FixturePending,
FixtureInvalid,
FixtureNotVerified,
SeedProjectionFailed,
AuthFailureScaffoldFailed,
ContactFixtureNotExcluded
};
struct HushChatCompatibilityFixtureImportCandidate {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
};
struct HushChatCompatibilityFixtureImportItem {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
bool supplied = false;
bool pending = false;
bool replacement_eligible = false;
bool seed_projection_verified = false;
bool future_auth_failure_required = false;
bool structurally_ready_for_future_auth_check = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
HushChatCompatibilityFixtureFileParseResult parsed;
HushChatSeedPublicKeyProjectionResult seed_projection;
HushChatCorruptedAuthFailureReadinessResult auth_failure_readiness;
};
struct HushChatCompatibilityFixtureImportChecklistResult {
bool ok = false;
bool feature_enabled = false;
bool replacement_ready = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
std::size_t required_count = 0;
std::size_t supplied_count = 0;
std::size_t missing_count = 0;
std::size_t pending_count = 0;
std::size_t verified_count = 0;
std::size_t seed_projection_verified_count = 0;
std::size_t future_auth_failure_required_count = 0;
std::size_t auth_failure_structural_ready_count = 0;
std::size_t excluded_count = 0;
std::size_t rejected_count = 0;
std::vector<HushChatCompatibilityFixtureImportItem> items;
};
struct HushChatCompatibilityFixtureReplacementReportItem {
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string path;
bool supplied = false;
bool pending = false;
bool replacement_eligible = false;
bool refused = true;
bool seed_projection_verified = false;
bool future_auth_failure_required = false;
bool structurally_ready_for_future_auth_check = false;
bool cont_excluded = false;
bool decrypted = false;
bool authenticated = false;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
};
struct HushChatCompatibilityFixtureReplacementDryRunResult {
bool ok = false;
bool feature_enabled = false;
bool dry_run_only = true;
bool redacted_report = true;
bool would_replace = false;
bool replacement_refused = true;
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
const char* error_name = "None";
std::size_t required_count = 0;
std::size_t supplied_count = 0;
std::size_t missing_count = 0;
std::size_t pending_count = 0;
std::size_t verified_count = 0;
std::size_t seed_projection_verified_count = 0;
std::size_t future_auth_failure_required_count = 0;
std::size_t auth_failure_structural_ready_count = 0;
std::size_t excluded_count = 0;
std::size_t rejected_count = 0;
std::vector<HushChatCompatibilityFixtureReplacementReportItem> report_items;
};
enum class HushChatCaptureManifestError {
None,
FeatureDisabled,
FileReadFailed,
InvalidJson,
JsonNotObject,
InvalidSchema,
MissingManifestId,
MissingStatus,
UnknownStatus,
MissingFixtureDirectory,
MissingDryRunCommand,
InvalidDryRunCommand,
MissingProvenance,
MissingSourceClient,
InvalidSourceClient,
MissingSourceClientVersion,
MissingCaptureDate,
MissingNetwork,
MissingCaptureMethod,
MissingHandling,
MissingHandlingFlag,
HandlingFlagNotTrue,
MissingCategories,
InvalidCategoryEntry,
UnknownCategory,
DuplicateCategory,
MissingRequiredCategory,
ProhibitedFieldPresent
};
enum class HushChatCaptureManifestStatus {
Staged
};
struct HushChatCaptureManifestCategoryReport {
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
std::string staged_filename;
bool declared = false;
};
struct HushChatCaptureManifestValidationResult {
bool ok = false;
bool feature_enabled = false;
bool redacted_report = true;
bool validates_provenance_only = true;
bool no_sensitive_material_declared = false;
bool has_dry_run_command = false;
HushChatCaptureManifestError error = HushChatCaptureManifestError::None;
const char* error_name = "None";
HushChatCaptureManifestStatus status = HushChatCaptureManifestStatus::Staged;
std::string manifest_path;
std::string fixture_directory;
std::size_t required_count = 0;
std::size_t declared_count = 0;
std::size_t missing_count = 0;
std::size_t duplicate_count = 0;
std::size_t prohibited_field_count = 0;
std::size_t handling_flag_count = 0;
std::vector<HushChatCaptureManifestCategoryReport> categories;
};
constexpr std::size_t kHushChatSecretstreamABytes = 17;
constexpr std::size_t kHushChatStoredChatKeyByteLength = 32;
constexpr std::size_t kHushChatStoredChatKeyHexLength = kHushChatStoredChatKeyByteLength * 2;
constexpr std::size_t kHushChatSeedByteLength = 32;
constexpr std::size_t kHushChatPublicKeyByteLength = kHushChatPublicKeyHexLength / 2;
constexpr std::size_t kHushChatSecretstreamHeaderByteLength = kHushChatSecretstreamHeaderHexLength / 2;
constexpr const char* kHushChatCompatibilityFixtureSchema = "dragonx.hushchat.compat-fixture.v1";
constexpr const char* kHushChatCaptureManifestSchema = "dragonx.hushchat.capture-manifest.v1";
HushChatDecryptPreflightResult validateHushChatMemoDecryptPreflight(
const HushChatDecryptPreflightInput& input,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatHexDecodeResult decodeHushChatHexBytes(const std::string& hex,
std::size_t expectedByteLength);
HushChatDecryptInputPreparationResult prepareHushChatDecryptInput(
const HushChatDecryptInputMaterial& material,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatDecryptFixtureReadinessResult inspectHushChatDecryptFixtureReadiness(
const HushChatPreparedDecryptInput& prepared);
HushChatSessionKeySelection hushChatSessionKeySelectionForDirection(HushChatDecryptDirection direction);
HushChatCompatibilityFixtureVerificationResult verifyHushChatCompatibilityFixture(
const HushChatCompatibilityFixture& fixture,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureFileParseResult parseHushChatCompatibilityFixtureFile(
const std::string& jsonText,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureFileParseResult loadHushChatCompatibilityFixtureFile(
const std::string& path,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatSeedPublicKeyProjectionResult verifyHushChatSeedPublicKeyProjection(
const HushChatCompatibilityFixture& fixture,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCorruptedAuthFailureReadinessResult inspectHushChatCorruptedAuthFailureReadiness(
const HushChatCompatibilityFixtureFileParseResult& parsed,
const HushChatSeedPublicKeyProjectionResult& seedProjection,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
std::vector<HushChatCompatibilityFixtureKind> hushChatRequiredCompatibilityFixtureKinds();
HushChatCompatibilityFixtureImportChecklistResult inspectHushChatCompatibilityFixtureImportChecklist(
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCompatibilityFixtureReplacementDryRunResult inspectHushChatCompatibilityFixtureReplacementDryRun(
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCaptureManifestValidationResult validateHushChatCaptureManifest(
const std::string& jsonText,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
HushChatCaptureManifestValidationResult loadHushChatCaptureManifestFile(
const std::string& path,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
const char* hushChatDecryptPreflightErrorName(HushChatDecryptPreflightError error);
const char* hushChatHexDecodeErrorName(HushChatHexDecodeError error);
const char* hushChatDecryptDirectionName(HushChatDecryptDirection direction);
const char* hushChatSessionKeySelectionName(HushChatSessionKeySelection selection);
const char* hushChatDecryptInputErrorName(HushChatDecryptInputError error);
const char* hushChatCompatibilityFixtureErrorName(HushChatCompatibilityFixtureError error);
const char* hushChatCompatibilityFixtureKindName(HushChatCompatibilityFixtureKind kind);
const char* hushChatCompatibilityFixtureFileStatusName(HushChatCompatibilityFixtureFileStatus status);
const char* hushChatCompatibilityFixtureFileErrorName(HushChatCompatibilityFixtureFileError error);
const char* hushChatSeedPublicKeyProjectionErrorName(HushChatSeedPublicKeyProjectionError error);
const char* hushChatCorruptedAuthFailureReadinessErrorName(HushChatCorruptedAuthFailureReadinessError error);
const char* hushChatCompatibilityFixtureImportErrorName(HushChatCompatibilityFixtureImportError error);
const char* hushChatCaptureManifestErrorName(HushChatCaptureManifestError error);
} // namespace dragonx::chat

View File

@@ -1,70 +0,0 @@
// DragonX Wallet - HushChat identity derivation (implementation).
#include "chat_identity.h"
#include <sodium.h>
namespace dragonx::chat {
// The KDF context is used as the BLAKE2b key, so its length must sit within the primitive's
// key bounds.
static_assert(kChatIdentityKdfContextLen >= crypto_generichash_KEYBYTES_MIN,
"chat identity KDF context is shorter than BLAKE2b's minimum key length");
static_assert(kChatIdentityKdfContextLen <= crypto_generichash_KEYBYTES_MAX,
"chat identity KDF context is longer than BLAKE2b's maximum key length");
const char* chatIdentityStatusName(ChatIdentityStatus status) {
switch (status) {
case ChatIdentityStatus::Ready: return "Ready";
case ChatIdentityStatus::FeatureDisabled: return "FeatureDisabled";
case ChatIdentityStatus::SecretUnavailable: return "SecretUnavailable";
case ChatIdentityStatus::DerivationFailed: return "DerivationFailed";
}
return "Unknown";
}
std::string chatIdentityPublicKeyHex(const ChatKeyPair& keys) {
char hex[crypto_kx_PUBLICKEYBYTES * 2 + 1];
sodium_bin2hex(hex, sizeof hex, keys.public_key.data(), keys.public_key.size());
return std::string(hex);
}
ChatIdentityResult deriveChatIdentityFromSecret(const std::string& stableSecret,
ChatKeyPair& outKeys,
bool featureEnabled) {
ChatIdentityResult result;
auto finish = [&result](ChatIdentityStatus status) -> ChatIdentityResult {
result.status = status;
result.error_name = chatIdentityStatusName(status);
return result;
};
if (!featureEnabled) return finish(ChatIdentityStatus::FeatureDisabled);
if (stableSecret.empty()) return finish(ChatIdentityStatus::SecretUnavailable);
if (sodium_init() < 0) return finish(ChatIdentityStatus::DerivationFailed);
unsigned char kxSeed[crypto_kx_SEEDBYTES];
static_assert(sizeof(kxSeed) == kChatKeyBytes, "kx seed size mismatch");
const int hashStatus = crypto_generichash(
kxSeed, sizeof kxSeed,
reinterpret_cast<const unsigned char*>(stableSecret.data()), stableSecret.size(),
reinterpret_cast<const unsigned char*>(kChatIdentityKdfContext), kChatIdentityKdfContextLen);
if (hashStatus != 0) {
sodium_memzero(kxSeed, sizeof kxSeed);
return finish(ChatIdentityStatus::DerivationFailed);
}
const int keypairStatus =
crypto_kx_seed_keypair(outKeys.public_key.data(), outKeys.secret_key.data(), kxSeed);
sodium_memzero(kxSeed, sizeof kxSeed); // wipe the seed immediately, success or failure
if (keypairStatus != 0) {
wipeChatKeyPair(outKeys);
return finish(ChatIdentityStatus::DerivationFailed);
}
result.public_key_hex = chatIdentityPublicKeyHex(outKeys);
return finish(ChatIdentityStatus::Ready);
}
} // namespace dragonx::chat

View File

@@ -1,57 +0,0 @@
#pragma once
// DragonX Wallet - HushChat identity derivation.
//
// The DragonX-native chat identity is an X25519 (crypto_kx) keypair derived from a stable
// per-wallet secret via a domain-separated keyed BLAKE2b KDF. This deliberately does NOT
// use SDXL's UTF-8-hex-seed quirk (that quirk is only for the Phase-4 "import an existing
// SDXL identity" path). Interop is unaffected: identity derivation is local — peers only
// exchange public keys. See docs/_archive/contacts-chat-tab-plan-2026-07-05.md §5.6.
#include "chat_crypto.h" // ChatKeyPair
#include "chat_protocol.h" // hushChatFeatureEnabledAtBuild()
#include <cstddef>
#include <string>
namespace dragonx::chat {
// Domain-separation label — used as the BLAKE2b key so a different app/version cannot
// derive the same identity from the same wallet secret. A char[] (not const char*) so its
// length is a compile-time constant for the KEYBYTES-bounds static_assert.
inline constexpr char kChatIdentityKdfContext[] = "DragonX-HushChat-Identity-v1";
inline constexpr std::size_t kChatIdentityKdfContextLen = sizeof(kChatIdentityKdfContext) - 1;
enum class ChatIdentityStatus {
Ready,
FeatureDisabled, // DRAGONX_ENABLE_CHAT off (or caller passed featureEnabled=false)
SecretUnavailable, // no stable secret (wallet locked / not open / empty)
DerivationFailed // libsodium init or KDF/keypair failure
};
const char* chatIdentityStatusName(ChatIdentityStatus status);
struct ChatIdentityResult {
ChatIdentityStatus status = ChatIdentityStatus::FeatureDisabled;
std::string public_key_hex; // 64 lowercase hex chars when Ready
const char* error_name = "FeatureDisabled"; // == chatIdentityStatusName(status)
};
// Pure, no-I/O derivation: hashes `stableSecret` (variable-length: a mnemonic on lite, a
// spending key on full-node) with the KDF context as the BLAKE2b key into a clean 32-byte
// crypto_kx seed, then crypto_kx_seed_keypair() into `outKeys`. Deterministic for a given
// secret. On any non-Ready result `outKeys` is left wiped. featureEnabled defaults to the
// build predicate but tests pass true to exercise the crypto in an OFF build.
//
// OWNERSHIP: `stableSecret` is BORROWED (const&) and is NOT wiped here — the caller owns it
// and MUST sodium_memzero its backing buffer after this returns. The per-variant provider
// that fetches the wallet secret (mnemonic / spending key) should hold it in a wipeable
// buffer, not a plain std::string literal, in production.
ChatIdentityResult deriveChatIdentityFromSecret(const std::string& stableSecret,
ChatKeyPair& outKeys,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
// 64-char lowercase hex of the public key.
std::string chatIdentityPublicKeyHex(const ChatKeyPair& keys);
} // namespace dragonx::chat

View File

@@ -1,32 +0,0 @@
#pragma once
// DragonX Wallet - HushChat decrypted message model. Held in memory by ChatStore and persisted at
// rest (encrypted under a seed-derived key) by ChatDatabase.
#include <cstdint>
#include <string>
namespace dragonx::chat {
enum class ChatDirection { Incoming, Outgoing };
enum class ChatMessageKind { Message, ContactRequest };
// Outgoing delivery status. Sending = broadcast in flight (async op not yet resolved); Sent = the
// daemon accepted + broadcast the tx; Failed = it didn't (not connected, no funded address, rejected).
// Always Sent for incoming. NB: the values are persisted (chat DB serializes the int), so Sent MUST
// stay 0 and new states are APPENDED — never reordered.
enum class ChatDelivery { Sent, Failed, Sending };
struct ChatMessage {
ChatDirection direction = ChatDirection::Incoming;
ChatMessageKind kind = ChatMessageKind::Message;
std::string txid;
std::string conversation_id; // cid — the conversation thread key
std::string peer_zaddr; // header "z": peer's reply z-address
std::string peer_public_key_hex; // header "p": peer's crypto_kx public key
std::string body; // decrypted plaintext (Message) or request text (ContactRequest)
std::int64_t timestamp = 0; // tx time in seconds; set by the ingesting caller
std::size_t payload_position = 0; // together with txid, the dedup key
ChatDelivery delivery = ChatDelivery::Sent; // outgoing only
};
} // namespace dragonx::chat

View File

@@ -1,112 +0,0 @@
// DragonX Wallet - HushChat outgoing memo construction (implementation).
#include "chat_outgoing.h"
#include "chat_protocol.h" // kHushChat* constants
#include <nlohmann/json.hpp>
namespace dragonx::chat {
namespace {
// Serialize the HushChat header. nlohmann emits object keys in sorted (alphabetical) order —
// cid,e,h,p,t,v,z — which is exactly SilentDragonXLite's on-wire key order.
std::string buildHeaderMemo(const std::string& replyZaddr,
const std::string& conversationId,
const char* type,
const std::string& streamHeaderHex,
const std::string& publicKeyHex,
std::int64_t sentAt)
{
nlohmann::json header;
header["h"] = 1; // header number (>= 1)
header["v"] = kHushChatSupportedVersion; // 0
header["z"] = replyZaddr; // where the peer should reply (my address)
header["cid"] = conversationId;
header["t"] = type; // "Memo" or "Cont"
header["e"] = streamHeaderHex; // 48-hex secretstream header (Memo) / "" (Cont)
header["p"] = publicKeyHex; // my 64-hex crypto_kx public key
if (sentAt > 0) header["ts"] = sentAt; // optional sender compose time (Unix s) — receiver shows this
return header.dump();
}
bool present(const std::string& value) { return !value.empty(); }
} // namespace
std::array<ChatSendOutput, 2> chatSendOutputs(const OutgoingChatMemos& memos, bool utf8Prefix)
{
const std::string prefix = utf8Prefix ? "utf8:" : "";
return {{
{ memos.recipientZaddr, prefix + memos.headerMemo }, // header — the lower memo position
{ memos.recipientZaddr, prefix + memos.payloadMemo },
}};
}
ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine,
const std::string& myPublicKeyHex,
const std::string& myReplyZaddr,
const std::string& peerPublicKeyHex,
const std::string& peerZaddr,
const std::string& conversationId,
const std::string& plaintext,
OutgoingChatMemos& out)
{
if (plaintext.empty()) return ChatComposeStatus::EmptyBody;
if (!present(myPublicKeyHex) || !present(myReplyZaddr) || !present(peerZaddr) ||
!present(conversationId)) {
return ChatComposeStatus::MissingField;
}
if (peerPublicKeyHex.size() != kHushChatPublicKeyHexLength) return ChatComposeStatus::BadPeerKey;
std::string streamHeaderHex;
std::string ciphertextHex;
if (encryptOutgoing(mine, peerPublicKeyHex, plaintext, streamHeaderHex, ciphertextHex)
!= ChatCryptoStatus::Ok) {
return ChatComposeStatus::EncryptFailed;
}
OutgoingChatMemos memos;
memos.recipientZaddr = peerZaddr;
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex, myPublicKeyHex,
static_cast<std::int64_t>(std::time(nullptr)));
memos.payloadMemo = ciphertextHex;
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
return ChatComposeStatus::TooLong;
}
out = std::move(memos);
return ChatComposeStatus::Ok;
}
ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
const std::string& myReplyZaddr,
const std::string& peerZaddr,
const std::string& conversationId,
const std::string& requestText,
OutgoingChatMemos& out)
{
if (requestText.empty()) return ChatComposeStatus::EmptyBody;
// The receive parser treats any memo starting with '{' as a header, so a request payload must
// not start with one (see isContactPayloadCandidate in chat_protocol.cpp).
if (requestText.front() == '{') return ChatComposeStatus::BadRequestText;
if (!present(myPublicKeyHex) || !present(myReplyZaddr) || !present(peerZaddr) ||
!present(conversationId)) {
return ChatComposeStatus::MissingField;
}
OutgoingChatMemos memos;
memos.recipientZaddr = peerZaddr;
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Cont", "", myPublicKeyHex,
static_cast<std::int64_t>(std::time(nullptr)));
memos.payloadMemo = requestText;
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
return ChatComposeStatus::TooLong;
}
out = std::move(memos);
return ChatComposeStatus::Ok;
}
} // namespace dragonx::chat

View File

@@ -1,66 +0,0 @@
#pragma once
// DragonX Wallet - HushChat outgoing memo construction (the inverse of the receive parser).
//
// Given the sender's identity and the peer, produce the header memo JSON + payload memo that,
// sent as two 0-value memo outputs to the peer's z-address (header at the LOWER memo position),
// another HushChat client parses and decrypts. The byte format matches SilentDragonXLite: the
// header keys serialize alphabetically (nlohmann default) to cid,e,h,p,t,v,z. Pure — no I/O, no
// network; broadcasting the memos is the caller's job (the transport lands in a later phase).
#include "chat_crypto.h" // ChatKeyPair
#include <array>
#include <string>
namespace dragonx::chat {
struct OutgoingChatMemos {
std::string recipientZaddr; // the peer's z-address (recipient of both memo outputs)
std::string headerMemo; // JSON header — MUST occupy the lower memo position on the wire
std::string payloadMemo; // ciphertext hex (Message) or plaintext (ContactRequest)
};
// One of the two 0-value memo outputs a HushChat send produces (amount is always 0).
struct ChatSendOutput {
std::string address; // the peer's z-address
std::string memo; // memo encoded for the target transport (utf8:-prefixed or raw)
};
// The two outputs for a HushChat send, HEADER FIRST (it must occupy the lower memo position).
// `utf8Prefix` prepends the daemon's "utf8:" marker required by full-node z_sendmany (which then
// UTF-8-encodes the bytes on-chain, byte-identical to SDXLite's Memo::from_str); lite backends take
// raw UTF-8, so pass false there.
std::array<ChatSendOutput, 2> chatSendOutputs(const OutgoingChatMemos& memos, bool utf8Prefix);
enum class ChatComposeStatus {
Ok,
EmptyBody,
MissingField,
BadPeerKey,
BadRequestText, // a contact request text must not start with '{' (parser would read it as a header)
EncryptFailed,
TooLong // a resulting memo exceeds the HushChat 512-byte memo limit
};
// Build an ENCRYPTED message to a peer whose public key you already learned from a memo they sent
// you. `mine` is the sender's identity keypair; `myPublicKeyHex` its public half (goes in header p).
ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine,
const std::string& myPublicKeyHex,
const std::string& myReplyZaddr,
const std::string& peerPublicKeyHex,
const std::string& peerZaddr,
const std::string& conversationId,
const std::string& plaintext,
OutgoingChatMemos& out);
// Build a plaintext contact request — no peer public key needed yet; this is how the peer first
// learns your public key + reply address. The payload is the (plaintext) request text.
ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
const std::string& myReplyZaddr,
const std::string& peerZaddr,
const std::string& conversationId,
const std::string& requestText,
OutgoingChatMemos& out);
} // namespace dragonx::chat

View File

@@ -1,317 +0,0 @@
#include "chat_protocol.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cctype>
#include <optional>
#include <utility>
namespace dragonx::chat {
namespace {
bool isHexString(const std::string& value)
{
for (unsigned char ch : value) {
if (!std::isxdigit(ch)) return false;
}
return true;
}
bool isCiphertextPayloadCandidate(const std::string& value)
{
return !value.empty() && value.size() <= kHushChatMemoByteLimit &&
value.size() % 2 == 0 && isHexString(value);
}
bool isContactPayloadCandidate(const std::string& value)
{
return !value.empty() && value.size() <= kHushChatMemoByteLimit && value.front() != '{';
}
bool readRequiredString(const nlohmann::json& object,
const char* key,
std::string& value,
std::string& error)
{
auto it = object.find(key);
if (it == object.end()) {
error = std::string("missing field: ") + key;
return false;
}
if (!it->is_string()) {
error = std::string("field is not a string: ") + key;
return false;
}
value = it->get<std::string>();
return true;
}
bool readRequiredInt(const nlohmann::json& object,
const char* key,
int& value,
std::string& error)
{
auto it = object.find(key);
if (it == object.end()) {
error = std::string("missing field: ") + key;
return false;
}
if (!it->is_number_integer()) {
error = std::string("field is not an integer: ") + key;
return false;
}
value = it->get<int>();
return true;
}
HushChatHeaderParseResult fail(std::string error)
{
HushChatHeaderParseResult result;
result.error = std::move(error);
return result;
}
} // namespace
const char* hushChatHeaderTypeName(HushChatHeaderType type)
{
switch (type) {
case HushChatHeaderType::Message:
return "Memo";
case HushChatHeaderType::ContactRequest:
return "Cont";
}
return "Unknown";
}
const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue)
{
switch (issue) {
case HushChatMemoGroupingIssue::InvalidHeader:
return "InvalidHeader";
case HushChatMemoGroupingIssue::MissingPayload:
return "MissingPayload";
case HushChatMemoGroupingIssue::DuplicateHeader:
return "DuplicateHeader";
case HushChatMemoGroupingIssue::OversizedMemo:
return "OversizedMemo";
}
return "Unknown";
}
HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo)
{
if (memo.empty()) return fail("empty memo");
if (memo.size() > kHushChatMemoByteLimit) return fail("memo exceeds HushChat memo byte limit");
if (memo.front() != '{') return fail("memo is not a HushChat header JSON object");
nlohmann::json object;
try {
object = nlohmann::json::parse(memo);
} catch (const nlohmann::json::parse_error& e) {
return fail(std::string("invalid JSON: ") + e.what());
}
if (!object.is_object()) return fail("header memo JSON is not an object");
HushChatHeader header;
std::string type;
std::string error;
if (!readRequiredInt(object, "h", header.header_number, error)) return fail(error);
if (!readRequiredInt(object, "v", header.version, error)) return fail(error);
if (!readRequiredString(object, "z", header.reply_zaddr, error)) return fail(error);
if (!readRequiredString(object, "cid", header.conversation_id, error)) return fail(error);
if (!readRequiredString(object, "t", type, error)) return fail(error);
if (!readRequiredString(object, "e", header.secretstream_header_hex, error)) return fail(error);
if (!readRequiredString(object, "p", header.public_key_hex, error)) return fail(error);
// Optional sender compose time (Unix seconds). Absent on older senders — leave sent_at = 0 so the
// receiver falls back to the tx/receive time. Read leniently; never fail the header on a bad value.
if (auto it = object.find("ts"); it != object.end() && it->is_number_integer())
header.sent_at = it->get<std::int64_t>();
if (header.header_number < 1) return fail("header number must be positive");
if (header.version != kHushChatSupportedVersion) return fail("unsupported HushChat version");
if (header.reply_zaddr.empty()) return fail("reply z-address is empty");
if (header.conversation_id.empty()) return fail("conversation id is empty");
if (type == "Memo") {
header.type = HushChatHeaderType::Message;
} else if (type == "Cont") {
header.type = HushChatHeaderType::ContactRequest;
} else {
return fail("unknown HushChat header type");
}
if (header.public_key_hex.size() != kHushChatPublicKeyHexLength || !isHexString(header.public_key_hex)) {
return fail("public key must be 32 bytes encoded as hex");
}
if (header.type == HushChatHeaderType::Message) {
if (header.secretstream_header_hex.size() != kHushChatSecretstreamHeaderHexLength ||
!isHexString(header.secretstream_header_hex)) {
return fail("message header must include a 24 byte secretstream header encoded as hex");
}
} else if (!header.secretstream_header_hex.empty()) {
return fail("contact request header must not include a secretstream header");
}
HushChatHeaderParseResult result;
result.ok = true;
result.header = std::move(header);
return result;
}
HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMemoOutput>& outputs)
{
struct OrderedOutput {
std::size_t input_index = 0;
HushChatMemoOutput output;
};
std::vector<OrderedOutput> ordered;
ordered.reserve(outputs.size());
for (std::size_t index = 0; index < outputs.size(); ++index) {
ordered.push_back(OrderedOutput{index, outputs[index]});
}
std::stable_sort(ordered.begin(), ordered.end(), [](const OrderedOutput& left, const OrderedOutput& right) {
if (left.output.position == right.output.position) return left.input_index < right.input_index;
return left.output.position < right.output.position;
});
HushChatMemoGroupingResult result;
std::optional<HushChatMemoOutput> pending_header_output;
std::optional<HushChatHeader> pending_header;
// A payload memo seen BEFORE its header. The full-node daemon shuffles the two 0-value memo
// outputs of a chat tx (transaction_builder ShuffleOutputs), so the header is NOT guaranteed to
// occupy the lower position. We hold an orphan payload until its header arrives and pair them
// regardless of on-chain order (a chat tx carries exactly one header + one payload).
std::optional<HushChatMemoOutput> pending_payload_output;
auto addIssue = [&](HushChatMemoGroupingIssue issue, std::size_t position, std::string detail) {
result.issues.push_back(HushChatMemoGroupingIssueInfo{issue, position, std::move(detail)});
};
auto payloadMatchesHeader = [&](const std::string& memo) {
return pending_header->type == HushChatHeaderType::Message
? isCiphertextPayloadCandidate(memo)
: isContactPayloadCandidate(memo);
};
auto emitPair = [&](const HushChatMemoOutput& payloadOutput) {
HushChatMemoPair pair;
pair.header = std::move(*pending_header);
pair.header_position = pending_header_output->position;
pair.payload_position = payloadOutput.position;
pair.payload_memo = payloadOutput.memo;
result.pairs.push_back(std::move(pair));
pending_header_output.reset();
pending_header.reset();
};
// Pair a held (early) payload with the now-pending header, if it matches the header's type.
auto tryPairHeldPayload = [&]() {
if (!pending_header || !pending_payload_output) return;
if (payloadMatchesHeader(pending_payload_output->memo)) {
emitPair(*pending_payload_output);
pending_payload_output.reset();
}
};
auto clearPendingAsMissing = [&]() {
if (!pending_header_output) return;
addIssue(HushChatMemoGroupingIssue::MissingPayload,
pending_header_output->position,
"header did not have a matching payload memo");
pending_header_output.reset();
pending_header.reset();
};
for (const auto& entry : ordered) {
const auto& output = entry.output;
if (output.memo.size() > kHushChatMemoByteLimit) {
addIssue(HushChatMemoGroupingIssue::OversizedMemo,
output.position,
"memo exceeds HushChat memo byte limit");
continue;
}
if (!output.memo.empty() && output.memo.front() == '{') {
auto parsed = parseHushChatHeaderMemo(output.memo);
if (!parsed.ok) {
addIssue(HushChatMemoGroupingIssue::InvalidHeader, output.position, parsed.error);
continue;
}
if (pending_header_output) {
addIssue(HushChatMemoGroupingIssue::DuplicateHeader,
output.position,
"encountered another HushChat header before a payload");
clearPendingAsMissing();
}
pending_header_output = output;
pending_header = std::move(parsed.header);
tryPairHeldPayload(); // the payload may have arrived first (shuffled output order)
continue;
}
// Non-header memo.
if (pending_header_output && pending_header) {
if (payloadMatchesHeader(output.memo)) {
emitPair(output);
} else {
++result.ignored_memo_count;
}
} else if (!pending_payload_output && !output.memo.empty()) {
// No header yet — hold this as a candidate payload for a header still to come.
pending_payload_output = output;
} else {
++result.ignored_memo_count;
}
}
clearPendingAsMissing();
if (pending_payload_output) ++result.ignored_memo_count; // orphan payload, no header arrived
return result;
}
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
const HushChatTransactionInput& transaction,
bool featureEnabled)
{
HushChatTransactionExtractionResult result;
result.feature_enabled = featureEnabled;
if (!featureEnabled || transaction.txid.empty()) return result;
auto grouped = groupHushChatMemoOutputs(transaction.outputs);
result.ignored_memo_count = grouped.ignored_memo_count;
result.issues.reserve(grouped.issues.size());
for (const auto& issue : grouped.issues) {
result.issues.push_back(HushChatMemoGroupingIssueInfo{
issue.issue,
issue.position,
hushChatMemoGroupingIssueName(issue.issue)
});
}
result.metadata.reserve(grouped.pairs.size());
for (const auto& pair : grouped.pairs) {
HushChatTransactionMetadata metadata;
metadata.txid = transaction.txid;
metadata.type = pair.header.type;
metadata.conversation_id = pair.header.conversation_id;
metadata.reply_zaddr = pair.header.reply_zaddr;
metadata.header_position = pair.header_position;
metadata.payload_position = pair.payload_position;
metadata.payload_size = pair.payload_memo.size();
metadata.sender_public_key_hex = pair.header.public_key_hex;
metadata.secretstream_header_hex = pair.header.secretstream_header_hex;
metadata.payload_memo = pair.payload_memo;
metadata.sent_at = pair.header.sent_at; // carry the sender's compose time (0 if absent)
result.metadata.push_back(std::move(metadata));
}
return result;
}
} // namespace dragonx::chat

View File

@@ -1,114 +0,0 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
#ifndef DRAGONX_ENABLE_CHAT
#define DRAGONX_ENABLE_CHAT 0
#endif
namespace dragonx::chat {
enum class HushChatHeaderType {
Message,
ContactRequest
};
struct HushChatHeader {
int header_number = 0;
int version = 0;
std::string reply_zaddr;
std::string conversation_id;
HushChatHeaderType type = HushChatHeaderType::Message;
std::string secretstream_header_hex;
std::string public_key_hex;
// Optional sender-stamped compose time (header "ts", Unix seconds). 0 = absent (older sender) → the
// receiver falls back to the tx/receive time. Lets both sides show the SAME (send) time.
std::int64_t sent_at = 0;
};
struct HushChatHeaderParseResult {
bool ok = false;
HushChatHeader header;
std::string error;
};
struct HushChatMemoOutput {
std::size_t position = 0;
std::string memo;
};
struct HushChatMemoPair {
HushChatHeader header;
std::size_t header_position = 0;
std::size_t payload_position = 0;
std::string payload_memo;
};
enum class HushChatMemoGroupingIssue {
InvalidHeader,
MissingPayload,
DuplicateHeader,
OversizedMemo
};
struct HushChatMemoGroupingIssueInfo {
HushChatMemoGroupingIssue issue = HushChatMemoGroupingIssue::InvalidHeader;
std::size_t position = 0;
std::string detail;
};
struct HushChatMemoGroupingResult {
std::vector<HushChatMemoPair> pairs;
std::vector<HushChatMemoGroupingIssueInfo> issues;
std::size_t ignored_memo_count = 0;
};
struct HushChatTransactionInput {
std::string txid;
std::vector<HushChatMemoOutput> outputs;
};
struct HushChatTransactionMetadata {
std::string txid;
HushChatHeaderType type = HushChatHeaderType::Message;
std::string conversation_id;
std::string reply_zaddr;
std::size_t header_position = 0;
std::size_t payload_position = 0;
std::size_t payload_size = 0;
// Decrypt inputs carried through from the paired header + payload memos so the chat
// service can actually decrypt (a Message) or read the request (a ContactRequest).
std::string sender_public_key_hex; // header "p": peer crypto_kx public key (hex)
std::string secretstream_header_hex; // header "e": secretstream header (hex; empty for ContactRequest)
std::string payload_memo; // ciphertext hex (Message) or plaintext request text (ContactRequest)
std::int64_t sent_at = 0; // header "ts": sender compose time (Unix s); 0 = absent → use tx time
};
struct HushChatTransactionExtractionResult {
bool feature_enabled = false;
std::vector<HushChatTransactionMetadata> metadata;
std::vector<HushChatMemoGroupingIssueInfo> issues;
std::size_t ignored_memo_count = 0;
};
constexpr int kHushChatSupportedVersion = 0;
constexpr std::size_t kHushChatMemoByteLimit = 512;
constexpr std::size_t kHushChatPublicKeyHexLength = 64;
constexpr std::size_t kHushChatSecretstreamHeaderHexLength = 48;
constexpr bool hushChatFeatureEnabledAtBuild()
{
return DRAGONX_ENABLE_CHAT != 0;
}
HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo);
HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMemoOutput>& outputs);
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
const HushChatTransactionInput& transaction,
bool featureEnabled = hushChatFeatureEnabledAtBuild());
const char* hushChatHeaderTypeName(HushChatHeaderType type);
const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue);
} // namespace dragonx::chat

View File

@@ -1,147 +0,0 @@
// DragonX Wallet - HushChat service (implementation).
#include "chat_service.h"
#include "chat_database.h"
#include "chat_identity.h" // chatIdentityPublicKeyHex
#include <utility>
namespace dragonx::chat {
ChatService::~ChatService() {
clearIdentity();
}
void ChatService::setIdentity(const ChatKeyPair& keys) {
identity_ = keys;
has_identity_ = true;
}
void ChatService::clearIdentity() {
wipeChatKeyPair(identity_);
has_identity_ = false;
}
int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata,
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
std::int64_t fallbackTimestamp,
std::vector<std::string>* newIncomingCids) {
if (!has_identity_) return 0;
const std::string myPubKey = chatIdentityPublicKeyHex(identity_);
int added = 0;
for (const auto& meta : metadata) {
// A memo whose sender is our OWN identity is something we sent (only we hold our key). The local
// echo already records it as outgoing — ingesting it as incoming would duplicate it as a phantom
// "from peer" message. (This also collapses same-seed self-chat, where the "peer" wallet shares
// our identity, so every message would otherwise loop back.)
if (!myPubKey.empty() && meta.sender_public_key_hex == myPubKey) continue;
ChatMessage message;
message.direction = ChatDirection::Incoming;
message.txid = meta.txid;
message.conversation_id = meta.conversation_id;
message.peer_zaddr = meta.reply_zaddr;
message.peer_public_key_hex = meta.sender_public_key_hex;
// Reference time: the tx/receive time (block time if confirmed, else the receiver's wall clock for
// a mempool receive).
const auto timeIt = txTimestamps.find(meta.txid);
const std::int64_t refTime = timeIt != txTimestamps.end() ? timeIt->second : fallbackTimestamp;
// Prefer the sender's stamped compose time (header "ts") — the true send time, shown identically on
// both ends. But REJECT a value implausibly in the FUTURE vs the reference: a wrong/ahead peer clock
// would otherwise pin their messages to the bottom of the thread forever. A compose time in the
// PAST is fine — the note buffer can broadcast a queued message long after it was composed, and a
// confirmed tx's block time is always >= the compose time.
constexpr std::int64_t kSenderTsFutureToleranceSec = 3600; // 1 hour of clock skew tolerated
if (meta.sent_at > 0 && (refTime <= 0 || meta.sent_at <= refTime + kSenderTsFutureToleranceSec)) {
message.timestamp = meta.sent_at;
} else {
message.timestamp = refTime;
}
message.payload_position = meta.payload_position;
if (meta.type == HushChatHeaderType::ContactRequest) {
message.kind = ChatMessageKind::ContactRequest;
message.body = meta.payload_memo; // plaintext request text
} else {
message.kind = ChatMessageKind::Message;
std::string plaintext;
const ChatCryptoStatus status = decryptIncoming(
identity_, meta.sender_public_key_hex, meta.secretstream_header_hex,
meta.payload_memo, plaintext);
if (status != ChatCryptoStatus::Ok) continue; // drop undecryptable silently
message.body = std::move(plaintext);
}
// In-memory store dedups (txid+position); only persist the genuinely new ones. On the next
// session loadFromDatabase() repopulates the store, so re-scanning the chain re-ingests but
// the store dedup prevents a duplicate write.
if (store_.append(message)) {
if (db_) db_->append(message);
++added;
// Every ingested message is incoming — report its cid so the caller can notify without
// relying on a seen-watermark delta (which block-time vs wall-clock skew can swallow).
if (newIncomingCids) newIncomingCids->push_back(message.conversation_id);
}
}
return added;
}
void ChatService::loadFromDatabase() {
if (!db_) return;
for (const auto& message : db_->load()) {
store_.append(message);
}
}
std::string ChatService::identityPublicKeyHex() const {
if (!has_identity_) return {};
return chatIdentityPublicKeyHex(identity_);
}
ChatComposeStatus ChatService::composeMessage(const std::string& myReplyZaddr,
const std::string& peerPublicKeyHex,
const std::string& peerZaddr,
const std::string& conversationId,
const std::string& plaintext,
OutgoingChatMemos& out) const {
if (!has_identity_) return ChatComposeStatus::MissingField;
return buildOutgoingMessage(identity_, chatIdentityPublicKeyHex(identity_), myReplyZaddr,
peerPublicKeyHex, peerZaddr, conversationId, plaintext, out);
}
ChatComposeStatus ChatService::composeContactRequest(const std::string& myReplyZaddr,
const std::string& peerZaddr,
const std::string& conversationId,
const std::string& requestText,
OutgoingChatMemos& out) const {
if (!has_identity_) return ChatComposeStatus::MissingField;
return buildOutgoingContactRequest(chatIdentityPublicKeyHex(identity_), myReplyZaddr,
peerZaddr, conversationId, requestText, out);
}
bool ChatService::recordOutgoing(const ChatMessage& message) {
if (store_.append(message)) {
if (db_) db_->append(message);
return true;
}
return false;
}
bool ChatService::recordOutgoingPending(const ChatMessage& message) {
// Persist immediately (as Sending) so a send survives an app quit before the broadcast resolves;
// resolveOutgoing() then UPSERTS the row to the final status. A stray persisted Sending (crash mid-
// broadcast) loads as Sent (see ChatDatabase::deserialize).
const bool appended = store_.append(message);
if (appended && db_) db_->upsert(message);
return appended;
}
void ChatService::resolveOutgoing(const std::string& txid, ChatDelivery delivery) {
const ChatMessage* updated = store_.updateDelivery(txid, delivery);
if (updated && db_) db_->upsert(*updated); // overwrite the Sending row with the final status
}
} // namespace dragonx::chat

View File

@@ -1,95 +0,0 @@
#pragma once
// DragonX Wallet - HushChat service: turns harvested memo metadata into decrypted, threaded
// messages. Owns the long-lived chat identity keypair (a secret) + the in-memory store.
// Move-disabled (the secret stays pinned); wipes the secret on destruction/clear.
// Not thread-safe — drive from the main thread (where refresh results are applied).
#include "chat_crypto.h" // ChatKeyPair
#include "chat_protocol.h" // HushChatTransactionMetadata
#include "chat_outgoing.h" // OutgoingChatMemos, ChatComposeStatus
#include "chat_store.h"
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
namespace dragonx::chat {
class ChatDatabase; // optional persistent backing (Phase 2); set via setPersistence
class ChatService {
public:
ChatService() = default;
~ChatService();
ChatService(const ChatService&) = delete;
ChatService& operator=(const ChatService&) = delete;
ChatService(ChatService&&) = delete;
ChatService& operator=(ChatService&&) = delete;
// Provision (or replace) the chat identity. Copies the keypair — the caller should wipe
// its own copy afterwards (see chat_identity.h ownership note).
void setIdentity(const ChatKeyPair& keys);
bool hasIdentity() const { return has_identity_; }
void clearIdentity(); // wipes the held secret key
// Decrypt/record each metadata entry (a Message is decrypted; a ContactRequest carries its
// plaintext through) and thread it into the store. Each message is stamped with its own
// transaction time via `txTimestamps` (keyed by txid), falling back to `fallbackTimestamp`
// when the txid isn't present. Newly-added messages are also persisted (if a database is
// attached). Returns the number of NEW messages added; 0 with no identity. Undecryptable
// Messages are dropped silently (no logging of memo/plaintext).
// When `newIncomingCids` is non-null it is filled with the conversation ids of the genuinely-new
// incoming (non-request) messages appended this call — a reliable "a new message arrived here"
// signal for notifications that doesn't depend on any timestamp/seen-watermark comparison.
int ingest(const std::vector<HushChatTransactionMetadata>& metadata,
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
std::int64_t fallbackTimestamp = 0,
std::vector<std::string>* newIncomingCids = nullptr);
// Attach a persistent backing store (Phase 2). Not owned. Pass nullptr to detach. New messages
// from ingest() are written through; loadFromDatabase() rehydrates the in-memory store from it.
void setPersistence(ChatDatabase* db) { db_ = db; }
// Load previously-persisted messages (already decrypted at ingest, re-encrypted at rest under
// the seed-derived key) into the in-memory store. No-op without an unlocked database.
void loadFromDatabase();
// --- Outgoing (compose) ---
// My chat public key (hex), or "" without an identity — goes in an outgoing header's "p".
std::string identityPublicKeyHex() const;
// Construct the outgoing memos for an ENCRYPTED message, using the held identity to encrypt.
ChatComposeStatus composeMessage(const std::string& myReplyZaddr,
const std::string& peerPublicKeyHex,
const std::string& peerZaddr,
const std::string& conversationId,
const std::string& plaintext,
OutgoingChatMemos& out) const;
// Construct the outgoing memos for a plaintext contact request (no peer key needed yet).
ChatComposeStatus composeContactRequest(const std::string& myReplyZaddr,
const std::string& peerZaddr,
const std::string& conversationId,
const std::string& requestText,
OutgoingChatMemos& out) const;
// Echo a locally-composed outgoing message into the store (and DB). Returns true if new. (We
// never harvest our own sent memos — they land on the peer's address — so this echo is the
// only local record of what we sent.)
bool recordOutgoing(const ChatMessage& message);
// Two-phase echo for delivery tracking: record + persist immediately as Sending (survives an app
// quit), then resolveOutgoing() upserts the final status once the broadcast completes. A stray
// persisted Sending (crash mid-broadcast) loads back as Sent.
bool recordOutgoingPending(const ChatMessage& message);
void resolveOutgoing(const std::string& txid, ChatDelivery delivery);
const ChatStore& store() const { return store_; }
ChatStore& store() { return store_; }
private:
ChatKeyPair identity_{};
bool has_identity_ = false;
ChatStore store_;
ChatDatabase* db_ = nullptr; // optional; not owned
};
} // namespace dragonx::chat

View File

@@ -1,61 +0,0 @@
// DragonX Wallet - HushChat in-memory message store (implementation).
#include "chat_store.h"
#include <algorithm>
namespace dragonx::chat {
std::string ChatStore::dedupKey(const ChatMessage& message) {
return message.txid + ":" + std::to_string(message.payload_position);
}
bool ChatStore::append(const ChatMessage& message) {
if (!seen_.insert(dedupKey(message)).second) return false;
messages_.push_back(message);
return true;
}
std::vector<ChatMessage> ChatStore::conversation(const std::string& conversationId) const {
std::vector<ChatMessage> out;
for (const auto& message : messages_) {
if (message.conversation_id == conversationId) out.push_back(message);
}
// Return chronological (oldest→newest). messages_ is in scan/insertion order, which is NOT
// time-ordered (a full scan harvests txids in std::set/unordered_map order) — that would mis-order the
// rendered thread AND let the reply-target pin (first-seen peer address) latch onto a non-establishing
// message (B2). timestamp is the block/tx time (peer can't set it); tie-break txid + payload_position
// for determinism.
std::stable_sort(out.begin(), out.end(), [](const ChatMessage& a, const ChatMessage& b) {
if (a.timestamp != b.timestamp) return a.timestamp < b.timestamp;
if (a.txid != b.txid) return a.txid < b.txid;
return a.payload_position < b.payload_position;
});
return out;
}
const ChatMessage* ChatStore::updateDelivery(const std::string& txid, ChatDelivery delivery) {
for (auto& message : messages_) {
if (message.txid == txid) {
message.delivery = delivery;
return &message;
}
}
return nullptr;
}
std::vector<std::string> ChatStore::conversationIds() const {
std::vector<std::string> ids;
std::unordered_set<std::string> seenIds;
for (const auto& message : messages_) {
if (seenIds.insert(message.conversation_id).second) ids.push_back(message.conversation_id);
}
return ids;
}
void ChatStore::clear() {
messages_.clear();
seen_.clear();
}
} // namespace dragonx::chat

View File

@@ -1,52 +0,0 @@
#pragma once
// DragonX Wallet - HushChat in-memory message store: the fast read model / dedup view. Durable
// persistence lives in ChatDatabase; ChatService rehydrates this store from it on unlock.
#include "chat_message.h"
#include <string>
#include <unordered_set>
#include <vector>
namespace dragonx::chat {
// Threads messages by conversation_id (cid) and deduplicates by (txid, payload_position) so
// re-scanning the chain never double-inserts. Not thread-safe — drive from the main thread.
class ChatStore {
public:
// Returns true if newly inserted, false if a duplicate was ignored.
bool append(const ChatMessage& message);
// Messages in a conversation, in insertion order.
std::vector<ChatMessage> conversation(const std::string& conversationId) const;
// Update an outgoing echo's delivery status by its local txid. Returns a pointer to the updated
// message (for the caller to persist), or nullptr if no message has that txid.
const ChatMessage* updateDelivery(const std::string& txid, ChatDelivery delivery);
// Distinct conversation ids, in first-seen order.
std::vector<std::string> conversationIds() const;
// The set of on-chain txids that carried a chat message (sent or received, messages + contact
// requests) — used by the History tab to badge / filter chat transactions. O(messages), no copies.
std::unordered_set<std::string> chatTxids() const {
std::unordered_set<std::string> out;
out.reserve(messages_.size());
for (const auto& m : messages_)
if (!m.txid.empty()) out.insert(m.txid);
return out;
}
std::size_t size() const { return messages_.size(); }
bool empty() const { return messages_.empty(); }
void clear();
private:
static std::string dedupKey(const ChatMessage& message);
std::vector<ChatMessage> messages_;
std::unordered_set<std::string> seen_;
};
} // namespace dragonx::chat

View File

@@ -11,12 +11,8 @@
#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>
#include <ctime>
#include <algorithm>
#include <type_traits>
#include "../util/logger.h"
#include "../util/platform.h"
#ifdef _WIN32
#include <shlobj.h>
@@ -34,87 +30,35 @@ namespace config {
Settings::Settings() = default;
Settings::~Settings() = default;
namespace {
Settings::LiteServerSelectionPreferenceMode parseLiteServerSelectionPreferenceMode(
const json& value)
{
if (!value.is_string()) return Settings::LiteServerSelectionPreferenceMode::Sticky;
const std::string mode = value.get<std::string>();
if (mode == "random" || mode == "Random") {
return Settings::LiteServerSelectionPreferenceMode::Random;
}
return Settings::LiteServerSelectionPreferenceMode::Sticky;
}
const char* liteServerSelectionPreferenceModeName(
Settings::LiteServerSelectionPreferenceMode mode)
{
switch (mode) {
case Settings::LiteServerSelectionPreferenceMode::Sticky:
return "sticky";
case Settings::LiteServerSelectionPreferenceMode::Random:
return "random";
}
return "sticky";
}
Settings::PoolSelectMode parsePoolSelectMode(const json& value)
{
if (!value.is_string()) return Settings::PoolSelectMode::Manual;
const std::string mode = value.get<std::string>();
if (mode == "auto_balance" || mode == "auto") {
return Settings::PoolSelectMode::AutoBalance;
}
return Settings::PoolSelectMode::Manual;
}
const char* poolSelectModeName(Settings::PoolSelectMode mode)
{
switch (mode) {
case Settings::PoolSelectMode::Manual: return "manual";
case Settings::PoolSelectMode::AutoBalance: return "auto_balance";
}
return "manual";
}
// True if j[key] exists and holds a JSON value convertible to T. Guards every scalar
// read so a malformed value (wrong type / missing key) leaves the field's default
// instead of throwing out of the whole load().
template <typename T>
bool jsonHasType(const json& v)
{
if constexpr (std::is_same_v<T, bool>) return v.is_boolean();
else if constexpr (std::is_same_v<T, std::string>) return v.is_string();
else if constexpr (std::is_floating_point_v<T>) return v.is_number();
else if constexpr (std::is_integral_v<T>) return v.is_number_integer();
else return false;
}
// Reads j[key] into field only when present AND of the matching type.
template <typename T>
void loadScalar(const json& j, const char* key, T& field)
{
if (j.contains(key) && jsonHasType<T>(j[key])) field = j[key].get<T>();
}
// Same as loadScalar but clamps the read value into [lo, hi].
template <typename T>
void loadClamped(const json& j, const char* key, T& field, T lo, T hi)
{
if (j.contains(key) && jsonHasType<T>(j[key]))
field = std::max(lo, std::min(hi, j[key].get<T>()));
}
} // namespace
std::string Settings::getDefaultPath()
{
// Single per-platform, per-variant config dir (util::Platform::getConfigDir handles the
// _WIN32 / __APPLE__ / XDG split and the DRAGONX_APP_NAME variant suffix in one place).
const std::string dir = util::Platform::getConfigDir();
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
std::string dir = std::string(path) + "\\ObsidianDragon";
fs::create_directories(dir);
return (fs::path(dir) / "settings.json").string();
return dir + "\\settings.json";
}
return "settings.json";
#elif defined(__APPLE__)
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/Library/Application Support/ObsidianDragon";
fs::create_directories(dir);
return dir + "/settings.json";
#else
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/.config/ObsidianDragon";
fs::create_directories(dir);
return dir + "/settings.json";
#endif
}
bool Settings::load()
@@ -135,50 +79,29 @@ bool Settings::load(const std::string& path)
json j;
file >> j;
loadScalar(j, "theme", theme_);
loadScalar(j, "save_ztxs", save_ztxs_);
loadScalar(j, "auto_shield", auto_shield_);
loadScalar(j, "use_tor", use_tor_);
loadScalar(j, "allow_custom_fees", allow_custom_fees_);
loadScalar(j, "default_fee", default_fee_);
loadScalar(j, "fetch_prices", fetch_prices_);
loadScalar(j, "tx_explorer_url", tx_explorer_url_);
loadScalar(j, "address_explorer_url", address_explorer_url_);
loadScalar(j, "language", language_);
loadScalar(j, "skin_id", skin_id_);
loadScalar(j, "chat_reply_zaddr", chat_reply_zaddr_);
if (j.contains("muted_chat_cids") && j["muted_chat_cids"].is_array()) {
muted_chat_cids_.clear();
for (const auto& c : j["muted_chat_cids"])
if (c.is_string()) muted_chat_cids_.push_back(c.get<std::string>());
}
if (j.contains("hidden_chat_cids") && j["hidden_chat_cids"].is_array()) {
hidden_chat_cids_.clear();
for (const auto& c : j["hidden_chat_cids"])
if (c.is_string()) hidden_chat_cids_.push_back(c.get<std::string>());
}
// Chat-tab customization (re-clamped through the setters so hand-edited JSON stays in range).
loadScalar(j, "chat_emoji_color", chat_emoji_color_);
loadScalar(j, "chat_poll_rate_sec", chat_poll_rate_sec_); setChatPollRateSec(chat_poll_rate_sec_);
loadScalar(j, "chat_bubble_style", chat_bubble_style_); setChatBubbleStyle(chat_bubble_style_);
loadScalar(j, "chat_bubble_accent", chat_bubble_accent_); setChatBubbleAccent(chat_bubble_accent_);
loadScalar(j, "chat_density", chat_density_); setChatDensity(chat_density_);
loadScalar(j, "chat_font_scale", chat_font_scale_); setChatFontScale(chat_font_scale_);
loadScalar(j, "chat_time_format", chat_time_format_); setChatTimeFormat(chat_time_format_);
loadScalar(j, "chat_enter_sends", chat_enter_sends_);
loadScalar(j, "time_format", time_format_); setTimeFormat(time_format_);
loadScalar(j, "acrylic_enabled", acrylic_enabled_);
loadScalar(j, "acrylic_quality", acrylic_quality_);
loadScalar(j, "blur_multiplier", blur_multiplier_);
loadScalar(j, "noise_opacity", noise_opacity_);
loadScalar(j, "gradient_background", gradient_background_);
if (j.contains("theme")) theme_ = j["theme"].get<std::string>();
if (j.contains("save_ztxs")) save_ztxs_ = j["save_ztxs"].get<bool>();
if (j.contains("auto_shield")) auto_shield_ = j["auto_shield"].get<bool>();
if (j.contains("use_tor")) use_tor_ = j["use_tor"].get<bool>();
if (j.contains("allow_custom_fees")) allow_custom_fees_ = j["allow_custom_fees"].get<bool>();
if (j.contains("default_fee")) default_fee_ = j["default_fee"].get<double>();
if (j.contains("fetch_prices")) fetch_prices_ = j["fetch_prices"].get<bool>();
if (j.contains("tx_explorer_url")) tx_explorer_url_ = j["tx_explorer_url"].get<std::string>();
if (j.contains("address_explorer_url")) address_explorer_url_ = j["address_explorer_url"].get<std::string>();
if (j.contains("language")) language_ = j["language"].get<std::string>();
if (j.contains("skin_id")) skin_id_ = j["skin_id"].get<std::string>();
if (j.contains("acrylic_enabled")) acrylic_enabled_ = j["acrylic_enabled"].get<bool>();
if (j.contains("acrylic_quality")) acrylic_quality_ = j["acrylic_quality"].get<int>();
if (j.contains("blur_multiplier")) blur_multiplier_ = j["blur_multiplier"].get<float>();
if (j.contains("noise_opacity")) noise_opacity_ = j["noise_opacity"].get<float>();
if (j.contains("gradient_background")) gradient_background_ = j["gradient_background"].get<bool>();
// Migrate legacy reduced_transparency bool -> ui_opacity float
if (j.contains("ui_opacity")) {
ui_opacity_ = j["ui_opacity"].get<float>();
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) {
ui_opacity_ = 1.0f; // legacy: reduced = fully opaque
}
loadScalar(j, "window_opacity", window_opacity_);
if (j.contains("window_opacity")) window_opacity_ = j["window_opacity"].get<float>();
if (j.contains("balance_layout")) {
if (j["balance_layout"].is_string())
balance_layout_ = j["balance_layout"].get<std::string>();
@@ -192,18 +115,7 @@ bool Settings::load(const std::string& path)
if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
}
}
loadScalar(j, "portfolio_style", portfolio_style_);
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
loadScalar(j, "contacts_view_mode", contacts_view_mode_);
if (contacts_view_mode_ < 0 || contacts_view_mode_ > 2) contacts_view_mode_ = 0;
loadScalar(j, "contacts_avatar_shape", contacts_avatar_shape_); setContactsAvatarShape(contacts_avatar_shape_);
loadScalar(j, "contacts_list_scale", contacts_list_scale_); setContactsListScale(contacts_list_scale_);
loadScalar(j, "animate_avatars", animate_avatars_);
loadScalar(j, "scanline_enabled", scanline_enabled_);
loadScalar(j, "console_line_accents", console_line_accents_);
loadScalar(j, "console_text_color", console_text_color_);
loadScalar(j, "console_zoom", console_zoom_);
if (!(console_zoom_ >= 0.25f && console_zoom_ <= 4.0f)) console_zoom_ = 1.0f; // guard bad/NaN
if (j.contains("scanline_enabled")) scanline_enabled_ = j["scanline_enabled"].get<bool>();
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
hidden_addresses_.clear();
for (const auto& a : j["hidden_addresses"])
@@ -229,112 +141,39 @@ bool Settings::load(const std::string& path)
address_meta_[addr] = m;
}
}
loadScalar(j, "wizard_completed", wizard_completed_);
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
loadScalar(j, "daemon_update_prompted_size", daemon_update_prompted_size_);
loadScalar(j, "active_wallet_file", active_wallet_file_);
loadScalar(j, "seed_migration_pending", seed_migration_pending_);
loadScalar(j, "seed_migration_dest", seed_migration_dest_);
loadScalar(j, "seed_migration_temp_dir", seed_migration_temp_dir_);
loadScalar(j, "seed_migration_sweep_txid", seed_migration_sweep_txid_);
loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
loadScalar(j, "unlock_duration", unlock_duration_);
loadScalar(j, "pin_enabled", pin_enabled_);
loadScalar(j, "keep_daemon_running", keep_daemon_running_);
loadScalar(j, "stop_external_daemon", stop_external_daemon_);
loadScalar(j, "max_connections", max_connections_);
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
const auto& lite = j["lite_wallet"];
if (lite.contains("server_selection_mode")) {
lite_server_selection_mode_ = parseLiteServerSelectionPreferenceMode(
lite["server_selection_mode"]);
}
if (lite.contains("sticky_server_url") && lite["sticky_server_url"].is_string()) {
lite_sticky_server_url_ = lite["sticky_server_url"].get<std::string>();
}
if (lite.contains("chain_name") && lite["chain_name"].is_string()) {
lite_chain_name_ = lite["chain_name"].get<std::string>();
}
// Migration: the SDXL backend only accepts main/test/regtest and hard-panics
// (process abort) on any other chain name. Older builds persisted the "DRAGONX"
// ticker here, which crashed the lite backend on launch. Rewrite any invalid
// value to "main" and flag a re-save so the corrected setting persists.
if (lite_chain_name_ != "main" && lite_chain_name_ != "test" &&
lite_chain_name_ != "regtest") {
lite_chain_name_ = "main";
needs_upgrade_save_ = true;
}
if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_unsigned()) {
lite_random_selection_seed_ = lite["random_selection_seed"].get<std::size_t>();
} else if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_integer()) {
const auto seed = lite["random_selection_seed"].get<long long>();
lite_random_selection_seed_ = seed > 0 ? static_cast<std::size_t>(seed) : 0;
}
if (lite.contains("persist_selected_server") && lite["persist_selected_server"].is_boolean()) {
lite_persist_selected_server_ = lite["persist_selected_server"].get<bool>();
}
if (lite.contains("servers") && lite["servers"].is_array()) {
lite_servers_.clear();
for (const auto& server : lite["servers"]) {
if (!server.is_object()) continue;
LiteServerPreference preference;
if (server.contains("url") && server["url"].is_string()) {
preference.url = server["url"].get<std::string>();
}
if (server.contains("label") && server["label"].is_string()) {
preference.label = server["label"].get<std::string>();
}
if (server.contains("enabled") && server["enabled"].is_boolean()) {
preference.enabled = server["enabled"].get<bool>();
}
lite_servers_.push_back(preference);
}
}
if (lite.contains("rollout_override") && lite["rollout_override"].is_string()) {
const auto v = lite["rollout_override"].get<std::string>();
lite_rollout_override_ = (v == "force_on" || v == "force_off") ? v : "auto";
}
if (lite.contains("install_id") && lite["install_id"].is_string()) {
lite_install_id_ = lite["install_id"].get<std::string>();
}
if (lite.contains("hidden_servers") && lite["hidden_servers"].is_array()) {
lite_hidden_servers_.clear();
for (const auto& u : lite["hidden_servers"])
if (u.is_string()) lite_hidden_servers_.insert(u.get<std::string>());
}
}
loadScalar(j, "verbose_logging", verbose_logging_);
if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get<bool>();
if (j.contains("auto_lock_timeout")) auto_lock_timeout_ = j["auto_lock_timeout"].get<int>();
if (j.contains("unlock_duration")) unlock_duration_ = j["unlock_duration"].get<int>();
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();
for (const auto& c : j["debug_categories"])
if (c.is_string()) debug_categories_.insert(c.get<std::string>());
}
loadScalar(j, "theme_effects_enabled", theme_effects_enabled_);
loadScalar(j, "low_spec_mode", low_spec_mode_);
loadScalar(j, "reduce_motion", reduce_motion_);
loadScalar(j, "selected_exchange", selected_exchange_);
loadScalar(j, "selected_pair", selected_pair_);
loadScalar(j, "chart_interval", chart_interval_);
loadScalar(j, "chart_style", chart_style_);
loadScalar(j, "pool_url", pool_url_);
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>();
// Migrate old default pool URL that was missing the stratum port
if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
loadScalar(j, "pool_algo", pool_algo_);
loadScalar(j, "pool_worker", pool_worker_);
loadScalar(j, "pool_threads", pool_threads_);
loadScalar(j, "pool_tls", pool_tls_);
loadScalar(j, "pool_hugepages", pool_hugepages_);
loadScalar(j, "pool_mode", pool_mode_);
if (j.contains("pool_select_mode")) pool_select_mode_ = parsePoolSelectMode(j["pool_select_mode"]);
loadScalar(j, "mine_when_idle", mine_when_idle_);
loadScalar(j, "xmrig_version", xmrig_version_);
// Lower-bounded only (min 30s), matching setMineIdleDelay; now type-guarded too.
if (j.contains("mine_idle_delay") && j["mine_idle_delay"].is_number_integer())
mine_idle_delay_ = std::max(30, j["mine_idle_delay"].get<int>());
loadScalar(j, "idle_thread_scaling", idle_thread_scaling_);
loadScalar(j, "idle_threads_active", idle_threads_active_);
loadScalar(j, "idle_threads_idle", idle_threads_idle_);
loadScalar(j, "idle_gpu_aware", idle_gpu_aware_);
if (j.contains("pool_algo")) pool_algo_ = j["pool_algo"].get<std::string>();
if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get<std::string>();
if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get<int>();
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>();
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>());
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"])
@@ -345,56 +184,15 @@ bool Settings::load(const std::string& path)
for (const auto& w : j["saved_pool_workers"])
if (w.is_string()) saved_pool_workers_.push_back(w.get<std::string>());
}
if (j.contains("portfolio_entries") && j["portfolio_entries"].is_array()) {
portfolio_entries_.clear();
for (const auto& e : j["portfolio_entries"]) {
if (!e.is_object()) continue;
PortfolioEntry entry;
if (e.contains("label") && e["label"].is_string())
entry.label = e["label"].get<std::string>();
if (e.contains("addresses") && e["addresses"].is_array())
for (const auto& a : e["addresses"])
if (a.is_string()) entry.addresses.push_back(a.get<std::string>());
if (e.contains("icon") && e["icon"].is_string())
entry.icon = e["icon"].get<std::string>();
if (e.contains("color") && e["color"].is_number())
entry.color = e["color"].get<unsigned int>();
if (e.contains("outline_opacity") && e["outline_opacity"].is_number_integer())
entry.outlineOpacity = e["outline_opacity"].get<int>();
if (e.contains("price_basis") && e["price_basis"].is_number_integer())
entry.priceBasis = e["price_basis"].get<int>();
if (e.contains("manual_price") && e["manual_price"].is_number())
entry.manualPrice = e["manual_price"].get<double>();
if (e.contains("manual_currency") && e["manual_currency"].is_string())
entry.manualCurrency = e["manual_currency"].get<std::string>();
if (e.contains("show_drgx") && e["show_drgx"].is_boolean())
entry.showDrgx = e["show_drgx"].get<bool>();
if (e.contains("show_value") && e["show_value"].is_boolean())
entry.showValue = e["show_value"].get<bool>();
if (e.contains("show_24h") && e["show_24h"].is_boolean())
entry.show24h = e["show_24h"].get<bool>();
if (e.contains("show_sparkline") && e["show_sparkline"].is_boolean())
entry.showSparkline = e["show_sparkline"].get<bool>();
if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
entry.sparklineInterval = e["sparkline_interval"].get<int>();
entry.scope = e.value("scope", "");
if (e.contains("grid_col") && e["grid_col"].is_number_integer())
entry.gridCol = e["grid_col"].get<int>();
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
entry.gridRow = e["grid_row"].get<int>();
if (e.contains("grid_w") && e["grid_w"].is_number_integer())
entry.gridW = e["grid_w"].get<int>();
if (e.contains("grid_h") && e["grid_h"].is_number_integer())
entry.gridH = e["grid_h"].get<int>();
if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry));
}
}
loadClamped(j, "font_scale", font_scale_, 1.0f, 1.5f);
loadScalar(j, "window_width", window_width_);
loadScalar(j, "window_height", window_height_);
if (j.contains("font_scale") && j["font_scale"].is_number())
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>()));
if (j.contains("window_width") && j["window_width"].is_number_integer())
window_width_ = j["window_width"].get<int>();
if (j.contains("window_height") && j["window_height"].is_number_integer())
window_height_ = j["window_height"].get<int>();
// Version tracking — detect upgrades so we can re-save with new defaults
loadScalar(j, "settings_version", settings_version_);
if (j.contains("settings_version")) settings_version_ = j["settings_version"].get<std::string>();
if (settings_version_ != DRAGONX_VERSION) {
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
settings_version_.empty() ? "(none)" : settings_version_.c_str(),
@@ -405,17 +203,6 @@ bool Settings::load(const std::string& path)
return true;
} catch (const std::exception& e) {
DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
// The file exists but is unparseable (truncated/corrupt). Quarantine it so the
// next save() doesn't silently overwrite it with defaults — the user's data stays
// recoverable. Proceed with in-memory defaults.
file.close();
std::error_code ec;
const std::string quarantine =
path + ".corrupt-" + std::to_string(static_cast<long long>(std::time(nullptr)));
fs::rename(path, quarantine, ec);
if (!ec) {
DEBUG_LOGF("Quarantined corrupt settings to %s\n", quarantine.c_str());
}
return false;
}
}
@@ -443,22 +230,6 @@ bool Settings::save(const std::string& path)
j["address_explorer_url"] = address_explorer_url_;
j["language"] = language_;
j["skin_id"] = skin_id_;
j["chat_reply_zaddr"] = chat_reply_zaddr_;
j["muted_chat_cids"] = json::array();
for (const auto& c : muted_chat_cids_)
j["muted_chat_cids"].push_back(c);
j["hidden_chat_cids"] = json::array();
for (const auto& c : hidden_chat_cids_)
j["hidden_chat_cids"].push_back(c);
j["chat_emoji_color"] = chat_emoji_color_;
j["chat_poll_rate_sec"] = chat_poll_rate_sec_;
j["chat_bubble_style"] = chat_bubble_style_;
j["chat_bubble_accent"] = chat_bubble_accent_;
j["chat_density"] = chat_density_;
j["chat_font_scale"] = chat_font_scale_;
j["chat_time_format"] = chat_time_format_;
j["chat_enter_sends"] = chat_enter_sends_;
j["time_format"] = time_format_;
j["acrylic_enabled"] = acrylic_enabled_;
j["acrylic_quality"] = acrylic_quality_;
j["blur_multiplier"] = blur_multiplier_;
@@ -467,15 +238,7 @@ bool Settings::save(const std::string& path)
j["ui_opacity"] = ui_opacity_;
j["window_opacity"] = window_opacity_;
j["balance_layout"] = balance_layout_; // saved as string ID
j["portfolio_style"] = portfolio_style_;
j["contacts_view_mode"] = contacts_view_mode_;
j["contacts_avatar_shape"] = contacts_avatar_shape_;
j["contacts_list_scale"] = contacts_list_scale_;
j["animate_avatars"] = animate_avatars_;
j["scanline_enabled"] = scanline_enabled_;
j["console_line_accents"] = console_line_accents_;
j["console_text_color"] = console_text_color_;
j["console_zoom"] = console_zoom_;
j["hidden_addresses"] = json::array();
for (const auto& addr : hidden_addresses_)
j["hidden_addresses"].push_back(addr);
@@ -496,40 +259,12 @@ bool Settings::save(const std::string& path)
j["address_meta"] = meta_obj;
}
j["wizard_completed"] = wizard_completed_;
j["seed_backup_reminded"] = seed_backup_reminded_;
j["daemon_update_prompted_size"] = daemon_update_prompted_size_;
j["active_wallet_file"] = active_wallet_file_;
j["seed_migration_pending"] = seed_migration_pending_;
j["seed_migration_dest"] = seed_migration_dest_;
j["seed_migration_temp_dir"] = seed_migration_temp_dir_;
j["seed_migration_sweep_txid"] = seed_migration_sweep_txid_;
j["auto_lock_timeout"] = auto_lock_timeout_;
j["unlock_duration"] = unlock_duration_;
j["pin_enabled"] = pin_enabled_;
j["keep_daemon_running"] = keep_daemon_running_;
j["stop_external_daemon"] = stop_external_daemon_;
j["max_connections"] = max_connections_;
{
json lite = json::object();
lite["server_selection_mode"] = liteServerSelectionPreferenceModeName(lite_server_selection_mode_);
lite["sticky_server_url"] = lite_sticky_server_url_;
lite["chain_name"] = lite_chain_name_;
lite["random_selection_seed"] = lite_random_selection_seed_;
lite["persist_selected_server"] = lite_persist_selected_server_;
lite["servers"] = json::array();
for (const auto& server : lite_servers_) {
json entry = json::object();
entry["url"] = server.url;
entry["label"] = server.label;
entry["enabled"] = server.enabled;
lite["servers"].push_back(entry);
}
lite["rollout_override"] = lite_rollout_override_;
lite["install_id"] = lite_install_id_;
lite["hidden_servers"] = json::array();
for (const auto& u : lite_hidden_servers_) lite["hidden_servers"].push_back(u);
j["lite_wallet"] = lite;
}
j["verbose_logging"] = verbose_logging_;
j["debug_categories"] = json::array();
for (const auto& cat : debug_categories_)
@@ -539,8 +274,6 @@ bool Settings::save(const std::string& path)
j["reduce_motion"] = reduce_motion_;
j["selected_exchange"] = selected_exchange_;
j["selected_pair"] = selected_pair_;
j["chart_interval"] = chart_interval_;
j["chart_style"] = chart_style_;
j["pool_url"] = pool_url_;
j["pool_algo"] = pool_algo_;
j["pool_worker"] = pool_worker_;
@@ -548,9 +281,7 @@ bool Settings::save(const std::string& path)
j["pool_tls"] = pool_tls_;
j["pool_hugepages"] = pool_hugepages_;
j["pool_mode"] = pool_mode_;
j["pool_select_mode"] = poolSelectModeName(pool_select_mode_);
j["mine_when_idle"] = mine_when_idle_;
j["xmrig_version"] = xmrig_version_;
j["mine_idle_delay"]= mine_idle_delay_;
j["idle_thread_scaling"] = idle_thread_scaling_;
j["idle_threads_active"] = idle_threads_active_;
@@ -562,30 +293,6 @@ bool Settings::save(const std::string& path)
j["saved_pool_workers"] = json::array();
for (const auto& w : saved_pool_workers_)
j["saved_pool_workers"].push_back(w);
j["portfolio_entries"] = json::array();
for (const auto& e : portfolio_entries_) {
json entry;
entry["label"] = e.label;
entry["addresses"] = json::array();
for (const auto& a : e.addresses) entry["addresses"].push_back(a);
entry["icon"] = e.icon;
entry["color"] = e.color;
entry["outline_opacity"] = e.outlineOpacity;
entry["price_basis"] = e.priceBasis;
entry["manual_price"] = e.manualPrice;
entry["manual_currency"] = e.manualCurrency;
entry["show_drgx"] = e.showDrgx;
entry["show_value"] = e.showValue;
entry["show_24h"] = e.show24h;
entry["show_sparkline"] = e.showSparkline;
entry["sparkline_interval"] = e.sparklineInterval;
entry["scope"] = e.scope;
entry["grid_col"] = e.gridCol;
entry["grid_row"] = e.gridRow;
entry["grid_w"] = e.gridW;
entry["grid_h"] = e.gridH;
j["portfolio_entries"].push_back(std::move(entry));
}
j["font_scale"] = font_scale_;
j["settings_version"] = std::string(DRAGONX_VERSION);
if (window_width_ > 0 && window_height_ > 0) {
@@ -594,11 +301,17 @@ bool Settings::save(const std::string& path)
}
try {
// Atomic + durable: write to a temp file, fsync, then rename over the real file.
// A crash mid-write can no longer truncate settings.json (which would silently
// reset every preference on the next launch). Owner-only (0600) — it carries the
// lite-server list and address metadata.
return util::Platform::writeFileAtomically(path, j.dump(4), /*restrictPermissions=*/true);
// Ensure directory exists
fs::path p(path);
fs::create_directories(p.parent_path());
std::ofstream file(path);
if (!file.is_open()) {
return false;
}
file << j.dump(4);
return true;
} catch (const std::exception& e) {
DEBUG_LOGF("Failed to save settings: %s\n", e.what());
return false;

View File

@@ -5,7 +5,6 @@
#pragma once
#include <algorithm>
#include <cstddef>
#include <map>
#include <string>
#include <set>
@@ -55,56 +54,6 @@ public:
*/
static std::string getDefaultPath();
enum class LiteServerSelectionPreferenceMode {
Sticky,
Random
};
// Pool selection mode for the mining tab: Manual (user picks the pool) or
// AutoBalance (the wallet spreads miners across the official pools by hashrate).
enum class PoolSelectMode {
Manual,
AutoBalance
};
struct LiteServerPreference {
std::string url;
std::string label;
bool enabled = true;
};
// A user-defined portfolio entry: a custom label tied to a group of wallet addresses.
// The Market tab's portfolio card sums these addresses' balances under the label.
struct PortfolioEntry {
std::string label;
std::vector<std::string> addresses;
std::string icon; // project_icons wallet-icon name; empty = no icon
unsigned int color = 0; // packed IM_COL32 accent; 0 = theme default
int outlineOpacity = 25; // accent-outline opacity, percent (0-100)
// Per-group price data. priceBasis: 0 = live market (USD), 1 = live market (BTC),
// 2 = DRGX only (no fiat value), 3 = manual price. Defaults preserve prior behavior
// (show DRGX + USD value).
int priceBasis = 0;
double manualPrice = 0.0; // price per DRGX for the Manual basis
std::string manualCurrency = "USD";
bool showDrgx = true; // show the DRGX amount on the card
bool showValue = true; // show the converted/fiat value on the card
bool show24h = false; // show the 24h % change (live-market bases only)
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
int sparklineInterval = 4; // 0=minute 1=hour 2=day 3=week 4=month (default month: a real curve
// from the daily series, vs the young in-session minute buffer)
// Per-wallet visibility: "" (shown in every wallet — legacy/global) or a wallet-identity
// hash (shown only when that wallet is active). New entries are tagged with the current
// wallet so a portfolio built for wallet A doesn't clutter wallet B.
std::string scope;
// Legacy dashboard-grid placement (deprecated by the row layout; kept for back-compat so an
// older build's saved positions aren't dropped, but no longer used by the renderer).
int gridCol = -1;
int gridRow = -1;
int gridW = 8;
int gridH = 3;
};
// Theme
std::string getTheme() const { return theme_; }
void setTheme(const std::string& theme) { theme_ = theme; }
@@ -113,58 +62,6 @@ public:
std::string getSkinId() const { return skin_id_; }
void setSkinId(const std::string& id) { skin_id_ = id; }
// Stable z-address chosen for HushChat: the reply-to address in outgoing headers, and (for
// legacy/non-mnemonic wallets) the source of the seed-derived chat identity. Persisted so the
// identity + reply address don't shift when new addresses are generated.
std::string getChatReplyZaddr() const { return chat_reply_zaddr_; }
void setChatReplyZaddr(const std::string& z) { chat_reply_zaddr_ = z; }
// Muted chat conversations (by cid) — muted conversations don't badge or raise a toast (Q10).
bool isChatMuted(const std::string& cid) const {
return std::find(muted_chat_cids_.begin(), muted_chat_cids_.end(), cid) != muted_chat_cids_.end();
}
void setChatMuted(const std::string& cid, bool muted) {
const bool already = isChatMuted(cid);
if (muted && !already) muted_chat_cids_.push_back(cid);
else if (!muted && already)
muted_chat_cids_.erase(std::remove(muted_chat_cids_.begin(), muted_chat_cids_.end(), cid),
muted_chat_cids_.end());
}
// Hidden chat conversations (by cid) — hidden ones are filtered out of the list; a new incoming
// message un-hides them (you can't un-receive) so nothing is silently lost.
bool isChatHidden(const std::string& cid) const {
return std::find(hidden_chat_cids_.begin(), hidden_chat_cids_.end(), cid) != hidden_chat_cids_.end();
}
void setChatHidden(const std::string& cid, bool hidden) {
const bool already = isChatHidden(cid);
if (hidden && !already) hidden_chat_cids_.push_back(cid);
else if (!hidden && already)
hidden_chat_cids_.erase(std::remove(hidden_chat_cids_.begin(), hidden_chat_cids_.end(), cid),
hidden_chat_cids_.end());
}
// ── Chat-tab customization (chat settings modal + Settings → Chat & Contacts) ──────
bool getChatEmojiColor() const { return chat_emoji_color_; }
void setChatEmojiColor(bool v) { chat_emoji_color_ = v; }
float getChatPollRateSec() const { return chat_poll_rate_sec_; }
void setChatPollRateSec(float v) { chat_poll_rate_sec_ = std::max(0.5f, std::min(15.0f, v)); }
int getChatBubbleStyle() const { return chat_bubble_style_; }
void setChatBubbleStyle(int v) { chat_bubble_style_ = (v < 0 || v > 2) ? 0 : v; }
int getChatBubbleAccent() const { return chat_bubble_accent_; }
void setChatBubbleAccent(int v) { chat_bubble_accent_ = (v < 0 || v > 5) ? 0 : v; }
int getChatDensity() const { return chat_density_; }
void setChatDensity(int v) { chat_density_ = (v < 0 || v > 1) ? 0 : v; }
float getChatFontScale() const { return chat_font_scale_; }
void setChatFontScale(float v) { chat_font_scale_ = std::max(0.8f, std::min(1.5f, v)); }
int getChatTimeFormat() const { return chat_time_format_; } // 0=follow global, 1=24h, 2=12h
void setChatTimeFormat(int v) { chat_time_format_ = (v < 0 || v > 2) ? 0 : v; }
bool getChatEnterSends() const { return chat_enter_sends_; }
void setChatEnterSends(bool v) { chat_enter_sends_ = v; }
// Global clock format (0=24h, 1=12h) — chat can override it for the Chat tab only.
int getTimeFormat() const { return time_format_; }
void setTimeFormat(int v) { time_format_ = (v < 0 || v > 1) ? 0 : v; }
// Privacy
bool getSaveZtxs() const { return save_ztxs_; }
void setSaveZtxs(bool save) { save_ztxs_ = save; }
@@ -227,36 +124,10 @@ public:
std::string getBalanceLayout() const { return balance_layout_; }
void setBalanceLayout(const std::string& v) { balance_layout_ = v; }
// Market-tab portfolio row style: 0 = Table (borderless grid), 1 = Cards (glass card + Z/T bar),
// 2 = Spotlight (hero value). Cycled with Left/Right arrows or set in the Market settings modal.
int getPortfolioStyle() const { return portfolio_style_; }
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
// Contacts tab address-list view: 0 = cards, 1 = list, 2 = table.
int getContactsViewMode() const { return contacts_view_mode_; }
void setContactsViewMode(int v) { contacts_view_mode_ = (v < 0 || v > 2) ? 0 : v; }
// Contacts customization (gear modal): avatar shape + card/list row scale.
// Avatar shape: 0 = circle, 1 = rounded square, 2 = full-row-height left tab (rounded-left, flat right).
int getContactsAvatarShape() const { return contacts_avatar_shape_; }
void setContactsAvatarShape(int v) { contacts_avatar_shape_ = (v < 0 || v > 2) ? 0 : v; }
float getContactsListScale() const { return contacts_list_scale_; }
void setContactsListScale(float v) { contacts_list_scale_ = std::max(0.8f, std::min(1.5f, v)); }
// Play animated contact avatars (GIF/WebP). Off = show the first frame only.
bool getAnimateAvatars() const { return animate_avatars_; }
void setAnimateAvatars(bool v) { animate_avatars_ = v; }
// Console scanline effect
bool getScanlineEnabled() const { return scanline_enabled_; }
void setScanlineEnabled(bool v) { scanline_enabled_ = v; }
// Console output appearance: per-line left color accent bars, and per-channel text coloring.
// (Defaults match the ConsoleTab statics so an upgrade re-save doesn't flip visible behavior.)
bool getConsoleLineAccents() const { return console_line_accents_; }
void setConsoleLineAccents(bool v) { console_line_accents_ = v; }
bool getConsoleTextColor() const { return console_text_color_; }
void setConsoleTextColor(bool v) { console_text_color_ = v; }
float getConsoleZoom() const { return console_zoom_; }
void setConsoleZoom(float v) { console_zoom_ = v; }
// Hidden addresses (addresses hidden from the UI by the user)
const std::set<std::string>& getHiddenAddresses() const { return hidden_addresses_; }
bool isAddressHidden(const std::string& addr) const { return hidden_addresses_.count(addr) > 0; }
@@ -323,34 +194,6 @@ public:
bool getWizardCompleted() const { return wizard_completed_; }
void setWizardCompleted(bool v) { wizard_completed_ = v; }
// Whether the one-time "back up your seed phrase" reminder has already been shown.
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
// Bundled-daemon size we last prompted to install (see App::renderDaemonUpdatePrompt). Lets the
// "a newer node is bundled — update?" prompt fire once per wallet version, never re-nagging.
long long getDaemonUpdatePromptedSize() const { return daemon_update_prompted_size_; }
void setDaemonUpdatePromptedSize(long long v) { daemon_update_prompted_size_ = v; }
// Active wallet file the daemon loads via -wallet=<name> (multi-wallet). A plain filename in
// the datadir; defaults to the daemon's own default. Used at launch to know which wallet we're
// on before connect + to scope per-wallet data.
std::string getActiveWalletFile() const { return active_wallet_file_; }
void setActiveWalletFile(const std::string& v) { active_wallet_file_ = v; }
// Pending "migrate to a seed wallet" state (Phase 1 created the wallet; a later sweep/adopt
// step consumes it). dest = the new wallet's sweep-target z-address; tempDir = its datadir.
bool getSeedMigrationPending() const { return seed_migration_pending_; }
void setSeedMigrationPending(bool v) { seed_migration_pending_ = v; }
std::string getSeedMigrationDest() const { return seed_migration_dest_; }
void setSeedMigrationDest(const std::string& v) { seed_migration_dest_ = v; }
std::string getSeedMigrationTempDir() const { return seed_migration_temp_dir_; }
void setSeedMigrationTempDir(const std::string& v) { seed_migration_temp_dir_ = v; }
// The sweep transaction id, persisted once the sweep is submitted — non-empty means the
// migration is past the sweep, so a resume goes to the confirm/adopt stage (not sweep again).
std::string getSeedMigrationSweepTxid() const { return seed_migration_sweep_txid_; }
void setSeedMigrationSweepTxid(const std::string& v) { seed_migration_sweep_txid_ = v; }
// Security — auto-lock timeout (seconds; 0 = disabled)
int getAutoLockTimeout() const { return auto_lock_timeout_; }
void setAutoLockTimeout(int seconds) { auto_lock_timeout_ = seconds; }
@@ -375,39 +218,6 @@ public:
int getMaxConnections() const { return max_connections_; }
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
// Lite wallet server selection
LiteServerSelectionPreferenceMode getLiteServerSelectionMode() const { return lite_server_selection_mode_; }
void setLiteServerSelectionMode(LiteServerSelectionPreferenceMode mode) { lite_server_selection_mode_ = mode; }
std::string getLiteStickyServerUrl() const { return lite_sticky_server_url_; }
void setLiteStickyServerUrl(const std::string& url) { lite_sticky_server_url_ = url; }
std::string getLiteChainName() const { return lite_chain_name_; }
void setLiteChainName(const std::string& chainName) { lite_chain_name_ = chainName; }
std::size_t getLiteRandomSelectionSeed() const { return lite_random_selection_seed_; }
void setLiteRandomSelectionSeed(std::size_t seed) { lite_random_selection_seed_ = seed; }
bool getLitePersistSelectedServer() const { return lite_persist_selected_server_; }
void setLitePersistSelectedServer(bool persist) { lite_persist_selected_server_ = persist; }
const std::vector<LiteServerPreference>& getLiteServers() const { return lite_servers_; }
void setLiteServers(const std::vector<LiteServerPreference>& servers) { lite_servers_ = servers; }
// User-defined portfolio entries (Market tab). "All funds" is implicit, not stored here.
const std::vector<PortfolioEntry>& getPortfolioEntries() const { return portfolio_entries_; }
void setPortfolioEntries(const std::vector<PortfolioEntry>& entries) { portfolio_entries_ = entries; }
// Lite servers the user has hidden from the Network tab (kept by URL, shown via a toggle).
const std::set<std::string>& getLiteHiddenServers() const { return lite_hidden_servers_; }
bool isLiteServerHidden(const std::string& url) const { return lite_hidden_servers_.count(url) > 0; }
void hideLiteServer(const std::string& url) { lite_hidden_servers_.insert(url); }
void unhideLiteServer(const std::string& url) { lite_hidden_servers_.erase(url); }
// Lite wallet rollout / kill-switch (see wallet/lite_rollout_policy.h).
// Override: "auto" (honor rollout manifest), "force_on", or "force_off".
std::string getLiteRolloutOverride() const { return lite_rollout_override_; }
void setLiteRolloutOverride(const std::string& v) { lite_rollout_override_ = v; }
// Stable, locally-generated install id used only to derive the staged-rollout bucket.
// Never transmitted; carries no PII. Generated on first use if empty.
std::string getLiteInstallId() const { return lite_install_id_; }
void setLiteInstallId(const std::string& v) { lite_install_id_ = v; }
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
bool getVerboseLogging() const { return verbose_logging_; }
void setVerboseLogging(bool v) { verbose_logging_ = v; }
@@ -438,10 +248,6 @@ public:
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
std::string getSelectedPair() const { return selected_pair_; }
void setSelectedPair(const std::string& v) { selected_pair_ = v; }
int getChartInterval() const { return chart_interval_; } // Market chart range 0=Live..4=1M
void setChartInterval(int v) { chart_interval_ = v; }
int getChartStyle() const { return chart_style_; } // 0 = line, 1 = candlestick
void setChartStyle(int v) { chart_style_ = v; }
// Pool mining
std::string getPoolUrl() const { return pool_url_; }
@@ -458,12 +264,6 @@ public:
void setPoolHugepages(bool v) { pool_hugepages_ = v; }
bool getPoolMode() const { return pool_mode_; }
void setPoolMode(bool v) { pool_mode_ = v; }
PoolSelectMode getPoolSelectMode() const { return pool_select_mode_; }
void setPoolSelectMode(PoolSelectMode v) { pool_select_mode_ = v; }
// Installed DRG-XMRig release tag (for in-app miner update detection); empty if unknown/bundled.
std::string getXmrigVersion() const { return xmrig_version_; }
void setXmrigVersion(const std::string& v) { xmrig_version_ = v; }
// Mine when idle (auto-start mining when system is idle)
bool getMineWhenIdle() const { return mine_when_idle_; }
@@ -525,19 +325,6 @@ private:
// Settings values
std::string theme_ = "dragonx";
std::string skin_id_ = "dragonx";
std::string chat_reply_zaddr_;
std::vector<std::string> muted_chat_cids_; // muted chat conversations by cid (Q10)
std::vector<std::string> hidden_chat_cids_; // hidden chat conversations by cid
// Chat-tab customization (chat settings modal + Settings → Chat & Contacts).
bool chat_emoji_color_ = true; // true = color (needs FreeType; falls back to mono if absent), false = monochrome
float chat_poll_rate_sec_ = 2.5f; // 0-conf chat fast-scan cadence (full node)
int chat_bubble_style_ = 0; // 0 = rounded, 1 = square, 2 = minimal
int chat_bubble_accent_ = 0; // outgoing-bubble accent preset (0 = theme primary)
int chat_density_ = 0; // 0 = comfortable, 1 = compact
float chat_font_scale_ = 1.0f; // message text scale
int chat_time_format_ = 0; // 0 = follow global, 1 = 24h, 2 = 12h (Chat tab only)
bool chat_enter_sends_ = true; // Enter sends (vs. inserts newline; Ctrl+Enter sends)
int time_format_ = 0; // global clock: 0 = 24h, 1 = 12h
bool save_ztxs_ = true;
bool auto_shield_ = true;
bool use_tor_ = false;
@@ -554,69 +341,30 @@ private:
bool gradient_background_ = false;
#ifdef _WIN32
float ui_opacity_ = 0.50f; // Card/sidebar opacity (0.31.0, 1.0 = opaque)
float window_opacity_ = 0.90f; // Background alpha (0.31.0, <1 = desktop visible)
float window_opacity_ = 0.75f; // Background alpha (0.31.0, <1 = desktop visible)
#else
float ui_opacity_ = 1.0f; // Mac/Linux: default fully opaque
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
#endif
std::string balance_layout_ = "classic";
int portfolio_style_ = 0; // Market portfolio row style (0 Table / 1 Cards / 2 Spotlight)
int contacts_view_mode_ = 0; // Contacts address-list view (0 cards / 1 list / 2 table)
int contacts_avatar_shape_ = 0; // 0 = circle, 1 = rounded square, 2 = full-height left tab
float contacts_list_scale_ = 1.0f; // card/list row scale (does not affect the table view)
bool animate_avatars_ = true; // play animated (GIF/WebP) contact avatars
bool scanline_enabled_ = true;
bool console_line_accents_ = true; // left color accent bars in console output
bool console_text_color_ = true; // per-channel text coloring in console output
float console_zoom_ = 1.0f; // console output font zoom factor
std::set<std::string> hidden_addresses_;
std::set<std::string> favorite_addresses_;
std::map<std::string, AddressMeta> address_meta_;
bool wizard_completed_ = false;
bool seed_backup_reminded_ = false;
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt
std::string active_wallet_file_ = "wallet.dat"; // -wallet=<name> the daemon loads (multi-wallet)
bool seed_migration_pending_ = false;
std::string seed_migration_dest_;
std::string seed_migration_temp_dir_;
std::string seed_migration_sweep_txid_;
int auto_lock_timeout_ = 900; // 15 minutes
int unlock_duration_ = 600; // 10 minutes
bool pin_enabled_ = false;
bool keep_daemon_running_ = false;
bool stop_external_daemon_ = false;
int max_connections_ = 0; // 0 = daemon default
// Lite wallet server preferences. These are user/server settings only;
// wallet secrets, wallet files, and lifecycle state are never stored here.
LiteServerSelectionPreferenceMode lite_server_selection_mode_ = LiteServerSelectionPreferenceMode::Sticky;
std::string lite_sticky_server_url_ = "https://lite.dragonx.is";
std::string lite_chain_name_ = "main"; // SDXL backend chain id; must be main/test/regtest
std::size_t lite_random_selection_seed_ = 0;
bool lite_persist_selected_server_ = true;
std::string lite_rollout_override_ = "auto"; // auto|force_on|force_off
std::string lite_install_id_; // random local-only id; rollout-bucket source
std::vector<LiteServerPreference> lite_servers_ = {
{"https://lite.dragonx.is", "DragonX Lite", true},
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
{"https://lite2.dragonx.is", "DragonX Lite 2", true},
{"https://lite3.dragonx.is", "DragonX Lite 3", true},
{"https://lite4.dragonx.is", "DragonX Lite 4", true},
{"https://lite5.dragonx.is", "DragonX Lite 5", true}
};
std::set<std::string> lite_hidden_servers_; // server URLs hidden from the Network tab
std::vector<PortfolioEntry> portfolio_entries_; // Market tab custom portfolio groups
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_ = "Nonkyc.io";
std::string selected_pair_ = "DRGX/USDT";
int chart_interval_ = 4; // Market chart range (0=Live 1=1H 2=1D 3=1W 4=1M)
int chart_style_ = 1; // Market chart style (0=line, 1=candlestick)
std::string selected_exchange_ = "TradeOgre";
std::string selected_pair_ = "DRGX/BTC";
// Pool mining
std::string pool_url_ = "pool.dragonx.is:3433";
@@ -626,8 +374,6 @@ private:
bool pool_tls_ = false;
bool pool_hugepages_ = true;
bool pool_mode_ = false; // false=solo, true=pool
PoolSelectMode pool_select_mode_ = PoolSelectMode::Manual; // manual vs auto-balance pool choice
std::string xmrig_version_; // installed DRG-XMRig release tag (update detection)
bool mine_when_idle_ = false; // auto-start mining when system idle
int mine_idle_delay_= 120; // seconds of idle before mining starts
bool idle_thread_scaling_ = false; // scale threads instead of start/stop

View File

@@ -1,3 +1,31 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "dragonx_generated_version.h"
// !! 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.2.0-rc1"
#define DRAGONX_VERSION_MAJOR 1
#define DRAGONX_VERSION_MINOR 2
#define DRAGONX_VERSION_PATCH 0
#define DRAGONX_APP_NAME "ObsidianDragon"
#define DRAGONX_ORG_NAME "Hush"
// Default RPC settings
#define DRAGONX_DEFAULT_RPC_HOST "127.0.0.1"
#define DRAGONX_DEFAULT_RPC_PORT "21769"
// Coin parameters
#define DRAGONX_TICKER "DRGX"
#define DRAGONX_COIN_NAME "DragonX"
#define DRAGONX_URI_SCHEME "drgx"
#define DRAGONX_ZATOSHI_PER_COIN 100000000
#define DRAGONX_DEFAULT_FEE 0.0001
// Config file names
#define DRAGONX_CONF_FILENAME "DRAGONX.conf"
#define DRAGONX_WALLET_FILENAME "wallet.dat"

View File

@@ -4,16 +4,15 @@
#pragma once
// !! DO NOT EDIT generated version output — it is generated from version.h.in by CMake.
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...) for the full-node app,
// !! or DRAGONX_LITE_VERSION for ObsidianDragonLite. DRAGONX_APP_VERSION is the active variant.
// !! 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 "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@"
#define DRAGONX_VERSION_MAJOR @DRAGONX_APP_VERSION_MAJOR@
#define DRAGONX_VERSION_MINOR @DRAGONX_APP_VERSION_MINOR@
#define DRAGONX_VERSION_PATCH @DRAGONX_APP_VERSION_PATCH@
#define DRAGONX_VERSION "@PROJECT_VERSION@@DRAGONX_VERSION_SUFFIX@"
#define DRAGONX_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define DRAGONX_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define DRAGONX_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define DRAGONX_APP_NAME "@DRAGONX_APP_NAME@"
#define DRAGONX_APP_NAME "ObsidianDragon"
#define DRAGONX_ORG_NAME "Hush"
// Default RPC settings

View File

@@ -1,10 +1,7 @@
#include "daemon_controller.h"
#include "../config/settings.h"
#include "../util/platform.h"
#include <algorithm>
#include <filesystem>
#include <system_error>
namespace dragonx {
namespace daemon {
@@ -26,18 +23,6 @@ void DaemonController::syncSettings(const config::Settings* settings)
if (!settings) return;
daemon_->setDebugCategories(settings->getDebugCategories());
daemon_->setMaxConnections(settings->getMaxConnections());
std::string walletFile = settings->getActiveWalletFile();
// The Wallets dialog opens an out-of-datadir wallet by linking it into the datadir under a
// "wallet-ip-<hash>.dat" name. If that link went dangling (the external file was moved / a USB was
// unplugged between sessions), don't let the daemon create a fresh EMPTY wallet at that name — fall
// back to the default this launch. fs::exists follows the link, so it's false for a dangling one.
if (walletFile.rfind("wallet-ip-", 0) == 0) {
std::error_code ec;
if (!std::filesystem::exists(util::Platform::getDragonXDataDir() + "/" + walletFile, ec))
walletFile = "wallet.dat";
}
daemon_->setWalletFile(walletFile);
}
bool DaemonController::start(const config::Settings* settings)
@@ -61,11 +46,6 @@ bool DaemonController::externalDaemonDetected() const
return daemon_->externalDaemonDetected();
}
void DaemonController::clearExternalDaemonDetected()
{
daemon_->clearExternalDaemonDetected();
}
DaemonController::State DaemonController::state() const
{
return daemon_->getState();
@@ -116,28 +96,12 @@ bool DaemonController::rescanOnNextStart() const
return daemon_->rescanOnNextStart();
}
void DaemonController::setZapOnNextStart(bool enabled)
{
daemon_->setZapOnNextStart(enabled);
}
void DaemonController::setSalvageOnNextStart(bool enabled)
{
daemon_->setSalvageOnNextStart(enabled);
}
bool DaemonController::zapOnNextStart() const
{
return daemon_->zapOnNextStart();
}
void DaemonController::prepareLifecycleOperation(const LifecycleDecision& decision,
const config::Settings* settings)
{
if (settings) syncSettings(settings);
if (decision.resetCrashCount) resetCrashCount();
if (decision.setRescanOnNextStart) setRescanOnNextStart(true);
if (decision.setZapOnNextStart) setZapOnNextStart(true);
}
DaemonController::ShutdownDecision DaemonController::shutdownDecision(

View File

@@ -31,7 +31,6 @@ public:
enum class LifecycleOperation {
ManualRestart,
Rescan,
RepairWallet, // restart with -zapwallettxes=2 (wipe & rebuild wallet tx records)
DeleteBlockchainData,
BootstrapStop
};
@@ -47,7 +46,6 @@ public:
bool setRescanOnNextStart = false;
bool disconnectRpc = false;
int restartDelayMs = 0;
bool setZapOnNextStart = false;
};
class LifecycleTaskContext {
@@ -93,7 +91,6 @@ public:
bool isRunning() const;
bool externalDaemonDetected() const;
void clearExternalDaemonDetected();
State state() const;
const std::string& lastError() const;
int crashCount() const;
@@ -105,9 +102,6 @@ public:
void resetCrashCount();
void setRescanOnNextStart(bool enabled);
bool rescanOnNextStart() const;
void setZapOnNextStart(bool enabled);
bool zapOnNextStart() const;
void setSalvageOnNextStart(bool enabled);
static ShutdownDecision evaluateShutdownPolicy(bool hasDaemon,
bool externalDaemonDetected,
@@ -147,13 +141,6 @@ public:
}
return {operation, true, daemonRunning, "rescan-blockchain", "Starting rescan...", "",
false, true, false, 3000};
case LifecycleOperation::RepairWallet:
if (!usingEmbeddedDaemon || !hasDaemon) {
return {operation, false, daemonRunning, "", "",
"Wallet repair requires embedded daemon. Restart your daemon with -zapwallettxes=2 manually."};
}
return {operation, true, daemonRunning, "repair-wallet", "Repairing wallet...", "",
false, false, false, 3000, true};
case LifecycleOperation::DeleteBlockchainData:
if (!usingEmbeddedDaemon || !hasDaemon) {
return {operation, false, daemonRunning, "", "",
@@ -203,7 +190,6 @@ public:
}
break;
case LifecycleOperation::Rescan:
case LifecycleOperation::RepairWallet:
case LifecycleOperation::DeleteBlockchainData:
runtime.stopDaemonWithPolicy();
result.stopped = true;
@@ -220,7 +206,6 @@ public:
}
if (decision.operation == LifecycleOperation::Rescan ||
decision.operation == LifecycleOperation::RepairWallet ||
decision.operation == LifecycleOperation::DeleteBlockchainData) {
runtime.resetOutputOffset();
}

View File

@@ -212,11 +212,6 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
"-addnode=node4.dragonx.is",
"-experimentalfeatures",
"-developerencryptwallet",
// Create fresh wallets from a BIP39 mnemonic so their 24-word phrase can be
// exported (z_exportmnemonic) and is portable to SDXLite/ObsidianDragonLite.
// The daemon reads this ONLY inside GenerateNewSeed() when a wallet has no seed
// yet, so it is inert on existing wallets — safe to pass unconditionally.
"-usemnemonic=1",
dbcache_arg
};
}
@@ -386,80 +381,52 @@ static std::string getPortOwnerInfo(int port)
#endif
}
// Check if a TCP port is already in use (something is LISTENING). The daemon binds BOTH 127.0.0.1 (IPv4)
// and ::1 (IPv6); during shutdown one can linger after the other releases (and a "Binding RPC on ::1 …
// failed" is fatal to a fresh start), so we treat the port as in use if EITHER localhost family has it.
// Check if a TCP port is already in use (something is LISTENING)
static bool isPortInUse(int port)
{
#ifdef _WIN32
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) return false;
bool inUse = false;
{ // IPv4 127.0.0.1
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock != INVALID_SOCKET) {
struct sockaddr_in addr; memset(&addr, 0, sizeof(addr));
if (sock == INVALID_SOCKET) { WSACleanup(); return false; }
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<u_short>(port));
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) inUse = true;
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
closesocket(sock);
}
}
if (!inUse) { // IPv6 ::1
SOCKET sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
if (sock != INVALID_SOCKET) {
struct sockaddr_in6 addr; memset(&addr, 0, sizeof(addr));
addr.sin6_family = AF_INET6;
addr.sin6_port = htons(static_cast<u_short>(port));
addr.sin6_addr = in6addr_loopback; // ::1 — avoids inet_pton's _WIN32_WINNT gating on mingw
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) inUse = true;
closesocket(sock);
}
}
WSACleanup();
return inUse;
return (result == 0);
#else
// On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp{,6} to avoid creating sockets. The
// parse is family-agnostic: %*X skips the local IP (8 hex for v4, 32 for v6), %X grabs the port.
auto scanProc = [port](const char* path) -> bool {
FILE* fp = fopen(path, "r");
if (!fp) return false;
char line[512];
// On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp to avoid
// creating sockets. Fall back to connect() if /proc is unavailable.
FILE* fp = fopen("/proc/net/tcp", "r");
if (fp) {
char line[256];
unsigned int localPort, state;
bool found = false;
while (fgets(line, sizeof(line), fp)) {
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X", &localPort, &state) == 2) {
if (localPort == static_cast<unsigned int>(port) && state == 0x0A) { found = true; break; }
if (localPort == static_cast<unsigned int>(port) && state == 0x0A) {
found = true;
break;
}
}
}
fclose(fp);
return found;
};
if (FILE* probe = fopen("/proc/net/tcp", "r")) { // /proc available → authoritative LISTEN check
fclose(probe);
return scanProc("/proc/net/tcp") || scanProc("/proc/net/tcp6");
}
// Fallback (macOS): connect() probe on both loopback families.
auto connProbe = [port](int family, const char* addr) -> bool {
int sock = socket(family, SOCK_STREAM, 0);
// Fallback (macOS): try to connect
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) return false;
bool ok = false;
if (family == AF_INET) {
struct sockaddr_in a; memset(&a, 0, sizeof(a));
a.sin_family = AF_INET; a.sin_port = htons(static_cast<uint16_t>(port));
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
ok = (connect(sock, (struct sockaddr*)&a, sizeof(a)) == 0);
} else {
struct sockaddr_in6 a; memset(&a, 0, sizeof(a));
a.sin6_family = AF_INET6; a.sin6_port = htons(static_cast<uint16_t>(port));
inet_pton(AF_INET6, addr, &a.sin6_addr);
ok = (connect(sock, (struct sockaddr*)&a, sizeof(a)) == 0);
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<uint16_t>(port));
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
close(sock);
return ok;
};
return connProbe(AF_INET, "127.0.0.1") || connProbe(AF_INET6, "::1");
return (result == 0);
#endif
}
@@ -476,10 +443,9 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
return true;
}
// Check if something is already listening on the RPC port. An isolated instance (migrate-to-
// seed) runs on its own non-default port alongside the main daemon, so it skips this bail.
// Check if something is already listening on the RPC port
int rpc_port = std::atoi(DRAGONX_DEFAULT_RPC_PORT);
if (!skip_port_check_ && isPortInUse(rpc_port)) {
if (isPortInUse(rpc_port)) {
std::string owner = getPortOwnerInfo(rpc_port);
VERBOSE_LOGF("[INFO] Port %d is already in use by %s — external daemon detected, will connect to it.\\n", rpc_port, owner.c_str());
external_daemon_detected_ = true;
@@ -516,46 +482,12 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
args.push_back("-maxconnections=" + std::to_string(max_connections_));
}
// Active wallet file (multi-wallet). The daemon loads <datadir>/<name>. Only pass it for a
// non-default name so the common case's command line is unchanged; skip during an isolated
// start (seed migration manages its own throwaway wallet).
if (!wallet_file_.empty() && wallet_file_ != "wallet.dat" && override_datadir_.empty()) {
DEBUG_LOGF("[INFO] Loading wallet file: %s\n", wallet_file_.c_str());
args.push_back("-wallet=" + wallet_file_);
}
// Add wallet-repair flag if requested (one-shot). Precedence: salvage > zap > rescan; each implies a
// rescan in the daemon, so we don't stack them.
if (salvage_on_next_start_.exchange(false)) {
// -salvagewallet recovers readable keypairs from a corrupt wallet.dat; the daemon then implies -rescan.
DEBUG_LOGF("[INFO] Adding -salvagewallet flag to recover a corrupt wallet\n");
args.push_back("-salvagewallet");
zap_on_next_start_.store(false);
rescan_on_next_start_.store(false);
} else if (zap_on_next_start_.exchange(false)) {
// -zapwallettxes=2 wipes all wallet tx/note records and rebuilds them from the chain (implies -rescan).
DEBUG_LOGF("[INFO] Adding -zapwallettxes=2 flag for wallet repair (zap & rebuild)\n");
args.push_back("-zapwallettxes=2");
rescan_on_next_start_.store(false); // implied by zap; avoid redundant -rescan
} else if (rescan_on_next_start_.exchange(false)) {
// Add -rescan flag if requested (one-shot)
if (rescan_on_next_start_.exchange(false)) {
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
args.push_back("-rescan");
}
// One-shot isolated-datadir override (migrate-to-seed flow): run this start against a
// throwaway datadir, plus any extra args (e.g. -connect=0). Consumed here so later starts
// revert to the normal datadir. The datadir's basename MUST be the assetchain name (DRAGONX)
// or the daemon mis-resolves its conf/port; it reads <datadir>/DRAGONX.conf automatically, so
// no -conf is passed (an explicit -conf confuses the Komodo/Hush path resolution).
if (!override_datadir_.empty()) {
DEBUG_LOGF("[INFO] Isolated start override: -datadir=%s\n", override_datadir_.c_str());
args.push_back("-datadir=" + override_datadir_);
}
for (const auto& a : override_extra_args_) args.push_back(a);
override_datadir_.clear();
override_extra_args_.clear();
if (!startProcess(daemon_path, args)) {
DEBUG_LOGF("[ERROR] Failed to start dragonxd process: %s\\n", last_error_.c_str());
setState(State::Error, "Failed to start dragonxd process");
@@ -691,24 +623,17 @@ static DWORD findProcessByName(const char* name)
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return 0;
// Use the explicit WIDE Toolhelp API + a wide compare so this is correct regardless of the UNICODE
// macro. (The non-suffixed PROCESSENTRY32/Process32First map to the wide variants when UNICODE is
// defined, in which case szExeFile is WCHAR[] and an ANSI _stricmp would compare garbage and NEVER
// match — silently making findProcessByName a no-op that returns 0 for a running process.)
wchar_t wname[MAX_PATH];
if (MultiByteToWideChar(CP_ACP, 0, name, -1, wname, MAX_PATH) == 0) { CloseHandle(snap); return 0; }
PROCESSENTRY32W entry;
PROCESSENTRY32 entry;
entry.dwSize = sizeof(entry);
DWORD pid = 0;
if (Process32FirstW(snap, &entry)) {
if (Process32First(snap, &entry)) {
do {
if (lstrcmpiW(entry.szExeFile, wname) == 0) { // Win32 case-insensitive wide compare
if (_stricmp(entry.szExeFile, name) == 0) {
pid = entry.th32ProcessID;
break;
}
} while (Process32NextW(snap, &entry));
} while (Process32Next(snap, &entry));
}
CloseHandle(snap);
return pid;
@@ -1284,33 +1209,5 @@ bool EmbeddedDaemon::isRpcPortInUse()
return isPortInUse(port);
}
bool EmbeddedDaemon::tcpPortInUse(int port)
{
return isPortInUse(port);
}
bool EmbeddedDaemon::isDaemonProcessRunning()
{
#ifdef _WIN32
return findProcessByName("dragonxd.exe") != 0;
#elif defined(__linux__)
// Scan /proc for a process whose comm is exactly "dragonxd". Iterate with an error_code so a proc
// entry vanishing mid-scan (a process exiting) can't throw.
std::error_code ec;
fs::directory_iterator it("/proc", ec), end;
for (; !ec && it != end; it.increment(ec)) {
const std::string pid = it->path().filename().string();
if (pid.empty() || pid[0] < '0' || pid[0] > '9') continue; // numeric pid dirs only
std::ifstream f((it->path() / "comm").string());
std::string comm;
if (f && std::getline(f, comm) && comm == "dragonxd") return true;
}
return false;
#else
// macOS has no /proc; fall back to the RPC-port probe (best-effort).
return isPortInUse(std::atoi(DRAGONX_DEFAULT_RPC_PORT));
#endif
}
} // namespace daemon
} // namespace dragonx

View File

@@ -142,10 +142,6 @@ public:
* When true the wallet should connect to it instead of showing an error.
*/
bool externalDaemonDetected() const { return external_daemon_detected_; }
// Clear the adopted-external latch before relaunching our OWN process (e.g. a wallet switch that
// stopped the adopted daemon), so the freshly spawned daemon is treated as owned. start() also
// clears it on the port-free fall-through, but only if not short-circuited by an early guard.
void clearExternalDaemonDetected() { external_daemon_detected_ = false; }
/**
* @brief Set callback for state changes
@@ -187,57 +183,6 @@ public:
void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; }
bool rescanOnNextStart() const { return rescan_on_next_start_.load(); }
// Active wallet file (multi-wallet). Passed to the daemon as -wallet=<name> so it loads
// <datadir>/<name>; empty or "wallet.dat" keeps the daemon default (no arg). Must be a plain
// filename in the datadir — the daemon rejects paths.
void setWalletFile(const std::string& v) { wallet_file_ = v; }
std::string walletFile() const { return wallet_file_; }
/**
* @brief Request a wallet repair (-zapwallettxes=2) on the next daemon start. This deletes all
* wallet transaction/note records and rebuilds them from the chain (keys are kept); the
* daemon implicitly rescans afterwards. One-shot, like the rescan flag.
*/
void setZapOnNextStart(bool v) { zap_on_next_start_ = v; }
bool zapOnNextStart() const { return zap_on_next_start_.load(); }
// -salvagewallet: attempt to recover keys from a corrupt wallet.dat on startup (implies -rescan in
// the daemon). One-shot, consumed on the next start. Used to repair a wallet a switch flagged corrupt.
void setSalvageOnNextStart(bool v) { salvage_on_next_start_ = v; }
bool salvageOnNextStart() const { return salvage_on_next_start_.load(); }
/**
* @brief One-shot isolated-datadir override for the NEXT start(): run the daemon against a
* different datadir (with its own DRAGONX.conf) plus the given extra args. Used by the
* "migrate to a seed wallet" flow to mint a fresh mnemonic wallet in a throwaway datadir
* without touching the real one. Consumed on the next start(); later starts are normal.
* The caller must serialize this with start() (no concurrent starts).
*/
void setNextStartOverride(const std::string& datadir, std::vector<std::string> extraArgs) {
override_datadir_ = datadir;
override_extra_args_ = std::move(extraArgs);
}
void clearNextStartOverride() { override_datadir_.clear(); override_extra_args_.clear(); }
/**
* @brief Skip the "default RPC port already in use → external daemon" bail in start().
* Set true only for an isolated instance running on its OWN (non-default) port
* alongside the main daemon (migrate-to-seed flow).
*/
void setSkipPortCheck(bool v) { skip_port_check_ = v; }
/**
* @brief True while ANY dragonxd process is running (by process name), regardless of who started it.
* Unlike isRpcPortInUse()/isRunning(), this reflects the actual PROCESS still being alive — a
* graceful shutdown stops accepting RPC (port reads "free") but keeps the datadir lock until the
* process exits, which can take up to ~90s. Use this to know a stopped node has FULLY released
* the datadir before starting a replacement. Matches the daemon binary name on all platforms.
*/
static bool isDaemonProcessRunning();
/** @brief Is an arbitrary TCP port currently in use on localhost? (used to pick a free port) */
static bool tcpPortInUse(int port);
/** Get number of consecutive daemon crashes (resets on successful start or manual reset) */
int getCrashCount() const { return crash_count_.load(); }
/** Reset crash counter (call on successful connection or manual restart) */
@@ -275,14 +220,8 @@ private:
std::atomic<bool> should_stop_{false};
std::set<std::string> debug_categories_;
int max_connections_ = 0; // 0 = daemon default
std::string wallet_file_; // -wallet=<name> for the active wallet; empty/"wallet.dat" = default
std::atomic<int> crash_count_{0}; // consecutive crash counter
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
std::atomic<bool> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start
std::atomic<bool> salvage_on_next_start_{false}; // -salvagewallet flag for next start
std::string override_datadir_; // one-shot: -datadir for the next start
std::vector<std::string> override_extra_args_; // one-shot: extra args for the next start
bool skip_port_check_ = false; // isolated instance on a non-default port
};
} // namespace daemon

View File

@@ -1,162 +0,0 @@
#include "daemon/seed_wallet_creator.h"
#include <chrono>
#include <filesystem>
#include <thread>
#include <sodium.h>
#include "daemon/embedded_daemon.h"
#include "rpc/rpc_client.h"
#include "util/platform.h"
namespace fs = std::filesystem;
namespace dragonx {
namespace daemon {
namespace {
// Random alphanumeric token for the isolated node's throwaway RPC credentials (libsodium CSPRNG;
// sodium_init() has already run at app startup for the chat crypto).
std::string randomToken(int n)
{
static const char cs[] =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
std::string s;
s.reserve(n);
for (int i = 0; i < n; ++i)
s.push_back(cs[randombytes_uniform(sizeof(cs) - 1)]);
return s;
}
// A free localhost port for the isolated node — just above the default so it never collides with
// the main daemon (which keeps running on the default port throughout).
int pickFreePort()
{
for (int p = 21770; p < 21900; ++p)
if (!EmbeddedDaemon::tcpPortInUse(p))
return p;
return 0;
}
} // namespace
SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
const std::function<void(const std::string&)>& progress)
{
auto report = [&](const std::string& m) { if (progress) progress(m); };
SeedWalletResult r;
std::error_code ec;
// 1. Isolated throwaway datadir. The Komodo/Hush daemon requires the datadir's basename to be
// the assetchain name (DRAGONX) — mirroring ~/.hush/DRAGONX — or it mis-resolves its conf and
// RPC port. So the wallet lives in <base>/DRAGONX; `base` is the migration root we clean up.
const std::string base = util::Platform::getConfigDir() + "/seed-migrate";
const std::string dataDir = base + "/DRAGONX";
fs::remove_all(base, ec);
fs::create_directories(dataDir, ec);
if (ec) { r.error = "Could not create the temporary wallet directory."; return r; }
// 2. Free port + fresh throwaway RPC credentials for the isolated node.
const int port = pickFreePort();
if (port <= 0) { r.error = "No free local port for the isolated node."; return r; }
const std::string user = randomToken(16);
const std::string pass = randomToken(32);
// 3. Minimal conf for the isolated node (own creds/port). The DRAGONX RPC is plaintext HTTP on
// localhost — `-tls=only` applies to P2P, not the RPC — so the client below connects without
// TLS, exactly as the main GUI does (its conf has no rpctls key either).
const std::string conf = "rpcuser=" + user + "\n"
"rpcpassword=" + pass + "\n"
"rpcport=" + std::to_string(port) + "\n"
"server=1\n";
if (!util::Platform::writeFileAtomically(dataDir + "/DRAGONX.conf", conf,
/*restrictPermissions=*/true)) {
r.error = "Could not write the isolated node config.";
fs::remove_all(base, ec);
return r;
}
// 4. Start the isolated daemon: fresh mnemonic wallet (-usemnemonic=1), no network/sync.
report("Starting an isolated node…");
EmbeddedDaemon temp;
temp.setSkipPortCheck(true); // runs on `port`, beside the main daemon on the default port
temp.setNextStartOverride(dataDir, {"-usemnemonic=1", "-connect=0", "-listen=0",
"-maxconnections=0"});
if (!temp.start("")) {
r.error = "Could not start the isolated node: " + temp.getLastError();
fs::remove_all(base, ec);
return r;
}
// 5. Connect to it, retrying until the RPC is responsive and past warmup.
report("Creating your new seed wallet…");
rpc::RPCClient cli;
bool ready = false;
for (int i = 0; i < 90 && !ready; ++i) {
if (cli.connect("127.0.0.1", std::to_string(port), user, pass, /*useTls=*/false)) {
try { cli.call("getinfo"); ready = true; } // succeeds only once past warmup (-28)
catch (...) { cli.disconnect(); }
}
if (!ready) std::this_thread::sleep_for(std::chrono::seconds(1));
}
if (!ready) {
r.error = "The isolated node did not become ready in time.";
temp.stop(20000);
fs::remove_all(base, ec);
return r;
}
// 6. Export the new seed phrase + a fresh shielded receive address (the future sweep target).
try {
auto m = cli.callSecret("z_exportmnemonic"); // zero the raw body too (B7)
if (m.contains("mnemonic") && m["mnemonic"].is_string()) {
// Take our copy, then scrub the json node's own copy so it isn't freed in the clear (B7).
auto& mn = m["mnemonic"].get_ref<std::string&>();
r.seedPhrase = mn;
if (!mn.empty()) sodium_memzero(&mn[0], mn.size());
}
r.destAddress = cli.call("z_getnewaddress").get<std::string>();
r.ok = !r.seedPhrase.empty() && !r.destAddress.empty();
if (!r.ok) r.error = "The isolated node returned an empty seed or address.";
} catch (const std::exception& e) {
const std::string what = e.what();
// "Method not found" (JSON-RPC -32601) means this dragonxd predates mnemonic support —
// it has no z_exportmnemonic RPC (the older bundled binary). Migrate-to-seed can't work
// until the daemon is updated, so give an actionable message, not the raw RPC error.
if (what.find("Method not found") != std::string::npos ||
what.find("-32601") != std::string::npos) {
r.error = "This DragonX daemon is too old to create a seed wallet — it lacks mnemonic "
"support (the z_exportmnemonic RPC). Update to the latest DragonX daemon "
"(Settings -> NODE & SECURITY -> Check for updates, or Install bundled), then "
"try again.";
} else {
r.error = std::string("Seed export failed: ") + what;
}
}
// W1-2: never hand back a live seed on a failure path. If the mnemonic was exported but a
// later step failed (empty address, or z_getnewaddress threw), the caller discards this
// result without wiping it, which would leave the seed resident. Success keeps it deliberately.
if (!r.ok && !r.seedPhrase.empty()) {
sodium_memzero(&r.seedPhrase[0], r.seedPhrase.size());
r.seedPhrase.clear();
}
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly).
cli.disconnect();
temp.stop(20000);
// 8. Keep the temp wallet for a later sweep/adopt step, or scrub it. tempDatadir is the
// migration root `base`; the new wallet.dat lives in <base>/DRAGONX.
r.tempDatadir = base;
if (!keepDatadir || !r.ok) {
fs::remove_all(base, ec);
r.tempDatadir.clear();
}
return r;
}
} // namespace daemon
} // namespace dragonx

View File

@@ -1,32 +0,0 @@
#pragma once
#include <functional>
#include <string>
namespace dragonx {
namespace daemon {
struct SeedWalletResult {
bool ok = false;
std::string seedPhrase; // SECRET — the caller must wipe it after use
std::string destAddress; // new shielded z-address (the Phase 2 sweep target)
std::string tempDatadir; // datadir holding the new wallet.dat (kept iff keepDatadir + ok)
std::string error;
};
// Mint a fresh BIP39 mnemonic wallet in an ISOLATED throwaway datadir by running a second
// dragonxd on its own port with no network, export its seed + a new z-address, then stop it.
//
// This is the safe first step of "migrate to a seed wallet": it moves no funds and never touches
// the main daemon or the real wallet.dat. Blocking — call it on a background thread.
//
// keepDatadir: keep the temp datadir + its wallet.dat so a later sweep/adopt step can use it, or
// delete it immediately (used when only the seed itself is wanted).
class SeedWalletCreator {
public:
static SeedWalletResult create(bool keepDatadir,
const std::function<void(const std::string&)>& progress = {});
};
} // namespace daemon
} // namespace dragonx

View File

@@ -2,13 +2,12 @@
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// xmrig_manager.cpp — Pool mining process management via drg-xmrig.
// 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"
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -23,7 +22,6 @@
#include <curl/curl.h>
#include "../util/logger.h"
#include "../util/pool_registry.h"
#ifdef _WIN32
#include <winsock2.h>
@@ -208,20 +206,19 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath)
try {
fs::create_directories(fs::path(outPath).parent_path());
std::ofstream ofs(outPath, std::ios::trunc);
std::ofstream ofs(outPath);
if (!ofs.is_open()) {
last_error_ = "Cannot write xmrig config: " + outPath;
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
return false;
}
#ifndef _WIN32
// Restrict to owner (0600) BEFORE writing any secret material (API token, wallet
// address, worker name). The file is still empty here, so the config is never
// world-readable — closing the window between creation and the previous post-write chmod.
chmod(outPath.c_str(), 0600);
#endif
ofs << j.dump(4);
ofs.close();
#ifndef _WIN32
// 0600 permissions — only owner can read/write
chmod(outPath.c_str(), 0600);
#endif
return true;
} catch (const std::exception& e) {
last_error_ = std::string("Config write error: ") + e.what();
@@ -721,11 +718,6 @@ void XmrigManager::fetchStatsHttp() {
std::lock_guard<std::mutex> lk(stats_mutex_);
// Miner version (top-level in /2/summary) — lets the UI show the actually
// running miner's version even when no release tag was persisted (bundled miner).
if (resp.contains("version") && resp["version"].is_string())
stats_.version = resp["version"].get<std::string>();
if (resp.contains("hashrate") && resp["hashrate"].contains("total")) {
auto& total = resp["hashrate"]["total"];
if (total.is_array() && total.size() >= 3) {
@@ -776,14 +768,10 @@ void XmrigManager::fetchStatsHttp() {
void XmrigManager::fetchPoolApiStats() {
if (state_ != State::Running || pool_host_.empty()) return;
// Resolve the stats endpoint + JSON schema for this pool. Known pools carry their
// own API shape (pool.dragonx.is = custom /api/stats; pool.dragonx.cc = Miningcore
// /api/pools); unknown/custom hosts fall back to the .is convention.
const util::KnownPool* known = util::findKnownPoolByUrl(pool_host_);
const std::string url = known ? known->statsUrl
: ("https://" + pool_host_ + "/api/stats");
// Query the pool's public stats API
std::string url = "https://" + pool_host_ + "/api/stats";
std::string responseData;
CURL* curl = curl_easy_init();
if (!curl) return;
@@ -794,99 +782,31 @@ void XmrigManager::fetchPoolApiStats() {
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
// pool.dragonx.cc sits behind Cloudflare and 403s odd User-Agents.
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; ObsidianDragon)");
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res != CURLE_OK) return;
bool ok = false;
const double poolHR = util::parsePoolHashrate(
known ? known->schema : util::PoolStatsSchema::DragonXIs,
responseData, known ? known->miningcorePoolId : std::string{}, ok);
if (!ok) return;
try {
json resp = json::parse(responseData);
// Pool stats API format: { "pools": { "<name>": { "hashrate": ... } } }
double poolHR = 0;
if (resp.contains("pools") && resp["pools"].is_object()) {
for (auto& [key, pool] : resp["pools"].items()) {
if (pool.contains("hashrate") && pool["hashrate"].is_number()) {
poolHR = pool["hashrate"].get<double>();
break; // Use the first pool entry
}
}
}
std::lock_guard<std::mutex> lk(stats_mutex_);
stats_.pool_hashrate = poolHR;
}
// ============================================================================
// Installed-miner version detection (`<binary> --version`, cached)
// ============================================================================
namespace {
std::mutex g_installed_ver_mutex;
std::string g_installed_ver;
std::atomic<bool> g_ver_detect_started{false};
// Extract the first "D.D[.D...]" version token from `--version` output (skips the
// build date, which uses '-' separators). Returns e.g. "6.21.0", or "" if none.
std::string parseMinerVersion(const std::string& out)
{
for (size_t i = 0; i < out.size(); ++i) {
if (std::isdigit(static_cast<unsigned char>(out[i]))) {
size_t j = i;
int dots = 0;
while (j < out.size() &&
(std::isdigit(static_cast<unsigned char>(out[j])) || out[j] == '.')) {
if (out[j] == '.') ++dots;
++j;
} catch (...) {
// Malformed response — ignore
}
if (dots >= 1 && (j - i) >= 3) {
// Include a trailing build suffix like "-hac" / "-drg1" (e.g. "6.25.1-hac"),
// matching what the running miner's API reports.
size_t end = j;
if (end < out.size() && out[end] == '-') {
size_t k = end + 1;
while (k < out.size() && std::isalnum(static_cast<unsigned char>(out[k]))) ++k;
if (k > end + 1) end = k;
}
return out.substr(i, end - i);
}
i = j;
}
}
return {};
}
} // namespace
void XmrigManager::startVersionDetection()
{
if (g_ver_detect_started.exchange(true)) return; // one-shot
std::thread([]() {
const std::string bin = findXmrigBinary();
std::string ver;
if (!bin.empty()) {
const std::string cmd = "\"" + bin + "\" --version 2>&1";
#ifdef _WIN32
FILE* fp = _popen(cmd.c_str(), "r");
#else
FILE* fp = popen(cmd.c_str(), "r");
#endif
if (fp) {
std::string out;
char buf[256];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) out.append(buf, n);
#ifdef _WIN32
_pclose(fp);
#else
pclose(fp);
#endif
ver = parseMinerVersion(out);
}
}
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
g_installed_ver = ver;
}).detach();
}
std::string XmrigManager::installedVersion()
{
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
return g_installed_ver;
}
} // namespace daemon

View File

@@ -43,7 +43,6 @@ public:
double pool_diff = 0;
std::string pool_url;
std::string algo;
std::string version; // miner version reported by the running xmrig API
bool connected = false;
// Memory usage
int64_t memory_free = 0; // bytes
@@ -138,18 +137,6 @@ public:
*/
static std::string findXmrigBinary();
/**
* @brief Kick a one-shot background `<binary> --version` detection (idempotent).
* Lets the UI show the installed miner's version before mining is ever started.
*/
static void startVersionDetection();
/**
* @brief Cached installed-miner version parsed by startVersionDetection().
* Empty until detection completes (or if no binary/parse failed). Thread-safe.
*/
static std::string installedVersion();
private:
bool generateConfig(const Config& cfg, const std::string& outPath);
bool startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads);

View File

@@ -9,7 +9,13 @@
#include <filesystem>
#include "../util/logger.h"
#include "../util/platform.h"
#ifdef _WIN32
#include <shlobj.h>
#else
#include <pwd.h>
#include <unistd.h>
#endif
namespace fs = std::filesystem;
using json = nlohmann::json;
@@ -22,11 +28,33 @@ AddressBook::~AddressBook() = default;
std::string AddressBook::getDefaultPath()
{
// Co-locate with settings.json in the per-variant config dir (Lite -> ObsidianDragonLite/).
// util::Platform::getConfigDir() owns the per-platform + per-variant path in one place.
const std::string dir = util::Platform::getConfigDir();
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
std::string dir = std::string(path) + "\\ObsidianDragon";
fs::create_directories(dir);
return (fs::path(dir) / "addressbook.json").string();
return dir + "\\addressbook.json";
}
return "addressbook.json";
#elif defined(__APPLE__)
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/Library/Application Support/ObsidianDragon";
fs::create_directories(dir);
return dir + "/addressbook.json";
#else
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/.config/ObsidianDragon";
fs::create_directories(dir);
return dir + "/addressbook.json";
#endif
}
bool AddressBook::load()
@@ -51,10 +79,6 @@ bool AddressBook::load()
e.label = entry.value("label", "");
e.address = entry.value("address", "");
e.notes = entry.value("notes", "");
// Legacy entries (no "scope") migrate to "global" so nothing disappears when
// multi-wallet scoping lands — a contact you already had stays visible everywhere.
e.scope = entry.value("scope", "global");
e.avatar = entry.value("avatar", "");
if (!e.address.empty()) {
entries_.push_back(e);
@@ -86,18 +110,20 @@ bool AddressBook::save()
e["label"] = entry.label;
e["address"] = entry.address;
e["notes"] = entry.notes;
e["scope"] = entry.scope.empty() ? std::string("global") : entry.scope;
if (!entry.avatar.empty()) e["avatar"] = entry.avatar;
j["entries"].push_back(e);
}
// Atomic + durable: temp file + fsync + rename, so a crash mid-write can't
// truncate addressbook.json (which is fully rewritten on every entry change).
// Owner-only (0600) — it holds the user's saved contacts.
if (!util::Platform::writeFileAtomically(file_path_, j.dump(2), /*restrictPermissions=*/true)) {
DEBUG_LOGF("Could not write address book: %s\n", file_path_.c_str());
// Ensure directory exists
fs::path p(file_path_);
fs::create_directories(p.parent_path());
std::ofstream file(file_path_);
if (!file.is_open()) {
DEBUG_LOGF("Could not open address book for writing: %s\n", file_path_.c_str());
return false;
}
file << j.dump(2);
DEBUG_LOGF("Address book saved: %zu entries\n", entries_.size());
return true;
@@ -109,9 +135,8 @@ bool AddressBook::save()
bool AddressBook::addEntry(const AddressBookEntry& entry)
{
// Reject a duplicate only within the same visible set (same wallet or global) — the same
// address may legitimately be a contact in two different wallets.
if (hasVisibleDuplicate(entry.address, entry.scope)) {
// Check for duplicate address
if (findByAddress(entry.address) >= 0) {
return false;
}
@@ -125,8 +150,9 @@ bool AddressBook::updateEntry(size_t index, const AddressBookEntry& entry)
return false;
}
// Check for a duplicate visible alongside this entry's scope (excluding the entry being edited)
if (hasVisibleDuplicate(entry.address, entry.scope, static_cast<int>(index))) {
// Check for duplicate address (excluding current entry)
int existing = findByAddress(entry.address);
if (existing >= 0 && static_cast<size_t>(existing) != index) {
return false;
}
@@ -144,20 +170,6 @@ bool AddressBook::removeEntry(size_t index)
return save();
}
int AddressBook::reattachLegacyScopes(const std::string& scopeId)
{
if (scopeId.empty()) return 0;
int rescoped = 0;
for (auto& e : entries_) {
if (e.isGlobal()) continue; // global stays global
if (e.scope.rfind("w:", 0) == 0) continue; // already a stable scope
e.scope = scopeId;
++rescoped;
}
if (rescoped > 0) save();
return rescoped;
}
int AddressBook::findByAddress(const std::string& address) const
{
for (size_t i = 0; i < entries_.size(); i++) {
@@ -168,19 +180,5 @@ int AddressBook::findByAddress(const std::string& address) const
return -1;
}
bool AddressBook::hasVisibleDuplicate(const std::string& address, const std::string& scope,
int excludeIndex) const
{
AddressBookEntry probe; probe.scope = scope; // reuse the isGlobal()/scope logic
for (size_t i = 0; i < entries_.size(); i++) {
if (static_cast<int>(i) == excludeIndex) continue;
const auto& e = entries_[i];
if (e.address != address) continue;
// Collides if they'd ever be shown together: same wallet scope, or either is global.
if (e.isGlobal() || probe.isGlobal() || e.scope == scope) return true;
}
return false;
}
} // namespace data
} // namespace dragonx

Some files were not shown because too many files have changed in this diff Show More