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
143 changed files with 18253 additions and 32375 deletions

1
.gitignore vendored
View File

@@ -37,6 +37,7 @@ asmap.dat
/memory /memory
/todo.md /todo.md
/.github/ /.github/
/ObsidianDragon-agent/
# macOS # macOS
.DS_Store .DS_Store

View File

@@ -3,12 +3,26 @@
# Released under the GPLv3 # Released under the GPLv3
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
# macOS: set deployment target and universal architectures BEFORE project()
# so they propagate to all targets, including FetchContent dependencies (SDL3, etc.)
if(APPLE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum macOS version" FORCE)
# Build universal binary (Apple Silicon + Intel) unless the user explicitly set architectures
if(NOT DEFINED CMAKE_OSX_ARCHITECTURES OR CMAKE_OSX_ARCHITECTURES STREQUAL "")
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures" FORCE)
endif()
endif()
project(ObsidianDragon project(ObsidianDragon
VERSION 1.1.1 VERSION 1.2.0
LANGUAGES C CXX LANGUAGES C CXX
DESCRIPTION "DragonX Cryptocurrency Wallet" DESCRIPTION "DragonX Cryptocurrency Wallet"
) )
# Pre-release suffix (e.g. "-rc1", "-beta2"). Leave empty for stable releases.
set(DRAGONX_VERSION_SUFFIX "-rc1")
# C++17 standard # C++17 standard
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -23,6 +37,8 @@ endif()
option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON) option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON)
option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON) option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON)
include(CTest)
# Output directories # Output directories
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
@@ -94,6 +110,32 @@ FetchContent_Declare(
) )
FetchContent_MakeAvailable(tomlplusplus) FetchContent_MakeAvailable(tomlplusplus)
# SQLite amalgamation - local Explorer block-summary cache
FetchContent_Declare(
sqlite3
URL https://www.sqlite.org/2024/sqlite-amalgamation-3450300.zip
URL_HASH SHA256=ea170e73e447703e8359308ca2e4366a3ae0c4304a8665896f068c736781c651
)
FetchContent_GetProperties(sqlite3)
if(NOT sqlite3_POPULATED)
FetchContent_Populate(sqlite3)
endif()
file(GLOB SQLITE3_AMALGAMATION_C CONFIGURE_DEPENDS
${sqlite3_SOURCE_DIR}/sqlite3.c
${sqlite3_SOURCE_DIR}/*/sqlite3.c
)
if(NOT SQLITE3_AMALGAMATION_C)
message(FATAL_ERROR "SQLite amalgamation source not found")
endif()
list(GET SQLITE3_AMALGAMATION_C 0 SQLITE3_SOURCE_FILE)
get_filename_component(SQLITE3_INCLUDE_DIR ${SQLITE3_SOURCE_FILE} DIRECTORY)
add_library(sqlite3_amalgamation STATIC ${SQLITE3_SOURCE_FILE})
target_include_directories(sqlite3_amalgamation PUBLIC ${SQLITE3_INCLUDE_DIR})
target_compile_definitions(sqlite3_amalgamation PRIVATE
SQLITE_THREADSAFE=1
SQLITE_OMIT_LOAD_EXTENSION
)
# libcurl for HTTPS RPC connections (more reliable than cpp-httplib with OpenSSL 3.x) # libcurl for HTTPS RPC connections (more reliable than cpp-httplib with OpenSSL 3.x)
if(WIN32) if(WIN32)
# For Windows cross-compilation, fetch and build libcurl statically # For Windows cross-compilation, fetch and build libcurl statically
@@ -233,22 +275,39 @@ set(APP_SOURCES
src/app_network.cpp src/app_network.cpp
src/app_security.cpp src/app_security.cpp
src/app_wizard.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/data/wallet_state.cpp src/data/wallet_state.cpp
src/data/transaction_history_cache.cpp
src/ui/theme.cpp src/ui/theme.cpp
src/ui/theme_loader.cpp src/ui/theme_loader.cpp
src/ui/explorer/explorer_block_cache.cpp
src/ui/material/color_theme.cpp src/ui/material/color_theme.cpp
src/ui/material/typography.cpp src/ui/material/typography.cpp
src/ui/notifications.cpp src/ui/notifications.cpp
src/ui/windows/main_window.cpp src/ui/windows/main_window.cpp
src/ui/windows/balance_tab.cpp src/ui/windows/balance_tab.cpp
src/ui/windows/balance_address_list.cpp
src/ui/windows/balance_recent_tx.cpp
src/ui/windows/balance_tab_helpers.cpp
src/ui/windows/send_tab.cpp src/ui/windows/send_tab.cpp
src/ui/windows/receive_tab.cpp src/ui/windows/receive_tab.cpp
src/ui/windows/transactions_tab.cpp src/ui/windows/transactions_tab.cpp
src/ui/windows/mining_tab.cpp src/ui/windows/mining_tab.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/peers_tab.cpp
src/ui/windows/explorer_tab.cpp src/ui/windows/explorer_tab.cpp
src/ui/windows/market_tab.cpp src/ui/windows/market_tab.cpp
src/ui/windows/console_tab.cpp src/ui/windows/console_tab.cpp
src/ui/windows/console_command_reference.cpp
src/ui/windows/console_input_model.cpp
src/ui/windows/console_output_model.cpp
src/ui/windows/console_tab_helpers.cpp
src/ui/windows/settings_window.cpp src/ui/windows/settings_window.cpp
src/ui/pages/settings_page.cpp src/ui/pages/settings_page.cpp
src/ui/windows/about_dialog.cpp src/ui/windows/about_dialog.cpp
@@ -272,6 +331,8 @@ set(APP_SOURCES
src/data/address_book.cpp src/data/address_book.cpp
src/data/exchange_info.cpp src/data/exchange_info.cpp
src/util/logger.cpp src/util/logger.cpp
src/util/async_task_manager.cpp
src/util/amount_format.cpp
src/util/base64.cpp src/util/base64.cpp
src/util/single_instance.cpp src/util/single_instance.cpp
src/util/i18n.cpp src/util/i18n.cpp
@@ -280,6 +341,8 @@ set(APP_SOURCES
src/util/texture_loader.cpp src/util/texture_loader.cpp
src/util/noise_texture.cpp src/util/noise_texture.cpp
src/daemon/embedded_daemon.cpp src/daemon/embedded_daemon.cpp
src/daemon/daemon_controller.cpp
src/daemon/lifecycle_adapters.cpp
src/daemon/xmrig_manager.cpp src/daemon/xmrig_manager.cpp
src/util/bootstrap.cpp src/util/bootstrap.cpp
src/util/secure_vault.cpp src/util/secure_vault.cpp
@@ -312,20 +375,38 @@ endif()
set(APP_HEADERS set(APP_HEADERS
src/app.h src/app.h
src/services/network_refresh_service.h
src/services/refresh_scheduler.h
src/services/wallet_security_controller.h
src/services/wallet_security_workflow.h
src/services/wallet_security_workflow_executor.h
src/config/version.h src/config/version.h
src/data/wallet_state.h src/data/wallet_state.h
src/data/transaction_history_cache.h
src/ui/theme.h src/ui/theme.h
src/ui/theme_loader.h src/ui/theme_loader.h
src/ui/explorer/explorer_block_cache.h
src/ui/notifications.h src/ui/notifications.h
src/ui/windows/main_window.h src/ui/windows/main_window.h
src/ui/windows/balance_tab.h src/ui/windows/balance_tab.h
src/ui/windows/balance_address_list.h
src/ui/windows/balance_recent_tx.h
src/ui/windows/balance_tab_helpers.h
src/ui/windows/send_tab.h src/ui/windows/send_tab.h
src/ui/windows/receive_tab.h src/ui/windows/receive_tab.h
src/ui/windows/transactions_tab.h src/ui/windows/transactions_tab.h
src/ui/windows/mining_tab.h src/ui/windows/mining_tab.h
src/ui/windows/mining_benchmark.h
src/ui/windows/mining_pool_panel.h
src/ui/windows/mining_tab_helpers.h
src/ui/windows/peers_tab.h src/ui/windows/peers_tab.h
src/ui/windows/explorer_tab.h src/ui/windows/explorer_tab.h
src/ui/windows/market_tab.h src/ui/windows/market_tab.h
src/ui/windows/console_command_reference.h
src/ui/windows/console_input_model.h
src/ui/windows/console_output_model.h
src/ui/windows/console_tab.h
src/ui/windows/console_tab_helpers.h
src/ui/windows/settings_window.h src/ui/windows/settings_window.h
src/ui/windows/about_dialog.h src/ui/windows/about_dialog.h
src/ui/windows/key_export_dialog.h src/ui/windows/key_export_dialog.h
@@ -349,6 +430,8 @@ set(APP_HEADERS
src/data/address_book.h src/data/address_book.h
src/data/exchange_info.h src/data/exchange_info.h
src/util/logger.h src/util/logger.h
src/util/async_task_manager.h
src/util/amount_format.h
src/util/base64.h src/util/base64.h
src/util/single_instance.h src/util/single_instance.h
src/util/i18n.h src/util/i18n.h
@@ -356,6 +439,8 @@ set(APP_HEADERS
src/util/payment_uri.h src/util/payment_uri.h
src/util/secure_vault.h src/util/secure_vault.h
src/daemon/embedded_daemon.h src/daemon/embedded_daemon.h
src/daemon/daemon_controller.h
src/daemon/lifecycle_adapters.h
src/daemon/xmrig_manager.h src/daemon/xmrig_manager.h
src/ui/effects/framebuffer.h src/ui/effects/framebuffer.h
src/ui/effects/blur_shader.h src/ui/effects/blur_shader.h
@@ -408,6 +493,21 @@ configure_file(
@ONLY @ONLY
) )
# INCBIN uses .incbin assembler directives that reference font files at
# assembly time — CMake doesn't track these implicit dependencies.
# Tell CMake that the generated source depends on the actual font binaries
# so a font file change triggers recompilation.
set_source_files_properties(
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
PROPERTIES OBJECT_DEPENDS
"${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-R.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
)
add_executable(ObsidianDragon add_executable(ObsidianDragon
${APP_SOURCES} ${APP_SOURCES}
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp ${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
@@ -437,6 +537,7 @@ target_link_libraries(ObsidianDragon PRIVATE
SDL3::SDL3 SDL3::SDL3
nlohmann_json::nlohmann_json nlohmann_json::nlohmann_json
tomlplusplus::tomlplusplus tomlplusplus::tomlplusplus
sqlite3_amalgamation
${CURL_LIBRARIES} ${CURL_LIBRARIES}
${SODIUM_LIBRARY} ${SODIUM_LIBRARY}
) )
@@ -499,6 +600,12 @@ endif()
# so edits to res/lang/*.json are picked up by 'make' without re-running cmake. # so edits to res/lang/*.json are picked up by 'make' without re-running cmake.
file(GLOB LANG_FILES ${CMAKE_SOURCE_DIR}/res/lang/*.json) file(GLOB LANG_FILES ${CMAKE_SOURCE_DIR}/res/lang/*.json)
if(LANG_FILES) if(LANG_FILES)
find_program(XXD_EXECUTABLE NAMES xxd)
if(NOT XXD_EXECUTABLE)
message(WARNING "xxd not found; runtime language JSON files will be copied, but embedded build/generated/embedded/lang_*.h files will not be regenerated")
endif()
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/generated/embedded)
foreach(LANG_FILE ${LANG_FILES}) foreach(LANG_FILE ${LANG_FILES})
get_filename_component(LANG_FILENAME ${LANG_FILE} NAME) get_filename_component(LANG_FILENAME ${LANG_FILE} NAME)
add_custom_command( add_custom_command(
@@ -512,16 +619,18 @@ if(LANG_FILES)
list(APPEND LANG_OUTPUTS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME}) list(APPEND LANG_OUTPUTS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME})
# Also regenerate the embedded header so the binary always has fresh translations # Also regenerate the embedded header so the binary always has fresh translations
get_filename_component(LANG_CODE ${LANG_FILENAME} NAME_WE) if(XXD_EXECUTABLE)
set(LANG_HEADER ${CMAKE_SOURCE_DIR}/src/embedded/lang_${LANG_CODE}.h) get_filename_component(LANG_CODE ${LANG_FILENAME} NAME_WE)
add_custom_command( set(LANG_HEADER ${CMAKE_BINARY_DIR}/generated/embedded/lang_${LANG_CODE}.h)
OUTPUT ${LANG_HEADER} add_custom_command(
COMMAND xxd -i "res/lang/${LANG_FILENAME}" > "src/embedded/lang_${LANG_CODE}.h" OUTPUT ${LANG_HEADER}
DEPENDS ${LANG_FILE} COMMAND ${XXD_EXECUTABLE} -i "res/lang/${LANG_FILENAME}" > "${LANG_HEADER}"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} DEPENDS ${LANG_FILE}
COMMENT "Embedding lang_${LANG_CODE}.h" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
) COMMENT "Embedding lang_${LANG_CODE}.h"
list(APPEND LANG_OUTPUTS ${LANG_HEADER}) )
list(APPEND LANG_OUTPUTS ${LANG_HEADER})
endif()
endforeach() endforeach()
add_custom_target(copy_langs ALL DEPENDS ${LANG_OUTPUTS}) add_custom_target(copy_langs ALL DEPENDS ${LANG_OUTPUTS})
add_dependencies(ObsidianDragon copy_langs) add_dependencies(ObsidianDragon copy_langs)
@@ -568,6 +677,7 @@ if(THEME_FILES AND Python3_EXECUTABLE)
message(STATUS " Theme files: ${THEME_FILES} (build-time expansion via Python)") message(STATUS " Theme files: ${THEME_FILES} (build-time expansion via Python)")
elseif(THEME_FILES) elseif(THEME_FILES)
# Fallback: plain copy if Python is not available # Fallback: plain copy if Python is not available
message(WARNING "Python3 not found; copying theme files without expand_themes.py layout merge")
foreach(THEME_FILE ${THEME_FILES}) foreach(THEME_FILE ${THEME_FILES})
get_filename_component(THEME_FILENAME ${THEME_FILE} NAME) get_filename_component(THEME_FILENAME ${THEME_FILE} NAME)
add_custom_command( add_custom_command(
@@ -621,6 +731,61 @@ install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res
OPTIONAL OPTIONAL
) )
# -----------------------------------------------------------------------------
# Tests
# -----------------------------------------------------------------------------
if(BUILD_TESTING)
add_executable(ObsidianDragonTests
tests/test_phase4.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/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_output_model.cpp
src/ui/windows/console_tab_helpers.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/data/wallet_state.cpp
src/data/transaction_history_cache.cpp
src/daemon/lifecycle_adapters.cpp
src/rpc/connection.cpp
src/resources/embedded_resources.cpp
src/util/secure_vault.cpp
src/util/platform.cpp
src/util/logger.cpp
)
target_include_directories(ObsidianDragonTests PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/resources
${CMAKE_SOURCE_DIR}/libs
${CMAKE_BINARY_DIR}/generated
${IMGUI_DIR}
${SODIUM_INCLUDE_DIR}
)
target_link_libraries(ObsidianDragonTests PRIVATE
nlohmann_json::nlohmann_json
sqlite3_amalgamation
${SODIUM_LIBRARY}
)
if(UNIX)
target_link_libraries(ObsidianDragonTests PRIVATE ${CMAKE_DL_LIBS})
endif()
add_test(NAME ObsidianDragonPhase4Tests COMMAND ObsidianDragonTests)
endif()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Summary # Summary
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -1,6 +1,8 @@
# DragonX Wallet - ImGui Edition # ObsidianDragon - DragonX Wallet
A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui. A lightweight, portable full-node cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
Current pre-release: **1.2.0-rc1**.
![License](https://img.shields.io/badge/License-GPLv3-blue.svg) ![License](https://img.shields.io/badge/License-GPLv3-blue.svg)
![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20Windows%20%7C%20macOS-green.svg) ![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20Windows%20%7C%20macOS-green.svg)
@@ -9,10 +11,13 @@ A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dea
- **Full Node Support**: Connects to dragonxd for complete blockchain verification - **Full Node Support**: Connects to dragonxd for complete blockchain verification
- **Shielded Transactions**: Full z-address support with encrypted memos - **Shielded Transactions**: Full z-address support with encrypted memos
- **Integrated Mining**: CPU mining controls with hashrate monitoring - **Address Management**: Labels, icons, favorites, hidden addresses, and address-to-address transfers
- **Integrated Mining**: Solo CPU mining plus pool mining through xmrig, with idle-mining controls
- **Explorer Tools**: Block/transaction lookup and bootstrap snapshot download
- **Market Data**: Real-time price charts from CoinGecko - **Market Data**: Real-time price charts from CoinGecko
- **QR Codes**: Generate and display QR codes for receiving addresses - **QR Codes**: Generate and display QR codes for receiving addresses
- **Multi-language**: i18n support (English, Spanish, more coming) - **Multi-language**: i18n support for English, German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese
- **CJK Fonts**: Bundled CJK subset font for translated interfaces
- **Lightweight**: ~5-10MB binary vs ~50MB+ for Qt version - **Lightweight**: ~5-10MB binary vs ~50MB+ for Qt version
- **Fast Builds**: Compiles in seconds, not minutes - **Fast Builds**: Compiles in seconds, not minutes
@@ -116,7 +121,8 @@ cd ObsidianDragon/
./ObsidianDragon ./ObsidianDragon
``` ```
The wallet will automatically connect to the daemon using credentials from \`~/.hush/DRAGONX/DRAGONX.conf\`. The wallet will automatically connect to the daemon using credentials from `~/.hush/DRAGONX/DRAGONX.conf`.
### Using Custom Node Binaries ### Using Custom Node Binaries
The wallet checks its **own directory first** when looking for DragonX node binaries. This means you can test new or different branch builds of `hush-arrakis-chain`/`hushd` without waiting for a new wallet release: The wallet checks its **own directory first** when looking for DragonX node binaries. This means you can test new or different branch builds of `hush-arrakis-chain`/`hushd` without waiting for a new wallet release:
@@ -131,9 +137,10 @@ The wallet checks its **own directory first** when looking for DragonX node bina
3. System-wide locations (`/usr/local/bin`, `~/dragonx/src`, etc.) 3. System-wide locations (`/usr/local/bin`, `~/dragonx/src`, etc.)
This is useful for testing new branches or hotfixes to the node software before they are bundled into a wallet release. This is useful for testing new branches or hotfixes to the node software before they are bundled into a wallet release.
## Configuration ## Configuration
Configuration is stored in \`~/.hush/DRAGONX/DRAGONX.conf\`: Configuration is stored in `~/.hush/DRAGONX/DRAGONX.conf`:
``` ```
rpcuser=your_rpc_user rpcuser=your_rpc_user
@@ -148,44 +155,46 @@ ObsidianDragon/
├── src/ ├── src/
│ ├── main.cpp # Entry point, SDL/ImGui setup │ ├── main.cpp # Entry point, SDL/ImGui setup
│ ├── app.cpp/h # Main application class │ ├── app.cpp/h # Main application class
│ ├── wallet_state.h # Wallet data structures │ ├── data/ # WalletState, address book, exchange info
│ ├── version.h # Version definitions │ ├── config/ # Settings persistence and committed/generated version.h
│ ├── ui/ │ ├── ui/
│ │ ├── theme.cpp/h # DragonX theme │ │ ├── schema/ # TOML UI schema and skin manager
│ │ ── windows/ # UI tabs and dialogs │ │ ── material/ # Material components, typography, layout
│ │ ├── windows/ # Tabs and dialogs
│ │ └── pages/ # Multi-page screens such as Settings
│ ├── rpc/ │ ├── rpc/
│ │ ├── rpc_client.cpp # JSON-RPC client │ │ ├── rpc_client.cpp # JSON-RPC client
│ │ └── connection.cpp # Daemon connection │ │ └── connection.cpp # Daemon connection
│ ├── config/ │ ├── resources/ # Embedded resource extraction
│ └── settings.cpp # Settings persistence ├── platform/ # Windows DX11/backdrop helpers
│ ├── util/ │ ├── util/
│ │ ├── i18n.cpp # Internationalization │ │ ├── i18n.cpp # Internationalization
│ │ └── ... │ │ └── ...
│ └── daemon/ │ └── daemon/
│ └── embedded_daemon.cpp │ └── embedded_daemon.cpp
├── res/ ├── res/
│ ├── fonts/ # Ubuntu font │ ├── fonts/ # Ubuntu, icon, and CJK fonts
│ └── lang/ # Translation files │ └── lang/ # Translation files
├── libs/ ├── libs/
│ └── qrcode/ # QR code generation │ └── qrcode/ # QR code generation
├── CMakeLists.txt ├── CMakeLists.txt
├── build-release.sh # Build script ├── build.sh # Release/cross-platform build script
└── create-appimage.sh # AppImage packaging └── scripts/create-appimage.sh # AppImage packaging
``` ```
## Dependencies ## Dependencies
Fetched automatically by CMake (no manual install needed): Fetched or discovered by CMake:
- **[SDL3](https://github.com/libsdl-org/SDL)** — Cross-platform windowing/input - **[SDL3](https://github.com/libsdl-org/SDL)** — Cross-platform windowing/input
- **[nlohmann/json](https://github.com/nlohmann/json)** — JSON parsing - **[nlohmann/json](https://github.com/nlohmann/json)** — JSON parsing
- **[toml++](https://github.com/marzer/tomlplusplus)** — TOML parsing (UI schema/themes) - **[toml++](https://github.com/marzer/tomlplusplus)** — TOML parsing (UI schema/themes)
- **[libcurl](https://curl.se/libcurl/)** — HTTPS RPC transport (system on Linux, fetched on Windows) - **[libcurl](https://curl.se/libcurl/)** — HTTP/HTTPS transport for daemon RPC and network calls (system on Linux/macOS, fetched on Windows)
Bundled in `libs/`: Bundled in `libs/`:
- **[Dear ImGui](https://github.com/ocornut/imgui)** — Immediate mode GUI - **[Dear ImGui](https://github.com/ocornut/imgui)** — Immediate mode GUI
- **[libsodium](https://libsodium.org)** — Cryptographic operations (fetched by `scripts/fetch-libsodium.sh`) - **[libsodium](https://libsodium.org)** — Cryptographic operations (system on Linux or fetched by `scripts/fetch-libsodium.sh`)
- **[QR-Code-generator](https://github.com/nayuki/QR-Code-generator)** — QR code rendering - **[QR-Code-generator](https://github.com/nayuki/QR-Code-generator)** — QR code rendering
- **[miniz](https://github.com/richgel999/miniz)** — ZIP compression - **[miniz](https://github.com/richgel999/miniz)** — ZIP compression
- **[GLAD](https://glad.dav1d.de/)** — OpenGL loader (Linux/macOS) - **[GLAD](https://glad.dav1d.de/)** — OpenGL loader (Linux/macOS)
@@ -202,9 +211,11 @@ Bundled in `libs/`:
## Translation ## Translation
Current language files live in `res/lang/` as `de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, and `zh` JSON files, with built-in English fallbacks.
To add a new language: To add a new language:
1. Copy \`res/lang/es.json\` to \`res/lang/<code>.json\` 1. Copy `res/lang/es.json` to `res/lang/<code>.json`
2. Translate all strings 2. Translate all strings
3. The language will appear in Settings automatically 3. The language will appear in Settings automatically

View File

@@ -361,7 +361,22 @@ https://www.apache.org/licenses/LICENSE-2.0
--- ---
## 13. IconFontCppHeaders ## 13. Material Design Icons Pickaxe Subset Font
- **Location:** `res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf`
- **Source:** https://github.com/Templarian/MaterialDesign-Webfont
- **Derived from:** Pictogrammers Material Design Icons webfont (`materialdesignicons-webfont.ttf`)
- **Copyright:** Pictogrammers contributors
- **License:** Apache License 2.0
This bundled font is a local one-glyph subset containing only the MDI pickaxe
icon, remapped onto a BMP private-use codepoint for Dear ImGui compatibility.
The full text of the Apache License 2.0 is available at:
https://www.apache.org/licenses/LICENSE-2.0
---
## 14. IconFontCppHeaders
- **Location:** `src/embedded/IconsMaterialDesign.h` - **Location:** `src/embedded/IconsMaterialDesign.h`
- **Source:** https://github.com/juliettef/IconFontCppHeaders - **Source:** https://github.com/juliettef/IconFontCppHeaders
@@ -390,7 +405,7 @@ freely, subject to the following restrictions:
--- ---
## 14. Ubuntu Font Family ## 15. Ubuntu Font Family
- **Location:** `res/fonts/Ubuntu-Light.ttf`, `Ubuntu-Medium.ttf`, `Ubuntu-R.ttf` - **Location:** `res/fonts/Ubuntu-Light.ttf`, `Ubuntu-Medium.ttf`, `Ubuntu-R.ttf`
- **Source:** https://design.ubuntu.com/font - **Source:** https://design.ubuntu.com/font

View File

@@ -20,7 +20,7 @@
set -e set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERSION="1.1.1" VERSION="1.2.0-rc1"
# ── Colours ────────────────────────────────────────────────────────────────── # ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m' RED='\033[0;31m'
@@ -197,7 +197,14 @@ bundle_linux_daemon() {
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
build_dev() { build_dev() {
header "Dev Build ($(uname -s) / $BUILD_TYPE)" header "Dev Build ($(uname -s) / $BUILD_TYPE)"
local bd="$SCRIPT_DIR/build/linux"
# Use platform-appropriate build directory
if [[ "$(uname -s)" == "Darwin" ]]; then
local bd="$SCRIPT_DIR/build/mac"
export MACOSX_DEPLOYMENT_TARGET="11.0"
else
local bd="$SCRIPT_DIR/build/linux"
fi
if $CLEAN; then if $CLEAN; then
info "Cleaning $bd ..."; rm -rf "$bd" info "Cleaning $bd ..."; rm -rf "$bd"
@@ -732,7 +739,9 @@ build_release_mac() {
fi fi
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)" info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
else else
MAC_ARCH=$(uname -m) # Native macOS: build universal binary (arm64 + x86_64)
MAC_ARCH="universal"
export MACOSX_DEPLOYMENT_TARGET="11.0"
fi fi
header "Release: macOS ($MAC_ARCH$(${IS_CROSS} && echo ' — cross-compile'))" header "Release: macOS ($MAC_ARCH$(${IS_CROSS} && echo ' — cross-compile'))"
@@ -811,12 +820,31 @@ TOOLCHAIN
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \ -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} ${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"}
else else
info "Configuring (native) ..." # Build libsodium as universal if needed
local need_sodium=false
if [[ ! -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]] && \
[[ ! -f "$SCRIPT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
need_sodium=true
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
# Rebuild if existing lib is not universal (single-arch won't link)
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then
info "Existing libsodium is not universal — rebuilding ..."
rm -rf "$SCRIPT_DIR/libs/libsodium"
need_sodium=true
fi
fi
if $need_sodium; then
info "Building libsodium (universal) ..."
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
fi
info "Configuring (native universal arm64+x86_64) ..."
cmake "$SCRIPT_DIR" \ cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE=Release \ -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \ -DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \ -DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
fi fi
info "Building with $JOBS jobs ..." info "Building with $JOBS jobs ..."
@@ -836,6 +864,11 @@ TOOLCHAIN
else else
info "Stripping ..." info "Stripping ..."
strip bin/ObsidianDragon strip bin/ObsidianDragon
# Verify universal binary
if command -v lipo &>/dev/null; then
info "Architecture info:"
lipo -info bin/ObsidianDragon
fi
fi fi
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)" info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"

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.

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

@@ -5,7 +5,7 @@
<assemblyIdentity <assemblyIdentity
type="win32" type="win32"
name="DragonX.ObsidianDragon.Wallet" name="DragonX.ObsidianDragon.Wallet"
version="1.1.1.0" version="1.2.0.0"
processorArchitecture="amd64" processorArchitecture="amd64"
/> />

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -43,6 +43,8 @@
"address_url": "Adress-URL", "address_url": "Adress-URL",
"addresses_appear_here": "Ihre Empfangsadressen erscheinen hier, sobald Sie verbunden sind.", "addresses_appear_here": "Ihre Empfangsadressen erscheinen hier, sobald Sie verbunden sind.",
"advanced": "ERWEITERT", "advanced": "ERWEITERT",
"advanced_effects": "Erweiterte Effekte...",
"ago": "her",
"all_filter": "Alle", "all_filter": "Alle",
"allow_custom_fees": "Benutzerdefinierte Gebühren erlauben", "allow_custom_fees": "Benutzerdefinierte Gebühren erlauben",
"amount": "Betrag", "amount": "Betrag",
@@ -90,12 +92,30 @@
"block_timestamp": "Zeitstempel:", "block_timestamp": "Zeitstempel:",
"block_transactions": "Transaktionen:", "block_transactions": "Transaktionen:",
"blockchain_syncing": "Blockchain synchronisiert (%.1f%%)... Guthaben könnten ungenau sein.", "blockchain_syncing": "Blockchain synchronisiert (%.1f%%)... Guthaben könnten ungenau sein.",
"bootstrap_daemon_running": "Daemon läuft",
"bootstrap_daemon_stopped": "Daemon gestoppt",
"bootstrap_daemon_stopping": "Daemon wird gestoppt...",
"bootstrap_desc": "Laden Sie einen Blockchain-Bootstrap herunter, um die anfängliche Synchronisierung drastisch zu beschleunigen. Dies lädt einen Snapshot der Blockchain herunter und extrahiert ihn in Ihr Datenverzeichnis.",
"bootstrap_downloading": "Bootstrap wird heruntergeladen...",
"bootstrap_extracting": "Blockchain-Daten werden extrahiert...",
"bootstrap_failed": "Bootstrap fehlgeschlagen",
"bootstrap_mirror": "Spiegel",
"bootstrap_mirror_tooltip": "Vom Spiegel herunterladen (bootstrap2.dragonx.is).\nVerwenden Sie dies, wenn der Hauptdownload langsam ist oder fehlschlägt.",
"bootstrap_restart_daemon": "Daemon neu starten",
"bootstrap_success": "Bootstrap abgeschlossen",
"bootstrap_success_desc": "Blockchain-Daten wurden erfolgreich extrahiert. Starten Sie den Daemon, um ab dem Bootstrap-Punkt zu synchronisieren.",
"bootstrap_trust_warning": "Verwenden Sie nur bootstrap.dragonx.is oder bootstrap2.dragonx.is. Die Verwendung von Dateien aus nicht vertrauenswürdigen Quellen könnte Ihren Knoten gefährden.",
"bootstrap_verifying": "Prüfsummen werden überprüft...",
"bootstrap_wallet_protected": "(wallet.dat ist geschützt)",
"bootstrap_warning": "Vorhandene Blockdaten (blocks, chainstate, notarizations) werden gelöscht und ersetzt. Ihre wallet.dat wird NICHT verändert oder gelöscht.",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"characters": "Zeichen", "characters": "Zeichen",
"choose_icon": "Symbol wählen",
"clear": "Leeren", "clear": "Leeren",
"clear_all_bans": "Alle Sperren aufheben", "clear_all_bans": "Alle Sperren aufheben",
"clear_anyway": "Trotzdem löschen", "clear_anyway": "Trotzdem löschen",
"clear_form_confirm": "Alle Formularfelder leeren?", "clear_form_confirm": "Alle Formularfelder leeren?",
"clear_icon": "Symbol entfernen",
"clear_request": "Anfrage leeren", "clear_request": "Anfrage leeren",
"click_copy_address": "Klicken zum Kopieren der Adresse", "click_copy_address": "Klicken zum Kopieren der Adresse",
"click_copy_uri": "Klicken zum Kopieren der URI", "click_copy_uri": "Klicken zum Kopieren der URI",
@@ -106,8 +126,12 @@
"confirm_clear_ztx_title": "Z-Tx-Verlauf löschen bestätigen", "confirm_clear_ztx_title": "Z-Tx-Verlauf löschen bestätigen",
"confirm_clear_ztx_warning1": "Das Löschen des Z-Transaktionsverlaufs kann dazu führen, dass Ihr geschirmtes Guthaben als 0 angezeigt wird, bis ein Wallet-Rescan durchgeführt wird.", "confirm_clear_ztx_warning1": "Das Löschen des Z-Transaktionsverlaufs kann dazu führen, dass Ihr geschirmtes Guthaben als 0 angezeigt wird, bis ein Wallet-Rescan durchgeführt wird.",
"confirm_clear_ztx_warning2": "Wenn dies geschieht, müssen Sie Ihre Z-Adresse-Privatschlüssel mit aktiviertem Rescan neu importieren, um Ihr Guthaben wiederherzustellen.", "confirm_clear_ztx_warning2": "Wenn dies geschieht, müssen Sie Ihre Z-Adresse-Privatschlüssel mit aktiviertem Rescan neu importieren, um Ihr Guthaben wiederherzustellen.",
"confirm_delete_blockchain_msg": "Dies stoppt den Daemon, löscht alle Blockchain-Daten (blocks, chainstate, peers) und startet eine neue Synchronisierung. Dies kann mehrere Stunden dauern.",
"confirm_delete_blockchain_safe": "Ihre wallet.dat, Konfiguration und Transaktionshistorie sind sicher und werden nicht gelöscht.",
"confirm_delete_blockchain_title": "Blockchain-Daten löschen",
"confirm_send": "Senden bestätigen", "confirm_send": "Senden bestätigen",
"confirm_transaction": "Transaktion bestätigen", "confirm_transaction": "Transaktion bestätigen",
"confirm_transfer": "Überweisung bestätigen",
"confirmations": "Bestätigungen", "confirmations": "Bestätigungen",
"confirmations_display": "%d Bestätigungen | %s", "confirmations_display": "%d Bestätigungen | %s",
"confirmed": "Bestätigt", "confirmed": "Bestätigt",
@@ -172,6 +196,7 @@
"console_welcome": "Willkommen bei ObsidianDragon Konsole", "console_welcome": "Willkommen bei ObsidianDragon Konsole",
"console_zoom_in": "Vergrößern", "console_zoom_in": "Vergrößern",
"console_zoom_out": "Verkleinern", "console_zoom_out": "Verkleinern",
"copied": "Kopiert!",
"copy": "Kopieren", "copy": "Kopieren",
"copy_address": "Vollständige Adresse kopieren", "copy_address": "Vollständige Adresse kopieren",
"copy_error": "Fehler kopieren", "copy_error": "Fehler kopieren",
@@ -180,22 +205,45 @@
"copy_uri": "URI kopieren", "copy_uri": "URI kopieren",
"current_price": "Aktueller Preis", "current_price": "Aktueller Preis",
"custom_fees": "Benutzerdefinierte Gebühren", "custom_fees": "Benutzerdefinierte Gebühren",
"daemon_version": "Daemon",
"dark": "Dunkel", "dark": "Dunkel",
"date": "Datum", "date": "Datum",
"date_label": "Datum:", "date_label": "Datum:",
"debug_logging": "FEHLERPROTOKOLLIERUNG", "debug_logging": "FEHLERPROTOKOLLIERUNG",
"delete": "Löschen", "delete": "Löschen",
"delete_blockchain": "Blockchain löschen",
"delete_blockchain_confirm": "Löschen & Neu synchronisieren",
"deshielding_warning": "Warnung: Dies wird Gelder von einer privaten (Z) Adresse auf eine transparente (T) Adresse ent-schirmen.",
"difficulty": "Schwierigkeit", "difficulty": "Schwierigkeit",
"disconnected": "Getrennt", "disconnected": "Getrennt",
"dismiss": "Verwerfen", "dismiss": "Verwerfen",
"display": "Anzeige", "display": "Anzeige",
"download": "Herunterladen",
"download_bootstrap": "Bootstrap herunterladen",
"dragonx_green": "DragonX (Grün)", "dragonx_green": "DragonX (Grün)",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"error": "Fehler", "error": "Fehler",
"error_format": "Fehler: %s", "error_format": "Fehler: %s",
"est_time_to_block": "Gesch. Zeit bis Block", "est_time_to_block": "Gesch. Zeit bis Block",
"exit": "Beenden", "exit": "Beenden",
"explorer": "EXPLORER", "explorer": "Explorer",
"explorer_block_detail": "Block",
"explorer_block_hash": "Hash",
"explorer_block_height": "Höhe",
"explorer_block_merkle": "Merkle-Wurzel",
"explorer_block_size": "Größe",
"explorer_block_time": "Zeit",
"explorer_block_txs": "Transaktionen",
"explorer_chain_stats": "Kette",
"explorer_invalid_query": "Geben Sie eine Blockhöhe oder einen 64-stelligen Hash ein",
"explorer_mempool": "Mempool",
"explorer_mempool_size": "Größe",
"explorer_mempool_txs": "Transaktionen",
"explorer_recent_blocks": "Letzte Blöcke",
"explorer_search": "Suchen",
"explorer_section": "EXPLORER",
"explorer_tx_outputs": "Ausgaben",
"explorer_tx_size": "Größe",
"export": "Exportieren", "export": "Exportieren",
"export_csv": "CSV exportieren", "export_csv": "CSV exportieren",
"export_keys_btn": "Schlüssel exportieren", "export_keys_btn": "Schlüssel exportieren",
@@ -224,14 +272,22 @@
"fetch_prices": "Preise abrufen", "fetch_prices": "Preise abrufen",
"file": "Datei", "file": "Datei",
"file_save_location": "Datei wird gespeichert in: ~/.config/ObsidianDragon/", "file_save_location": "Datei wird gespeichert in: ~/.config/ObsidianDragon/",
"filter": "Filtern...",
"font_scale": "Schriftgröße", "font_scale": "Schriftgröße",
"force_quit": "Sofort beenden",
"force_quit_confirm_msg": "Dies wird den Daemon sofort beenden ohne sauberes Herunterfahren.\nDies kann den Blockchain-Index beschädigen und eine Neusynchronisierung erfordern.",
"force_quit_confirm_title": "Sofort beenden?",
"force_quit_warning": "Dies wird den Daemon sofort beenden ohne sauberes Herunterfahren. Kann eine Neusynchronisierung der Blockchain erfordern.",
"force_quit_yes": "Sofort beenden",
"from": "Von", "from": "Von",
"from_upper": "VON", "from_upper": "VON",
"full_details": "Alle Details", "full_details": "Alle Details",
"general": "Allgemein", "general": "Allgemein",
"generating": "Wird generiert",
"go_to_receive": "Zum Empfangen", "go_to_receive": "Zum Empfangen",
"height": "Höhe", "height": "Höhe",
"help": "Hilfe", "help": "Hilfe",
"hidden_tag": " (versteckt)",
"hide": "Ausblenden", "hide": "Ausblenden",
"hide_address": "Adresse ausblenden", "hide_address": "Adresse ausblenden",
"hide_zero_balances": "Nullsalden ausblenden", "hide_zero_balances": "Nullsalden ausblenden",
@@ -253,6 +309,9 @@
"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_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_key_z_format": "Z-Adresse Ausgabeschlüssel (secret-extended-key-...)",
"import_private_key": "Privaten Schlüssel importieren...", "import_private_key": "Privaten Schlüssel importieren...",
"incorrect_passphrase": "Falsches Passwort",
"incorrect_pin": "Falsche PIN",
"insufficient_funds": "Unzureichendes Guthaben für diesen Betrag plus Gebühr.",
"invalid_address": "Ungültiges Adressformat", "invalid_address": "Ungültiges Adressformat",
"ip_address": "IP-Adresse", "ip_address": "IP-Adresse",
"keep": "Behalten", "keep": "Behalten",
@@ -266,6 +325,7 @@
"key_export_viewing_keys_zonly": "Anzeigeschlüssel sind nur für geschirmte (z) Adressen verfügbar", "key_export_viewing_keys_zonly": "Anzeigeschlüssel sind nur für geschirmte (z) Adressen verfügbar",
"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.", "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:", "label": "Bezeichnung:",
"label_placeholder": "z.B. Ersparnisse, Mining...",
"language": "Sprache", "language": "Sprache",
"light": "Hell", "light": "Hell",
"loading": "Laden...", "loading": "Laden...",
@@ -286,6 +346,7 @@
"market_now": "Jetzt", "market_now": "Jetzt",
"market_pct_shielded": "%.0f%% Abgeschirmt", "market_pct_shielded": "%.0f%% Abgeschirmt",
"market_portfolio": "PORTFOLIO", "market_portfolio": "PORTFOLIO",
"market_price_loading": "Preisdaten werden geladen...",
"market_price_unavailable": "Preisdaten nicht verfügbar", "market_price_unavailable": "Preisdaten nicht verfügbar",
"market_refresh_price": "Preisdaten aktualisieren", "market_refresh_price": "Preisdaten aktualisieren",
"market_trade_on": "Handeln auf %s", "market_trade_on": "Handeln auf %s",
@@ -311,6 +372,13 @@
"mining_address_copied": "Mining-Adresse kopiert", "mining_address_copied": "Mining-Adresse kopiert",
"mining_all_time": "Gesamt", "mining_all_time": "Gesamt",
"mining_already_saved": "Pool-URL bereits gespeichert", "mining_already_saved": "Pool-URL bereits gespeichert",
"mining_benchmark_cancel": "Benchmark abbrechen",
"mining_benchmark_cooling": "Abkühlen",
"mining_benchmark_dismiss": "Schließen",
"mining_benchmark_result": "Optimal",
"mining_benchmark_stabilizing": "Stabilisierung",
"mining_benchmark_testing": "Testen",
"mining_benchmark_tooltip": "Optimale Thread-Anzahl für diese CPU finden",
"mining_block_copied": "Block-Hash kopiert", "mining_block_copied": "Block-Hash kopiert",
"mining_chart_1m_ago": "vor 1m", "mining_chart_1m_ago": "vor 1m",
"mining_chart_5m_ago": "vor 5m", "mining_chart_5m_ago": "vor 5m",
@@ -330,8 +398,15 @@
"mining_filter_tip_all": "Alle Einnahmen anzeigen", "mining_filter_tip_all": "Alle Einnahmen anzeigen",
"mining_filter_tip_pool": "Nur Pool-Einnahmen anzeigen", "mining_filter_tip_pool": "Nur Pool-Einnahmen anzeigen",
"mining_filter_tip_solo": "Nur Solo-Einnahmen anzeigen", "mining_filter_tip_solo": "Nur Solo-Einnahmen anzeigen",
"mining_generate_z_address_hint": "Generieren Sie eine Z-Adresse im Empfangen-Tab als Auszahlungsadresse",
"mining_idle_gpu_off_tooltip": "Uneingeschränkt: EIN\nNur Tastatur-/Mauseingabe bestimmt den Leerlauf\nKlicken für GPU-bewusste Erkennung",
"mining_idle_gpu_on_tooltip": "GPU-bewusst: EIN\nGPU-Aktivität (Video, Spiele) verhindert Leerlauf-Mining\nKlicken für uneingeschränkten Modus",
"mining_idle_off_tooltip": "Leerlauf-Mining aktivieren", "mining_idle_off_tooltip": "Leerlauf-Mining aktivieren",
"mining_idle_on_tooltip": "Leerlauf-Mining deaktivieren", "mining_idle_on_tooltip": "Leerlauf-Mining deaktivieren",
"mining_idle_scale_off_tooltip": "Start/Stopp-Modus: EIN\nKlicken zum Wechsel auf Thread-Skalierung",
"mining_idle_scale_on_tooltip": "Thread-Skalierung: EIN\nKlicken zum Wechsel auf Start/Stopp-Modus",
"mining_idle_threads_active_tooltip": "Threads bei Benutzeraktivität",
"mining_idle_threads_idle_tooltip": "Threads im Leerlauf",
"mining_local_hashrate": "Lokale Hashrate", "mining_local_hashrate": "Lokale Hashrate",
"mining_mine": "Minen", "mining_mine": "Minen",
"mining_mining_addr": "Mining-Adr.", "mining_mining_addr": "Mining-Adr.",
@@ -388,6 +463,7 @@
"no_addresses_available": "Keine Adressen verfügbar", "no_addresses_available": "Keine Adressen verfügbar",
"no_addresses_match": "Keine Adressen passen zum Filter", "no_addresses_match": "Keine Adressen passen zum Filter",
"no_addresses_with_balance": "Keine Adressen mit Guthaben", "no_addresses_with_balance": "Keine Adressen mit Guthaben",
"no_addresses_yet": "Noch keine Adressen",
"no_matching": "Keine passenden Transaktionen", "no_matching": "Keine passenden Transaktionen",
"no_recent_receives": "Keine kürzlichen Empfänge", "no_recent_receives": "Keine kürzlichen Empfänge",
"no_recent_sends": "Keine kürzlichen Sendungen", "no_recent_sends": "Keine kürzlichen Sendungen",
@@ -453,6 +529,7 @@
"peers_upper": "PEERS", "peers_upper": "PEERS",
"peers_version": "Version", "peers_version": "Version",
"pending": "Ausstehend", "pending": "Ausstehend",
"pin_not_set": "PIN nicht gesetzt. Verwenden Sie das Passwort zum Entsperren.",
"ping": "Ping", "ping": "Ping",
"price_chart": "Preisdiagramm", "price_chart": "Preisdiagramm",
"qr_code": "QR-Code", "qr_code": "QR-Code",
@@ -473,7 +550,9 @@
"recent_received": "KÜRZLICH EMPFANGEN", "recent_received": "KÜRZLICH EMPFANGEN",
"recent_sends": "KÜRZLICH GESENDET", "recent_sends": "KÜRZLICH GESENDET",
"recipient": "EMPFÄNGER", "recipient": "EMPFÄNGER",
"recipient_balance": "Empfänger: %.8f → %.8f DRGX",
"recv_type": "Empf.", "recv_type": "Empf.",
"reduce_motion": "Bewegung reduzieren",
"refresh": "Aktualisieren", "refresh": "Aktualisieren",
"refresh_now": "Jetzt aktualisieren", "refresh_now": "Jetzt aktualisieren",
"remove_favorite": "Favorit entfernen", "remove_favorite": "Favorit entfernen",
@@ -493,7 +572,10 @@
"request_uri_copied": "Zahlungs-URI in Zwischenablage kopiert", "request_uri_copied": "Zahlungs-URI in Zwischenablage kopiert",
"rescan": "Neu scannen", "rescan": "Neu scannen",
"reset_to_defaults": "Standardwerte zurücksetzen", "reset_to_defaults": "Standardwerte zurücksetzen",
"restarting_after_encryption": "Daemon wird nach Verschlüsselung neu gestartet...",
"restore_address": "Adresse wiederherstellen", "restore_address": "Adresse wiederherstellen",
"result_preview": "Ergebnisvorschau",
"retry": "Wiederholen",
"review_send": "Senden prüfen", "review_send": "Senden prüfen",
"rpc_host": "RPC-Host", "rpc_host": "RPC-Host",
"rpc_pass": "Passwort", "rpc_pass": "Passwort",
@@ -502,6 +584,39 @@
"save": "Speichern", "save": "Speichern",
"save_settings": "Einstellungen speichern", "save_settings": "Einstellungen speichern",
"save_z_transactions": "Z-Tx in Tx-Liste speichern", "save_z_transactions": "Z-Tx in Tx-Liste speichern",
"sb_auth_failed": "Authentifizierung fehlgeschlagen — rpcuser/rpcpassword prüfen",
"sb_block": "Block: %d",
"sb_connecting_daemon": "Verbindung zu dragonxd...",
"sb_connecting_err": "Verbindung zum Daemon — %s",
"sb_connecting_external": "Verbindung zu externem Daemon...",
"sb_connecting_generic": "Verbindung zum Daemon...",
"sb_daemon_crashed": "Daemon ist %d mal abgestürzt",
"sb_daemon_not_found": "Daemon nicht gefunden",
"sb_dragonxd_running": "dragonxd läuft",
"sb_dragonxd_stopped": "dragonxd gestoppt",
"sb_dragonxd_stopping": "dragonxd wird gestoppt...",
"sb_extracting_sapling": "Sapling-Parameter werden extrahiert...",
"sb_importing_keys": "Schlüssel importieren",
"sb_loading_config": "Konfiguration laden...",
"sb_mining_hs": "%.1f H/s",
"sb_net_ghs": "Netz: %.2f GH/s",
"sb_net_hs": "Netz: %.1f H/s",
"sb_net_khs": "Netz: %.2f KH/s",
"sb_net_mhs": "Netz: %.2f MH/s",
"sb_no_conf": "DRAGONX.conf nicht gefunden",
"sb_peers": "Peers: %zu",
"sb_rescanning": "Neuscan",
"sb_rescanning_pct": "Neuscan %.0f%%",
"sb_restarting_daemon": "Daemon wird neu gestartet...",
"sb_sapling_failed": "Sapling-Parameter-Extraktion fehlgeschlagen.",
"sb_sapling_not_found": "Sapling-Parameter nicht gefunden.",
"sb_starting_daemon": "dragonxd wird gestartet...",
"sb_syncing_basic": "Synchronisierung %.1f%% (%d übrig)",
"sb_syncing_eta": "Synchronisierung %.1f%% (%d übrig, %.0f Blk/s, ~%s)",
"sb_waiting_config": "Warten auf Daemon-Konfiguration...",
"sb_waiting_daemon": "Warten auf dragonxd...",
"sb_waiting_daemon_err": "Warten auf dragonxd — %s",
"sb_warming_up": "Aufwärmen...",
"search_placeholder": "Suchen...", "search_placeholder": "Suchen...",
"security": "SICHERHEIT", "security": "SICHERHEIT",
"select_address": "Adresse auswählen...", "select_address": "Adresse auswählen...",
@@ -553,12 +668,15 @@
"send_valid_transparent": "Gültige transparente Adresse", "send_valid_transparent": "Gültige transparente Adresse",
"send_wallet_empty": "Ihre Wallet ist leer", "send_wallet_empty": "Ihre Wallet ist leer",
"send_yes_clear": "Ja, leeren", "send_yes_clear": "Ja, leeren",
"sender_balance": "Absender: %.8f → %.8f DRGX",
"sending": "Transaktion wird gesendet", "sending": "Transaktion wird gesendet",
"sending_from": "SENDEN VON", "sending_from": "SENDEN VON",
"sends_full_balance_warning": "Dies sendet das gesamte Guthaben. Die Sendeadresse wird ein Nullguthaben haben.",
"sent": "gesendet", "sent": "gesendet",
"sent_filter": "Gesendet", "sent_filter": "Gesendet",
"sent_type": "Gesendet", "sent_type": "Gesendet",
"sent_upper": "GESENDET", "sent_upper": "GESENDET",
"set_label": "Label setzen...",
"settings": "Einstellungen", "settings": "Einstellungen",
"settings_about_text": "Eine geschirmte Kryptowährungs-Wallet für DragonX (DRGX), erstellt mit Dear ImGui für ein leichtes, portables Erlebnis.", "settings_about_text": "Eine geschirmte Kryptowährungs-Wallet für DragonX (DRGX), erstellt mit Dear ImGui für ein leichtes, portables Erlebnis.",
"settings_acrylic_level": "Acrylstufe:", "settings_acrylic_level": "Acrylstufe:",
@@ -647,13 +765,17 @@
"shield_utxo_limit": "UTXO-Limit:", "shield_utxo_limit": "UTXO-Limit:",
"shield_wildcard_hint": "Verwenden Sie '*' um von allen transparenten Adressen abzuschirmen", "shield_wildcard_hint": "Verwenden Sie '*' um von allen transparenten Adressen abzuschirmen",
"shielded": "Abgeschirmt", "shielded": "Abgeschirmt",
"shielded_address": "Geschirmte Adresse",
"shielded_to": "ABGESCHIRMT AN", "shielded_to": "ABGESCHIRMT AN",
"shielded_type": "Abgeschirmt", "shielded_type": "Abgeschirmt",
"shielding_notice": "Hinweis: Dies wird Gelder von einer transparenten (T) Adresse auf eine private (Z) Adresse schirmen.",
"show": "Anzeigen", "show": "Anzeigen",
"show_hidden": "Ausgeblendete anzeigen (%d)", "show_hidden": "Ausgeblendete anzeigen (%d)",
"show_qr_code": "QR-Code anzeigen", "show_qr_code": "QR-Code anzeigen",
"showing_transactions": "Zeige %d–%d von %d Transaktionen (gesamt: %zu)", "showing_transactions": "Zeige %d%d von %d Transaktionen (gesamt: %zu)",
"showing_x_of_y": "%d von %d Adressen angezeigt",
"simple_background": "Einfacher Hintergrund", "simple_background": "Einfacher Hintergrund",
"slider_off": "Aus",
"start_mining": "Mining starten", "start_mining": "Mining starten",
"status": "Status", "status": "Status",
"stop_external": "Externen Daemon stoppen", "stop_external": "Externen Daemon stoppen",
@@ -662,17 +784,26 @@
"success": "Erfolg", "success": "Erfolg",
"summary": "Zusammenfassung", "summary": "Zusammenfassung",
"syncing": "Synchronisiere...", "syncing": "Synchronisiere...",
"t_address": "T-Adresse",
"t_addresses": "T-Adressen", "t_addresses": "T-Adressen",
"test_connection": "Testen", "test_connection": "Testen",
"theme": "Design", "theme": "Design",
"theme_effects": "Design-Effekte", "theme_effects": "Design-Effekte",
"theme_language": "THEMA & SPRACHE",
"time_days_ago": "vor %d Tagen", "time_days_ago": "vor %d Tagen",
"time_hours_ago": "vor %d Stunden", "time_hours_ago": "vor %d Stunden",
"time_minutes_ago": "vor %d Minuten", "time_minutes_ago": "vor %d Minuten",
"time_seconds_ago": "vor %d Sekunden", "time_seconds_ago": "vor %d Sekunden",
"timeout_15min": "15 Min",
"timeout_1hour": "1 Stunde",
"timeout_1min": "1 Min",
"timeout_30min": "30 Min",
"timeout_5min": "5 Min",
"timeout_off": "Aus",
"to": "An", "to": "An",
"to_upper": "AN", "to_upper": "AN",
"tools": "WERKZEUGE", "tools": "WERKZEUGE",
"tools_actions": "Werkzeuge & Aktionen...",
"total": "Gesamt", "total": "Gesamt",
"transaction_id": "TRANSAKTIONS-ID", "transaction_id": "TRANSAKTIONS-ID",
"transaction_sent": "Transaktion erfolgreich gesendet", "transaction_sent": "Transaktion erfolgreich gesendet",
@@ -680,7 +811,13 @@
"transaction_url": "Transaktions-URL", "transaction_url": "Transaktions-URL",
"transactions": "Transaktionen", "transactions": "Transaktionen",
"transactions_upper": "TRANSAKTIONEN", "transactions_upper": "TRANSAKTIONEN",
"transfer_failed": "Überweisung fehlgeschlagen",
"transfer_funds": "Geld überweisen",
"transfer_sent": "Überweisung gesendet",
"transfer_sent_desc": "Ihre Überweisung wurde an das Netzwerk gesendet.",
"transfer_to": "Überweisen an:",
"transparent": "Transparent", "transparent": "Transparent",
"transparent_address": "Transparente Adresse",
"tt_addr_url": "Basis-URL zum Anzeigen von Adressen in einem Block-Explorer", "tt_addr_url": "Basis-URL zum Anzeigen von Adressen in einem Block-Explorer",
"tt_address_book": "Gespeicherte Adressen für schnelles Senden verwalten", "tt_address_book": "Gespeicherte Adressen für schnelles Senden verwalten",
"tt_auto_lock": "Wallet nach dieser Inaktivitätszeit sperren", "tt_auto_lock": "Wallet nach dieser Inaktivitätszeit sperren",
@@ -695,6 +832,8 @@
"tt_custom_theme": "Benutzerdefiniertes Theme aktiv", "tt_custom_theme": "Benutzerdefiniertes Theme aktiv",
"tt_debug_collapse": "Debug-Protokollierungsoptionen einklappen", "tt_debug_collapse": "Debug-Protokollierungsoptionen einklappen",
"tt_debug_expand": "Debug-Protokollierungsoptionen ausklappen", "tt_debug_expand": "Debug-Protokollierungsoptionen ausklappen",
"tt_delete_blockchain": "Alle Blockchain-Daten löschen und neu synchronisieren. wallet.dat und Konfiguration bleiben erhalten.",
"tt_download_bootstrap": "Blockchain-Bootstrap herunterladen, um die Synchronisierung zu beschleunigen\nVorhandene Blockdaten werden ersetzt",
"tt_encrypt": "wallet.dat mit einer Passphrase verschlüsseln", "tt_encrypt": "wallet.dat mit einer Passphrase verschlüsseln",
"tt_export_all": "Alle privaten Schlüssel in eine Datei exportieren", "tt_export_all": "Alle privaten Schlüssel in eine Datei exportieren",
"tt_export_csv": "Transaktionsverlauf als CSV-Tabelle exportieren", "tt_export_csv": "Transaktionsverlauf als CSV-Tabelle exportieren",
@@ -712,6 +851,7 @@
"tt_mine_idle": "Mining automatisch starten, wenn das\\nSystem inaktiv ist (keine Tastatur-/Mauseingabe)", "tt_mine_idle": "Mining automatisch starten, wenn das\\nSystem inaktiv ist (keine Tastatur-/Mauseingabe)",
"tt_noise": "Körnungstextur-Intensität (0%% = aus, 100%% = maximum)", "tt_noise": "Körnungstextur-Intensität (0%% = aus, 100%% = maximum)",
"tt_open_dir": "Klicken, um im Dateimanager zu öffnen", "tt_open_dir": "Klicken, um im Dateimanager zu öffnen",
"tt_reduce_motion": "Animierte Übergänge und Saldo-Lerp für Barrierefreiheit deaktivieren",
"tt_remove_encrypt": "Verschlüsselung entfernen und Wallet ungeschützt speichern", "tt_remove_encrypt": "Verschlüsselung entfernen und Wallet ungeschützt speichern",
"tt_remove_pin": "PIN entfernen und Passphrase zum Entsperren erfordern", "tt_remove_pin": "PIN entfernen und Passphrase zum Entsperren erfordern",
"tt_report_bug": "Ein Problem im Projekt-Tracker melden", "tt_report_bug": "Ein Problem im Projekt-Tracker melden",
@@ -789,7 +929,9 @@
"warning_upper": "WARNUNG!", "warning_upper": "WARNUNG!",
"website": "Webseite", "website": "Webseite",
"window_opacity": "Fenster-Transparenz", "window_opacity": "Fenster-Transparenz",
"wizard_daemon_start_failed": "Daemon-Start fehlgeschlagen — wird automatisch wiederholt",
"yes_clear": "Ja, leeren", "yes_clear": "Ja, leeren",
"your_addresses": "Ihre Adressen", "your_addresses": "Ihre Adressen",
"z_address": "Z-Adresse",
"z_addresses": "Z-Adressen" "z_addresses": "Z-Adressen"
} }

View File

@@ -43,6 +43,8 @@
"address_url": "URL de Dirección", "address_url": "URL de Dirección",
"addresses_appear_here": "Tus direcciones de recepción aparecerán aquí una vez conectado.", "addresses_appear_here": "Tus direcciones de recepción aparecerán aquí una vez conectado.",
"advanced": "AVANZADO", "advanced": "AVANZADO",
"advanced_effects": "Efectos Avanzados...",
"ago": "atrás",
"all_filter": "Todos", "all_filter": "Todos",
"allow_custom_fees": "Permitir comisiones personalizadas", "allow_custom_fees": "Permitir comisiones personalizadas",
"amount": "Cantidad", "amount": "Cantidad",
@@ -90,12 +92,30 @@
"block_timestamp": "Fecha y Hora:", "block_timestamp": "Fecha y Hora:",
"block_transactions": "Transacciones:", "block_transactions": "Transacciones:",
"blockchain_syncing": "Sincronizando blockchain (%.1f%%)... Los saldos pueden ser inexactos.", "blockchain_syncing": "Sincronizando blockchain (%.1f%%)... Los saldos pueden ser inexactos.",
"bootstrap_daemon_running": "Daemon ejecutándose",
"bootstrap_daemon_stopped": "Daemon detenido",
"bootstrap_daemon_stopping": "Deteniendo daemon...",
"bootstrap_desc": "Descarga un bootstrap de la blockchain para acelerar drásticamente la sincronización inicial. Esto descarga una instantánea de la blockchain y la extrae en tu directorio de datos.",
"bootstrap_downloading": "Descargando bootstrap...",
"bootstrap_extracting": "Extrayendo datos de blockchain...",
"bootstrap_failed": "Error en Bootstrap",
"bootstrap_mirror": "Espejo",
"bootstrap_mirror_tooltip": "Descargar desde espejo (bootstrap2.dragonx.is).\nUsa esto si la descarga principal es lenta o falla.",
"bootstrap_restart_daemon": "Reiniciar Daemon",
"bootstrap_success": "Bootstrap Completado",
"bootstrap_success_desc": "Los datos de la blockchain se han extraído correctamente. Inicie el daemon para comenzar a sincronizar desde el punto del bootstrap.",
"bootstrap_trust_warning": "Solo use bootstrap.dragonx.is o bootstrap2.dragonx.is. Usar archivos de fuentes no confiables podría comprometer su nodo.",
"bootstrap_verifying": "Verificando sumas de comprobación...",
"bootstrap_wallet_protected": "(wallet.dat está protegido)",
"bootstrap_warning": "Los datos de bloques existentes (blocks, chainstate, notarizations) se eliminarán y reemplazarán. Su wallet.dat NO será modificado ni eliminado.",
"cancel": "Cancelar", "cancel": "Cancelar",
"characters": "caracteres", "characters": "caracteres",
"choose_icon": "Elegir Icono",
"clear": "Limpiar", "clear": "Limpiar",
"clear_all_bans": "Limpiar Todos los Bloqueos", "clear_all_bans": "Limpiar Todos los Bloqueos",
"clear_anyway": "Limpiar de todos modos", "clear_anyway": "Limpiar de todos modos",
"clear_form_confirm": "¿Limpiar todos los campos del formulario?", "clear_form_confirm": "¿Limpiar todos los campos del formulario?",
"clear_icon": "Borrar Icono",
"clear_request": "Limpiar Solicitud", "clear_request": "Limpiar Solicitud",
"click_copy_address": "Clic para copiar dirección", "click_copy_address": "Clic para copiar dirección",
"click_copy_uri": "Clic para copiar URI", "click_copy_uri": "Clic para copiar URI",
@@ -106,8 +126,12 @@
"confirm_clear_ztx_title": "Confirmar limpieza del historial Z-Tx", "confirm_clear_ztx_title": "Confirmar limpieza del historial Z-Tx",
"confirm_clear_ztx_warning1": "Limpiar el historial de z-transacciones puede hacer que su saldo blindado se muestre como 0 hasta que se realice un reescaneo de la billetera.", "confirm_clear_ztx_warning1": "Limpiar el historial de z-transacciones puede hacer que su saldo blindado se muestre como 0 hasta que se realice un reescaneo de la billetera.",
"confirm_clear_ztx_warning2": "Si esto sucede, deberá reimportar las claves privadas de su dirección z con el reescaneo habilitado para recuperar su saldo.", "confirm_clear_ztx_warning2": "Si esto sucede, deberá reimportar las claves privadas de su dirección z con el reescaneo habilitado para recuperar su saldo.",
"confirm_delete_blockchain_msg": "Esto detendrá el daemon, eliminará todos los datos de la blockchain (blocks, chainstate, peers) y comenzará una nueva sincronización desde cero. Esto puede tardar varias horas.",
"confirm_delete_blockchain_safe": "Su wallet.dat, configuración e historial de transacciones están seguros y no se eliminarán.",
"confirm_delete_blockchain_title": "Eliminar Datos de Blockchain",
"confirm_send": "Confirmar Envío", "confirm_send": "Confirmar Envío",
"confirm_transaction": "Confirmar Transacción", "confirm_transaction": "Confirmar Transacción",
"confirm_transfer": "Confirmar Transferencia",
"confirmations": "Confirmaciones", "confirmations": "Confirmaciones",
"confirmations_display": "%d confirmaciones | %s", "confirmations_display": "%d confirmaciones | %s",
"confirmed": "Confirmada", "confirmed": "Confirmada",
@@ -172,6 +196,7 @@
"console_welcome": "Bienvenido a la Consola de ObsidianDragon", "console_welcome": "Bienvenido a la Consola de ObsidianDragon",
"console_zoom_in": "Acercar", "console_zoom_in": "Acercar",
"console_zoom_out": "Alejar", "console_zoom_out": "Alejar",
"copied": "¡Copiado!",
"copy": "Copiar", "copy": "Copiar",
"copy_address": "Copiar Dirección Completa", "copy_address": "Copiar Dirección Completa",
"copy_error": "Copiar Error", "copy_error": "Copiar Error",
@@ -180,22 +205,45 @@
"copy_uri": "Copiar URI", "copy_uri": "Copiar URI",
"current_price": "Precio Actual", "current_price": "Precio Actual",
"custom_fees": "Comisiones personalizadas", "custom_fees": "Comisiones personalizadas",
"daemon_version": "Daemon",
"dark": "Oscuro", "dark": "Oscuro",
"date": "Fecha", "date": "Fecha",
"date_label": "Fecha:", "date_label": "Fecha:",
"debug_logging": "REGISTRO DE DEPURACIÓN", "debug_logging": "REGISTRO DE DEPURACIÓN",
"delete": "Eliminar", "delete": "Eliminar",
"delete_blockchain": "Eliminar Blockchain",
"delete_blockchain_confirm": "Eliminar y Resincronizar",
"deshielding_warning": "Advertencia: Esto des-protegerá fondos de una dirección privada (Z) a una dirección transparente (T).",
"difficulty": "Dificultad", "difficulty": "Dificultad",
"disconnected": "Desconectado", "disconnected": "Desconectado",
"dismiss": "Descartar", "dismiss": "Descartar",
"display": "Pantalla", "display": "Pantalla",
"download": "Descargar",
"download_bootstrap": "Descargar Bootstrap",
"dragonx_green": "DragonX (Verde)", "dragonx_green": "DragonX (Verde)",
"edit": "Editar", "edit": "Editar",
"error": "Error", "error": "Error",
"error_format": "Error: %s", "error_format": "Error: %s",
"est_time_to_block": "Tiempo Est. al Bloque", "est_time_to_block": "Tiempo Est. al Bloque",
"exit": "Salir", "exit": "Salir",
"explorer": "EXPLORADOR", "explorer": "Explorador",
"explorer_block_detail": "Bloque",
"explorer_block_hash": "Hash",
"explorer_block_height": "Altura",
"explorer_block_merkle": "Raíz Merkle",
"explorer_block_size": "Tamaño",
"explorer_block_time": "Hora",
"explorer_block_txs": "Transacciones",
"explorer_chain_stats": "Cadena",
"explorer_invalid_query": "Ingrese una altura de bloque o un hash de 64 caracteres",
"explorer_mempool": "Mempool",
"explorer_mempool_size": "Tamaño",
"explorer_mempool_txs": "Transacciones",
"explorer_recent_blocks": "Bloques Recientes",
"explorer_search": "Buscar",
"explorer_section": "EXPLORADOR",
"explorer_tx_outputs": "Salidas",
"explorer_tx_size": "Tamaño",
"export": "Exportar", "export": "Exportar",
"export_csv": "Exportar CSV", "export_csv": "Exportar CSV",
"export_keys_btn": "Exportar Claves", "export_keys_btn": "Exportar Claves",
@@ -224,14 +272,22 @@
"fetch_prices": "Obtener precios", "fetch_prices": "Obtener precios",
"file": "Archivo", "file": "Archivo",
"file_save_location": "El archivo se guardará en: ~/.config/ObsidianDragon/", "file_save_location": "El archivo se guardará en: ~/.config/ObsidianDragon/",
"filter": "Filtrar...",
"font_scale": "Escala de fuente", "font_scale": "Escala de fuente",
"force_quit": "Forzar Salida",
"force_quit_confirm_msg": "Esto matará inmediatamente el daemon sin un apagado limpio.\nEsto puede corromper el índice de la blockchain y requerir una resincronización.",
"force_quit_confirm_title": "¿Forzar Salida?",
"force_quit_warning": "Esto matará inmediatamente el daemon sin un apagado limpio. Puede requerir una resincronización de la blockchain.",
"force_quit_yes": "Forzar Salida",
"from": "Desde", "from": "Desde",
"from_upper": "DESDE", "from_upper": "DESDE",
"full_details": "Detalles Completos", "full_details": "Detalles Completos",
"general": "General", "general": "General",
"generating": "Generando",
"go_to_receive": "Ir a Recibir", "go_to_receive": "Ir a Recibir",
"height": "Altura", "height": "Altura",
"help": "Ayuda", "help": "Ayuda",
"hidden_tag": " (oculto)",
"hide": "Ocultar", "hide": "Ocultar",
"hide_address": "Ocultar dirección", "hide_address": "Ocultar dirección",
"hide_zero_balances": "Ocultar saldos 0", "hide_zero_balances": "Ocultar saldos 0",
@@ -253,6 +309,9 @@
"import_key_warning": "Advertencia: ¡Nunca compartas tus claves privadas! Importar claves de fuentes no confiables puede comprometer tu cartera.", "import_key_warning": "Advertencia: ¡Nunca compartas tus claves privadas! Importar claves de fuentes no confiables puede comprometer tu cartera.",
"import_key_z_format": "Claves de gasto de direcciones Z (secret-extended-key-...)", "import_key_z_format": "Claves de gasto de direcciones Z (secret-extended-key-...)",
"import_private_key": "Importar Clave Privada...", "import_private_key": "Importar Clave Privada...",
"incorrect_passphrase": "Contraseña incorrecta",
"incorrect_pin": "PIN incorrecto",
"insufficient_funds": "Fondos insuficientes para este monto más la comisión.",
"invalid_address": "Formato de dirección inválido", "invalid_address": "Formato de dirección inválido",
"ip_address": "Dirección IP", "ip_address": "Dirección IP",
"keep": "Mantener", "keep": "Mantener",
@@ -266,6 +325,7 @@
"key_export_viewing_keys_zonly": "Las claves de visualización solo están disponibles para direcciones blindadas (z)", "key_export_viewing_keys_zonly": "Las claves de visualización solo están disponibles para direcciones blindadas (z)",
"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_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.",
"label": "Etiqueta:", "label": "Etiqueta:",
"label_placeholder": "ej. Ahorros, Minería...",
"language": "Idioma", "language": "Idioma",
"light": "Claro", "light": "Claro",
"loading": "Cargando...", "loading": "Cargando...",
@@ -286,6 +346,7 @@
"market_now": "Ahora", "market_now": "Ahora",
"market_pct_shielded": "%.0f%% Protegido", "market_pct_shielded": "%.0f%% Protegido",
"market_portfolio": "PORTAFOLIO", "market_portfolio": "PORTAFOLIO",
"market_price_loading": "Cargando datos de precio...",
"market_price_unavailable": "Datos de precio no disponibles", "market_price_unavailable": "Datos de precio no disponibles",
"market_refresh_price": "Actualizar datos de precio", "market_refresh_price": "Actualizar datos de precio",
"market_trade_on": "Operar en %s", "market_trade_on": "Operar en %s",
@@ -311,6 +372,13 @@
"mining_address_copied": "Dirección de minería copiada", "mining_address_copied": "Dirección de minería copiada",
"mining_all_time": "Todo el Tiempo", "mining_all_time": "Todo el Tiempo",
"mining_already_saved": "URL del pool ya guardada", "mining_already_saved": "URL del pool ya guardada",
"mining_benchmark_cancel": "Cancelar benchmark",
"mining_benchmark_cooling": "Enfriando",
"mining_benchmark_dismiss": "Cerrar",
"mining_benchmark_result": "Óptimo",
"mining_benchmark_stabilizing": "Estabilizando",
"mining_benchmark_testing": "Probando",
"mining_benchmark_tooltip": "Encontrar el número óptimo de hilos para esta CPU",
"mining_block_copied": "Hash de bloque copiado", "mining_block_copied": "Hash de bloque copiado",
"mining_chart_1m_ago": "hace 1m", "mining_chart_1m_ago": "hace 1m",
"mining_chart_5m_ago": "hace 5m", "mining_chart_5m_ago": "hace 5m",
@@ -330,8 +398,15 @@
"mining_filter_tip_all": "Mostrar todas las ganancias", "mining_filter_tip_all": "Mostrar todas las ganancias",
"mining_filter_tip_pool": "Mostrar solo ganancias del pool", "mining_filter_tip_pool": "Mostrar solo ganancias del pool",
"mining_filter_tip_solo": "Mostrar solo ganancias solo", "mining_filter_tip_solo": "Mostrar solo ganancias solo",
"mining_generate_z_address_hint": "Genere una dirección Z en la pestaña Recibir para usarla como dirección de pago",
"mining_idle_gpu_off_tooltip": "Sin restricción: ACTIVADO\nSolo la entrada de teclado/ratón determina el estado inactivo\nClic para activar detección de GPU",
"mining_idle_gpu_on_tooltip": "GPU-consciente: ACTIVADO\nLa actividad de GPU (video, juegos) previene la minería inactiva\nClic para modo sin restricción",
"mining_idle_off_tooltip": "Activar minería en reposo", "mining_idle_off_tooltip": "Activar minería en reposo",
"mining_idle_on_tooltip": "Desactivar minería en reposo", "mining_idle_on_tooltip": "Desactivar minería en reposo",
"mining_idle_scale_off_tooltip": "Modo inicio/parada: ACTIVADO\nClic para cambiar al modo de escala de hilos",
"mining_idle_scale_on_tooltip": "Escala de hilos: ACTIVADO\nClic para cambiar al modo de inicio/parada",
"mining_idle_threads_active_tooltip": "Hilos cuando el usuario está activo",
"mining_idle_threads_idle_tooltip": "Hilos cuando el sistema está inactivo",
"mining_local_hashrate": "Hashrate Local", "mining_local_hashrate": "Hashrate Local",
"mining_mine": "Minar", "mining_mine": "Minar",
"mining_mining_addr": "Dir. Minería", "mining_mining_addr": "Dir. Minería",
@@ -388,6 +463,7 @@
"no_addresses_available": "No hay direcciones disponibles", "no_addresses_available": "No hay direcciones disponibles",
"no_addresses_match": "No hay direcciones que coincidan con el filtro", "no_addresses_match": "No hay direcciones que coincidan con el filtro",
"no_addresses_with_balance": "No hay direcciones con saldo", "no_addresses_with_balance": "No hay direcciones con saldo",
"no_addresses_yet": "Aún no hay direcciones",
"no_matching": "No hay transacciones coincidentes", "no_matching": "No hay transacciones coincidentes",
"no_recent_receives": "No hay recepciones recientes", "no_recent_receives": "No hay recepciones recientes",
"no_recent_sends": "No hay envíos recientes", "no_recent_sends": "No hay envíos recientes",
@@ -453,6 +529,7 @@
"peers_upper": "NODOS", "peers_upper": "NODOS",
"peers_version": "Versión", "peers_version": "Versión",
"pending": "Pendiente", "pending": "Pendiente",
"pin_not_set": "PIN no configurado. Use la contraseña para desbloquear.",
"ping": "Ping", "ping": "Ping",
"price_chart": "Gráfico de Precios", "price_chart": "Gráfico de Precios",
"qr_code": "Código QR", "qr_code": "Código QR",
@@ -473,7 +550,9 @@
"recent_received": "RECIBIDOS RECIENTES", "recent_received": "RECIBIDOS RECIENTES",
"recent_sends": "ENVÍOS RECIENTES", "recent_sends": "ENVÍOS RECIENTES",
"recipient": "DESTINATARIO", "recipient": "DESTINATARIO",
"recipient_balance": "Destinatario: %.8f → %.8f DRGX",
"recv_type": "Recibido", "recv_type": "Recibido",
"reduce_motion": "Reducir Movimiento",
"refresh": "Actualizar", "refresh": "Actualizar",
"refresh_now": "Actualizar Ahora", "refresh_now": "Actualizar Ahora",
"remove_favorite": "Quitar favorito", "remove_favorite": "Quitar favorito",
@@ -493,7 +572,10 @@
"request_uri_copied": "URI de pago copiada al portapapeles", "request_uri_copied": "URI de pago copiada al portapapeles",
"rescan": "Re-escanear", "rescan": "Re-escanear",
"reset_to_defaults": "Restablecer Valores", "reset_to_defaults": "Restablecer Valores",
"restarting_after_encryption": "Reiniciando daemon después del cifrado...",
"restore_address": "Restaurar dirección", "restore_address": "Restaurar dirección",
"result_preview": "Vista previa del resultado",
"retry": "Reintentar",
"review_send": "Revisar Envío", "review_send": "Revisar Envío",
"rpc_host": "Host RPC", "rpc_host": "Host RPC",
"rpc_pass": "Contraseña", "rpc_pass": "Contraseña",
@@ -502,6 +584,39 @@
"save": "Guardar", "save": "Guardar",
"save_settings": "Guardar Configuración", "save_settings": "Guardar Configuración",
"save_z_transactions": "Guardar Z-tx en lista", "save_z_transactions": "Guardar Z-tx en lista",
"sb_auth_failed": "Autenticación fallida — verifique rpcuser/rpcpassword",
"sb_block": "Bloque: %d",
"sb_connecting_daemon": "Conectando a dragonxd...",
"sb_connecting_err": "Conectando al daemon — %s",
"sb_connecting_external": "Conectando a daemon externo...",
"sb_connecting_generic": "Conectando al daemon...",
"sb_daemon_crashed": "El daemon se bloqueó %d veces",
"sb_daemon_not_found": "Daemon no encontrado",
"sb_dragonxd_running": "dragonxd ejecutándose",
"sb_dragonxd_stopped": "dragonxd detenido",
"sb_dragonxd_stopping": "Deteniendo dragonxd...",
"sb_extracting_sapling": "Extrayendo parámetros Sapling...",
"sb_importing_keys": "Importando claves",
"sb_loading_config": "Cargando configuración...",
"sb_mining_hs": "%.1f H/s",
"sb_net_ghs": "Red: %.2f GH/s",
"sb_net_hs": "Red: %.1f H/s",
"sb_net_khs": "Red: %.2f KH/s",
"sb_net_mhs": "Red: %.2f MH/s",
"sb_no_conf": "DRAGONX.conf no encontrado",
"sb_peers": "Pares: %zu",
"sb_rescanning": "Reescaneando",
"sb_rescanning_pct": "Reescaneando %.0f%%",
"sb_restarting_daemon": "Reiniciando daemon...",
"sb_sapling_failed": "Error al extraer parámetros Sapling.",
"sb_sapling_not_found": "Parámetros Sapling no encontrados.",
"sb_starting_daemon": "Iniciando dragonxd...",
"sb_syncing_basic": "Sincronizando %.1f%% (%d restantes)",
"sb_syncing_eta": "Sincronizando %.1f%% (%d restantes, %.0f blk/s, ~%s)",
"sb_waiting_config": "Esperando configuración del daemon...",
"sb_waiting_daemon": "Esperando a dragonxd...",
"sb_waiting_daemon_err": "Esperando a dragonxd — %s",
"sb_warming_up": "Calentando...",
"search_placeholder": "Buscar...", "search_placeholder": "Buscar...",
"security": "SEGURIDAD", "security": "SEGURIDAD",
"select_address": "Seleccionar dirección...", "select_address": "Seleccionar dirección...",
@@ -553,13 +668,16 @@
"send_valid_transparent": "Dirección transparente válida", "send_valid_transparent": "Dirección transparente válida",
"send_wallet_empty": "Tu cartera está vacía", "send_wallet_empty": "Tu cartera está vacía",
"send_yes_clear": "Sí, Limpiar", "send_yes_clear": "Sí, Limpiar",
"sender_balance": "Remitente: %.8f → %.8f DRGX",
"sending": "Enviando transacción", "sending": "Enviando transacción",
"sending_from": "ENVIANDO DESDE", "sending_from": "ENVIANDO DESDE",
"sends_full_balance_warning": "Esto envía el saldo completo. La dirección de envío tendrá saldo cero.",
"sent": "enviado", "sent": "enviado",
"sent_filter": "Enviado", "sent_filter": "Enviado",
"sent_type": "Enviado", "sent_type": "Enviado",
"sent_upper": "ENVIADO", "sent_upper": "ENVIADO",
"settings": "Configuración", "set_label": "Establecer Etiqueta...",
"settings": "Ajustes",
"settings_about_text": "Una billetera de criptomonedas blindada para DragonX (DRGX), creada con Dear ImGui para una experiencia ligera y portátil.", "settings_about_text": "Una billetera de criptomonedas blindada para DragonX (DRGX), creada con Dear ImGui para una experiencia ligera y portátil.",
"settings_acrylic_level": "Nivel de acrílico:", "settings_acrylic_level": "Nivel de acrílico:",
"settings_address_book": "Libreta de direcciones...", "settings_address_book": "Libreta de direcciones...",
@@ -647,13 +765,17 @@
"shield_utxo_limit": "Límite UTXO:", "shield_utxo_limit": "Límite UTXO:",
"shield_wildcard_hint": "Usa '*' para proteger desde todas las direcciones transparentes", "shield_wildcard_hint": "Usa '*' para proteger desde todas las direcciones transparentes",
"shielded": "Blindada", "shielded": "Blindada",
"shielded_address": "Dirección Protegida",
"shielded_to": "PROTEGIDA PARA", "shielded_to": "PROTEGIDA PARA",
"shielded_type": "Protegido", "shielded_type": "Protegido",
"shielding_notice": "Nota: Esto blindará fondos de una dirección transparente (T) a una dirección privada (Z).",
"show": "Mostrar", "show": "Mostrar",
"show_hidden": "Mostrar ocultos (%d)", "show_hidden": "Mostrar ocultos (%d)",
"show_qr_code": "Mostrar Código QR", "show_qr_code": "Mostrar Código QR",
"showing_transactions": "Mostrando %d%d de %d transacciones (total: %zu)", "showing_transactions": "Mostrando %d%d de %d transacciones (total: %zu)",
"showing_x_of_y": "Mostrando %d de %d direcciones",
"simple_background": "Fondo simple", "simple_background": "Fondo simple",
"slider_off": "Apagado",
"start_mining": "Iniciar Minería", "start_mining": "Iniciar Minería",
"status": "Estado", "status": "Estado",
"stop_external": "Detener daemon externo", "stop_external": "Detener daemon externo",
@@ -662,17 +784,26 @@
"success": "Éxito", "success": "Éxito",
"summary": "Resumen", "summary": "Resumen",
"syncing": "Sincronizando...", "syncing": "Sincronizando...",
"t_address": "Dirección T",
"t_addresses": "Direcciones T", "t_addresses": "Direcciones T",
"test_connection": "Probar", "test_connection": "Probar",
"theme": "Tema", "theme": "Tema",
"theme_effects": "Efectos de tema", "theme_effects": "Efectos de tema",
"theme_language": "TEMA E IDIOMA",
"time_days_ago": "hace %d días", "time_days_ago": "hace %d días",
"time_hours_ago": "hace %d horas", "time_hours_ago": "hace %d horas",
"time_minutes_ago": "hace %d minutos", "time_minutes_ago": "hace %d minutos",
"time_seconds_ago": "hace %d segundos", "time_seconds_ago": "hace %d segundos",
"timeout_15min": "15 min",
"timeout_1hour": "1 hora",
"timeout_1min": "1 min",
"timeout_30min": "30 min",
"timeout_5min": "5 min",
"timeout_off": "Apagado",
"to": "Para", "to": "Para",
"to_upper": "PARA", "to_upper": "PARA",
"tools": "HERRAMIENTAS", "tools": "HERRAMIENTAS",
"tools_actions": "Herramientas y Acciones...",
"total": "Total", "total": "Total",
"transaction_id": "ID DE TRANSACCIÓN", "transaction_id": "ID DE TRANSACCIÓN",
"transaction_sent": "Transacción enviada exitosamente", "transaction_sent": "Transacción enviada exitosamente",
@@ -680,7 +811,13 @@
"transaction_url": "URL de Transacción", "transaction_url": "URL de Transacción",
"transactions": "Transacciones", "transactions": "Transacciones",
"transactions_upper": "TRANSACCIONES", "transactions_upper": "TRANSACCIONES",
"transfer_failed": "Transferencia Fallida",
"transfer_funds": "Transferir Fondos",
"transfer_sent": "Transferencia Enviada",
"transfer_sent_desc": "Su transferencia ha sido enviada a la red.",
"transfer_to": "Transferir a:",
"transparent": "Transparente", "transparent": "Transparente",
"transparent_address": "Dirección Transparente",
"tt_addr_url": "URL base para ver direcciones en un explorador de bloques", "tt_addr_url": "URL base para ver direcciones en un explorador de bloques",
"tt_address_book": "Administrar direcciones guardadas para envío rápido", "tt_address_book": "Administrar direcciones guardadas para envío rápido",
"tt_auto_lock": "Bloquear billetera después de este tiempo de inactividad", "tt_auto_lock": "Bloquear billetera después de este tiempo de inactividad",
@@ -695,6 +832,8 @@
"tt_custom_theme": "Tema personalizado activo", "tt_custom_theme": "Tema personalizado activo",
"tt_debug_collapse": "Colapsar opciones de registro de depuración", "tt_debug_collapse": "Colapsar opciones de registro de depuración",
"tt_debug_expand": "Expandir opciones de registro de depuración", "tt_debug_expand": "Expandir opciones de registro de depuración",
"tt_delete_blockchain": "Eliminar todos los datos de la blockchain e iniciar una nueva sincronización. Se preservan wallet.dat y la configuración.",
"tt_download_bootstrap": "Descargar bootstrap de blockchain para acelerar la sincronización\nLos datos de bloques existentes serán reemplazados",
"tt_encrypt": "Cifrar wallet.dat con una contraseña", "tt_encrypt": "Cifrar wallet.dat con una contraseña",
"tt_export_all": "Exportar todas las claves privadas a un archivo", "tt_export_all": "Exportar todas las claves privadas a un archivo",
"tt_export_csv": "Exportar historial de transacciones como hoja de cálculo CSV", "tt_export_csv": "Exportar historial de transacciones como hoja de cálculo CSV",
@@ -712,6 +851,7 @@
"tt_mine_idle": "Iniciar minería automáticamente cuando el\\nsistema esté inactivo (sin entrada de teclado/ratón)", "tt_mine_idle": "Iniciar minería automáticamente cuando el\\nsistema esté inactivo (sin entrada de teclado/ratón)",
"tt_noise": "Intensidad de textura granulada (0%% = apagado, 100%% = máximo)", "tt_noise": "Intensidad de textura granulada (0%% = apagado, 100%% = máximo)",
"tt_open_dir": "Clic para abrir en explorador de archivos", "tt_open_dir": "Clic para abrir en explorador de archivos",
"tt_reduce_motion": "Desactivar transiciones animadas y lerp de saldo para accesibilidad",
"tt_remove_encrypt": "Quitar cifrado y almacenar la billetera sin protección", "tt_remove_encrypt": "Quitar cifrado y almacenar la billetera sin protección",
"tt_remove_pin": "Quitar PIN y requerir contraseña para desbloquear", "tt_remove_pin": "Quitar PIN y requerir contraseña para desbloquear",
"tt_report_bug": "Reportar un problema en el rastreador del proyecto", "tt_report_bug": "Reportar un problema en el rastreador del proyecto",
@@ -789,7 +929,9 @@
"warning_upper": "¡ADVERTENCIA!", "warning_upper": "¡ADVERTENCIA!",
"website": "Sitio Web", "website": "Sitio Web",
"window_opacity": "Opacidad de ventana", "window_opacity": "Opacidad de ventana",
"wizard_daemon_start_failed": "Error al iniciar el daemon — se reintentará automáticamente",
"yes_clear": "Sí, Limpiar", "yes_clear": "Sí, Limpiar",
"your_addresses": "Sus Direcciones", "your_addresses": "Sus Direcciones",
"z_address": "Dirección Z",
"z_addresses": "Direcciones Z" "z_addresses": "Direcciones Z"
} }

View File

@@ -43,6 +43,8 @@
"address_url": "URL de l'adresse", "address_url": "URL de l'adresse",
"addresses_appear_here": "Vos adresses de réception apparaîtront ici une fois connecté.", "addresses_appear_here": "Vos adresses de réception apparaîtront ici une fois connecté.",
"advanced": "AVANCÉ", "advanced": "AVANCÉ",
"advanced_effects": "Effets avancés...",
"ago": "passé",
"all_filter": "Tout", "all_filter": "Tout",
"allow_custom_fees": "Autoriser les frais personnalisés", "allow_custom_fees": "Autoriser les frais personnalisés",
"amount": "Montant", "amount": "Montant",
@@ -90,12 +92,30 @@
"block_timestamp": "Horodatage :", "block_timestamp": "Horodatage :",
"block_transactions": "Transactions :", "block_transactions": "Transactions :",
"blockchain_syncing": "Synchronisation de la blockchain (%.1f%%)... Les soldes peuvent être inexacts.", "blockchain_syncing": "Synchronisation de la blockchain (%.1f%%)... Les soldes peuvent être inexacts.",
"bootstrap_daemon_running": "Daemon en cours",
"bootstrap_daemon_stopped": "Daemon arrêté",
"bootstrap_daemon_stopping": "Arrêt du daemon...",
"bootstrap_desc": "Téléchargez un bootstrap de la blockchain pour accélérer considérablement la synchronisation initiale. Cela télécharge un instantané de la blockchain et l'extrait dans votre répertoire de données.",
"bootstrap_downloading": "Téléchargement du bootstrap...",
"bootstrap_extracting": "Extraction des données blockchain...",
"bootstrap_failed": "Échec du Bootstrap",
"bootstrap_mirror": "Miroir",
"bootstrap_mirror_tooltip": "Télécharger depuis le miroir (bootstrap2.dragonx.is).\nUtilisez ceci si le téléchargement principal est lent ou échoue.",
"bootstrap_restart_daemon": "Redémarrer le Daemon",
"bootstrap_success": "Bootstrap terminé",
"bootstrap_success_desc": "Les données de la blockchain ont été extraites avec succès. Démarrez le daemon pour commencer la synchronisation à partir du point de bootstrap.",
"bootstrap_trust_warning": "N'utilisez que bootstrap.dragonx.is ou bootstrap2.dragonx.is. L'utilisation de fichiers provenant de sources non fiables pourrait compromettre votre nœud.",
"bootstrap_verifying": "Vérification des sommes de contrôle...",
"bootstrap_wallet_protected": "(wallet.dat est protégé)",
"bootstrap_warning": "Les données de blocs existantes (blocks, chainstate, notarizations) seront supprimées et remplacées. Votre wallet.dat ne sera PAS modifié ni supprimé.",
"cancel": "Annuler", "cancel": "Annuler",
"characters": "caractères", "characters": "caractères",
"choose_icon": "Choisir une icône",
"clear": "Effacer", "clear": "Effacer",
"clear_all_bans": "Lever tous les bannissements", "clear_all_bans": "Lever tous les bannissements",
"clear_anyway": "Effacer quand même", "clear_anyway": "Effacer quand même",
"clear_form_confirm": "Effacer tous les champs du formulaire ?", "clear_form_confirm": "Effacer tous les champs du formulaire ?",
"clear_icon": "Effacer l'icône",
"clear_request": "Effacer la demande", "clear_request": "Effacer la demande",
"click_copy_address": "Cliquez pour copier l'adresse", "click_copy_address": "Cliquez pour copier l'adresse",
"click_copy_uri": "Cliquez pour copier l'URI", "click_copy_uri": "Cliquez pour copier l'URI",
@@ -106,8 +126,12 @@
"confirm_clear_ztx_title": "Confirmer l'effacement de l'historique Z-Tx", "confirm_clear_ztx_title": "Confirmer l'effacement de l'historique Z-Tx",
"confirm_clear_ztx_warning1": "L'effacement de l'historique des z-transactions peut faire apparaître votre solde blindé à 0 jusqu'à ce qu'un rescan du portefeuille soit effectué.", "confirm_clear_ztx_warning1": "L'effacement de l'historique des z-transactions peut faire apparaître votre solde blindé à 0 jusqu'à ce qu'un rescan du portefeuille soit effectué.",
"confirm_clear_ztx_warning2": "Si cela se produit, vous devrez réimporter les clés privées de votre adresse z avec le rescan activé pour récupérer votre solde.", "confirm_clear_ztx_warning2": "Si cela se produit, vous devrez réimporter les clés privées de votre adresse z avec le rescan activé pour récupérer votre solde.",
"confirm_delete_blockchain_msg": "Cela arrêtera le daemon, supprimera toutes les données de la blockchain (blocks, chainstate, peers) et démarrera une nouvelle synchronisation. Cela peut prendre plusieurs heures.",
"confirm_delete_blockchain_safe": "Votre wallet.dat, votre configuration et votre historique de transactions sont en sécurité et ne seront pas supprimés.",
"confirm_delete_blockchain_title": "Supprimer les données Blockchain",
"confirm_send": "Confirmer l'envoi", "confirm_send": "Confirmer l'envoi",
"confirm_transaction": "Confirmer la transaction", "confirm_transaction": "Confirmer la transaction",
"confirm_transfer": "Confirmer le transfert",
"confirmations": "Confirmations", "confirmations": "Confirmations",
"confirmations_display": "%d confirmations | %s", "confirmations_display": "%d confirmations | %s",
"confirmed": "Confirmé", "confirmed": "Confirmé",
@@ -172,6 +196,7 @@
"console_welcome": "Bienvenue dans la console ObsidianDragon", "console_welcome": "Bienvenue dans la console ObsidianDragon",
"console_zoom_in": "Agrandir", "console_zoom_in": "Agrandir",
"console_zoom_out": "Réduire", "console_zoom_out": "Réduire",
"copied": "Copié !",
"copy": "Copier", "copy": "Copier",
"copy_address": "Copier l'adresse complète", "copy_address": "Copier l'adresse complète",
"copy_error": "Copier l'erreur", "copy_error": "Copier l'erreur",
@@ -180,22 +205,45 @@
"copy_uri": "Copier l'URI", "copy_uri": "Copier l'URI",
"current_price": "Prix actuel", "current_price": "Prix actuel",
"custom_fees": "Frais personnalisés", "custom_fees": "Frais personnalisés",
"daemon_version": "Daemon",
"dark": "Sombre", "dark": "Sombre",
"date": "Date", "date": "Date",
"date_label": "Date :", "date_label": "Date :",
"debug_logging": "JOURNALISATION DE DÉBOGAGE", "debug_logging": "JOURNALISATION DE DÉBOGAGE",
"delete": "Supprimer", "delete": "Supprimer",
"delete_blockchain": "Supprimer Blockchain",
"delete_blockchain_confirm": "Supprimer & Resynchroniser",
"deshielding_warning": "Attention : Cela va déblinder des fonds d'une adresse privée (Z) vers une adresse transparente (T).",
"difficulty": "Difficulté", "difficulty": "Difficulté",
"disconnected": "Déconnecté", "disconnected": "Déconnecté",
"dismiss": "Ignorer", "dismiss": "Ignorer",
"display": "Affichage", "display": "Affichage",
"download": "Télécharger",
"download_bootstrap": "Télécharger Bootstrap",
"dragonx_green": "DragonX (Vert)", "dragonx_green": "DragonX (Vert)",
"edit": "Modifier", "edit": "Modifier",
"error": "Erreur", "error": "Erreur",
"error_format": "Erreur : %s", "error_format": "Erreur : %s",
"est_time_to_block": "Temps est. par bloc", "est_time_to_block": "Temps est. par bloc",
"exit": "Quitter", "exit": "Quitter",
"explorer": "EXPLORATEUR", "explorer": "Explorateur",
"explorer_block_detail": "Bloc",
"explorer_block_hash": "Hash",
"explorer_block_height": "Hauteur",
"explorer_block_merkle": "Racine Merkle",
"explorer_block_size": "Taille",
"explorer_block_time": "Heure",
"explorer_block_txs": "Transactions",
"explorer_chain_stats": "Chaîne",
"explorer_invalid_query": "Entrez une hauteur de bloc ou un hash de 64 caractères",
"explorer_mempool": "Mempool",
"explorer_mempool_size": "Taille",
"explorer_mempool_txs": "Transactions",
"explorer_recent_blocks": "Blocs récents",
"explorer_search": "Rechercher",
"explorer_section": "EXPLORATEUR",
"explorer_tx_outputs": "Sorties",
"explorer_tx_size": "Taille",
"export": "Exporter", "export": "Exporter",
"export_csv": "Exporter en CSV", "export_csv": "Exporter en CSV",
"export_keys_btn": "Exporter les clés", "export_keys_btn": "Exporter les clés",
@@ -224,14 +272,22 @@
"fetch_prices": "Récupérer les prix", "fetch_prices": "Récupérer les prix",
"file": "Fichier", "file": "Fichier",
"file_save_location": "Le fichier sera enregistré dans : ~/.config/ObsidianDragon/", "file_save_location": "Le fichier sera enregistré dans : ~/.config/ObsidianDragon/",
"filter": "Filtrer...",
"font_scale": "Taille de police", "font_scale": "Taille de police",
"force_quit": "Forcer la fermeture",
"force_quit_confirm_msg": "Cela tuera immédiatement le daemon sans arrêt propre.\nCela peut corrompre l'index de la blockchain et nécessiter une resynchronisation.",
"force_quit_confirm_title": "Forcer la fermeture ?",
"force_quit_warning": "Cela tuera immédiatement le daemon sans arrêt propre. Peut nécessiter une resynchronisation de la blockchain.",
"force_quit_yes": "Forcer la fermeture",
"from": "De", "from": "De",
"from_upper": "DE", "from_upper": "DE",
"full_details": "Tous les détails", "full_details": "Tous les détails",
"general": "Général", "general": "Général",
"generating": "Génération",
"go_to_receive": "Aller à Recevoir", "go_to_receive": "Aller à Recevoir",
"height": "Hauteur", "height": "Hauteur",
"help": "Aide", "help": "Aide",
"hidden_tag": " (masqué)",
"hide": "Masquer", "hide": "Masquer",
"hide_address": "Masquer l'adresse", "hide_address": "Masquer l'adresse",
"hide_zero_balances": "Masquer les soldes à 0", "hide_zero_balances": "Masquer les soldes à 0",
@@ -253,6 +309,9 @@
"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_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_key_z_format": "Clés de dépenses z-adresse (secret-extended-key-...)",
"import_private_key": "Importer une clé privée...", "import_private_key": "Importer une clé privée...",
"incorrect_passphrase": "Mot de passe incorrect",
"incorrect_pin": "PIN incorrect",
"insufficient_funds": "Fonds insuffisants pour ce montant plus les frais.",
"invalid_address": "Format d'adresse invalide", "invalid_address": "Format d'adresse invalide",
"ip_address": "Adresse IP", "ip_address": "Adresse IP",
"keep": "Conserver", "keep": "Conserver",
@@ -266,6 +325,7 @@
"key_export_viewing_keys_zonly": "Les clés de visualisation ne sont disponibles que pour les adresses blindées (z)", "key_export_viewing_keys_zonly": "Les clés de visualisation ne sont disponibles que pour les adresses blindées (z)",
"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.", "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é :", "label": "Libellé :",
"label_placeholder": "ex. Épargne, Minage...",
"language": "Langue", "language": "Langue",
"light": "Clair", "light": "Clair",
"loading": "Chargement...", "loading": "Chargement...",
@@ -286,6 +346,7 @@
"market_now": "Maintenant", "market_now": "Maintenant",
"market_pct_shielded": "%.0f%% Blindé", "market_pct_shielded": "%.0f%% Blindé",
"market_portfolio": "PORTEFEUILLE", "market_portfolio": "PORTEFEUILLE",
"market_price_loading": "Chargement des données de prix...",
"market_price_unavailable": "Données de prix indisponibles", "market_price_unavailable": "Données de prix indisponibles",
"market_refresh_price": "Actualiser les données de prix", "market_refresh_price": "Actualiser les données de prix",
"market_trade_on": "Échanger sur %s", "market_trade_on": "Échanger sur %s",
@@ -311,6 +372,13 @@
"mining_address_copied": "Adresse de minage copiée", "mining_address_copied": "Adresse de minage copiée",
"mining_all_time": "Tout le temps", "mining_all_time": "Tout le temps",
"mining_already_saved": "URL du pool déjà enregistrée", "mining_already_saved": "URL du pool déjà enregistrée",
"mining_benchmark_cancel": "Annuler le benchmark",
"mining_benchmark_cooling": "Refroidissement",
"mining_benchmark_dismiss": "Fermer",
"mining_benchmark_result": "Optimal",
"mining_benchmark_stabilizing": "Stabilisation",
"mining_benchmark_testing": "Test",
"mining_benchmark_tooltip": "Trouver le nombre optimal de threads pour ce CPU",
"mining_block_copied": "Hash du bloc copié", "mining_block_copied": "Hash du bloc copié",
"mining_chart_1m_ago": "il y a 1m", "mining_chart_1m_ago": "il y a 1m",
"mining_chart_5m_ago": "il y a 5m", "mining_chart_5m_ago": "il y a 5m",
@@ -330,8 +398,15 @@
"mining_filter_tip_all": "Afficher tous les gains", "mining_filter_tip_all": "Afficher tous les gains",
"mining_filter_tip_pool": "Afficher uniquement les gains du pool", "mining_filter_tip_pool": "Afficher uniquement les gains du pool",
"mining_filter_tip_solo": "Afficher uniquement les gains solo", "mining_filter_tip_solo": "Afficher uniquement les gains solo",
"mining_generate_z_address_hint": "Générez une adresse Z dans l'onglet Recevoir pour l'utiliser comme adresse de paiement",
"mining_idle_gpu_off_tooltip": "Sans restriction : ACTIVÉ\nSeule l'entrée clavier/souris détermine l'inactivité\nCliquez pour activer la détection GPU",
"mining_idle_gpu_on_tooltip": "GPU-conscient : ACTIVÉ\nL'activité GPU (vidéo, jeux) empêche le minage inactif\nCliquez pour le mode sans restriction",
"mining_idle_off_tooltip": "Activer le minage au repos", "mining_idle_off_tooltip": "Activer le minage au repos",
"mining_idle_on_tooltip": "Désactiver le minage au repos", "mining_idle_on_tooltip": "Désactiver le minage au repos",
"mining_idle_scale_off_tooltip": "Mode démarrage/arrêt : ACTIVÉ\nCliquez pour passer au mode mise à l'échelle des threads",
"mining_idle_scale_on_tooltip": "Mise à l'échelle des threads : ACTIVÉ\nCliquez pour passer au mode démarrage/arrêt",
"mining_idle_threads_active_tooltip": "Threads quand l'utilisateur est actif",
"mining_idle_threads_idle_tooltip": "Threads quand le système est inactif",
"mining_local_hashrate": "Hashrate local", "mining_local_hashrate": "Hashrate local",
"mining_mine": "Miner", "mining_mine": "Miner",
"mining_mining_addr": "Adr. minage", "mining_mining_addr": "Adr. minage",
@@ -388,6 +463,7 @@
"no_addresses_available": "Aucune adresse disponible", "no_addresses_available": "Aucune adresse disponible",
"no_addresses_match": "Aucune adresse ne correspond au filtre", "no_addresses_match": "Aucune adresse ne correspond au filtre",
"no_addresses_with_balance": "Aucune adresse avec solde", "no_addresses_with_balance": "Aucune adresse avec solde",
"no_addresses_yet": "Pas encore d'adresses",
"no_matching": "Aucune transaction correspondante", "no_matching": "Aucune transaction correspondante",
"no_recent_receives": "Aucune réception récente", "no_recent_receives": "Aucune réception récente",
"no_recent_sends": "Aucun envoi récent", "no_recent_sends": "Aucun envoi récent",
@@ -453,6 +529,7 @@
"peers_upper": "PAIRS", "peers_upper": "PAIRS",
"peers_version": "Version", "peers_version": "Version",
"pending": "En attente", "pending": "En attente",
"pin_not_set": "PIN non défini. Utilisez le mot de passe pour déverrouiller.",
"ping": "Ping", "ping": "Ping",
"price_chart": "Graphique des prix", "price_chart": "Graphique des prix",
"qr_code": "Code QR", "qr_code": "Code QR",
@@ -473,7 +550,9 @@
"recent_received": "REÇUS RÉCENTS", "recent_received": "REÇUS RÉCENTS",
"recent_sends": "ENVOIS RÉCENTS", "recent_sends": "ENVOIS RÉCENTS",
"recipient": "DESTINATAIRE", "recipient": "DESTINATAIRE",
"recipient_balance": "Destinataire : %.8f → %.8f DRGX",
"recv_type": "Reçu", "recv_type": "Reçu",
"reduce_motion": "Réduire les animations",
"refresh": "Actualiser", "refresh": "Actualiser",
"refresh_now": "Actualiser maintenant", "refresh_now": "Actualiser maintenant",
"remove_favorite": "Retirer des favoris", "remove_favorite": "Retirer des favoris",
@@ -493,7 +572,10 @@
"request_uri_copied": "URI de paiement copiée dans le presse-papiers", "request_uri_copied": "URI de paiement copiée dans le presse-papiers",
"rescan": "Re-scanner", "rescan": "Re-scanner",
"reset_to_defaults": "Réinitialiser les paramètres", "reset_to_defaults": "Réinitialiser les paramètres",
"restarting_after_encryption": "Redémarrage du daemon après chiffrement...",
"restore_address": "Restaurer l'adresse", "restore_address": "Restaurer l'adresse",
"result_preview": "Aperçu du résultat",
"retry": "Réessayer",
"review_send": "Vérifier l'envoi", "review_send": "Vérifier l'envoi",
"rpc_host": "Hôte RPC", "rpc_host": "Hôte RPC",
"rpc_pass": "Mot de passe", "rpc_pass": "Mot de passe",
@@ -502,6 +584,39 @@
"save": "Enregistrer", "save": "Enregistrer",
"save_settings": "Enregistrer les paramètres", "save_settings": "Enregistrer les paramètres",
"save_z_transactions": "Enregistrer les Z-tx dans la liste", "save_z_transactions": "Enregistrer les Z-tx dans la liste",
"sb_auth_failed": "Authentification échouée — vérifiez rpcuser/rpcpassword",
"sb_block": "Bloc : %d",
"sb_connecting_daemon": "Connexion à dragonxd...",
"sb_connecting_err": "Connexion au daemon — %s",
"sb_connecting_external": "Connexion au daemon externe...",
"sb_connecting_generic": "Connexion au daemon...",
"sb_daemon_crashed": "Le daemon a planté %d fois",
"sb_daemon_not_found": "Daemon introuvable",
"sb_dragonxd_running": "dragonxd en cours",
"sb_dragonxd_stopped": "dragonxd arrêté",
"sb_dragonxd_stopping": "Arrêt de dragonxd...",
"sb_extracting_sapling": "Extraction des paramètres Sapling...",
"sb_importing_keys": "Importation des clés",
"sb_loading_config": "Chargement de la configuration...",
"sb_mining_hs": "%.1f H/s",
"sb_net_ghs": "Rés: %.2f GH/s",
"sb_net_hs": "Rés: %.1f H/s",
"sb_net_khs": "Rés: %.2f KH/s",
"sb_net_mhs": "Rés: %.2f MH/s",
"sb_no_conf": "DRAGONX.conf introuvable",
"sb_peers": "Pairs : %zu",
"sb_rescanning": "Rescan",
"sb_rescanning_pct": "Rescan %.0f%%",
"sb_restarting_daemon": "Redémarrage du daemon...",
"sb_sapling_failed": "Échec de l'extraction des paramètres Sapling.",
"sb_sapling_not_found": "Paramètres Sapling introuvables.",
"sb_starting_daemon": "Démarrage de dragonxd...",
"sb_syncing_basic": "Synchronisation %.1f%% (%d restants)",
"sb_syncing_eta": "Synchronisation %.1f%% (%d restants, %.0f blk/s, ~%s)",
"sb_waiting_config": "En attente de la configuration du daemon...",
"sb_waiting_daemon": "En attente de dragonxd...",
"sb_waiting_daemon_err": "En attente de dragonxd — %s",
"sb_warming_up": "Démarrage...",
"search_placeholder": "Rechercher...", "search_placeholder": "Rechercher...",
"security": "SÉCURITÉ", "security": "SÉCURITÉ",
"select_address": "Sélectionner une adresse...", "select_address": "Sélectionner une adresse...",
@@ -553,12 +668,15 @@
"send_valid_transparent": "Adresse transparente valide", "send_valid_transparent": "Adresse transparente valide",
"send_wallet_empty": "Votre portefeuille est vide", "send_wallet_empty": "Votre portefeuille est vide",
"send_yes_clear": "Oui, effacer", "send_yes_clear": "Oui, effacer",
"sender_balance": "Expéditeur : %.8f → %.8f DRGX",
"sending": "Envoi de la transaction", "sending": "Envoi de la transaction",
"sending_from": "ENVOI DEPUIS", "sending_from": "ENVOI DEPUIS",
"sends_full_balance_warning": "Cela envoie le solde complet. L'adresse d'envoi aura un solde nul.",
"sent": "envoyé", "sent": "envoyé",
"sent_filter": "Envoyé", "sent_filter": "Envoyé",
"sent_type": "Envoyé", "sent_type": "Envoyé",
"sent_upper": "ENVOYÉ", "sent_upper": "ENVOYÉ",
"set_label": "Définir le libellé...",
"settings": "Paramètres", "settings": "Paramètres",
"settings_about_text": "Un portefeuille de cryptomonnaie blindé pour DragonX (DRGX), construit avec Dear ImGui pour une expérience légère et portable.", "settings_about_text": "Un portefeuille de cryptomonnaie blindé pour DragonX (DRGX), construit avec Dear ImGui pour une expérience légère et portable.",
"settings_acrylic_level": "Niveau acrylique :", "settings_acrylic_level": "Niveau acrylique :",
@@ -647,13 +765,17 @@
"shield_utxo_limit": "Limite UTXO :", "shield_utxo_limit": "Limite UTXO :",
"shield_wildcard_hint": "Utilisez '*' pour blinder depuis toutes les adresses transparentes", "shield_wildcard_hint": "Utilisez '*' pour blinder depuis toutes les adresses transparentes",
"shielded": "Blindé", "shielded": "Blindé",
"shielded_address": "Adresse protégée",
"shielded_to": "BLINDÉ VERS", "shielded_to": "BLINDÉ VERS",
"shielded_type": "Blindé", "shielded_type": "Blindé",
"shielding_notice": "Note : Cela blindera des fonds d'une adresse transparente (T) vers une adresse privée (Z).",
"show": "Afficher", "show": "Afficher",
"show_hidden": "Afficher masqués (%d)", "show_hidden": "Afficher masqués (%d)",
"show_qr_code": "Afficher le code QR", "show_qr_code": "Afficher le code QR",
"showing_transactions": "Affichage %d–%d sur %d transactions (total : %zu)", "showing_transactions": "Affichage %d%d sur %d transactions (total : %zu)",
"showing_x_of_y": "Affichage de %d sur %d adresses",
"simple_background": "Arrière-plan simple", "simple_background": "Arrière-plan simple",
"slider_off": "Désactivé",
"start_mining": "Démarrer le minage", "start_mining": "Démarrer le minage",
"status": "Statut", "status": "Statut",
"stop_external": "Arrêter le daemon externe", "stop_external": "Arrêter le daemon externe",
@@ -662,17 +784,26 @@
"success": "Succès", "success": "Succès",
"summary": "Résumé", "summary": "Résumé",
"syncing": "Synchronisation...", "syncing": "Synchronisation...",
"t_address": "Adresse T",
"t_addresses": "Adresses T", "t_addresses": "Adresses T",
"test_connection": "Tester", "test_connection": "Tester",
"theme": "Thème", "theme": "Thème",
"theme_effects": "Effets de thème", "theme_effects": "Effets de thème",
"theme_language": "THÈME & LANGUE",
"time_days_ago": "il y a %d jours", "time_days_ago": "il y a %d jours",
"time_hours_ago": "il y a %d heures", "time_hours_ago": "il y a %d heures",
"time_minutes_ago": "il y a %d minutes", "time_minutes_ago": "il y a %d minutes",
"time_seconds_ago": "il y a %d secondes", "time_seconds_ago": "il y a %d secondes",
"timeout_15min": "15 min",
"timeout_1hour": "1 heure",
"timeout_1min": "1 min",
"timeout_30min": "30 min",
"timeout_5min": "5 min",
"timeout_off": "Désactivé",
"to": "À", "to": "À",
"to_upper": "À", "to_upper": "À",
"tools": "OUTILS", "tools": "OUTILS",
"tools_actions": "Outils & Actions...",
"total": "Total", "total": "Total",
"transaction_id": "ID DE TRANSACTION", "transaction_id": "ID DE TRANSACTION",
"transaction_sent": "Transaction envoyée avec succès", "transaction_sent": "Transaction envoyée avec succès",
@@ -680,7 +811,13 @@
"transaction_url": "URL de transaction", "transaction_url": "URL de transaction",
"transactions": "Transactions", "transactions": "Transactions",
"transactions_upper": "TRANSACTIONS", "transactions_upper": "TRANSACTIONS",
"transfer_failed": "Échec du transfert",
"transfer_funds": "Transférer des fonds",
"transfer_sent": "Transfert envoyé",
"transfer_sent_desc": "Votre transfert a été soumis au réseau.",
"transfer_to": "Transférer à :",
"transparent": "Transparent", "transparent": "Transparent",
"transparent_address": "Adresse transparente",
"tt_addr_url": "URL de base pour consulter les adresses dans un explorateur de blocs", "tt_addr_url": "URL de base pour consulter les adresses dans un explorateur de blocs",
"tt_address_book": "Gérer les adresses enregistrées pour un envoi rapide", "tt_address_book": "Gérer les adresses enregistrées pour un envoi rapide",
"tt_auto_lock": "Verrouiller le portefeuille après cette durée d'inactivité", "tt_auto_lock": "Verrouiller le portefeuille après cette durée d'inactivité",
@@ -695,6 +832,8 @@
"tt_custom_theme": "Thème personnalisé actif", "tt_custom_theme": "Thème personnalisé actif",
"tt_debug_collapse": "Réduire les options de journalisation de débogage", "tt_debug_collapse": "Réduire les options de journalisation de débogage",
"tt_debug_expand": "Développer les options de journalisation de débogage", "tt_debug_expand": "Développer les options de journalisation de débogage",
"tt_delete_blockchain": "Supprimer toutes les données de la blockchain et démarrer une nouvelle synchronisation. wallet.dat et la configuration sont préservés.",
"tt_download_bootstrap": "Télécharger le bootstrap blockchain pour accélérer la synchronisation\nLes données de blocs existantes seront remplacées",
"tt_encrypt": "Chiffrer wallet.dat avec une phrase secrète", "tt_encrypt": "Chiffrer wallet.dat avec une phrase secrète",
"tt_export_all": "Exporter toutes les clés privées dans un fichier", "tt_export_all": "Exporter toutes les clés privées dans un fichier",
"tt_export_csv": "Exporter l'historique des transactions en feuille de calcul CSV", "tt_export_csv": "Exporter l'historique des transactions en feuille de calcul CSV",
@@ -712,6 +851,7 @@
"tt_mine_idle": "Démarrer le minage automatiquement quand le\\nsystème est inactif (aucune entrée clavier/souris)", "tt_mine_idle": "Démarrer le minage automatiquement quand le\\nsystème est inactif (aucune entrée clavier/souris)",
"tt_noise": "Intensité de texture grainée (0%% = désactivé, 100%% = maximum)", "tt_noise": "Intensité de texture grainée (0%% = désactivé, 100%% = maximum)",
"tt_open_dir": "Cliquer pour ouvrir dans l'explorateur de fichiers", "tt_open_dir": "Cliquer pour ouvrir dans l'explorateur de fichiers",
"tt_reduce_motion": "Désactiver les transitions animées et le lerp de solde pour l'accessibilité",
"tt_remove_encrypt": "Supprimer le chiffrement et stocker le portefeuille sans protection", "tt_remove_encrypt": "Supprimer le chiffrement et stocker le portefeuille sans protection",
"tt_remove_pin": "Supprimer le PIN et exiger la phrase secrète pour déverrouiller", "tt_remove_pin": "Supprimer le PIN et exiger la phrase secrète pour déverrouiller",
"tt_report_bug": "Signaler un problème dans le suivi de projet", "tt_report_bug": "Signaler un problème dans le suivi de projet",
@@ -789,7 +929,9 @@
"warning_upper": "ATTENTION !", "warning_upper": "ATTENTION !",
"website": "Site web", "website": "Site web",
"window_opacity": "Opacité de la fenêtre", "window_opacity": "Opacité de la fenêtre",
"wizard_daemon_start_failed": "Échec du démarrage du daemon — sera réessayé automatiquement",
"yes_clear": "Oui, effacer", "yes_clear": "Oui, effacer",
"your_addresses": "Vos adresses", "your_addresses": "Vos adresses",
"z_address": "Adresse Z",
"z_addresses": "Adresses Z" "z_addresses": "Adresses Z"
} }

View File

@@ -43,6 +43,8 @@
"address_url": "アドレスURL", "address_url": "アドレスURL",
"addresses_appear_here": "接続後、受信アドレスがここに表示されます。", "addresses_appear_here": "接続後、受信アドレスがここに表示されます。",
"advanced": "詳細設定", "advanced": "詳細設定",
"advanced_effects": "高度なエフェクト...",
"ago": "前",
"all_filter": "すべて", "all_filter": "すべて",
"allow_custom_fees": "カスタム手数料を許可", "allow_custom_fees": "カスタム手数料を許可",
"amount": "金額", "amount": "金額",
@@ -90,12 +92,30 @@
"block_timestamp": "タイムスタンプ:", "block_timestamp": "タイムスタンプ:",
"block_transactions": "トランザクション:", "block_transactions": "トランザクション:",
"blockchain_syncing": "ブロックチェーン同期中 (%.1f%%)... 残高が不正確な場合があります。", "blockchain_syncing": "ブロックチェーン同期中 (%.1f%%)... 残高が不正確な場合があります。",
"bootstrap_daemon_running": "デーモン実行中",
"bootstrap_daemon_stopped": "デーモン停止",
"bootstrap_daemon_stopping": "デーモン停止中...",
"bootstrap_desc": "ブロックチェーンブートストラップをダウンロードして初期同期を劇的に高速化します。ブロックチェーンのスナップショットをダウンロードしてデータディレクトリに展開します。",
"bootstrap_downloading": "ブートストラップをダウンロード中...",
"bootstrap_extracting": "ブロックチェーンデータを展開中...",
"bootstrap_failed": "ブートストラップ失敗",
"bootstrap_mirror": "ミラー",
"bootstrap_mirror_tooltip": "ミラーからダウンロード (bootstrap2.dragonx.is)。\nメインのダウンロードが遅い場合や失敗する場合に使用してください。",
"bootstrap_restart_daemon": "デーモンを再起動",
"bootstrap_success": "ブートストラップ完了",
"bootstrap_success_desc": "ブロックチェーンデータが正常に展開されました。デーモンを起動してブートストラップポイントから同期を開始してください。",
"bootstrap_trust_warning": "bootstrap.dragonx.is または bootstrap2.dragonx.is のみを使用してください。信頼できないソースのファイルを使用するとノードが危険にさらされる可能性があります。",
"bootstrap_verifying": "チェックサムを検証中...",
"bootstrap_wallet_protected": "(wallet.dat は保護されています)",
"bootstrap_warning": "既存のブロックデータblocks、chainstate、notarizationsは削除され置き換えられます。wallet.dat は変更・削除されません。",
"cancel": "キャンセル", "cancel": "キャンセル",
"characters": "文字", "characters": "文字",
"choose_icon": "アイコンを選択",
"clear": "クリア", "clear": "クリア",
"clear_all_bans": "すべてのブロックを解除", "clear_all_bans": "すべてのブロックを解除",
"clear_anyway": "それでもクリア", "clear_anyway": "それでもクリア",
"clear_form_confirm": "すべてのフォームフィールドをクリアしますか?", "clear_form_confirm": "すべてのフォームフィールドをクリアしますか?",
"clear_icon": "アイコンをクリア",
"clear_request": "リクエストをクリア", "clear_request": "リクエストをクリア",
"click_copy_address": "クリックしてアドレスをコピー", "click_copy_address": "クリックしてアドレスをコピー",
"click_copy_uri": "クリックしてURIをコピー", "click_copy_uri": "クリックしてURIをコピー",
@@ -106,8 +126,12 @@
"confirm_clear_ztx_title": "Z-Tx 履歴クリアの確認", "confirm_clear_ztx_title": "Z-Tx 履歴クリアの確認",
"confirm_clear_ztx_warning1": "z-トランザクション履歴をクリアすると、ウォレットの再スキャンが実行されるまでシールド残高が0と表示される場合があります。", "confirm_clear_ztx_warning1": "z-トランザクション履歴をクリアすると、ウォレットの再スキャンが実行されるまでシールド残高が0と表示される場合があります。",
"confirm_clear_ztx_warning2": "これが発生した場合、残高を回復するにはz-アドレスの秘密鍵を再スキャンを有効にして再インポートする必要があります。", "confirm_clear_ztx_warning2": "これが発生した場合、残高を回復するにはz-アドレスの秘密鍵を再スキャンを有効にして再インポートする必要があります。",
"confirm_delete_blockchain_msg": "デーモンを停止し、すべてのブロックチェーンデータblocks、chainstate、peersを削除して、最初から再同期を開始します。数時間かかる場合があります。",
"confirm_delete_blockchain_safe": "wallet.dat、設定、トランザクション履歴は安全で削除されません。",
"confirm_delete_blockchain_title": "ブロックチェーンデータを削除",
"confirm_send": "送金を確認", "confirm_send": "送金を確認",
"confirm_transaction": "取引を確認", "confirm_transaction": "取引を確認",
"confirm_transfer": "送金を確認",
"confirmations": "確認数", "confirmations": "確認数",
"confirmations_display": "%d 確認 | %s", "confirmations_display": "%d 確認 | %s",
"confirmed": "確認済み", "confirmed": "確認済み",
@@ -172,6 +196,7 @@
"console_welcome": "ObsidianDragonコンソールへようこそ", "console_welcome": "ObsidianDragonコンソールへようこそ",
"console_zoom_in": "拡大", "console_zoom_in": "拡大",
"console_zoom_out": "縮小", "console_zoom_out": "縮小",
"copied": "コピーしました!",
"copy": "コピー", "copy": "コピー",
"copy_address": "完全なアドレスをコピー", "copy_address": "完全なアドレスをコピー",
"copy_error": "エラーをコピー", "copy_error": "エラーをコピー",
@@ -180,15 +205,21 @@
"copy_uri": "URIをコピー", "copy_uri": "URIをコピー",
"current_price": "現在の価格", "current_price": "現在の価格",
"custom_fees": "カスタム手数料", "custom_fees": "カスタム手数料",
"daemon_version": "デーモン",
"dark": "ダーク", "dark": "ダーク",
"date": "日付", "date": "日付",
"date_label": "日付:", "date_label": "日付:",
"debug_logging": "デバッグログ", "debug_logging": "デバッグログ",
"delete": "削除", "delete": "削除",
"delete_blockchain": "ブロックチェーンを削除",
"delete_blockchain_confirm": "削除して再同期",
"deshielding_warning": "警告:プライベート (Z) アドレスからトランスペアレント (T) アドレスへ資金をデシールドします。",
"difficulty": "難易度", "difficulty": "難易度",
"disconnected": "切断済み", "disconnected": "切断済み",
"dismiss": "閉じる", "dismiss": "閉じる",
"display": "表示", "display": "表示",
"download": "ダウンロード",
"download_bootstrap": "ブートストラップをダウンロード",
"dragonx_green": "DragonXグリーン", "dragonx_green": "DragonXグリーン",
"edit": "編集", "edit": "編集",
"error": "エラー", "error": "エラー",
@@ -196,6 +227,23 @@
"est_time_to_block": "予測ブロック時間", "est_time_to_block": "予測ブロック時間",
"exit": "終了", "exit": "終了",
"explorer": "エクスプローラー", "explorer": "エクスプローラー",
"explorer_block_detail": "ブロック",
"explorer_block_hash": "ハッシュ",
"explorer_block_height": "高さ",
"explorer_block_merkle": "マークルルート",
"explorer_block_size": "サイズ",
"explorer_block_time": "時刻",
"explorer_block_txs": "トランザクション",
"explorer_chain_stats": "チェーン",
"explorer_invalid_query": "ブロック高さまたは64文字のハッシュを入力してください",
"explorer_mempool": "メモリプール",
"explorer_mempool_size": "サイズ",
"explorer_mempool_txs": "トランザクション",
"explorer_recent_blocks": "最近のブロック",
"explorer_search": "検索",
"explorer_section": "エクスプローラー",
"explorer_tx_outputs": "出力",
"explorer_tx_size": "サイズ",
"export": "エクスポート", "export": "エクスポート",
"export_csv": "CSVエクスポート", "export_csv": "CSVエクスポート",
"export_keys_btn": "鍵をエクスポート", "export_keys_btn": "鍵をエクスポート",
@@ -224,14 +272,22 @@
"fetch_prices": "価格を取得", "fetch_prices": "価格を取得",
"file": "ファイル", "file": "ファイル",
"file_save_location": "ファイルの保存先:~/.config/ObsidianDragon/", "file_save_location": "ファイルの保存先:~/.config/ObsidianDragon/",
"filter": "フィルター...",
"font_scale": "フォントサイズ", "font_scale": "フォントサイズ",
"force_quit": "強制終了",
"force_quit_confirm_msg": "クリーンシャットダウンなしでデーモンを即座に終了します。\nブロックチェーンインデックスが破損し、再同期が必要になる可能性があります。",
"force_quit_confirm_title": "強制終了しますか?",
"force_quit_warning": "クリーンシャットダウンなしでデーモンを即座に終了します。ブロックチェーンの再同期が必要になる場合があります。",
"force_quit_yes": "強制終了",
"from": "送信元", "from": "送信元",
"from_upper": "送信元", "from_upper": "送信元",
"full_details": "詳細情報", "full_details": "詳細情報",
"general": "一般", "general": "一般",
"generating": "生成中",
"go_to_receive": "受信へ移動", "go_to_receive": "受信へ移動",
"height": "高さ", "height": "高さ",
"help": "ヘルプ", "help": "ヘルプ",
"hidden_tag": " (非表示)",
"hide": "非表示", "hide": "非表示",
"hide_address": "アドレスを非表示", "hide_address": "アドレスを非表示",
"hide_zero_balances": "残高0を非表示", "hide_zero_balances": "残高0を非表示",
@@ -253,6 +309,9 @@
"import_key_warning": "警告:秘密鍵を決して共有しないでください!信頼できないソースからの鍵のインポートはウォレットを危険にさらす可能性があります。", "import_key_warning": "警告:秘密鍵を決して共有しないでください!信頼できないソースからの鍵のインポートはウォレットを危険にさらす可能性があります。",
"import_key_z_format": "Zアドレス支出鍵 (secret-extended-key-...)", "import_key_z_format": "Zアドレス支出鍵 (secret-extended-key-...)",
"import_private_key": "秘密鍵をインポート...", "import_private_key": "秘密鍵をインポート...",
"incorrect_passphrase": "パスフレーズが正しくありません",
"incorrect_pin": "PINが正しくありません",
"insufficient_funds": "この金額と手数料に対して残高が不足しています。",
"invalid_address": "無効なアドレス形式", "invalid_address": "無効なアドレス形式",
"ip_address": "IPアドレス", "ip_address": "IPアドレス",
"keep": "保持", "keep": "保持",
@@ -266,6 +325,7 @@
"key_export_viewing_keys_zonly": "ビューイングキーはシールド (z) アドレスでのみ利用可能です", "key_export_viewing_keys_zonly": "ビューイングキーはシールド (z) アドレスでのみ利用可能です",
"key_export_viewing_warning": "この閲覧鍵を使うと、他者があなたの受信取引と残高を見ることができますが、資金を使うことはできません。信頼できる相手とのみ共有してください。", "key_export_viewing_warning": "この閲覧鍵を使うと、他者があなたの受信取引と残高を見ることができますが、資金を使うことはできません。信頼できる相手とのみ共有してください。",
"label": "ラベル:", "label": "ラベル:",
"label_placeholder": "例: 貯金、マイニング...",
"language": "言語", "language": "言語",
"light": "ライト", "light": "ライト",
"loading": "読み込み中...", "loading": "読み込み中...",
@@ -286,6 +346,7 @@
"market_now": "現在", "market_now": "現在",
"market_pct_shielded": "%.0f%% シールド済み", "market_pct_shielded": "%.0f%% シールド済み",
"market_portfolio": "ポートフォリオ", "market_portfolio": "ポートフォリオ",
"market_price_loading": "価格データを読み込み中...",
"market_price_unavailable": "価格データが利用できません", "market_price_unavailable": "価格データが利用できません",
"market_refresh_price": "価格データを更新", "market_refresh_price": "価格データを更新",
"market_trade_on": "%s で取引", "market_trade_on": "%s で取引",
@@ -311,6 +372,13 @@
"mining_address_copied": "マイニングアドレスをコピーしました", "mining_address_copied": "マイニングアドレスをコピーしました",
"mining_all_time": "全期間", "mining_all_time": "全期間",
"mining_already_saved": "プールURLは既に保存済みです", "mining_already_saved": "プールURLは既に保存済みです",
"mining_benchmark_cancel": "ベンチマークをキャンセル",
"mining_benchmark_cooling": "クーリング",
"mining_benchmark_dismiss": "閉じる",
"mining_benchmark_result": "最適",
"mining_benchmark_stabilizing": "安定化中",
"mining_benchmark_testing": "テスト中",
"mining_benchmark_tooltip": "このCPUに最適なスレッド数を検出",
"mining_block_copied": "ブロックハッシュをコピーしました", "mining_block_copied": "ブロックハッシュをコピーしました",
"mining_chart_1m_ago": "1分前", "mining_chart_1m_ago": "1分前",
"mining_chart_5m_ago": "5分前", "mining_chart_5m_ago": "5分前",
@@ -330,8 +398,15 @@
"mining_filter_tip_all": "すべての収益を表示", "mining_filter_tip_all": "すべての収益を表示",
"mining_filter_tip_pool": "プール収益のみ表示", "mining_filter_tip_pool": "プール収益のみ表示",
"mining_filter_tip_solo": "ソロ収益のみ表示", "mining_filter_tip_solo": "ソロ収益のみ表示",
"mining_generate_z_address_hint": "受信タブでZアドレスを生成して支払いアドレスとして使用してください",
"mining_idle_gpu_off_tooltip": "制限なし: オン\nキーボード/マウス入力のみがアイドル状態を決定\nGPU検出を有効にするにはクリック",
"mining_idle_gpu_on_tooltip": "GPU対応: オン\nGPUアクティビティ動画、ゲームがアイドルマイニングを防止\n制限なしモードに切り替えるにはクリック",
"mining_idle_off_tooltip": "アイドルマイニングを有効にする", "mining_idle_off_tooltip": "アイドルマイニングを有効にする",
"mining_idle_on_tooltip": "アイドルマイニングを無効にする", "mining_idle_on_tooltip": "アイドルマイニングを無効にする",
"mining_idle_scale_off_tooltip": "開始/停止モード: オン\nスレッドスケーリングモードに切り替えるにはクリック",
"mining_idle_scale_on_tooltip": "スレッドスケーリング: オン\n開始/停止モードに切り替えるにはクリック",
"mining_idle_threads_active_tooltip": "ユーザーアクティブ時のスレッド数",
"mining_idle_threads_idle_tooltip": "システムアイドル時のスレッド数",
"mining_local_hashrate": "ローカルハッシュレート", "mining_local_hashrate": "ローカルハッシュレート",
"mining_mine": "マイニング", "mining_mine": "マイニング",
"mining_mining_addr": "マイニングアドレス", "mining_mining_addr": "マイニングアドレス",
@@ -388,6 +463,7 @@
"no_addresses_available": "利用可能なアドレスがありません", "no_addresses_available": "利用可能なアドレスがありません",
"no_addresses_match": "フィルタに一致するアドレスがありません", "no_addresses_match": "フィルタに一致するアドレスがありません",
"no_addresses_with_balance": "残高のあるアドレスがありません", "no_addresses_with_balance": "残高のあるアドレスがありません",
"no_addresses_yet": "アドレスがまだありません",
"no_matching": "一致する取引がありません", "no_matching": "一致する取引がありません",
"no_recent_receives": "最近の受信がありません", "no_recent_receives": "最近の受信がありません",
"no_recent_sends": "最近の送信がありません", "no_recent_sends": "最近の送信がありません",
@@ -453,6 +529,7 @@
"peers_upper": "ピア", "peers_upper": "ピア",
"peers_version": "バージョン", "peers_version": "バージョン",
"pending": "保留中", "pending": "保留中",
"pin_not_set": "PINが設定されていません。パスフレーズで解除してください。",
"ping": "Ping", "ping": "Ping",
"price_chart": "価格チャート", "price_chart": "価格チャート",
"qr_code": "QRコード", "qr_code": "QRコード",
@@ -473,7 +550,9 @@
"recent_received": "最近の受信", "recent_received": "最近の受信",
"recent_sends": "最近の送信", "recent_sends": "最近の送信",
"recipient": "受取人", "recipient": "受取人",
"recipient_balance": "受取人: %.8f → %.8f DRGX",
"recv_type": "受信", "recv_type": "受信",
"reduce_motion": "モーションを減らす",
"refresh": "更新", "refresh": "更新",
"refresh_now": "今すぐ更新", "refresh_now": "今すぐ更新",
"remove_favorite": "お気に入りを削除", "remove_favorite": "お気に入りを削除",
@@ -493,7 +572,10 @@
"request_uri_copied": "支払いURIをクリップボードにコピーしました", "request_uri_copied": "支払いURIをクリップボードにコピーしました",
"rescan": "再スキャン", "rescan": "再スキャン",
"reset_to_defaults": "デフォルトにリセット", "reset_to_defaults": "デフォルトにリセット",
"restarting_after_encryption": "暗号化後にデーモンを再起動中...",
"restore_address": "アドレスを復元", "restore_address": "アドレスを復元",
"result_preview": "結果プレビュー",
"retry": "再試行",
"review_send": "送金を確認", "review_send": "送金を確認",
"rpc_host": "RPCホスト", "rpc_host": "RPCホスト",
"rpc_pass": "パスワード", "rpc_pass": "パスワード",
@@ -502,6 +584,39 @@
"save": "保存", "save": "保存",
"save_settings": "設定を保存", "save_settings": "設定を保存",
"save_z_transactions": "Z取引を取引リストに保存", "save_z_transactions": "Z取引を取引リストに保存",
"sb_auth_failed": "認証失敗 — rpcuser/rpcpassword を確認してください",
"sb_block": "ブロック: %d",
"sb_connecting_daemon": "dragonxd に接続中...",
"sb_connecting_err": "デーモンに接続中 — %s",
"sb_connecting_external": "外部デーモンに接続中...",
"sb_connecting_generic": "デーモンに接続中...",
"sb_daemon_crashed": "デーモンが %d 回クラッシュしました",
"sb_daemon_not_found": "デーモンが見つかりません",
"sb_dragonxd_running": "dragonxd 実行中",
"sb_dragonxd_stopped": "dragonxd 停止",
"sb_dragonxd_stopping": "dragonxd を停止中...",
"sb_extracting_sapling": "Sapling パラメータを展開中...",
"sb_importing_keys": "鍵をインポート中",
"sb_loading_config": "設定を読み込み中...",
"sb_mining_hs": "%.1f H/s",
"sb_net_ghs": "ネット: %.2f GH/s",
"sb_net_hs": "ネット: %.1f H/s",
"sb_net_khs": "ネット: %.2f KH/s",
"sb_net_mhs": "ネット: %.2f MH/s",
"sb_no_conf": "DRAGONX.conf が見つかりません",
"sb_peers": "ピア: %zu",
"sb_rescanning": "再スキャン",
"sb_rescanning_pct": "再スキャン %.0f%%",
"sb_restarting_daemon": "デーモンを再起動中...",
"sb_sapling_failed": "Sapling パラメータの展開に失敗しました。",
"sb_sapling_not_found": "Sapling パラメータが見つかりません。",
"sb_starting_daemon": "dragonxd を起動中...",
"sb_syncing_basic": "同期中 %.1f%% (残り %d)",
"sb_syncing_eta": "同期中 %.1f%% (残り %d, %.0f ブロック/秒, ~%s)",
"sb_waiting_config": "デーモン設定を待機中...",
"sb_waiting_daemon": "dragonxd を待機中...",
"sb_waiting_daemon_err": "dragonxd を待機中 — %s",
"sb_warming_up": "ウォームアップ中...",
"search_placeholder": "検索...", "search_placeholder": "検索...",
"security": "セキュリティ", "security": "セキュリティ",
"select_address": "アドレスを選択...", "select_address": "アドレスを選択...",
@@ -553,12 +668,15 @@
"send_valid_transparent": "有効な透明アドレス", "send_valid_transparent": "有効な透明アドレス",
"send_wallet_empty": "ウォレットは空です", "send_wallet_empty": "ウォレットは空です",
"send_yes_clear": "はい、クリア", "send_yes_clear": "はい、クリア",
"sender_balance": "送信者: %.8f → %.8f DRGX",
"sending": "取引を送信中", "sending": "取引を送信中",
"sending_from": "送信元", "sending_from": "送信元",
"sends_full_balance_warning": "全残高を送信します。送信アドレスの残高はゼロになります。",
"sent": "送信済み", "sent": "送信済み",
"sent_filter": "送信済み", "sent_filter": "送信済み",
"sent_type": "送信済み", "sent_type": "送信済み",
"sent_upper": "送信済み", "sent_upper": "送信済み",
"set_label": "ラベルを設定...",
"settings": "設定", "settings": "設定",
"settings_about_text": "DragonX (DRGX) 用のシールド暗号通貨ウォレット。Dear ImGui で構築された軽量でポータブルな体験。", "settings_about_text": "DragonX (DRGX) 用のシールド暗号通貨ウォレット。Dear ImGui で構築された軽量でポータブルな体験。",
"settings_acrylic_level": "アクリルレベル:", "settings_acrylic_level": "アクリルレベル:",
@@ -647,13 +765,17 @@
"shield_utxo_limit": "UTXO制限", "shield_utxo_limit": "UTXO制限",
"shield_wildcard_hint": "'*' を使用してすべての透明アドレスからシールド", "shield_wildcard_hint": "'*' を使用してすべての透明アドレスからシールド",
"shielded": "シールド", "shielded": "シールド",
"shielded_address": "シールドアドレス",
"shielded_to": "シールド先", "shielded_to": "シールド先",
"shielded_type": "シールド", "shielded_type": "シールド",
"shielding_notice": "注意:トランスペアレント (T) アドレスからプライベート (Z) アドレスへ資金をシールドします。",
"show": "表示", "show": "表示",
"show_hidden": "非表示を表示 (%d)", "show_hidden": "非表示を表示 (%d)",
"show_qr_code": "QRコードを表示", "show_qr_code": "QRコードを表示",
"showing_transactions": "%d–%d / %d 件の取引を表示中(合計:%zu", "showing_transactions": "%d%d / %d 件の取引を表示中(合計:%zu",
"showing_x_of_y": "%d / %d アドレスを表示",
"simple_background": "シンプル背景", "simple_background": "シンプル背景",
"slider_off": "オフ",
"start_mining": "マイニング開始", "start_mining": "マイニング開始",
"status": "ステータス", "status": "ステータス",
"stop_external": "外部デーモンを停止", "stop_external": "外部デーモンを停止",
@@ -662,17 +784,26 @@
"success": "成功", "success": "成功",
"summary": "概要", "summary": "概要",
"syncing": "同期中...", "syncing": "同期中...",
"t_address": "Tアドレス",
"t_addresses": "Tアドレス", "t_addresses": "Tアドレス",
"test_connection": "テスト", "test_connection": "テスト",
"theme": "テーマ", "theme": "テーマ",
"theme_effects": "テーマ効果", "theme_effects": "テーマ効果",
"theme_language": "テーマと言語",
"time_days_ago": "%d日前", "time_days_ago": "%d日前",
"time_hours_ago": "%d時間前", "time_hours_ago": "%d時間前",
"time_minutes_ago": "%d分前", "time_minutes_ago": "%d分前",
"time_seconds_ago": "%d秒前", "time_seconds_ago": "%d秒前",
"timeout_15min": "15分",
"timeout_1hour": "1時間",
"timeout_1min": "1分",
"timeout_30min": "30分",
"timeout_5min": "5分",
"timeout_off": "オフ",
"to": "宛先", "to": "宛先",
"to_upper": "宛先", "to_upper": "宛先",
"tools": "ツール", "tools": "ツール",
"tools_actions": "ツールとアクション...",
"total": "合計", "total": "合計",
"transaction_id": "取引ID", "transaction_id": "取引ID",
"transaction_sent": "取引の送信に成功しました", "transaction_sent": "取引の送信に成功しました",
@@ -680,7 +811,13 @@
"transaction_url": "取引URL", "transaction_url": "取引URL",
"transactions": "取引", "transactions": "取引",
"transactions_upper": "取引", "transactions_upper": "取引",
"transfer_failed": "送金失敗",
"transfer_funds": "資金を送金",
"transfer_sent": "送金完了",
"transfer_sent_desc": "送金がネットワークに送信されました。",
"transfer_to": "送金先:",
"transparent": "透明", "transparent": "透明",
"transparent_address": "トランスペアレントアドレス",
"tt_addr_url": "ブロックエクスプローラーでアドレスを表示するためのベース URL", "tt_addr_url": "ブロックエクスプローラーでアドレスを表示するためのベース URL",
"tt_address_book": "クイック送信用の保存済みアドレスを管理", "tt_address_book": "クイック送信用の保存済みアドレスを管理",
"tt_auto_lock": "この無操作時間後にウォレットをロック", "tt_auto_lock": "この無操作時間後にウォレットをロック",
@@ -695,6 +832,8 @@
"tt_custom_theme": "カスタムテーマがアクティブ", "tt_custom_theme": "カスタムテーマがアクティブ",
"tt_debug_collapse": "デバッグログオプションを折りたたむ", "tt_debug_collapse": "デバッグログオプションを折りたたむ",
"tt_debug_expand": "デバッグログオプションを展開", "tt_debug_expand": "デバッグログオプションを展開",
"tt_delete_blockchain": "すべてのブロックチェーンデータを削除して新規同期を開始します。wallet.dat と設定は保持されます。",
"tt_download_bootstrap": "ブロックチェーンブートストラップをダウンロードして同期を高速化\n既存のブロックデータは置き換えられます",
"tt_encrypt": "パスフレーズで wallet.dat を暗号化", "tt_encrypt": "パスフレーズで wallet.dat を暗号化",
"tt_export_all": "すべての秘密鍵をファイルにエクスポート", "tt_export_all": "すべての秘密鍵をファイルにエクスポート",
"tt_export_csv": "トランザクション履歴を CSV スプレッドシートとしてエクスポート", "tt_export_csv": "トランザクション履歴を CSV スプレッドシートとしてエクスポート",
@@ -712,6 +851,7 @@
"tt_mine_idle": "システムがアイドル状態(キーボード/マウス入力なし)\\nのとき自動的にマイニングを開始", "tt_mine_idle": "システムがアイドル状態(キーボード/マウス入力なし)\\nのとき自動的にマイニングを開始",
"tt_noise": "グレインテクスチャ強度0%% = オフ、100%% = 最大)", "tt_noise": "グレインテクスチャ強度0%% = オフ、100%% = 最大)",
"tt_open_dir": "クリックしてファイルエクスプローラーで開く", "tt_open_dir": "クリックしてファイルエクスプローラーで開く",
"tt_reduce_motion": "アクセシビリティのためにアニメーション遷移と残高補間を無効にする",
"tt_remove_encrypt": "暗号化を解除してウォレットを保護なしで保存", "tt_remove_encrypt": "暗号化を解除してウォレットを保護なしで保存",
"tt_remove_pin": "PIN を削除しアンロックにパスフレーズを要求", "tt_remove_pin": "PIN を削除しアンロックにパスフレーズを要求",
"tt_report_bug": "プロジェクトトラッカーで問題を報告", "tt_report_bug": "プロジェクトトラッカーで問題を報告",
@@ -789,7 +929,9 @@
"warning_upper": "警告!", "warning_upper": "警告!",
"website": "ウェブサイト", "website": "ウェブサイト",
"window_opacity": "ウィンドウ透明度", "window_opacity": "ウィンドウ透明度",
"wizard_daemon_start_failed": "デーモンの起動に失敗しました — 自動的に再試行されます",
"yes_clear": "はい、クリア", "yes_clear": "はい、クリア",
"your_addresses": "あなたのアドレス", "your_addresses": "あなたのアドレス",
"z_address": "Zアドレス",
"z_addresses": "Zアドレス" "z_addresses": "Zアドレス"
} }

View File

@@ -43,6 +43,8 @@
"address_url": "주소 URL", "address_url": "주소 URL",
"addresses_appear_here": "연결 후 수신 주소가 여기에 표시됩니다.", "addresses_appear_here": "연결 후 수신 주소가 여기에 표시됩니다.",
"advanced": "고급 설정", "advanced": "고급 설정",
"advanced_effects": "고급 효과...",
"ago": "전",
"all_filter": "전체", "all_filter": "전체",
"allow_custom_fees": "사용자 정의 수수료 허용", "allow_custom_fees": "사용자 정의 수수료 허용",
"amount": "금액", "amount": "금액",
@@ -90,12 +92,30 @@
"block_timestamp": "타임스탬프:", "block_timestamp": "타임스탬프:",
"block_transactions": "트랜잭션:", "block_transactions": "트랜잭션:",
"blockchain_syncing": "블록체인 동기화 중 (%.1f%%)... 잔액이 정확하지 않을 수 있습니다.", "blockchain_syncing": "블록체인 동기화 중 (%.1f%%)... 잔액이 정확하지 않을 수 있습니다.",
"bootstrap_daemon_running": "데몬 실행 중",
"bootstrap_daemon_stopped": "데몬 중지됨",
"bootstrap_daemon_stopping": "데몬 중지 중...",
"bootstrap_desc": "블록체인 부트스트랩을 다운로드하여 초기 동기화를 대폭 가속합니다. 블록체인 스냅샷을 다운로드하고 데이터 디렉토리에 추출합니다.",
"bootstrap_downloading": "부트스트랩 다운로드 중...",
"bootstrap_extracting": "블록체인 데이터 추출 중...",
"bootstrap_failed": "부트스트랩 실패",
"bootstrap_mirror": "미러",
"bootstrap_mirror_tooltip": "미러에서 다운로드 (bootstrap2.dragonx.is).\n메인 다운로드가 느리거나 실패할 경우 사용하세요.",
"bootstrap_restart_daemon": "데몬 재시작",
"bootstrap_success": "부트스트랩 완료",
"bootstrap_success_desc": "블록체인 데이터가 성공적으로 추출되었습니다. 데몬을 시작하여 부트스트랩 지점부터 동기화를 시작하세요.",
"bootstrap_trust_warning": "bootstrap.dragonx.is 또는 bootstrap2.dragonx.is만 사용하세요. 신뢰할 수 없는 출처의 파일을 사용하면 노드가 손상될 수 있습니다.",
"bootstrap_verifying": "체크섬 확인 중...",
"bootstrap_wallet_protected": "(wallet.dat 보호됨)",
"bootstrap_warning": "기존 블록 데이터(blocks, chainstate, notarizations)가 삭제되고 교체됩니다. wallet.dat는 수정되거나 삭제되지 않습니다.",
"cancel": "취소", "cancel": "취소",
"characters": "문자", "characters": "문자",
"choose_icon": "아이콘 선택",
"clear": "지우기", "clear": "지우기",
"clear_all_bans": "모든 차단 해제", "clear_all_bans": "모든 차단 해제",
"clear_anyway": "그래도 삭제", "clear_anyway": "그래도 삭제",
"clear_form_confirm": "모든 양식 필드를 지우시겠습니까?", "clear_form_confirm": "모든 양식 필드를 지우시겠습니까?",
"clear_icon": "아이콘 지우기",
"clear_request": "요청 지우기", "clear_request": "요청 지우기",
"click_copy_address": "클릭하여 주소 복사", "click_copy_address": "클릭하여 주소 복사",
"click_copy_uri": "클릭하여 URI 복사", "click_copy_uri": "클릭하여 URI 복사",
@@ -106,8 +126,12 @@
"confirm_clear_ztx_title": "Z-Tx 기록 삭제 확인", "confirm_clear_ztx_title": "Z-Tx 기록 삭제 확인",
"confirm_clear_ztx_warning1": "z-트랜잭션 기록을 삭제하면 지갑 재스캔이 수행될 때까지 차폐 잔액이 0으로 표시될 수 있습니다.", "confirm_clear_ztx_warning1": "z-트랜잭션 기록을 삭제하면 지갑 재스캔이 수행될 때까지 차폐 잔액이 0으로 표시될 수 있습니다.",
"confirm_clear_ztx_warning2": "이런 경우, 잔액을 복구하려면 재스캔을 활성화하여 z-주소 개인키를 다시 가져와야 합니다.", "confirm_clear_ztx_warning2": "이런 경우, 잔액을 복구하려면 재스캔을 활성화하여 z-주소 개인키를 다시 가져와야 합니다.",
"confirm_delete_blockchain_msg": "데몬을 중지하고 모든 블록체인 데이터(blocks, chainstate, peers)를 삭제한 후 처음부터 다시 동기화합니다. 몇 시간이 걸릴 수 있습니다.",
"confirm_delete_blockchain_safe": "wallet.dat, 설정 및 거래 내역은 안전하며 삭제되지 않습니다.",
"confirm_delete_blockchain_title": "블록체인 데이터 삭제",
"confirm_send": "전송 확인", "confirm_send": "전송 확인",
"confirm_transaction": "거래 확인", "confirm_transaction": "거래 확인",
"confirm_transfer": "이체 확인",
"confirmations": "확인 수", "confirmations": "확인 수",
"confirmations_display": "%d 확인 | %s", "confirmations_display": "%d 확인 | %s",
"confirmed": "확인됨", "confirmed": "확인됨",
@@ -172,6 +196,7 @@
"console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다", "console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다",
"console_zoom_in": "확대", "console_zoom_in": "확대",
"console_zoom_out": "축소", "console_zoom_out": "축소",
"copied": "복사됨!",
"copy": "복사", "copy": "복사",
"copy_address": "전체 주소 복사", "copy_address": "전체 주소 복사",
"copy_error": "오류 복사", "copy_error": "오류 복사",
@@ -180,15 +205,21 @@
"copy_uri": "URI 복사", "copy_uri": "URI 복사",
"current_price": "현재 가격", "current_price": "현재 가격",
"custom_fees": "사용자 정의 수수료", "custom_fees": "사용자 정의 수수료",
"daemon_version": "데몬",
"dark": "다크", "dark": "다크",
"date": "날짜", "date": "날짜",
"date_label": "날짜:", "date_label": "날짜:",
"debug_logging": "디버그 로깅", "debug_logging": "디버그 로깅",
"delete": "삭제", "delete": "삭제",
"delete_blockchain": "블록체인 삭제",
"delete_blockchain_confirm": "삭제 후 재동기화",
"deshielding_warning": "경고: 프라이빗 (Z) 주소에서 투명 (T) 주소로 자금을 디실딩합니다.",
"difficulty": "난이도", "difficulty": "난이도",
"disconnected": "연결 끊김", "disconnected": "연결 끊김",
"dismiss": "닫기", "dismiss": "닫기",
"display": "디스플레이", "display": "디스플레이",
"download": "다운로드",
"download_bootstrap": "부트스트랩 다운로드",
"dragonx_green": "DragonX(그린)", "dragonx_green": "DragonX(그린)",
"edit": "편집", "edit": "편집",
"error": "오류", "error": "오류",
@@ -196,6 +227,23 @@
"est_time_to_block": "예상 블록 시간", "est_time_to_block": "예상 블록 시간",
"exit": "종료", "exit": "종료",
"explorer": "탐색기", "explorer": "탐색기",
"explorer_block_detail": "블록",
"explorer_block_hash": "해시",
"explorer_block_height": "높이",
"explorer_block_merkle": "머클 루트",
"explorer_block_size": "크기",
"explorer_block_time": "시간",
"explorer_block_txs": "트랜잭션",
"explorer_chain_stats": "체인",
"explorer_invalid_query": "블록 높이 또는 64자 해시를 입력하세요",
"explorer_mempool": "멤풀",
"explorer_mempool_size": "크기",
"explorer_mempool_txs": "트랜잭션",
"explorer_recent_blocks": "최근 블록",
"explorer_search": "검색",
"explorer_section": "탐색기",
"explorer_tx_outputs": "출력",
"explorer_tx_size": "크기",
"export": "내보내기", "export": "내보내기",
"export_csv": "CSV 내보내기", "export_csv": "CSV 내보내기",
"export_keys_btn": "키 내보내기", "export_keys_btn": "키 내보내기",
@@ -224,14 +272,22 @@
"fetch_prices": "가격 조회", "fetch_prices": "가격 조회",
"file": "파일", "file": "파일",
"file_save_location": "파일 저장 위치: ~/.config/ObsidianDragon/", "file_save_location": "파일 저장 위치: ~/.config/ObsidianDragon/",
"filter": "필터...",
"font_scale": "글꼴 크기", "font_scale": "글꼴 크기",
"force_quit": "강제 종료",
"force_quit_confirm_msg": "정상 종료 없이 데몬을 즉시 종료합니다.\n블록체인 인덱스가 손상되어 재동기화가 필요할 수 있습니다.",
"force_quit_confirm_title": "강제 종료하시겠습니까?",
"force_quit_warning": "정상 종료 없이 데몬을 즉시 종료합니다. 블록체인 재동기화가 필요할 수 있습니다.",
"force_quit_yes": "강제 종료",
"from": "보낸 곳", "from": "보낸 곳",
"from_upper": "보낸 곳", "from_upper": "보낸 곳",
"full_details": "전체 세부 정보", "full_details": "전체 세부 정보",
"general": "일반", "general": "일반",
"generating": "생성 중",
"go_to_receive": "수신으로 이동", "go_to_receive": "수신으로 이동",
"height": "높이", "height": "높이",
"help": "도움말", "help": "도움말",
"hidden_tag": " (숨김)",
"hide": "숨기기", "hide": "숨기기",
"hide_address": "주소 숨기기", "hide_address": "주소 숨기기",
"hide_zero_balances": "잔액 0 숨기기", "hide_zero_balances": "잔액 0 숨기기",
@@ -253,6 +309,9 @@
"import_key_warning": "경고: 개인 키를 절대 공유하지 마세요! 신뢰할 수 없는 소스의 키를 가져오면 지갑이 위험해질 수 있습니다.", "import_key_warning": "경고: 개인 키를 절대 공유하지 마세요! 신뢰할 수 없는 소스의 키를 가져오면 지갑이 위험해질 수 있습니다.",
"import_key_z_format": "Z 주소 지출 키 (secret-extended-key-...)", "import_key_z_format": "Z 주소 지출 키 (secret-extended-key-...)",
"import_private_key": "개인 키 가져오기...", "import_private_key": "개인 키 가져오기...",
"incorrect_passphrase": "잘못된 암호",
"incorrect_pin": "잘못된 PIN",
"insufficient_funds": "이 금액과 수수료를 위한 잔액이 부족합니다.",
"invalid_address": "잘못된 주소 형식", "invalid_address": "잘못된 주소 형식",
"ip_address": "IP 주소", "ip_address": "IP 주소",
"keep": "유지", "keep": "유지",
@@ -266,6 +325,7 @@
"key_export_viewing_keys_zonly": "보기 키는 차폐 (z) 주소에만 사용할 수 있습니다", "key_export_viewing_keys_zonly": "보기 키는 차폐 (z) 주소에만 사용할 수 있습니다",
"key_export_viewing_warning": "이 조회 키를 사용하면 다른 사람이 수신 거래와 잔액을 볼 수 있지만 자금을 사용할 수는 없습니다. 신뢰할 수 있는 사람에게만 공유하세요.", "key_export_viewing_warning": "이 조회 키를 사용하면 다른 사람이 수신 거래와 잔액을 볼 수 있지만 자금을 사용할 수는 없습니다. 신뢰할 수 있는 사람에게만 공유하세요.",
"label": "라벨:", "label": "라벨:",
"label_placeholder": "예: 저축, 채굴...",
"language": "언어", "language": "언어",
"light": "라이트", "light": "라이트",
"loading": "로딩 중...", "loading": "로딩 중...",
@@ -286,6 +346,7 @@
"market_now": "현재", "market_now": "현재",
"market_pct_shielded": "%.0f%% 차폐됨", "market_pct_shielded": "%.0f%% 차폐됨",
"market_portfolio": "포트폴리오", "market_portfolio": "포트폴리오",
"market_price_loading": "가격 데이터를 불러오는 중...",
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다", "market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
"market_refresh_price": "가격 데이터 새로고침", "market_refresh_price": "가격 데이터 새로고침",
"market_trade_on": "%s에서 거래", "market_trade_on": "%s에서 거래",
@@ -311,6 +372,13 @@
"mining_address_copied": "채굴 주소가 복사되었습니다", "mining_address_copied": "채굴 주소가 복사되었습니다",
"mining_all_time": "전체 기간", "mining_all_time": "전체 기간",
"mining_already_saved": "풀 URL이 이미 저장되어 있습니다", "mining_already_saved": "풀 URL이 이미 저장되어 있습니다",
"mining_benchmark_cancel": "벤치마크 취소",
"mining_benchmark_cooling": "쿨링",
"mining_benchmark_dismiss": "닫기",
"mining_benchmark_result": "최적",
"mining_benchmark_stabilizing": "안정화 중",
"mining_benchmark_testing": "테스트 중",
"mining_benchmark_tooltip": "이 CPU에 최적의 스레드 수 찾기",
"mining_block_copied": "블록 해시가 복사되었습니다", "mining_block_copied": "블록 해시가 복사되었습니다",
"mining_chart_1m_ago": "1분 전", "mining_chart_1m_ago": "1분 전",
"mining_chart_5m_ago": "5분 전", "mining_chart_5m_ago": "5분 전",
@@ -330,8 +398,15 @@
"mining_filter_tip_all": "모든 수익 표시", "mining_filter_tip_all": "모든 수익 표시",
"mining_filter_tip_pool": "풀 수익만 표시", "mining_filter_tip_pool": "풀 수익만 표시",
"mining_filter_tip_solo": "솔로 수익만 표시", "mining_filter_tip_solo": "솔로 수익만 표시",
"mining_generate_z_address_hint": "수신 탭에서 Z 주소를 생성하여 지급 주소로 사용하세요",
"mining_idle_gpu_off_tooltip": "무제한: 켜짐\n키보드/마우스 입력만 유휴 상태를 결정\nGPU 감지를 활성화하려면 클릭",
"mining_idle_gpu_on_tooltip": "GPU 감지: 켜짐\nGPU 활동(비디오, 게임)이 유휴 채굴을 방지\n무제한 모드로 전환하려면 클릭",
"mining_idle_off_tooltip": "유휴 채굴 활성화", "mining_idle_off_tooltip": "유휴 채굴 활성화",
"mining_idle_on_tooltip": "유휴 채굴 비활성화", "mining_idle_on_tooltip": "유휴 채굴 비활성화",
"mining_idle_scale_off_tooltip": "시작/중지 모드: 켜짐\n스레드 스케일링 모드로 전환하려면 클릭",
"mining_idle_scale_on_tooltip": "스레드 스케일링: 켜짐\n시작/중지 모드로 전환하려면 클릭",
"mining_idle_threads_active_tooltip": "사용자 활성 시 스레드",
"mining_idle_threads_idle_tooltip": "시스템 유휴 시 스레드",
"mining_local_hashrate": "로컬 해시레이트", "mining_local_hashrate": "로컬 해시레이트",
"mining_mine": "채굴", "mining_mine": "채굴",
"mining_mining_addr": "채굴 주소", "mining_mining_addr": "채굴 주소",
@@ -388,6 +463,7 @@
"no_addresses_available": "사용 가능한 주소 없음", "no_addresses_available": "사용 가능한 주소 없음",
"no_addresses_match": "필터와 일치하는 주소가 없습니다", "no_addresses_match": "필터와 일치하는 주소가 없습니다",
"no_addresses_with_balance": "잔액이 있는 주소가 없습니다", "no_addresses_with_balance": "잔액이 있는 주소가 없습니다",
"no_addresses_yet": "아직 주소가 없습니다",
"no_matching": "일치하는 거래가 없습니다", "no_matching": "일치하는 거래가 없습니다",
"no_recent_receives": "최근 수신 내역 없음", "no_recent_receives": "최근 수신 내역 없음",
"no_recent_sends": "최근 전송 내역 없음", "no_recent_sends": "최근 전송 내역 없음",
@@ -453,6 +529,7 @@
"peers_upper": "피어", "peers_upper": "피어",
"peers_version": "버전", "peers_version": "버전",
"pending": "대기 중", "pending": "대기 중",
"pin_not_set": "PIN이 설정되지 않았습니다. 암호를 사용하여 잠금 해제하세요.",
"ping": "Ping", "ping": "Ping",
"price_chart": "가격 차트", "price_chart": "가격 차트",
"qr_code": "QR 코드", "qr_code": "QR 코드",
@@ -473,7 +550,9 @@
"recent_received": "최근 수신", "recent_received": "최근 수신",
"recent_sends": "최근 전송", "recent_sends": "최근 전송",
"recipient": "수신자", "recipient": "수신자",
"recipient_balance": "수신자: %.8f → %.8f DRGX",
"recv_type": "수신", "recv_type": "수신",
"reduce_motion": "모션 줄이기",
"refresh": "새로고침", "refresh": "새로고침",
"refresh_now": "지금 새로고침", "refresh_now": "지금 새로고침",
"remove_favorite": "즐겨찾기 제거", "remove_favorite": "즐겨찾기 제거",
@@ -493,7 +572,10 @@
"request_uri_copied": "결제 URI가 클립보드에 복사되었습니다", "request_uri_copied": "결제 URI가 클립보드에 복사되었습니다",
"rescan": "재스캔", "rescan": "재스캔",
"reset_to_defaults": "기본값으로 재설정", "reset_to_defaults": "기본값으로 재설정",
"restarting_after_encryption": "암호화 후 데몬 재시작 중...",
"restore_address": "주소 복원", "restore_address": "주소 복원",
"result_preview": "결과 미리보기",
"retry": "재시도",
"review_send": "전송 검토", "review_send": "전송 검토",
"rpc_host": "RPC 호스트", "rpc_host": "RPC 호스트",
"rpc_pass": "비밀번호", "rpc_pass": "비밀번호",
@@ -502,6 +584,39 @@
"save": "저장", "save": "저장",
"save_settings": "설정 저장", "save_settings": "설정 저장",
"save_z_transactions": "Z 거래를 거래 목록에 저장", "save_z_transactions": "Z 거래를 거래 목록에 저장",
"sb_auth_failed": "인증 실패 — rpcuser/rpcpassword를 확인하세요",
"sb_block": "블록: %d",
"sb_connecting_daemon": "dragonxd에 연결 중...",
"sb_connecting_err": "데몬 연결 중 — %s",
"sb_connecting_external": "외부 데몬에 연결 중...",
"sb_connecting_generic": "데몬에 연결 중...",
"sb_daemon_crashed": "데몬이 %d회 충돌함",
"sb_daemon_not_found": "데몬을 찾을 수 없음",
"sb_dragonxd_running": "dragonxd 실행 중",
"sb_dragonxd_stopped": "dragonxd 중지됨",
"sb_dragonxd_stopping": "dragonxd 중지 중...",
"sb_extracting_sapling": "Sapling 매개변수 추출 중...",
"sb_importing_keys": "키 가져오기 중",
"sb_loading_config": "설정 불러오는 중...",
"sb_mining_hs": "%.1f H/s",
"sb_net_ghs": "네트: %.2f GH/s",
"sb_net_hs": "네트: %.1f H/s",
"sb_net_khs": "네트: %.2f KH/s",
"sb_net_mhs": "네트: %.2f MH/s",
"sb_no_conf": "DRAGONX.conf를 찾을 수 없음",
"sb_peers": "피어: %zu",
"sb_rescanning": "재스캔",
"sb_rescanning_pct": "재스캔 %.0f%%",
"sb_restarting_daemon": "데몬 재시작 중...",
"sb_sapling_failed": "Sapling 매개변수 추출 실패.",
"sb_sapling_not_found": "Sapling 매개변수를 찾을 수 없음.",
"sb_starting_daemon": "dragonxd 시작 중...",
"sb_syncing_basic": "동기화 %.1f%% (%d 남음)",
"sb_syncing_eta": "동기화 %.1f%% (%d 남음, %.0f 블록/초, ~%s)",
"sb_waiting_config": "데몬 설정 대기 중...",
"sb_waiting_daemon": "dragonxd 대기 중...",
"sb_waiting_daemon_err": "dragonxd 대기 중 — %s",
"sb_warming_up": "워밍업 중...",
"search_placeholder": "검색...", "search_placeholder": "검색...",
"security": "보안", "security": "보안",
"select_address": "주소 선택...", "select_address": "주소 선택...",
@@ -553,12 +668,15 @@
"send_valid_transparent": "유효한 투명 주소", "send_valid_transparent": "유효한 투명 주소",
"send_wallet_empty": "지갑이 비어 있습니다", "send_wallet_empty": "지갑이 비어 있습니다",
"send_yes_clear": "예, 지우기", "send_yes_clear": "예, 지우기",
"sender_balance": "발신자: %.8f → %.8f DRGX",
"sending": "거래 전송 중", "sending": "거래 전송 중",
"sending_from": "보내는 곳", "sending_from": "보내는 곳",
"sends_full_balance_warning": "전체 잔액을 전송합니다. 보내는 주소의 잔액이 0이 됩니다.",
"sent": "전송됨", "sent": "전송됨",
"sent_filter": "전송됨", "sent_filter": "전송됨",
"sent_type": "전송됨", "sent_type": "전송됨",
"sent_upper": "전송됨", "sent_upper": "전송됨",
"set_label": "라벨 설정...",
"settings": "설정", "settings": "설정",
"settings_about_text": "DragonX (DRGX)용 차폐 암호화폐 지갑으로, Dear ImGui로 제작되어 가볍고 휴대 가능합니다.", "settings_about_text": "DragonX (DRGX)용 차폐 암호화폐 지갑으로, Dear ImGui로 제작되어 가볍고 휴대 가능합니다.",
"settings_acrylic_level": "아크릴 레벨:", "settings_acrylic_level": "아크릴 레벨:",
@@ -647,13 +765,17 @@
"shield_utxo_limit": "UTXO 제한:", "shield_utxo_limit": "UTXO 제한:",
"shield_wildcard_hint": "'*'를 사용하여 모든 투명 주소에서 차폐", "shield_wildcard_hint": "'*'를 사용하여 모든 투명 주소에서 차폐",
"shielded": "차폐", "shielded": "차폐",
"shielded_address": "보호 주소",
"shielded_to": "차폐 대상", "shielded_to": "차폐 대상",
"shielded_type": "차폐", "shielded_type": "차폐",
"shielding_notice": "참고: 투명 (T) 주소에서 프라이빗 (Z) 주소로 자금을 실딩합니다.",
"show": "표시", "show": "표시",
"show_hidden": "숨겨진 항목 표시 (%d)", "show_hidden": "숨겨진 항목 표시 (%d)",
"show_qr_code": "QR 코드 표시", "show_qr_code": "QR 코드 표시",
"showing_transactions": "%d–%d / %d건의 거래 표시 중 (총: %zu)", "showing_transactions": "%d%d / %d건의 거래 표시 중 (총: %zu)",
"showing_x_of_y": "%d / %d 주소 표시",
"simple_background": "단순 배경", "simple_background": "단순 배경",
"slider_off": "끔",
"start_mining": "채굴 시작", "start_mining": "채굴 시작",
"status": "상태", "status": "상태",
"stop_external": "외부 데몬 중지", "stop_external": "외부 데몬 중지",
@@ -662,17 +784,26 @@
"success": "성공", "success": "성공",
"summary": "요약", "summary": "요약",
"syncing": "동기화 중...", "syncing": "동기화 중...",
"t_address": "T 주소",
"t_addresses": "T 주소", "t_addresses": "T 주소",
"test_connection": "테스트", "test_connection": "테스트",
"theme": "테마", "theme": "테마",
"theme_effects": "테마 효과", "theme_effects": "테마 효과",
"theme_language": "테마 및 언어",
"time_days_ago": "%d일 전", "time_days_ago": "%d일 전",
"time_hours_ago": "%d시간 전", "time_hours_ago": "%d시간 전",
"time_minutes_ago": "%d분 전", "time_minutes_ago": "%d분 전",
"time_seconds_ago": "%d초 전", "time_seconds_ago": "%d초 전",
"timeout_15min": "15분",
"timeout_1hour": "1시간",
"timeout_1min": "1분",
"timeout_30min": "30분",
"timeout_5min": "5분",
"timeout_off": "끔",
"to": "받는 곳", "to": "받는 곳",
"to_upper": "받는 곳", "to_upper": "받는 곳",
"tools": "도구", "tools": "도구",
"tools_actions": "도구 및 작업...",
"total": "합계", "total": "합계",
"transaction_id": "거래 ID", "transaction_id": "거래 ID",
"transaction_sent": "거래 전송 성공", "transaction_sent": "거래 전송 성공",
@@ -680,7 +811,13 @@
"transaction_url": "거래 URL", "transaction_url": "거래 URL",
"transactions": "거래", "transactions": "거래",
"transactions_upper": "거래", "transactions_upper": "거래",
"transfer_failed": "이체 실패",
"transfer_funds": "자금 이체",
"transfer_sent": "이체 전송됨",
"transfer_sent_desc": "이체가 네트워크에 제출되었습니다.",
"transfer_to": "이체 대상:",
"transparent": "투명", "transparent": "투명",
"transparent_address": "투명 주소",
"tt_addr_url": "블록 탐색기에서 주소를 보기 위한 기본 URL", "tt_addr_url": "블록 탐색기에서 주소를 보기 위한 기본 URL",
"tt_address_book": "빠른 전송을 위해 저장된 주소 관리", "tt_address_book": "빠른 전송을 위해 저장된 주소 관리",
"tt_auto_lock": "이 비활성 시간 후 지갑 잠금", "tt_auto_lock": "이 비활성 시간 후 지갑 잠금",
@@ -695,6 +832,8 @@
"tt_custom_theme": "사용자 지정 테마 활성화됨", "tt_custom_theme": "사용자 지정 테마 활성화됨",
"tt_debug_collapse": "디버그 로깅 옵션 접기", "tt_debug_collapse": "디버그 로깅 옵션 접기",
"tt_debug_expand": "디버그 로깅 옵션 펼치기", "tt_debug_expand": "디버그 로깅 옵션 펼치기",
"tt_delete_blockchain": "모든 블록체인 데이터를 삭제하고 새로 동기화합니다. wallet.dat 및 설정은 보존됩니다.",
"tt_download_bootstrap": "블록체인 부트스트랩을 다운로드하여 동기화 가속\n기존 블록 데이터가 교체됩니다",
"tt_encrypt": "비밀번호로 wallet.dat 암호화", "tt_encrypt": "비밀번호로 wallet.dat 암호화",
"tt_export_all": "모든 개인키를 파일로 내보내기", "tt_export_all": "모든 개인키를 파일로 내보내기",
"tt_export_csv": "거래 내역을 CSV 스프레드시트로 내보내기", "tt_export_csv": "거래 내역을 CSV 스프레드시트로 내보내기",
@@ -712,6 +851,7 @@
"tt_mine_idle": "시스템이 유휴 상태(키보드/마우스 입력 없음)일 때\\n자동으로 채굴 시작", "tt_mine_idle": "시스템이 유휴 상태(키보드/마우스 입력 없음)일 때\\n자동으로 채굴 시작",
"tt_noise": "그레인 텍스처 강도 (0%% = 끔, 100%% = 최대)", "tt_noise": "그레인 텍스처 강도 (0%% = 끔, 100%% = 최대)",
"tt_open_dir": "파일 탐색기에서 열려면 클릭", "tt_open_dir": "파일 탐색기에서 열려면 클릭",
"tt_reduce_motion": "접근성을 위해 애니메이션 전환 및 잔액 보간 비활성화",
"tt_remove_encrypt": "암호화를 제거하고 지갑을 보호 없이 저장", "tt_remove_encrypt": "암호화를 제거하고 지갑을 보호 없이 저장",
"tt_remove_pin": "PIN을 제거하고 잠금 해제 시 비밀번호 요구", "tt_remove_pin": "PIN을 제거하고 잠금 해제 시 비밀번호 요구",
"tt_report_bug": "프로젝트 트래커에서 문제 보고", "tt_report_bug": "프로젝트 트래커에서 문제 보고",
@@ -789,7 +929,9 @@
"warning_upper": "경고!", "warning_upper": "경고!",
"website": "웹사이트", "website": "웹사이트",
"window_opacity": "창 투명도", "window_opacity": "창 투명도",
"wizard_daemon_start_failed": "데몬 시작 실패 — 자동으로 재시도됩니다",
"yes_clear": "예, 지우기", "yes_clear": "예, 지우기",
"your_addresses": "내 주소", "your_addresses": "내 주소",
"z_address": "Z 주소",
"z_addresses": "Z 주소" "z_addresses": "Z 주소"
} }

View File

@@ -43,6 +43,8 @@
"address_url": "URL do Endereço", "address_url": "URL do Endereço",
"addresses_appear_here": "Seus endereços de recebimento aparecerão aqui após a conexão.", "addresses_appear_here": "Seus endereços de recebimento aparecerão aqui após a conexão.",
"advanced": "AVANÇADO", "advanced": "AVANÇADO",
"advanced_effects": "Efeitos Avançados...",
"ago": "atrás",
"all_filter": "Todos", "all_filter": "Todos",
"allow_custom_fees": "Permitir taxas personalizadas", "allow_custom_fees": "Permitir taxas personalizadas",
"amount": "Valor", "amount": "Valor",
@@ -90,12 +92,30 @@
"block_timestamp": "Carimbo de Data:", "block_timestamp": "Carimbo de Data:",
"block_transactions": "Transações:", "block_transactions": "Transações:",
"blockchain_syncing": "Blockchain sincronizando (%.1f%%)... Os saldos podem ser imprecisos.", "blockchain_syncing": "Blockchain sincronizando (%.1f%%)... Os saldos podem ser imprecisos.",
"bootstrap_daemon_running": "Daemon em execução",
"bootstrap_daemon_stopped": "Daemon parado",
"bootstrap_daemon_stopping": "Parando daemon...",
"bootstrap_desc": "Baixe um bootstrap da blockchain para acelerar drasticamente a sincronização inicial. Isso baixa um instantâneo da blockchain e o extrai no seu diretório de dados.",
"bootstrap_downloading": "Baixando bootstrap...",
"bootstrap_extracting": "Extraindo dados da blockchain...",
"bootstrap_failed": "Falha no Bootstrap",
"bootstrap_mirror": "Espelho",
"bootstrap_mirror_tooltip": "Baixar do espelho (bootstrap2.dragonx.is).\nUse isto se o download principal estiver lento ou falhando.",
"bootstrap_restart_daemon": "Reiniciar Daemon",
"bootstrap_success": "Bootstrap Completo",
"bootstrap_success_desc": "Os dados da blockchain foram extraídos com sucesso. Inicie o daemon para começar a sincronizar a partir do ponto do bootstrap.",
"bootstrap_trust_warning": "Use apenas bootstrap.dragonx.is ou bootstrap2.dragonx.is. Usar arquivos de fontes não confiáveis pode comprometer seu nó.",
"bootstrap_verifying": "Verificando somas de verificação...",
"bootstrap_wallet_protected": "(wallet.dat está protegido)",
"bootstrap_warning": "Os dados de blocos existentes (blocks, chainstate, notarizations) serão excluídos e substituídos. Seu wallet.dat NÃO será modificado ou excluído.",
"cancel": "Cancelar", "cancel": "Cancelar",
"characters": "caracteres", "characters": "caracteres",
"choose_icon": "Escolher Ícone",
"clear": "Limpar", "clear": "Limpar",
"clear_all_bans": "Remover Todos os Banimentos", "clear_all_bans": "Remover Todos os Banimentos",
"clear_anyway": "Limpar mesmo assim", "clear_anyway": "Limpar mesmo assim",
"clear_form_confirm": "Limpar todos os campos do formulário?", "clear_form_confirm": "Limpar todos os campos do formulário?",
"clear_icon": "Limpar Ícone",
"clear_request": "Limpar Solicitação", "clear_request": "Limpar Solicitação",
"click_copy_address": "Clique para copiar o endereço", "click_copy_address": "Clique para copiar o endereço",
"click_copy_uri": "Clique para copiar a URI", "click_copy_uri": "Clique para copiar a URI",
@@ -106,8 +126,12 @@
"confirm_clear_ztx_title": "Confirmar limpeza do histórico Z-Tx", "confirm_clear_ztx_title": "Confirmar limpeza do histórico Z-Tx",
"confirm_clear_ztx_warning1": "Limpar o histórico de z-transações pode fazer com que seu saldo blindado apareça como 0 até que um reescaneamento da carteira seja realizado.", "confirm_clear_ztx_warning1": "Limpar o histórico de z-transações pode fazer com que seu saldo blindado apareça como 0 até que um reescaneamento da carteira seja realizado.",
"confirm_clear_ztx_warning2": "Se isso acontecer, você precisará reimportar as chaves privadas do seu endereço z com reescaneamento habilitado para recuperar seu saldo.", "confirm_clear_ztx_warning2": "Se isso acontecer, você precisará reimportar as chaves privadas do seu endereço z com reescaneamento habilitado para recuperar seu saldo.",
"confirm_delete_blockchain_msg": "Isso irá parar o daemon, excluir todos os dados da blockchain (blocks, chainstate, peers) e iniciar uma nova sincronização do zero. Isso pode levar várias horas.",
"confirm_delete_blockchain_safe": "Seu wallet.dat, configuração e histórico de transações estão seguros e não serão excluídos.",
"confirm_delete_blockchain_title": "Excluir Dados da Blockchain",
"confirm_send": "Confirmar Envio", "confirm_send": "Confirmar Envio",
"confirm_transaction": "Confirmar Transação", "confirm_transaction": "Confirmar Transação",
"confirm_transfer": "Confirmar Transferência",
"confirmations": "Confirmações", "confirmations": "Confirmações",
"confirmations_display": "%d confirmações | %s", "confirmations_display": "%d confirmações | %s",
"confirmed": "Confirmado", "confirmed": "Confirmado",
@@ -172,6 +196,7 @@
"console_welcome": "Bem-vindo ao Console ObsidianDragon", "console_welcome": "Bem-vindo ao Console ObsidianDragon",
"console_zoom_in": "Aumentar zoom", "console_zoom_in": "Aumentar zoom",
"console_zoom_out": "Diminuir zoom", "console_zoom_out": "Diminuir zoom",
"copied": "Copiado!",
"copy": "Copiar", "copy": "Copiar",
"copy_address": "Copiar Endereço Completo", "copy_address": "Copiar Endereço Completo",
"copy_error": "Copiar Erro", "copy_error": "Copiar Erro",
@@ -180,22 +205,45 @@
"copy_uri": "Copiar URI", "copy_uri": "Copiar URI",
"current_price": "Preço Atual", "current_price": "Preço Atual",
"custom_fees": "Taxas personalizadas", "custom_fees": "Taxas personalizadas",
"daemon_version": "Daemon",
"dark": "Escuro", "dark": "Escuro",
"date": "Data", "date": "Data",
"date_label": "Data:", "date_label": "Data:",
"debug_logging": "REGISTRO DE DEPURAÇÃO", "debug_logging": "REGISTRO DE DEPURAÇÃO",
"delete": "Excluir", "delete": "Excluir",
"delete_blockchain": "Excluir Blockchain",
"delete_blockchain_confirm": "Excluir e Ressincronizar",
"deshielding_warning": "Aviso: Isso irá des-blindar fundos de um endereço privado (Z) para um endereço transparente (T).",
"difficulty": "Dificuldade", "difficulty": "Dificuldade",
"disconnected": "Desconectado", "disconnected": "Desconectado",
"dismiss": "Dispensar", "dismiss": "Dispensar",
"display": "Exibição", "display": "Exibição",
"download": "Baixar",
"download_bootstrap": "Baixar Bootstrap",
"dragonx_green": "DragonX (Verde)", "dragonx_green": "DragonX (Verde)",
"edit": "Editar", "edit": "Editar",
"error": "Erro", "error": "Erro",
"error_format": "Erro: %s", "error_format": "Erro: %s",
"est_time_to_block": "Tempo Est. por Bloco", "est_time_to_block": "Tempo Est. por Bloco",
"exit": "Sair", "exit": "Sair",
"explorer": "EXPLORADOR", "explorer": "Explorador",
"explorer_block_detail": "Bloco",
"explorer_block_hash": "Hash",
"explorer_block_height": "Altura",
"explorer_block_merkle": "Raiz Merkle",
"explorer_block_size": "Tamanho",
"explorer_block_time": "Hora",
"explorer_block_txs": "Transações",
"explorer_chain_stats": "Cadeia",
"explorer_invalid_query": "Insira uma altura de bloco ou um hash de 64 caracteres",
"explorer_mempool": "Mempool",
"explorer_mempool_size": "Tamanho",
"explorer_mempool_txs": "Transações",
"explorer_recent_blocks": "Blocos Recentes",
"explorer_search": "Pesquisar",
"explorer_section": "EXPLORADOR",
"explorer_tx_outputs": "Saídas",
"explorer_tx_size": "Tamanho",
"export": "Exportar", "export": "Exportar",
"export_csv": "Exportar CSV", "export_csv": "Exportar CSV",
"export_keys_btn": "Exportar Chaves", "export_keys_btn": "Exportar Chaves",
@@ -224,14 +272,22 @@
"fetch_prices": "Buscar preços", "fetch_prices": "Buscar preços",
"file": "Arquivo", "file": "Arquivo",
"file_save_location": "O arquivo será salvo em: ~/.config/ObsidianDragon/", "file_save_location": "O arquivo será salvo em: ~/.config/ObsidianDragon/",
"filter": "Filtrar...",
"font_scale": "Escala da Fonte", "font_scale": "Escala da Fonte",
"force_quit": "Forçar Saída",
"force_quit_confirm_msg": "Isso matará imediatamente o daemon sem um desligamento limpo.\nIsso pode corromper o índice da blockchain e exigir uma ressincronização.",
"force_quit_confirm_title": "Forçar Saída?",
"force_quit_warning": "Isso matará imediatamente o daemon sem um desligamento limpo. Pode exigir uma ressincronização da blockchain.",
"force_quit_yes": "Forçar Saída",
"from": "De", "from": "De",
"from_upper": "DE", "from_upper": "DE",
"full_details": "Detalhes Completos", "full_details": "Detalhes Completos",
"general": "Geral", "general": "Geral",
"generating": "Gerando",
"go_to_receive": "Ir para Receber", "go_to_receive": "Ir para Receber",
"height": "Altura", "height": "Altura",
"help": "Ajuda", "help": "Ajuda",
"hidden_tag": " (oculto)",
"hide": "Ocultar", "hide": "Ocultar",
"hide_address": "Ocultar endereço", "hide_address": "Ocultar endereço",
"hide_zero_balances": "Ocultar saldos zero", "hide_zero_balances": "Ocultar saldos zero",
@@ -253,6 +309,9 @@
"import_key_warning": "Aviso: Nunca compartilhe suas chaves privadas! Importar chaves de fontes não confiáveis pode comprometer sua carteira.", "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_key_z_format": "Chaves de gasto de z-endereço (secret-extended-key-...)",
"import_private_key": "Importar Chave Privada...", "import_private_key": "Importar Chave Privada...",
"incorrect_passphrase": "Senha incorreta",
"incorrect_pin": "PIN incorreto",
"insufficient_funds": "Fundos insuficientes para este valor mais taxa.",
"invalid_address": "Formato de endereço inválido", "invalid_address": "Formato de endereço inválido",
"ip_address": "Endereço IP", "ip_address": "Endereço IP",
"keep": "Manter", "keep": "Manter",
@@ -266,6 +325,7 @@
"key_export_viewing_keys_zonly": "As chaves de visualização estão disponíveis apenas para endereços blindados (z)", "key_export_viewing_keys_zonly": "As chaves de visualização estão disponíveis apenas para endereços blindados (z)",
"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.", "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:", "label": "Rótulo:",
"label_placeholder": "ex. Poupança, Mineração...",
"language": "Idioma", "language": "Idioma",
"light": "Claro", "light": "Claro",
"loading": "Carregando...", "loading": "Carregando...",
@@ -286,6 +346,7 @@
"market_now": "Agora", "market_now": "Agora",
"market_pct_shielded": "%.0f%% Blindado", "market_pct_shielded": "%.0f%% Blindado",
"market_portfolio": "PORTFÓLIO", "market_portfolio": "PORTFÓLIO",
"market_price_loading": "Carregando dados de preço...",
"market_price_unavailable": "Dados de preço indisponíveis", "market_price_unavailable": "Dados de preço indisponíveis",
"market_refresh_price": "Atualizar dados de preço", "market_refresh_price": "Atualizar dados de preço",
"market_trade_on": "Negociar no %s", "market_trade_on": "Negociar no %s",
@@ -311,6 +372,13 @@
"mining_address_copied": "Endereço de mineração copiado", "mining_address_copied": "Endereço de mineração copiado",
"mining_all_time": "Todo o Tempo", "mining_all_time": "Todo o Tempo",
"mining_already_saved": "URL do pool já salva", "mining_already_saved": "URL do pool já salva",
"mining_benchmark_cancel": "Cancelar benchmark",
"mining_benchmark_cooling": "Resfriando",
"mining_benchmark_dismiss": "Fechar",
"mining_benchmark_result": "Ótimo",
"mining_benchmark_stabilizing": "Estabilizando",
"mining_benchmark_testing": "Testando",
"mining_benchmark_tooltip": "Encontrar o número ideal de threads para esta CPU",
"mining_block_copied": "Hash do bloco copiado", "mining_block_copied": "Hash do bloco copiado",
"mining_chart_1m_ago": "1m atrás", "mining_chart_1m_ago": "1m atrás",
"mining_chart_5m_ago": "5m atrás", "mining_chart_5m_ago": "5m atrás",
@@ -330,8 +398,15 @@
"mining_filter_tip_all": "Mostrar todos os ganhos", "mining_filter_tip_all": "Mostrar todos os ganhos",
"mining_filter_tip_pool": "Mostrar apenas ganhos do pool", "mining_filter_tip_pool": "Mostrar apenas ganhos do pool",
"mining_filter_tip_solo": "Mostrar apenas ganhos solo", "mining_filter_tip_solo": "Mostrar apenas ganhos solo",
"mining_generate_z_address_hint": "Gere um endereço Z na aba Receber para usar como endereço de pagamento",
"mining_idle_gpu_off_tooltip": "Sem restrição: ATIVADO\nApenas entrada de teclado/mouse determina o estado ocioso\nClique para ativar detecção de GPU",
"mining_idle_gpu_on_tooltip": "GPU-consciente: ATIVADO\nAtividade de GPU (vídeo, jogos) impede mineração ociosa\nClique para modo sem restrição",
"mining_idle_off_tooltip": "Ativar mineração ociosa", "mining_idle_off_tooltip": "Ativar mineração ociosa",
"mining_idle_on_tooltip": "Desativar mineração ociosa", "mining_idle_on_tooltip": "Desativar mineração ociosa",
"mining_idle_scale_off_tooltip": "Modo iniciar/parar: ATIVADO\nClique para mudar para modo de escala de threads",
"mining_idle_scale_on_tooltip": "Escala de threads: ATIVADO\nClique para mudar para modo iniciar/parar",
"mining_idle_threads_active_tooltip": "Threads quando o usuário está ativo",
"mining_idle_threads_idle_tooltip": "Threads quando o sistema está ocioso",
"mining_local_hashrate": "Hashrate Local", "mining_local_hashrate": "Hashrate Local",
"mining_mine": "Minerar", "mining_mine": "Minerar",
"mining_mining_addr": "End. Mineração", "mining_mining_addr": "End. Mineração",
@@ -388,6 +463,7 @@
"no_addresses_available": "Nenhum endereço disponível", "no_addresses_available": "Nenhum endereço disponível",
"no_addresses_match": "Nenhum endereço corresponde ao filtro", "no_addresses_match": "Nenhum endereço corresponde ao filtro",
"no_addresses_with_balance": "Nenhum endereço com saldo", "no_addresses_with_balance": "Nenhum endereço com saldo",
"no_addresses_yet": "Nenhum endereço ainda",
"no_matching": "Nenhuma transação correspondente", "no_matching": "Nenhuma transação correspondente",
"no_recent_receives": "Nenhum recebimento recente", "no_recent_receives": "Nenhum recebimento recente",
"no_recent_sends": "Nenhum envio recente", "no_recent_sends": "Nenhum envio recente",
@@ -400,7 +476,7 @@
"notes": "Notas", "notes": "Notas",
"notes_optional": "Notas (opcional):", "notes_optional": "Notas (opcional):",
"output_filename": "Nome do arquivo de saída:", "output_filename": "Nome do arquivo de saída:",
"overview": "Visão Geral", "overview": "Resumo",
"paste": "Colar", "paste": "Colar",
"paste_from_clipboard": "Colar da Área de Transferência", "paste_from_clipboard": "Colar da Área de Transferência",
"pay_from": "Pagar de", "pay_from": "Pagar de",
@@ -453,6 +529,7 @@
"peers_upper": "PARES", "peers_upper": "PARES",
"peers_version": "Versão", "peers_version": "Versão",
"pending": "Pendente", "pending": "Pendente",
"pin_not_set": "PIN não definido. Use a senha para desbloquear.",
"ping": "Ping", "ping": "Ping",
"price_chart": "Gráfico de Preços", "price_chart": "Gráfico de Preços",
"qr_code": "Código QR", "qr_code": "Código QR",
@@ -473,7 +550,9 @@
"recent_received": "RECEBIDOS RECENTES", "recent_received": "RECEBIDOS RECENTES",
"recent_sends": "ENVIOS RECENTES", "recent_sends": "ENVIOS RECENTES",
"recipient": "DESTINATÁRIO", "recipient": "DESTINATÁRIO",
"recipient_balance": "Destinatário: %.8f → %.8f DRGX",
"recv_type": "Receb.", "recv_type": "Receb.",
"reduce_motion": "Reduzir Movimento",
"refresh": "Atualizar", "refresh": "Atualizar",
"refresh_now": "Atualizar Agora", "refresh_now": "Atualizar Agora",
"remove_favorite": "Remover favorito", "remove_favorite": "Remover favorito",
@@ -493,7 +572,10 @@
"request_uri_copied": "URI de pagamento copiada para a área de transferência", "request_uri_copied": "URI de pagamento copiada para a área de transferência",
"rescan": "Reescanear", "rescan": "Reescanear",
"reset_to_defaults": "Redefinir Padrões", "reset_to_defaults": "Redefinir Padrões",
"restarting_after_encryption": "Reiniciando daemon após criptografia...",
"restore_address": "Restaurar endereço", "restore_address": "Restaurar endereço",
"result_preview": "Pré-visualização do resultado",
"retry": "Tentar novamente",
"review_send": "Revisar Envio", "review_send": "Revisar Envio",
"rpc_host": "Host RPC", "rpc_host": "Host RPC",
"rpc_pass": "Senha", "rpc_pass": "Senha",
@@ -502,6 +584,39 @@
"save": "Salvar", "save": "Salvar",
"save_settings": "Salvar Configurações", "save_settings": "Salvar Configurações",
"save_z_transactions": "Salvar Z-tx na lista de tx", "save_z_transactions": "Salvar Z-tx na lista de tx",
"sb_auth_failed": "Autenticação falhou — verifique rpcuser/rpcpassword",
"sb_block": "Bloco: %d",
"sb_connecting_daemon": "Conectando ao dragonxd...",
"sb_connecting_err": "Conectando ao daemon — %s",
"sb_connecting_external": "Conectando ao daemon externo...",
"sb_connecting_generic": "Conectando ao daemon...",
"sb_daemon_crashed": "O daemon travou %d vezes",
"sb_daemon_not_found": "Daemon não encontrado",
"sb_dragonxd_running": "dragonxd em execução",
"sb_dragonxd_stopped": "dragonxd parado",
"sb_dragonxd_stopping": "Parando dragonxd...",
"sb_extracting_sapling": "Extraindo parâmetros Sapling...",
"sb_importing_keys": "Importando chaves",
"sb_loading_config": "Carregando configuração...",
"sb_mining_hs": "%.1f H/s",
"sb_net_ghs": "Rede: %.2f GH/s",
"sb_net_hs": "Rede: %.1f H/s",
"sb_net_khs": "Rede: %.2f KH/s",
"sb_net_mhs": "Rede: %.2f MH/s",
"sb_no_conf": "DRAGONX.conf não encontrado",
"sb_peers": "Pares: %zu",
"sb_rescanning": "Reescaneando",
"sb_rescanning_pct": "Reescaneando %.0f%%",
"sb_restarting_daemon": "Reiniciando daemon...",
"sb_sapling_failed": "Falha ao extrair parâmetros Sapling.",
"sb_sapling_not_found": "Parâmetros Sapling não encontrados.",
"sb_starting_daemon": "Iniciando dragonxd...",
"sb_syncing_basic": "Sincronizando %.1f%% (%d restantes)",
"sb_syncing_eta": "Sincronizando %.1f%% (%d restantes, %.0f blk/s, ~%s)",
"sb_waiting_config": "Aguardando configuração do daemon...",
"sb_waiting_daemon": "Aguardando dragonxd...",
"sb_waiting_daemon_err": "Aguardando dragonxd — %s",
"sb_warming_up": "Aquecendo...",
"search_placeholder": "Pesquisar...", "search_placeholder": "Pesquisar...",
"security": "SEGURANÇA", "security": "SEGURANÇA",
"select_address": "Selecionar endereço...", "select_address": "Selecionar endereço...",
@@ -553,13 +668,16 @@
"send_valid_transparent": "Endereço transparente válido", "send_valid_transparent": "Endereço transparente válido",
"send_wallet_empty": "Sua carteira está vazia", "send_wallet_empty": "Sua carteira está vazia",
"send_yes_clear": "Sim, Limpar", "send_yes_clear": "Sim, Limpar",
"sender_balance": "Remetente: %.8f → %.8f DRGX",
"sending": "Enviando transação", "sending": "Enviando transação",
"sending_from": "ENVIANDO DE", "sending_from": "ENVIANDO DE",
"sends_full_balance_warning": "Isso envia o saldo total. O endereço de envio terá saldo zero.",
"sent": "enviado", "sent": "enviado",
"sent_filter": "Enviado", "sent_filter": "Enviado",
"sent_type": "Enviado", "sent_type": "Enviado",
"sent_upper": "ENVIADO", "sent_upper": "ENVIADO",
"settings": "Configurações", "set_label": "Definir Rótulo...",
"settings": "Ajustes",
"settings_about_text": "Uma carteira de criptomoeda blindada para DragonX (DRGX), criada com Dear ImGui para uma experiência leve e portátil.", "settings_about_text": "Uma carteira de criptomoeda blindada para DragonX (DRGX), criada com Dear ImGui para uma experiência leve e portátil.",
"settings_acrylic_level": "Nível acrílico:", "settings_acrylic_level": "Nível acrílico:",
"settings_address_book": "Livro de endereços...", "settings_address_book": "Livro de endereços...",
@@ -647,13 +765,17 @@
"shield_utxo_limit": "Limite UTXO:", "shield_utxo_limit": "Limite UTXO:",
"shield_wildcard_hint": "Use '*' para blindar de todos os endereços transparentes", "shield_wildcard_hint": "Use '*' para blindar de todos os endereços transparentes",
"shielded": "Blindado", "shielded": "Blindado",
"shielded_address": "Endereço Blindado",
"shielded_to": "BLINDADO PARA", "shielded_to": "BLINDADO PARA",
"shielded_type": "Blindado", "shielded_type": "Blindado",
"shielding_notice": "Nota: Isso blindará fundos de um endereço transparente (T) para um endereço privado (Z).",
"show": "Mostrar", "show": "Mostrar",
"show_hidden": "Mostrar ocultos (%d)", "show_hidden": "Mostrar ocultos (%d)",
"show_qr_code": "Mostrar Código QR", "show_qr_code": "Mostrar Código QR",
"showing_transactions": "Mostrando %d–%d de %d transações (total: %zu)", "showing_transactions": "Mostrando %d%d de %d transações (total: %zu)",
"showing_x_of_y": "Mostrando %d de %d endereços",
"simple_background": "Fundo simples", "simple_background": "Fundo simples",
"slider_off": "Desligado",
"start_mining": "Iniciar Mineração", "start_mining": "Iniciar Mineração",
"status": "Status", "status": "Status",
"stop_external": "Parar daemon externo", "stop_external": "Parar daemon externo",
@@ -662,17 +784,26 @@
"success": "Sucesso", "success": "Sucesso",
"summary": "Resumo", "summary": "Resumo",
"syncing": "Sincronizando...", "syncing": "Sincronizando...",
"t_address": "Endereço T",
"t_addresses": "Endereços T", "t_addresses": "Endereços T",
"test_connection": "Testar", "test_connection": "Testar",
"theme": "Tema", "theme": "Tema",
"theme_effects": "Efeitos de tema", "theme_effects": "Efeitos de tema",
"theme_language": "TEMA E IDIOMA",
"time_days_ago": "há %d dias", "time_days_ago": "há %d dias",
"time_hours_ago": "há %d horas", "time_hours_ago": "há %d horas",
"time_minutes_ago": "há %d minutos", "time_minutes_ago": "há %d minutos",
"time_seconds_ago": "há %d segundos", "time_seconds_ago": "há %d segundos",
"timeout_15min": "15 min",
"timeout_1hour": "1 hora",
"timeout_1min": "1 min",
"timeout_30min": "30 min",
"timeout_5min": "5 min",
"timeout_off": "Desligado",
"to": "Para", "to": "Para",
"to_upper": "PARA", "to_upper": "PARA",
"tools": "FERRAMENTAS", "tools": "FERRAMENTAS",
"tools_actions": "Ferramentas e Ações...",
"total": "Total", "total": "Total",
"transaction_id": "ID DA TRANSAÇÃO", "transaction_id": "ID DA TRANSAÇÃO",
"transaction_sent": "Transação enviada com sucesso", "transaction_sent": "Transação enviada com sucesso",
@@ -680,7 +811,13 @@
"transaction_url": "URL da Transação", "transaction_url": "URL da Transação",
"transactions": "Transações", "transactions": "Transações",
"transactions_upper": "TRANSAÇÕES", "transactions_upper": "TRANSAÇÕES",
"transfer_failed": "Transferência Falhou",
"transfer_funds": "Transferir Fundos",
"transfer_sent": "Transferência Enviada",
"transfer_sent_desc": "Sua transferência foi enviada à rede.",
"transfer_to": "Transferir para:",
"transparent": "Transparente", "transparent": "Transparente",
"transparent_address": "Endereço Transparente",
"tt_addr_url": "URL base para visualizar endereços em um explorador de blocos", "tt_addr_url": "URL base para visualizar endereços em um explorador de blocos",
"tt_address_book": "Gerenciar endereços salvos para envio rápido", "tt_address_book": "Gerenciar endereços salvos para envio rápido",
"tt_auto_lock": "Bloquear carteira após este tempo de inatividade", "tt_auto_lock": "Bloquear carteira após este tempo de inatividade",
@@ -695,6 +832,8 @@
"tt_custom_theme": "Tema personalizado ativo", "tt_custom_theme": "Tema personalizado ativo",
"tt_debug_collapse": "Recolher opções de registro de depuração", "tt_debug_collapse": "Recolher opções de registro de depuração",
"tt_debug_expand": "Expandir opções de registro de depuração", "tt_debug_expand": "Expandir opções de registro de depuração",
"tt_delete_blockchain": "Excluir todos os dados da blockchain e iniciar uma nova sincronização. wallet.dat e configuração são preservados.",
"tt_download_bootstrap": "Baixar bootstrap da blockchain para acelerar a sincronização\nDados de blocos existentes serão substituídos",
"tt_encrypt": "Encriptar wallet.dat com uma frase secreta", "tt_encrypt": "Encriptar wallet.dat com uma frase secreta",
"tt_export_all": "Exportar todas as chaves privadas para um arquivo", "tt_export_all": "Exportar todas as chaves privadas para um arquivo",
"tt_export_csv": "Exportar histórico de transações como planilha CSV", "tt_export_csv": "Exportar histórico de transações como planilha CSV",
@@ -712,6 +851,7 @@
"tt_mine_idle": "Iniciar mineração automaticamente quando o\\nsistema estiver ocioso (sem entrada de teclado/mouse)", "tt_mine_idle": "Iniciar mineração automaticamente quando o\\nsistema estiver ocioso (sem entrada de teclado/mouse)",
"tt_noise": "Intensidade de textura granulada (0%% = desligado, 100%% = máximo)", "tt_noise": "Intensidade de textura granulada (0%% = desligado, 100%% = máximo)",
"tt_open_dir": "Clique para abrir no explorador de arquivos", "tt_open_dir": "Clique para abrir no explorador de arquivos",
"tt_reduce_motion": "Desativar transições animadas e lerp de saldo para acessibilidade",
"tt_remove_encrypt": "Remover encriptação e armazenar a carteira desprotegida", "tt_remove_encrypt": "Remover encriptação e armazenar a carteira desprotegida",
"tt_remove_pin": "Remover PIN e exigir frase secreta para desbloquear", "tt_remove_pin": "Remover PIN e exigir frase secreta para desbloquear",
"tt_report_bug": "Reportar um problema no rastreador do projeto", "tt_report_bug": "Reportar um problema no rastreador do projeto",
@@ -789,7 +929,9 @@
"warning_upper": "AVISO!", "warning_upper": "AVISO!",
"website": "Website", "website": "Website",
"window_opacity": "Opacidade da Janela", "window_opacity": "Opacidade da Janela",
"wizard_daemon_start_failed": "Falha ao iniciar o daemon — será tentado novamente automaticamente",
"yes_clear": "Sim, Limpar", "yes_clear": "Sim, Limpar",
"your_addresses": "Seus Endereços", "your_addresses": "Seus Endereços",
"z_address": "Endereço Z",
"z_addresses": "Endereços Z" "z_addresses": "Endereços Z"
} }

View File

@@ -42,7 +42,9 @@
"address_upper": "АДРЕС", "address_upper": "АДРЕС",
"address_url": "URL адреса", "address_url": "URL адреса",
"addresses_appear_here": "Ваши адреса для получения появятся здесь после подключения.", "addresses_appear_here": "Ваши адреса для получения появятся здесь после подключения.",
"advanced": "РАСШИРЕННЫЕ", "advanced": "ПРОЧЕЕ",
"advanced_effects": "Расширенные эффекты...",
"ago": "назад",
"all_filter": "Все", "all_filter": "Все",
"allow_custom_fees": "Разрешить пользовательские комиссии", "allow_custom_fees": "Разрешить пользовательские комиссии",
"amount": "Сумма", "amount": "Сумма",
@@ -90,12 +92,30 @@
"block_timestamp": "Временная метка:", "block_timestamp": "Временная метка:",
"block_transactions": "Транзакции:", "block_transactions": "Транзакции:",
"blockchain_syncing": "Синхронизация блокчейна (%.1f%%)... Балансы могут быть неточными.", "blockchain_syncing": "Синхронизация блокчейна (%.1f%%)... Балансы могут быть неточными.",
"bootstrap_daemon_running": "Демон запущен",
"bootstrap_daemon_stopped": "Демон остановлен",
"bootstrap_daemon_stopping": "Остановка демона...",
"bootstrap_desc": "Загрузите бутстрап блокчейна для значительного ускорения начальной синхронизации. Это загружает снимок блокчейна и извлекает его в ваш каталог данных.",
"bootstrap_downloading": "Загрузка бутстрапа...",
"bootstrap_extracting": "Извлечение данных блокчейна...",
"bootstrap_failed": "Ошибка бутстрапа",
"bootstrap_mirror": "Зеркало",
"bootstrap_mirror_tooltip": "Скачать с зеркала (bootstrap2.dragonx.is).\nИспользуйте, если основная загрузка медленная или не работает.",
"bootstrap_restart_daemon": "Перезапустить демон",
"bootstrap_success": "Бутстрап завершён",
"bootstrap_success_desc": "Данные блокчейна успешно извлечены. Запустите демон для начала синхронизации с точки бутстрапа.",
"bootstrap_trust_warning": "Используйте только bootstrap.dragonx.is или bootstrap2.dragonx.is. Использование файлов из ненадёжных источников может скомпрометировать ваш узел.",
"bootstrap_verifying": "Проверка контрольных сумм...",
"bootstrap_wallet_protected": "(wallet.dat защищён)",
"bootstrap_warning": "Существующие данные блоков (blocks, chainstate, notarizations) будут удалены и заменены. Ваш wallet.dat НЕ будет изменён или удалён.",
"cancel": "Отмена", "cancel": "Отмена",
"characters": "символов", "characters": "символов",
"choose_icon": "Выбрать иконку",
"clear": "Очистить", "clear": "Очистить",
"clear_all_bans": "Снять все блокировки", "clear_all_bans": "Снять все блокировки",
"clear_anyway": "Всё равно очистить", "clear_anyway": "Всё равно очистить",
"clear_form_confirm": "Очистить все поля формы?", "clear_form_confirm": "Очистить все поля формы?",
"clear_icon": "Удалить иконку",
"clear_request": "Очистить запрос", "clear_request": "Очистить запрос",
"click_copy_address": "Нажмите, чтобы скопировать адрес", "click_copy_address": "Нажмите, чтобы скопировать адрес",
"click_copy_uri": "Нажмите, чтобы скопировать URI", "click_copy_uri": "Нажмите, чтобы скопировать URI",
@@ -106,8 +126,12 @@
"confirm_clear_ztx_title": "Подтвердить очистку истории Z-Tx", "confirm_clear_ztx_title": "Подтвердить очистку истории Z-Tx",
"confirm_clear_ztx_warning1": "Очистка истории z-транзакций может привести к отображению защищённого баланса как 0, пока не будет выполнено пересканирование кошелька.", "confirm_clear_ztx_warning1": "Очистка истории z-транзакций может привести к отображению защищённого баланса как 0, пока не будет выполнено пересканирование кошелька.",
"confirm_clear_ztx_warning2": "Если это произойдёт, вам потребуется повторно импортировать приватные ключи вашего z-адреса с включённым пересканированием для восстановления баланса.", "confirm_clear_ztx_warning2": "Если это произойдёт, вам потребуется повторно импортировать приватные ключи вашего z-адреса с включённым пересканированием для восстановления баланса.",
"confirm_delete_blockchain_msg": "Это остановит демон, удалит все данные блокчейна (blocks, chainstate, peers) и начнёт синхронизацию с нуля. Это может занять несколько часов.",
"confirm_delete_blockchain_safe": "Ваш wallet.dat, конфигурация и история транзакций в безопасности и не будут удалены.",
"confirm_delete_blockchain_title": "Удалить данные блокчейна",
"confirm_send": "Подтвердить отправку", "confirm_send": "Подтвердить отправку",
"confirm_transaction": "Подтвердить транзакцию", "confirm_transaction": "Подтвердить транзакцию",
"confirm_transfer": "Подтвердить перевод",
"confirmations": "Подтверждения", "confirmations": "Подтверждения",
"confirmations_display": "%d подтверждений | %s", "confirmations_display": "%d подтверждений | %s",
"confirmed": "Подтверждено", "confirmed": "Подтверждено",
@@ -172,6 +196,7 @@
"console_welcome": "Добро пожаловать в консоль ObsidianDragon", "console_welcome": "Добро пожаловать в консоль ObsidianDragon",
"console_zoom_in": "Увеличить", "console_zoom_in": "Увеличить",
"console_zoom_out": "Уменьшить", "console_zoom_out": "Уменьшить",
"copied": "Скопировано!",
"copy": "Копировать", "copy": "Копировать",
"copy_address": "Копировать полный адрес", "copy_address": "Копировать полный адрес",
"copy_error": "Копировать ошибку", "copy_error": "Копировать ошибку",
@@ -180,22 +205,45 @@
"copy_uri": "Копировать URI", "copy_uri": "Копировать URI",
"current_price": "Текущая цена", "current_price": "Текущая цена",
"custom_fees": "Пользовательские комиссии", "custom_fees": "Пользовательские комиссии",
"daemon_version": "Демон",
"dark": "Тёмная", "dark": "Тёмная",
"date": "Дата", "date": "Дата",
"date_label": "Дата:", "date_label": "Дата:",
"debug_logging": "ЖУРНАЛ ОТЛАДКИ", "debug_logging": "ЖУРНАЛ ОТЛАДКИ",
"delete": "Удалить", "delete": "Удалить",
"delete_blockchain": "Удалить блокчейн",
"delete_blockchain_confirm": "Удалить и пересинхронизировать",
"deshielding_warning": "Внимание: это переведёт средства из приватного (Z) адреса на прозрачный (T) адрес.",
"difficulty": "Сложность", "difficulty": "Сложность",
"disconnected": "Отключено", "disconnected": "Отключено",
"dismiss": "Отклонить", "dismiss": "Отклонить",
"display": "Отображение", "display": "Отображение",
"download": "Скачать",
"download_bootstrap": "Скачать бутстрап",
"dragonx_green": "DragonX (Зелёная)", "dragonx_green": "DragonX (Зелёная)",
"edit": "Редактировать", "edit": "Редактировать",
"error": "Ошибка", "error": "Ошибка",
"error_format": "Ошибка: %s", "error_format": "Ошибка: %s",
"est_time_to_block": "Расч. время до блока", "est_time_to_block": "Расч. время до блока",
"exit": "Выход", "exit": "Выход",
"explorer": "ОБОЗРЕВАТЕЛЬ", "explorer": "Проводник",
"explorer_block_detail": "Блок",
"explorer_block_hash": "Хеш",
"explorer_block_height": "Высота",
"explorer_block_merkle": "Корень Меркла",
"explorer_block_size": "Размер",
"explorer_block_time": "Время",
"explorer_block_txs": "Транзакции",
"explorer_chain_stats": "Цепочка",
"explorer_invalid_query": "Введите высоту блока или 64-символьный хеш",
"explorer_mempool": "Мемпул",
"explorer_mempool_size": "Размер",
"explorer_mempool_txs": "Транзакции",
"explorer_recent_blocks": "Последние блоки",
"explorer_search": "Поиск",
"explorer_section": "ОБОЗРЕВАТЕЛЬ",
"explorer_tx_outputs": "Выходы",
"explorer_tx_size": "Размер",
"export": "Экспорт", "export": "Экспорт",
"export_csv": "Экспорт в CSV", "export_csv": "Экспорт в CSV",
"export_keys_btn": "Экспорт ключей", "export_keys_btn": "Экспорт ключей",
@@ -224,14 +272,22 @@
"fetch_prices": "Получить цены", "fetch_prices": "Получить цены",
"file": "Файл", "file": "Файл",
"file_save_location": "Файл будет сохранён в: ~/.config/ObsidianDragon/", "file_save_location": "Файл будет сохранён в: ~/.config/ObsidianDragon/",
"filter": "Фильтр...",
"font_scale": "Масштаб шрифта", "font_scale": "Масштаб шрифта",
"force_quit": "Принудительный выход",
"force_quit_confirm_msg": "Это немедленно завершит демон без корректного завершения.\nЭто может повредить индекс блокчейна и потребовать повторной синхронизации.",
"force_quit_confirm_title": "Принудительный выход?",
"force_quit_warning": "Это немедленно завершит демон без корректного завершения. Может потребоваться повторная синхронизация блокчейна.",
"force_quit_yes": "Принудительный выход",
"from": "От", "from": "От",
"from_upper": "ОТ", "from_upper": "ОТ",
"full_details": "Полные детали", "full_details": "Полные детали",
"general": "Общие", "general": "Общие",
"generating": "Генерация",
"go_to_receive": "Перейти к получению", "go_to_receive": "Перейти к получению",
"height": "Высота", "height": "Высота",
"help": "Справка", "help": "Справка",
"hidden_tag": " (скрыт)",
"hide": "Скрыть", "hide": "Скрыть",
"hide_address": "Скрыть адрес", "hide_address": "Скрыть адрес",
"hide_zero_balances": "Скрыть нулевые балансы", "hide_zero_balances": "Скрыть нулевые балансы",
@@ -253,6 +309,9 @@
"import_key_warning": "Предупреждение: Никогда не делитесь своими приватными ключами! Импорт ключей из ненадёжных источников может скомпрометировать ваш кошелёк.", "import_key_warning": "Предупреждение: Никогда не делитесь своими приватными ключами! Импорт ключей из ненадёжных источников может скомпрометировать ваш кошелёк.",
"import_key_z_format": "Ключи расходования z-адресов (secret-extended-key-...)", "import_key_z_format": "Ключи расходования z-адресов (secret-extended-key-...)",
"import_private_key": "Импорт приватного ключа...", "import_private_key": "Импорт приватного ключа...",
"incorrect_passphrase": "Неверный пароль",
"incorrect_pin": "Неверный PIN",
"insufficient_funds": "Недостаточно средств для этой суммы плюс комиссия.",
"invalid_address": "Неверный формат адреса", "invalid_address": "Неверный формат адреса",
"ip_address": "IP-адрес", "ip_address": "IP-адрес",
"keep": "Сохранить", "keep": "Сохранить",
@@ -266,6 +325,7 @@
"key_export_viewing_keys_zonly": "Ключи просмотра доступны только для экранированных (z) адресов", "key_export_viewing_keys_zonly": "Ключи просмотра доступны только для экранированных (z) адресов",
"key_export_viewing_warning": "Этот ключ просмотра позволяет другим видеть входящие транзакции и баланс, но НЕ тратить ваши средства. Делитесь только с доверенными лицами.", "key_export_viewing_warning": "Этот ключ просмотра позволяет другим видеть входящие транзакции и баланс, но НЕ тратить ваши средства. Делитесь только с доверенными лицами.",
"label": "Метка:", "label": "Метка:",
"label_placeholder": "напр. Накопления, Майнинг...",
"language": "Язык", "language": "Язык",
"light": "Светлая", "light": "Светлая",
"loading": "Загрузка...", "loading": "Загрузка...",
@@ -286,6 +346,7 @@
"market_now": "Сейчас", "market_now": "Сейчас",
"market_pct_shielded": "%.0f%% Экранировано", "market_pct_shielded": "%.0f%% Экранировано",
"market_portfolio": "ПОРТФЕЛЬ", "market_portfolio": "ПОРТФЕЛЬ",
"market_price_loading": "Загрузка данных о ценах...",
"market_price_unavailable": "Данные о ценах недоступны", "market_price_unavailable": "Данные о ценах недоступны",
"market_refresh_price": "Обновить данные о ценах", "market_refresh_price": "Обновить данные о ценах",
"market_trade_on": "Торговать на %s", "market_trade_on": "Торговать на %s",
@@ -311,6 +372,13 @@
"mining_address_copied": "Адрес майнинга скопирован", "mining_address_copied": "Адрес майнинга скопирован",
"mining_all_time": "За всё время", "mining_all_time": "За всё время",
"mining_already_saved": "URL пула уже сохранён", "mining_already_saved": "URL пула уже сохранён",
"mining_benchmark_cancel": "Отменить тест",
"mining_benchmark_cooling": "Охлаждение",
"mining_benchmark_dismiss": "Закрыть",
"mining_benchmark_result": "Оптимально",
"mining_benchmark_stabilizing": "Стабилизация",
"mining_benchmark_testing": "Тестирование",
"mining_benchmark_tooltip": "Найти оптимальное количество потоков для этого процессора",
"mining_block_copied": "Хэш блока скопирован", "mining_block_copied": "Хэш блока скопирован",
"mining_chart_1m_ago": "1м назад", "mining_chart_1m_ago": "1м назад",
"mining_chart_5m_ago": "5м назад", "mining_chart_5m_ago": "5м назад",
@@ -330,8 +398,15 @@
"mining_filter_tip_all": "Показать все доходы", "mining_filter_tip_all": "Показать все доходы",
"mining_filter_tip_pool": "Показать только доходы пула", "mining_filter_tip_pool": "Показать только доходы пула",
"mining_filter_tip_solo": "Показать только доходы соло", "mining_filter_tip_solo": "Показать только доходы соло",
"mining_generate_z_address_hint": "Создайте Z-адрес на вкладке «Получить» для использования в качестве адреса выплат",
"mining_idle_gpu_off_tooltip": "Без ограничений: ВКЛ\nТолько ввод с клавиатуры/мыши определяет состояние простоя\nНажмите для GPU-контроля",
"mining_idle_gpu_on_tooltip": "GPU-контроль: ВКЛ\nАктивность GPU (видео, игры) предотвращает майнинг в простое\nНажмите для режима без ограничений",
"mining_idle_off_tooltip": "Включить майнинг в простое", "mining_idle_off_tooltip": "Включить майнинг в простое",
"mining_idle_on_tooltip": "Отключить майнинг в простое", "mining_idle_on_tooltip": "Отключить майнинг в простое",
"mining_idle_scale_off_tooltip": "Режим старт/стоп: ВКЛ\nНажмите для переключения на масштабирование потоков",
"mining_idle_scale_on_tooltip": "Масштабирование потоков: ВКЛ\nНажмите для переключения на режим старт/стоп",
"mining_idle_threads_active_tooltip": "Потоки при активности пользователя",
"mining_idle_threads_idle_tooltip": "Потоки при простое системы",
"mining_local_hashrate": "Локальный хешрейт", "mining_local_hashrate": "Локальный хешрейт",
"mining_mine": "Майнить", "mining_mine": "Майнить",
"mining_mining_addr": "Адрес майн.", "mining_mining_addr": "Адрес майн.",
@@ -388,6 +463,7 @@
"no_addresses_available": "Нет доступных адресов", "no_addresses_available": "Нет доступных адресов",
"no_addresses_match": "Нет адресов, соответствующих фильтру", "no_addresses_match": "Нет адресов, соответствующих фильтру",
"no_addresses_with_balance": "Нет адресов с балансом", "no_addresses_with_balance": "Нет адресов с балансом",
"no_addresses_yet": "Пока нет адресов",
"no_matching": "Нет подходящих транзакций", "no_matching": "Нет подходящих транзакций",
"no_recent_receives": "Нет недавних получений", "no_recent_receives": "Нет недавних получений",
"no_recent_sends": "Нет недавних отправлений", "no_recent_sends": "Нет недавних отправлений",
@@ -453,6 +529,7 @@
"peers_upper": "УЗЛЫ", "peers_upper": "УЗЛЫ",
"peers_version": "Версия", "peers_version": "Версия",
"pending": "Ожидание", "pending": "Ожидание",
"pin_not_set": "PIN не установлен. Используйте пароль для разблокировки.",
"ping": "Пинг", "ping": "Пинг",
"price_chart": "График цен", "price_chart": "График цен",
"qr_code": "QR-код", "qr_code": "QR-код",
@@ -473,7 +550,9 @@
"recent_received": "НЕДАВНО ПОЛУЧЕНО", "recent_received": "НЕДАВНО ПОЛУЧЕНО",
"recent_sends": "НЕДАВНО ОТПРАВЛЕНО", "recent_sends": "НЕДАВНО ОТПРАВЛЕНО",
"recipient": "ПОЛУЧАТЕЛЬ", "recipient": "ПОЛУЧАТЕЛЬ",
"recipient_balance": "Получатель: %.8f → %.8f DRGX",
"recv_type": "Получ.", "recv_type": "Получ.",
"reduce_motion": "Уменьшить анимацию",
"refresh": "Обновить", "refresh": "Обновить",
"refresh_now": "Обновить сейчас", "refresh_now": "Обновить сейчас",
"remove_favorite": "Удалить из избранного", "remove_favorite": "Удалить из избранного",
@@ -493,7 +572,10 @@
"request_uri_copied": "URI платежа скопирован в буфер обмена", "request_uri_copied": "URI платежа скопирован в буфер обмена",
"rescan": "Пересканировать", "rescan": "Пересканировать",
"reset_to_defaults": "Сбросить настройки", "reset_to_defaults": "Сбросить настройки",
"restarting_after_encryption": "Перезапуск демона после шифрования...",
"restore_address": "Восстановить адрес", "restore_address": "Восстановить адрес",
"result_preview": "Предпросмотр результата",
"retry": "Повторить",
"review_send": "Проверить отправку", "review_send": "Проверить отправку",
"rpc_host": "RPC-хост", "rpc_host": "RPC-хост",
"rpc_pass": "Пароль", "rpc_pass": "Пароль",
@@ -502,6 +584,39 @@
"save": "Сохранить", "save": "Сохранить",
"save_settings": "Сохранить настройки", "save_settings": "Сохранить настройки",
"save_z_transactions": "Сохранять Z-tx в списке транзакций", "save_z_transactions": "Сохранять Z-tx в списке транзакций",
"sb_auth_failed": "Ошибка авторизации — проверьте rpcuser/rpcpassword",
"sb_block": "Блок: %d",
"sb_connecting_daemon": "Подключение к dragonxd...",
"sb_connecting_err": "Подключение к демону — %s",
"sb_connecting_external": "Подключение к внешнему демону...",
"sb_connecting_generic": "Подключение к демону...",
"sb_daemon_crashed": "Демон упал %d раз",
"sb_daemon_not_found": "Демон не найден",
"sb_dragonxd_running": "dragonxd запущен",
"sb_dragonxd_stopped": "dragonxd остановлен",
"sb_dragonxd_stopping": "Остановка dragonxd...",
"sb_extracting_sapling": "Извлечение параметров Sapling...",
"sb_importing_keys": "Импорт ключей",
"sb_loading_config": "Загрузка конфигурации...",
"sb_mining_hs": "%.1f H/s",
"sb_net_ghs": "Сеть: %.2f GH/s",
"sb_net_hs": "Сеть: %.1f H/s",
"sb_net_khs": "Сеть: %.2f KH/s",
"sb_net_mhs": "Сеть: %.2f MH/s",
"sb_no_conf": "DRAGONX.conf не найден",
"sb_peers": "Пиры: %zu",
"sb_rescanning": "Пересканирование",
"sb_rescanning_pct": "Пересканирование %.0f%%",
"sb_restarting_daemon": "Перезапуск демона...",
"sb_sapling_failed": "Ошибка извлечения параметров Sapling.",
"sb_sapling_not_found": "Параметры Sapling не найдены.",
"sb_starting_daemon": "Запуск dragonxd...",
"sb_syncing_basic": "Синхронизация %.1f%% (%d осталось)",
"sb_syncing_eta": "Синхронизация %.1f%% (%d осталось, %.0f блк/с, ~%s)",
"sb_waiting_config": "Ожидание конфигурации демона...",
"sb_waiting_daemon": "Ожидание dragonxd...",
"sb_waiting_daemon_err": "Ожидание dragonxd — %s",
"sb_warming_up": "Прогрев...",
"search_placeholder": "Поиск...", "search_placeholder": "Поиск...",
"security": "БЕЗОПАСНОСТЬ", "security": "БЕЗОПАСНОСТЬ",
"select_address": "Выбрать адрес...", "select_address": "Выбрать адрес...",
@@ -553,12 +668,15 @@
"send_valid_transparent": "Действительный прозрачный адрес", "send_valid_transparent": "Действительный прозрачный адрес",
"send_wallet_empty": "Ваш кошелёк пуст", "send_wallet_empty": "Ваш кошелёк пуст",
"send_yes_clear": "Да, очистить", "send_yes_clear": "Да, очистить",
"sender_balance": "Отправитель: %.8f → %.8f DRGX",
"sending": "Отправка транзакции", "sending": "Отправка транзакции",
"sending_from": "ОТПРАВКА С", "sending_from": "ОТПРАВКА С",
"sends_full_balance_warning": "Это отправит весь баланс. Адрес отправителя останется с нулевым балансом.",
"sent": "отправлено", "sent": "отправлено",
"sent_filter": "Отправлено", "sent_filter": "Отправлено",
"sent_type": "Отправлено", "sent_type": "Отправлено",
"sent_upper": "ОТПРАВЛЕНО", "sent_upper": "ОТПРАВЛЕНО",
"set_label": "Установить метку...",
"settings": "Настройки", "settings": "Настройки",
"settings_about_text": "Защищённый криптовалютный кошелёк для DragonX (DRGX), созданный на Dear ImGui для лёгкого и портативного использования.", "settings_about_text": "Защищённый криптовалютный кошелёк для DragonX (DRGX), созданный на Dear ImGui для лёгкого и портативного использования.",
"settings_acrylic_level": "Уровень акрила:", "settings_acrylic_level": "Уровень акрила:",
@@ -647,13 +765,17 @@
"shield_utxo_limit": "Лимит UTXO:", "shield_utxo_limit": "Лимит UTXO:",
"shield_wildcard_hint": "Используйте '*' для экранирования со всех прозрачных адресов", "shield_wildcard_hint": "Используйте '*' для экранирования со всех прозрачных адресов",
"shielded": "Экранированный", "shielded": "Экранированный",
"shielded_address": "Экранированный адрес",
"shielded_to": "ЭКРАНИРОВАНО НА", "shielded_to": "ЭКРАНИРОВАНО НА",
"shielded_type": "Экранированный", "shielded_type": "Экранированный",
"shielding_notice": "Примечание: это переведёт средства с прозрачного (T) адреса на приватный (Z) адрес.",
"show": "Показать", "show": "Показать",
"show_hidden": "Показать скрытые (%d)", "show_hidden": "Показать скрытые (%d)",
"show_qr_code": "Показать QR-код", "show_qr_code": "Показать QR-код",
"showing_transactions": "Показано %d–%d из %d транзакций (всего: %zu)", "showing_transactions": "Показано %d%d из %d транзакций (всего: %zu)",
"showing_x_of_y": "Показано %d из %d адресов",
"simple_background": "Простой фон", "simple_background": "Простой фон",
"slider_off": "Выкл",
"start_mining": "Начать майнинг", "start_mining": "Начать майнинг",
"status": "Статус", "status": "Статус",
"stop_external": "Остановить внешний daemon", "stop_external": "Остановить внешний daemon",
@@ -662,17 +784,26 @@
"success": "Успешно", "success": "Успешно",
"summary": "Итоги", "summary": "Итоги",
"syncing": "Синхронизация...", "syncing": "Синхронизация...",
"t_address": "T-адрес",
"t_addresses": "T-адреса", "t_addresses": "T-адреса",
"test_connection": "Тест", "test_connection": "Тест",
"theme": "Тема", "theme": "Тема",
"theme_effects": "Эффекты темы", "theme_effects": "Эффекты темы",
"theme_language": "ТЕМА И ЯЗЫК",
"time_days_ago": "%d дней назад", "time_days_ago": "%d дней назад",
"time_hours_ago": "%d часов назад", "time_hours_ago": "%d часов назад",
"time_minutes_ago": "%d минут назад", "time_minutes_ago": "%d минут назад",
"time_seconds_ago": "%d секунд назад", "time_seconds_ago": "%d секунд назад",
"timeout_15min": "15 мин",
"timeout_1hour": "1 час",
"timeout_1min": "1 мин",
"timeout_30min": "30 мин",
"timeout_5min": "5 мин",
"timeout_off": "Выкл",
"to": "Кому", "to": "Кому",
"to_upper": "КОМУ", "to_upper": "КОМУ",
"tools": "ИНСТРУМЕНТЫ", "tools": "УТИЛИТЫ",
"tools_actions": "Инструменты и действия...",
"total": "Итого", "total": "Итого",
"transaction_id": "ID ТРАНЗАКЦИИ", "transaction_id": "ID ТРАНЗАКЦИИ",
"transaction_sent": "Транзакция успешно отправлена", "transaction_sent": "Транзакция успешно отправлена",
@@ -680,7 +811,13 @@
"transaction_url": "URL транзакции", "transaction_url": "URL транзакции",
"transactions": "Транзакции", "transactions": "Транзакции",
"transactions_upper": "ТРАНЗАКЦИИ", "transactions_upper": "ТРАНЗАКЦИИ",
"transfer_failed": "Ошибка перевода",
"transfer_funds": "Перевести средства",
"transfer_sent": "Перевод отправлен",
"transfer_sent_desc": "Ваш перевод отправлен в сеть.",
"transfer_to": "Перевести на:",
"transparent": "Прозрачный", "transparent": "Прозрачный",
"transparent_address": "Прозрачный адрес",
"tt_addr_url": "Базовый URL для просмотра адресов в обозревателе блоков", "tt_addr_url": "Базовый URL для просмотра адресов в обозревателе блоков",
"tt_address_book": "Управление сохранёнными адресами для быстрой отправки", "tt_address_book": "Управление сохранёнными адресами для быстрой отправки",
"tt_auto_lock": "Заблокировать кошелёк после этого времени бездействия", "tt_auto_lock": "Заблокировать кошелёк после этого времени бездействия",
@@ -695,6 +832,8 @@
"tt_custom_theme": "Пользовательская тема активна", "tt_custom_theme": "Пользовательская тема активна",
"tt_debug_collapse": "Свернуть параметры журнала отладки", "tt_debug_collapse": "Свернуть параметры журнала отладки",
"tt_debug_expand": "Развернуть параметры журнала отладки", "tt_debug_expand": "Развернуть параметры журнала отладки",
"tt_delete_blockchain": "Удалить все данные блокчейна и начать синхронизацию заново. wallet.dat и конфигурация сохраняются.",
"tt_download_bootstrap": "Скачать бутстрап блокчейна для ускорения синхронизации\nСуществующие данные блоков будут заменены",
"tt_encrypt": "Зашифровать wallet.dat паролем", "tt_encrypt": "Зашифровать wallet.dat паролем",
"tt_export_all": "Экспортировать все приватные ключи в файл", "tt_export_all": "Экспортировать все приватные ключи в файл",
"tt_export_csv": "Экспортировать историю транзакций в виде таблицы CSV", "tt_export_csv": "Экспортировать историю транзакций в виде таблицы CSV",
@@ -712,6 +851,7 @@
"tt_mine_idle": "Автоматически начать майнинг при\\nпростое системы (нет ввода с клавиатуры/мыши)", "tt_mine_idle": "Автоматически начать майнинг при\\nпростое системы (нет ввода с клавиатуры/мыши)",
"tt_noise": "Интенсивность зернистой текстуры (0%% = выкл., 100%% = максимум)", "tt_noise": "Интенсивность зернистой текстуры (0%% = выкл., 100%% = максимум)",
"tt_open_dir": "Нажмите, чтобы открыть в проводнике", "tt_open_dir": "Нажмите, чтобы открыть в проводнике",
"tt_reduce_motion": "Отключить анимированные переходы и плавное изменение баланса для доступности",
"tt_remove_encrypt": "Удалить шифрование и хранить кошелёк без защиты", "tt_remove_encrypt": "Удалить шифрование и хранить кошелёк без защиты",
"tt_remove_pin": "Удалить PIN и требовать пароль для разблокировки", "tt_remove_pin": "Удалить PIN и требовать пароль для разблокировки",
"tt_report_bug": "Сообщить о проблеме в трекере проекта", "tt_report_bug": "Сообщить о проблеме в трекере проекта",
@@ -789,7 +929,9 @@
"warning_upper": "ПРЕДУПРЕЖДЕНИЕ!", "warning_upper": "ПРЕДУПРЕЖДЕНИЕ!",
"website": "Веб-сайт", "website": "Веб-сайт",
"window_opacity": "Прозрачность окна", "window_opacity": "Прозрачность окна",
"wizard_daemon_start_failed": "Не удалось запустить демон — будет повторено автоматически",
"yes_clear": "Да, очистить", "yes_clear": "Да, очистить",
"your_addresses": "Ваши адреса", "your_addresses": "Ваши адреса",
"z_address": "Z-адрес",
"z_addresses": "Z-адреса" "z_addresses": "Z-адреса"
} }

View File

@@ -43,6 +43,8 @@
"address_url": "地址 URL", "address_url": "地址 URL",
"addresses_appear_here": "连接后,您的接收地址将显示在此处。", "addresses_appear_here": "连接后,您的接收地址将显示在此处。",
"advanced": "高级", "advanced": "高级",
"advanced_effects": "高级特效...",
"ago": "前",
"all_filter": "全部", "all_filter": "全部",
"allow_custom_fees": "允许自定义手续费", "allow_custom_fees": "允许自定义手续费",
"amount": "金额", "amount": "金额",
@@ -90,12 +92,30 @@
"block_timestamp": "时间戳:", "block_timestamp": "时间戳:",
"block_transactions": "交易:", "block_transactions": "交易:",
"blockchain_syncing": "区块链同步中 (%.1f%%)... 余额可能不准确。", "blockchain_syncing": "区块链同步中 (%.1f%%)... 余额可能不准确。",
"bootstrap_daemon_running": "守护进程运行中",
"bootstrap_daemon_stopped": "守护进程已停止",
"bootstrap_daemon_stopping": "正在停止守护进程...",
"bootstrap_desc": "下载区块链引导程序以大幅加速初始同步。这将下载区块链快照并将其提取到您的数据目录中。",
"bootstrap_downloading": "正在下载引导程序...",
"bootstrap_extracting": "正在提取区块链数据...",
"bootstrap_failed": "引导程序失败",
"bootstrap_mirror": "镜像",
"bootstrap_mirror_tooltip": "从镜像下载 (bootstrap2.dragonx.is)。\n如果主下载速度慢或失败请使用此选项。",
"bootstrap_restart_daemon": "重启守护进程",
"bootstrap_success": "引导程序完成",
"bootstrap_success_desc": "区块链数据已成功提取。启动守护进程以从引导点开始同步。",
"bootstrap_trust_warning": "仅使用 bootstrap.dragonx.is 或 bootstrap2.dragonx.is。使用不受信任来源的文件可能会危及您的节点。",
"bootstrap_verifying": "正在验证校验和...",
"bootstrap_wallet_protected": "(wallet.dat 已受保护)",
"bootstrap_warning": "现有区块数据blocks、chainstate、notarizations将被删除并替换。您的 wallet.dat 不会被修改或删除。",
"cancel": "取消", "cancel": "取消",
"characters": "字符", "characters": "字符",
"choose_icon": "选择图标",
"clear": "清除", "clear": "清除",
"clear_all_bans": "解除所有封禁", "clear_all_bans": "解除所有封禁",
"clear_anyway": "仍然清除", "clear_anyway": "仍然清除",
"clear_form_confirm": "清除所有表单字段?", "clear_form_confirm": "清除所有表单字段?",
"clear_icon": "清除图标",
"clear_request": "清除请求", "clear_request": "清除请求",
"click_copy_address": "点击复制地址", "click_copy_address": "点击复制地址",
"click_copy_uri": "点击复制 URI", "click_copy_uri": "点击复制 URI",
@@ -106,8 +126,12 @@
"confirm_clear_ztx_title": "确认清除 Z-Tx 历史", "confirm_clear_ztx_title": "确认清除 Z-Tx 历史",
"confirm_clear_ztx_warning1": "清除 z-交易历史可能导致您的屏蔽余额显示为 0直到执行钱包重新扫描。", "confirm_clear_ztx_warning1": "清除 z-交易历史可能导致您的屏蔽余额显示为 0直到执行钱包重新扫描。",
"confirm_clear_ztx_warning2": "如果发生这种情况,您需要在启用重新扫描的情况下重新导入 z-地址私钥以恢复余额。", "confirm_clear_ztx_warning2": "如果发生这种情况,您需要在启用重新扫描的情况下重新导入 z-地址私钥以恢复余额。",
"confirm_delete_blockchain_msg": "这将停止守护进程删除所有区块链数据blocks、chainstate、peers并从头开始重新同步。这可能需要几个小时。",
"confirm_delete_blockchain_safe": "您的 wallet.dat、配置和交易历史是安全的不会被删除。",
"confirm_delete_blockchain_title": "删除区块链数据",
"confirm_send": "确认发送", "confirm_send": "确认发送",
"confirm_transaction": "确认交易", "confirm_transaction": "确认交易",
"confirm_transfer": "确认转账",
"confirmations": "确认数", "confirmations": "确认数",
"confirmations_display": "%d 次确认 | %s", "confirmations_display": "%d 次确认 | %s",
"confirmed": "已确认", "confirmed": "已确认",
@@ -172,6 +196,7 @@
"console_welcome": "欢迎使用 ObsidianDragon 控制台", "console_welcome": "欢迎使用 ObsidianDragon 控制台",
"console_zoom_in": "放大", "console_zoom_in": "放大",
"console_zoom_out": "缩小", "console_zoom_out": "缩小",
"copied": "已复制!",
"copy": "复制", "copy": "复制",
"copy_address": "复制完整地址", "copy_address": "复制完整地址",
"copy_error": "复制错误", "copy_error": "复制错误",
@@ -180,15 +205,21 @@
"copy_uri": "复制 URI", "copy_uri": "复制 URI",
"current_price": "当前价格", "current_price": "当前价格",
"custom_fees": "自定义手续费", "custom_fees": "自定义手续费",
"daemon_version": "守护进程",
"dark": "深色", "dark": "深色",
"date": "日期", "date": "日期",
"date_label": "日期:", "date_label": "日期:",
"debug_logging": "调试日志", "debug_logging": "调试日志",
"delete": "删除", "delete": "删除",
"delete_blockchain": "删除区块链",
"delete_blockchain_confirm": "删除并重新同步",
"deshielding_warning": "警告:这将把资金从隐私 (Z) 地址转移到透明 (T) 地址。",
"difficulty": "难度", "difficulty": "难度",
"disconnected": "已断开", "disconnected": "已断开",
"dismiss": "关闭", "dismiss": "关闭",
"display": "显示", "display": "显示",
"download": "下载",
"download_bootstrap": "下载引导程序",
"dragonx_green": "DragonX绿色", "dragonx_green": "DragonX绿色",
"edit": "编辑", "edit": "编辑",
"error": "错误", "error": "错误",
@@ -196,6 +227,23 @@
"est_time_to_block": "预计出块时间", "est_time_to_block": "预计出块时间",
"exit": "退出", "exit": "退出",
"explorer": "浏览器", "explorer": "浏览器",
"explorer_block_detail": "区块",
"explorer_block_hash": "哈希",
"explorer_block_height": "高度",
"explorer_block_merkle": "Merkle 根",
"explorer_block_size": "大小",
"explorer_block_time": "时间",
"explorer_block_txs": "交易",
"explorer_chain_stats": "链",
"explorer_invalid_query": "输入区块高度或64位哈希",
"explorer_mempool": "内存池",
"explorer_mempool_size": "大小",
"explorer_mempool_txs": "交易",
"explorer_recent_blocks": "最近区块",
"explorer_search": "搜索",
"explorer_section": "浏览器",
"explorer_tx_outputs": "输出",
"explorer_tx_size": "大小",
"export": "导出", "export": "导出",
"export_csv": "导出 CSV", "export_csv": "导出 CSV",
"export_keys_btn": "导出密钥", "export_keys_btn": "导出密钥",
@@ -224,14 +272,22 @@
"fetch_prices": "获取价格", "fetch_prices": "获取价格",
"file": "文件", "file": "文件",
"file_save_location": "文件将保存至:~/.config/ObsidianDragon/", "file_save_location": "文件将保存至:~/.config/ObsidianDragon/",
"filter": "筛选...",
"font_scale": "字体大小", "font_scale": "字体大小",
"force_quit": "强制退出",
"force_quit_confirm_msg": "这将立即终止守护进程而不进行正常关闭。\n这可能会损坏区块链索引并需要重新同步。",
"force_quit_confirm_title": "强制退出?",
"force_quit_warning": "这将立即终止守护进程而不进行正常关闭。可能需要重新同步区块链。",
"force_quit_yes": "强制退出",
"from": "从", "from": "从",
"from_upper": "从", "from_upper": "从",
"full_details": "完整详情", "full_details": "完整详情",
"general": "常规", "general": "常规",
"generating": "正在生成",
"go_to_receive": "前往接收", "go_to_receive": "前往接收",
"height": "高度", "height": "高度",
"help": "帮助", "help": "帮助",
"hidden_tag": " (已隐藏)",
"hide": "隐藏", "hide": "隐藏",
"hide_address": "隐藏地址", "hide_address": "隐藏地址",
"hide_zero_balances": "隐藏零余额", "hide_zero_balances": "隐藏零余额",
@@ -253,6 +309,9 @@
"import_key_warning": "警告:切勿分享您的私钥!从不可信来源导入密钥可能会危及您的钱包安全。", "import_key_warning": "警告:切勿分享您的私钥!从不可信来源导入密钥可能会危及您的钱包安全。",
"import_key_z_format": "Z 地址花费密钥 (secret-extended-key-...)", "import_key_z_format": "Z 地址花费密钥 (secret-extended-key-...)",
"import_private_key": "导入私钥...", "import_private_key": "导入私钥...",
"incorrect_passphrase": "密码错误",
"incorrect_pin": "PIN 错误",
"insufficient_funds": "余额不足以支付此金额加手续费。",
"invalid_address": "无效的地址格式", "invalid_address": "无效的地址格式",
"ip_address": "IP 地址", "ip_address": "IP 地址",
"keep": "保留", "keep": "保留",
@@ -266,6 +325,7 @@
"key_export_viewing_keys_zonly": "查看密钥仅适用于屏蔽 (z) 地址", "key_export_viewing_keys_zonly": "查看密钥仅适用于屏蔽 (z) 地址",
"key_export_viewing_warning": "此查看密钥允许他人查看您的入账交易和余额,但不能花费您的资金。仅与信任的人分享。", "key_export_viewing_warning": "此查看密钥允许他人查看您的入账交易和余额,但不能花费您的资金。仅与信任的人分享。",
"label": "标签:", "label": "标签:",
"label_placeholder": "例如 储蓄、挖矿...",
"language": "语言", "language": "语言",
"light": "浅色", "light": "浅色",
"loading": "加载中...", "loading": "加载中...",
@@ -286,6 +346,7 @@
"market_now": "现在", "market_now": "现在",
"market_pct_shielded": "%.0f%% 屏蔽", "market_pct_shielded": "%.0f%% 屏蔽",
"market_portfolio": "投资组合", "market_portfolio": "投资组合",
"market_price_loading": "正在加载价格数据...",
"market_price_unavailable": "价格数据不可用", "market_price_unavailable": "价格数据不可用",
"market_refresh_price": "刷新价格数据", "market_refresh_price": "刷新价格数据",
"market_trade_on": "在 %s 交易", "market_trade_on": "在 %s 交易",
@@ -311,6 +372,13 @@
"mining_address_copied": "挖矿地址已复制", "mining_address_copied": "挖矿地址已复制",
"mining_all_time": "所有时间", "mining_all_time": "所有时间",
"mining_already_saved": "矿池 URL 已保存", "mining_already_saved": "矿池 URL 已保存",
"mining_benchmark_cancel": "取消基准测试",
"mining_benchmark_cooling": "冷却中",
"mining_benchmark_dismiss": "关闭",
"mining_benchmark_result": "最佳",
"mining_benchmark_stabilizing": "稳定中",
"mining_benchmark_testing": "测试中",
"mining_benchmark_tooltip": "为此 CPU 找到最佳线程数",
"mining_block_copied": "区块哈希已复制", "mining_block_copied": "区块哈希已复制",
"mining_chart_1m_ago": "1分钟前", "mining_chart_1m_ago": "1分钟前",
"mining_chart_5m_ago": "5分钟前", "mining_chart_5m_ago": "5分钟前",
@@ -330,8 +398,15 @@
"mining_filter_tip_all": "显示所有收益", "mining_filter_tip_all": "显示所有收益",
"mining_filter_tip_pool": "仅显示矿池收益", "mining_filter_tip_pool": "仅显示矿池收益",
"mining_filter_tip_solo": "仅显示单人收益", "mining_filter_tip_solo": "仅显示单人收益",
"mining_generate_z_address_hint": "在接收标签页生成一个 Z 地址作为支付地址",
"mining_idle_gpu_off_tooltip": "无限制:开启\n仅键盘/鼠标输入决定空闲状态\n点击启用GPU感知检测",
"mining_idle_gpu_on_tooltip": "GPU感知开启\nGPU活动视频、游戏阻止空闲挖矿\n点击切换到无限制模式",
"mining_idle_off_tooltip": "启用空闲挖矿", "mining_idle_off_tooltip": "启用空闲挖矿",
"mining_idle_on_tooltip": "禁用空闲挖矿", "mining_idle_on_tooltip": "禁用空闲挖矿",
"mining_idle_scale_off_tooltip": "启动/停止模式:开启\n点击切换到线程缩放模式",
"mining_idle_scale_on_tooltip": "线程缩放:开启\n点击切换到启动/停止模式",
"mining_idle_threads_active_tooltip": "用户活跃时的线程数",
"mining_idle_threads_idle_tooltip": "系统空闲时的线程数",
"mining_local_hashrate": "本地算力", "mining_local_hashrate": "本地算力",
"mining_mine": "挖矿", "mining_mine": "挖矿",
"mining_mining_addr": "挖矿地址", "mining_mining_addr": "挖矿地址",
@@ -388,6 +463,7 @@
"no_addresses_available": "无可用地址", "no_addresses_available": "无可用地址",
"no_addresses_match": "没有匹配过滤器的地址", "no_addresses_match": "没有匹配过滤器的地址",
"no_addresses_with_balance": "没有有余额的地址", "no_addresses_with_balance": "没有有余额的地址",
"no_addresses_yet": "暂无地址",
"no_matching": "没有匹配的交易", "no_matching": "没有匹配的交易",
"no_recent_receives": "没有最近的接收", "no_recent_receives": "没有最近的接收",
"no_recent_sends": "没有最近的发送", "no_recent_sends": "没有最近的发送",
@@ -453,6 +529,7 @@
"peers_upper": "节点", "peers_upper": "节点",
"peers_version": "版本", "peers_version": "版本",
"pending": "待处理", "pending": "待处理",
"pin_not_set": "未设置 PIN。使用密码解锁。",
"ping": "延迟", "ping": "延迟",
"price_chart": "价格图表", "price_chart": "价格图表",
"qr_code": "二维码", "qr_code": "二维码",
@@ -473,7 +550,9 @@
"recent_received": "最近接收", "recent_received": "最近接收",
"recent_sends": "最近发送", "recent_sends": "最近发送",
"recipient": "收款方", "recipient": "收款方",
"recipient_balance": "接收方: %.8f → %.8f DRGX",
"recv_type": "接收", "recv_type": "接收",
"reduce_motion": "减少动画",
"refresh": "刷新", "refresh": "刷新",
"refresh_now": "立即刷新", "refresh_now": "立即刷新",
"remove_favorite": "移除收藏", "remove_favorite": "移除收藏",
@@ -493,7 +572,10 @@
"request_uri_copied": "付款 URI 已复制到剪贴板", "request_uri_copied": "付款 URI 已复制到剪贴板",
"rescan": "重新扫描", "rescan": "重新扫描",
"reset_to_defaults": "重置为默认值", "reset_to_defaults": "重置为默认值",
"restarting_after_encryption": "加密后重启守护进程...",
"restore_address": "恢复地址", "restore_address": "恢复地址",
"result_preview": "结果预览",
"retry": "重试",
"review_send": "审核发送", "review_send": "审核发送",
"rpc_host": "RPC 主机", "rpc_host": "RPC 主机",
"rpc_pass": "密码", "rpc_pass": "密码",
@@ -502,6 +584,39 @@
"save": "保存", "save": "保存",
"save_settings": "保存设置", "save_settings": "保存设置",
"save_z_transactions": "将 Z 交易保存到列表", "save_z_transactions": "将 Z 交易保存到列表",
"sb_auth_failed": "认证失败 — 请检查 rpcuser/rpcpassword",
"sb_block": "区块: %d",
"sb_connecting_daemon": "正在连接 dragonxd...",
"sb_connecting_err": "连接守护进程 — %s",
"sb_connecting_external": "正在连接外部守护进程...",
"sb_connecting_generic": "正在连接守护进程...",
"sb_daemon_crashed": "守护进程崩溃 %d 次",
"sb_daemon_not_found": "未找到守护进程",
"sb_dragonxd_running": "dragonxd 运行中",
"sb_dragonxd_stopped": "dragonxd 已停止",
"sb_dragonxd_stopping": "正在停止 dragonxd...",
"sb_extracting_sapling": "正在提取 Sapling 参数...",
"sb_importing_keys": "正在导入密钥",
"sb_loading_config": "正在加载配置...",
"sb_mining_hs": "%.1f H/s",
"sb_net_ghs": "网络: %.2f GH/s",
"sb_net_hs": "网络: %.1f H/s",
"sb_net_khs": "网络: %.2f KH/s",
"sb_net_mhs": "网络: %.2f MH/s",
"sb_no_conf": "未找到 DRAGONX.conf",
"sb_peers": "节点: %zu",
"sb_rescanning": "重新扫描",
"sb_rescanning_pct": "重新扫描 %.0f%%",
"sb_restarting_daemon": "正在重启守护进程...",
"sb_sapling_failed": "提取 Sapling 参数失败。",
"sb_sapling_not_found": "未找到 Sapling 参数。",
"sb_starting_daemon": "正在启动 dragonxd...",
"sb_syncing_basic": "同步中 %.1f%% (剩余 %d)",
"sb_syncing_eta": "同步中 %.1f%% (剩余 %d, %.0f 块/秒, ~%s)",
"sb_waiting_config": "等待守护进程配置...",
"sb_waiting_daemon": "等待 dragonxd 启动...",
"sb_waiting_daemon_err": "等待 dragonxd — %s",
"sb_warming_up": "正在预热...",
"search_placeholder": "搜索...", "search_placeholder": "搜索...",
"security": "安全", "security": "安全",
"select_address": "选择地址...", "select_address": "选择地址...",
@@ -553,12 +668,15 @@
"send_valid_transparent": "有效的透明地址", "send_valid_transparent": "有效的透明地址",
"send_wallet_empty": "您的钱包是空的", "send_wallet_empty": "您的钱包是空的",
"send_yes_clear": "是,清除", "send_yes_clear": "是,清除",
"sender_balance": "发送方: %.8f → %.8f DRGX",
"sending": "正在发送交易", "sending": "正在发送交易",
"sending_from": "发送来源", "sending_from": "发送来源",
"sends_full_balance_warning": "这将发送全部余额。发送地址将变为零余额。",
"sent": "已发送", "sent": "已发送",
"sent_filter": "已发送", "sent_filter": "已发送",
"sent_type": "已发送", "sent_type": "已发送",
"sent_upper": "已发送", "sent_upper": "已发送",
"set_label": "设置标签...",
"settings": "设置", "settings": "设置",
"settings_about_text": "DragonX (DRGX) 屏蔽加密货币钱包,使用 Dear ImGui 构建,提供轻量、便携的体验。", "settings_about_text": "DragonX (DRGX) 屏蔽加密货币钱包,使用 Dear ImGui 构建,提供轻量、便携的体验。",
"settings_acrylic_level": "亚克力级别:", "settings_acrylic_level": "亚克力级别:",
@@ -647,13 +765,17 @@
"shield_utxo_limit": "UTXO 限制:", "shield_utxo_limit": "UTXO 限制:",
"shield_wildcard_hint": "使用 '*' 从所有透明地址屏蔽", "shield_wildcard_hint": "使用 '*' 从所有透明地址屏蔽",
"shielded": "屏蔽", "shielded": "屏蔽",
"shielded_address": "隐蔽地址",
"shielded_to": "屏蔽至", "shielded_to": "屏蔽至",
"shielded_type": "屏蔽", "shielded_type": "屏蔽",
"shielding_notice": "注意:这将把资金从透明 (T) 地址转移到隐私 (Z) 地址。",
"show": "显示", "show": "显示",
"show_hidden": "显示已隐藏 (%d)", "show_hidden": "显示已隐藏 (%d)",
"show_qr_code": "显示二维码", "show_qr_code": "显示二维码",
"showing_transactions": "显示第 %d–%d 笔,共 %d 笔交易(总计:%zu", "showing_transactions": "显示第 %d%d 笔,共 %d 笔交易(总计:%zu",
"showing_x_of_y": "显示 %d / %d 个地址",
"simple_background": "简单背景", "simple_background": "简单背景",
"slider_off": "关闭",
"start_mining": "开始挖矿", "start_mining": "开始挖矿",
"status": "状态", "status": "状态",
"stop_external": "停止外部守护进程", "stop_external": "停止外部守护进程",
@@ -662,17 +784,26 @@
"success": "成功", "success": "成功",
"summary": "摘要", "summary": "摘要",
"syncing": "同步中...", "syncing": "同步中...",
"t_address": "T 地址",
"t_addresses": "T 地址", "t_addresses": "T 地址",
"test_connection": "测试", "test_connection": "测试",
"theme": "主题", "theme": "主题",
"theme_effects": "主题效果", "theme_effects": "主题效果",
"theme_language": "主题与语言",
"time_days_ago": "%d 天前", "time_days_ago": "%d 天前",
"time_hours_ago": "%d 小时前", "time_hours_ago": "%d 小时前",
"time_minutes_ago": "%d 分钟前", "time_minutes_ago": "%d 分钟前",
"time_seconds_ago": "%d 秒前", "time_seconds_ago": "%d 秒前",
"timeout_15min": "15分钟",
"timeout_1hour": "1小时",
"timeout_1min": "1分钟",
"timeout_30min": "30分钟",
"timeout_5min": "5分钟",
"timeout_off": "关闭",
"to": "至", "to": "至",
"to_upper": "至", "to_upper": "至",
"tools": "工具", "tools": "工具",
"tools_actions": "工具与操作...",
"total": "合计", "total": "合计",
"transaction_id": "交易 ID", "transaction_id": "交易 ID",
"transaction_sent": "交易发送成功", "transaction_sent": "交易发送成功",
@@ -680,7 +811,13 @@
"transaction_url": "交易 URL", "transaction_url": "交易 URL",
"transactions": "交易", "transactions": "交易",
"transactions_upper": "交易", "transactions_upper": "交易",
"transfer_failed": "转账失败",
"transfer_funds": "转账",
"transfer_sent": "转账已发送",
"transfer_sent_desc": "您的转账已提交到网络。",
"transfer_to": "转账至:",
"transparent": "透明", "transparent": "透明",
"transparent_address": "透明地址",
"tt_addr_url": "在区块浏览器中查看地址的基础 URL", "tt_addr_url": "在区块浏览器中查看地址的基础 URL",
"tt_address_book": "管理已保存的地址以快速发送", "tt_address_book": "管理已保存的地址以快速发送",
"tt_auto_lock": "在此不活动时间后锁定钱包", "tt_auto_lock": "在此不活动时间后锁定钱包",
@@ -695,6 +832,8 @@
"tt_custom_theme": "自定义主题已激活", "tt_custom_theme": "自定义主题已激活",
"tt_debug_collapse": "折叠调试日志选项", "tt_debug_collapse": "折叠调试日志选项",
"tt_debug_expand": "展开调试日志选项", "tt_debug_expand": "展开调试日志选项",
"tt_delete_blockchain": "删除所有区块链数据并重新同步。wallet.dat 和配置将被保留。",
"tt_download_bootstrap": "下载区块链引导程序以加速同步\n现有区块数据将被替换",
"tt_encrypt": "使用密码加密 wallet.dat", "tt_encrypt": "使用密码加密 wallet.dat",
"tt_export_all": "将所有私钥导出到文件", "tt_export_all": "将所有私钥导出到文件",
"tt_export_csv": "将交易历史导出为 CSV 电子表格", "tt_export_csv": "将交易历史导出为 CSV 电子表格",
@@ -712,6 +851,7 @@
"tt_mine_idle": "系统空闲时自动开始挖矿\\n无键盘/鼠标输入)", "tt_mine_idle": "系统空闲时自动开始挖矿\\n无键盘/鼠标输入)",
"tt_noise": "颗粒纹理强度0%% = 关闭100%% = 最大)", "tt_noise": "颗粒纹理强度0%% = 关闭100%% = 最大)",
"tt_open_dir": "点击在文件管理器中打开", "tt_open_dir": "点击在文件管理器中打开",
"tt_reduce_motion": "禁用动画过渡和余额渐变以提高无障碍性",
"tt_remove_encrypt": "移除加密并以未受保护状态存储钱包", "tt_remove_encrypt": "移除加密并以未受保护状态存储钱包",
"tt_remove_pin": "移除 PIN 并要求密码解锁", "tt_remove_pin": "移除 PIN 并要求密码解锁",
"tt_report_bug": "在项目跟踪器中报告问题", "tt_report_bug": "在项目跟踪器中报告问题",
@@ -789,7 +929,9 @@
"warning_upper": "警告!", "warning_upper": "警告!",
"website": "网站", "website": "网站",
"window_opacity": "窗口透明度", "window_opacity": "窗口透明度",
"wizard_daemon_start_failed": "启动守护进程失败 — 将自动重试",
"yes_clear": "是,清除", "yes_clear": "是,清除",
"your_addresses": "您的地址", "your_addresses": "您的地址",
"z_address": "Z 地址",
"z_addresses": "Z 地址" "z_addresses": "Z 地址"
} }

View File

@@ -485,12 +485,12 @@ suggestion-trunc-len = { size = 50 }
fee-rounding = { size = 10.0 } fee-rounding = { size = 10.0 }
amount-bar-max-btn-width = { size = 80.0 } amount-bar-max-btn-width = { size = 80.0 }
amount-bar-height = { size = 22.0 } amount-bar-height = { size = 22.0 }
confirm-popup-max-width = { size = 420.0 } confirm-popup-max-width = { size = 560.0 }
confirm-addr-card-height = { size = 28.0 } confirm-addr-card-height = { size = 28.0 }
confirm-amount-card-height = { size = 70.0 } confirm-amount-card-height = { size = 96.0 }
confirm-row-step = { size = 16.0 } confirm-row-step = { size = 24.0 }
confirm-val-col-x = { size = 90.0 } confirm-val-col-x = { size = 150.0 }
confirm-usd-col-x = { size = 80.0 } confirm-usd-col-x = { size = 116.0 }
progress-card-height = { size = 36.0 } progress-card-height = { size = 36.0 }
progress-card-height-txid = { size = 52.0 } progress-card-height-txid = { size = 52.0 }
progress-card-pad-x = { size = 12.0 } progress-card-pad-x = { size = 12.0 }
@@ -516,10 +516,10 @@ error-icon-inset = { size = 20.0 }
error-btn-rounding = { size = 4.0 } error-btn-rounding = { size = 4.0 }
progress-glass-rounding-ratio = { size = 0.75 } progress-glass-rounding-ratio = { size = 0.75 }
confirm-addr-card-min-height = { size = 24.0 } confirm-addr-card-min-height = { size = 24.0 }
confirm-val-col-min-x = { size = 70.0 } confirm-val-col-min-x = { size = 112.0 }
confirm-usd-col-min-x = { size = 60.0 } confirm-usd-col-min-x = { size = 92.0 }
confirm-amount-card-min-height = { size = 54.0 } confirm-amount-card-min-height = { size = 82.0 }
confirm-row-step-min = { size = 12.0 } confirm-row-step-min = { size = 20.0 }
action-btn-min-height = { size = 26.0 } action-btn-min-height = { size = 26.0 }
recent-icon-min-size = { size = 3.5 } recent-icon-min-size = { size = 3.5 }
recent-icon-size = { size = 5.0 } recent-icon-size = { size = 5.0 }
@@ -566,7 +566,7 @@ txid-trunc-len = { size = 14 }
txid-label-x-offset = { size = 20.0 } txid-label-x-offset = { size = 20.0 }
txid-copy-btn-right-offset = { size = 50.0 } txid-copy-btn-right-offset = { size = 50.0 }
txid-copy-btn-y-offset = { size = 2.0 } txid-copy-btn-y-offset = { size = 2.0 }
confirm-popup-width-ratio = { size = 0.85 } confirm-popup-width-ratio = { size = 0.92 }
confirm-glass-rounding-ratio = { size = 0.75 } confirm-glass-rounding-ratio = { size = 0.75 }
confirm-addr-trunc-len = { size = 48 } confirm-addr-trunc-len = { size = 48 }
confirm-divider-thickness = { size = 1.0 } confirm-divider-thickness = { size = 1.0 }
@@ -1230,8 +1230,9 @@ width = { size = 140.0 }
collapsed-width = { size = 64.0 } collapsed-width = { size = 64.0 }
collapse-anim-speed = { size = 10.0 } collapse-anim-speed = { size = 10.0 }
auto-collapse-threshold = { size = 800.0 } auto-collapse-threshold = { size = 800.0 }
section-gap = { size = 4.0 } section-gap = { size = 8.0 }
section-label-pad-left = { size = 16.0 } section-label-pad-left = { size = 16.0 }
section-label-pad-bottom = { size = 4.0 }
item-height = { size = 36.0 } item-height = { size = 36.0 }
item-pad-x = { size = 8.0 } item-pad-x = { size = 8.0 }
min-height = { size = 360.0 } min-height = { size = 360.0 }
@@ -1248,8 +1249,8 @@ icon-half-size = { size = 7.0 }
icon-label-gap = { size = 8.0 } icon-label-gap = { size = 8.0 }
badge-radius-dot = { size = 4.0 } badge-radius-dot = { size = 4.0 }
badge-radius-number = { size = 8.0 } badge-radius-number = { size = 8.0 }
button-spacing = { size = 4.0 } button-spacing = { size = 6.0 }
bottom-padding = { size = 0.0 } bottom-padding = { size = 4.0 }
exit-icon-gap = { size = 4.0 } exit-icon-gap = { size = 4.0 }
cutout-shadow-alpha = { size = 55 } cutout-shadow-alpha = { size = 55 }
cutout-highlight-alpha = { size = 8 } cutout-highlight-alpha = { size = 8 }
@@ -1278,7 +1279,7 @@ page-fade-speed = { size = 8.0 }
collapse-hysteresis = { size = 60.0 } collapse-hysteresis = { size = 60.0 }
header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" } header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" }
coin-icon = { icon = "logos/logo_dragonx_128.png" } coin-icon = { icon = "logos/logo_dragonx_128.png" }
header-title = { font = "subtitle1", size = 14.0, pad-x = 22.0, pad-y = 6.0, logo-gap = 4.0, opacity = 0.7, offset-y = 4.0 } header-title = { font = "subtitle1", size = 12.0, pad-x = 8.0, pad-y = 10.0, logo-gap = 4.0, opacity = 0.7, offset-y = -2.0 }
[components.main-window.window] [components.main-window.window]
padding = [12, 36] padding = [12, 36]
@@ -1377,6 +1378,17 @@ summary = { min-width = 280.0, max-width = 400.0, width-ratio = 0.32, min-height
side-panel = { min-width = 280.0, max-width = 450.0, width-ratio = 0.4, min-height = 200.0, max-height = 350.0, height-ratio = 0.8 } side-panel = { min-width = 280.0, max-width = 450.0, width-ratio = 0.4, min-height = 200.0, max-height = 350.0, height-ratio = 0.8 }
table = { min-height = 150.0, height-ratio = 0.45, min-remaining = 100.0, default-reserve = 30.0 } table = { min-height = 150.0, height-ratio = 0.45, min-remaining = 100.0, default-reserve = 30.0 }
[dialog]
width-default = 480.0
width-lg = 600.0
width-xl = 660.0
min-width = 280.0
form-width = 400.0
action-width = 100.0
action-gap = 8.0
max-height-ratio = 0.94
compact-bottom-ratio = 0.64
[button] [button]
min-width = 180.0 min-width = 180.0
width = 140.0 width = 140.0

View File

@@ -1275,6 +1275,831 @@ TRANSLATIONS = {
"pt": "Clique para copiar", "ru": "Нажмите для копирования", "zh": "点击复制", "pt": "Clique para copiar", "ru": "Нажмите для копирования", "zh": "点击复制",
"ja": "クリックしてコピー", "ko": "복사하려면 클릭" "ja": "クリックしてコピー", "ko": "복사하려면 클릭"
}, },
# ── NEW BATCH: 100 missing keys ──────────────────────────────────────
"advanced_effects": {
"es": "Efectos Avanzados...", "de": "Erweiterte Effekte...", "fr": "Effets avancés...",
"pt": "Efeitos Avançados...", "ru": "Расширенные эффекты...", "zh": "高级特效...",
"ja": "高度なエフェクト...", "ko": "고급 효과..."
},
"ago": {
"es": "atrás", "de": "her", "fr": "passé",
"pt": "atrás", "ru": "назад", "zh": "",
"ja": "", "ko": ""
},
"bootstrap_daemon_running": {
"es": "Daemon ejecutándose", "de": "Daemon läuft", "fr": "Daemon en cours",
"pt": "Daemon em execução", "ru": "Демон запущен", "zh": "守护进程运行中",
"ja": "デーモン実行中", "ko": "데몬 실행 중"
},
"bootstrap_daemon_stopped": {
"es": "Daemon detenido", "de": "Daemon gestoppt", "fr": "Daemon arrêté",
"pt": "Daemon parado", "ru": "Демон остановлен", "zh": "守护进程已停止",
"ja": "デーモン停止", "ko": "데몬 중지됨"
},
"bootstrap_daemon_stopping": {
"es": "Deteniendo daemon...", "de": "Daemon wird gestoppt...", "fr": "Arrêt du daemon...",
"pt": "Parando daemon...", "ru": "Остановка демона...", "zh": "正在停止守护进程...",
"ja": "デーモン停止中...", "ko": "데몬 중지 중..."
},
"bootstrap_desc": {
"es": "Descarga un bootstrap de la blockchain para acelerar drásticamente la sincronización inicial. Esto descarga una instantánea de la blockchain y la extrae en tu directorio de datos.",
"de": "Laden Sie einen Blockchain-Bootstrap herunter, um die anfängliche Synchronisierung drastisch zu beschleunigen. Dies lädt einen Snapshot der Blockchain herunter und extrahiert ihn in Ihr Datenverzeichnis.",
"fr": "Téléchargez un bootstrap de la blockchain pour accélérer considérablement la synchronisation initiale. Cela télécharge un instantané de la blockchain et l'extrait dans votre répertoire de données.",
"pt": "Baixe um bootstrap da blockchain para acelerar drasticamente a sincronização inicial. Isso baixa um instantâneo da blockchain e o extrai no seu diretório de dados.",
"ru": "Загрузите бутстрап блокчейна для значительного ускорения начальной синхронизации. Это загружает снимок блокчейна и извлекает его в ваш каталог данных.",
"zh": "下载区块链引导程序以大幅加速初始同步。这将下载区块链快照并将其提取到您的数据目录中。",
"ja": "ブロックチェーンブートストラップをダウンロードして初期同期を劇的に高速化します。ブロックチェーンのスナップショットをダウンロードしてデータディレクトリに展開します。",
"ko": "블록체인 부트스트랩을 다운로드하여 초기 동기화를 대폭 가속합니다. 블록체인 스냅샷을 다운로드하고 데이터 디렉토리에 추출합니다."
},
"bootstrap_downloading": {
"es": "Descargando bootstrap...", "de": "Bootstrap wird heruntergeladen...", "fr": "Téléchargement du bootstrap...",
"pt": "Baixando bootstrap...", "ru": "Загрузка бутстрапа...", "zh": "正在下载引导程序...",
"ja": "ブートストラップをダウンロード中...", "ko": "부트스트랩 다운로드 중..."
},
"bootstrap_extracting": {
"es": "Extrayendo datos de blockchain...", "de": "Blockchain-Daten werden extrahiert...", "fr": "Extraction des données blockchain...",
"pt": "Extraindo dados da blockchain...", "ru": "Извлечение данных блокчейна...", "zh": "正在提取区块链数据...",
"ja": "ブロックチェーンデータを展開中...", "ko": "블록체인 데이터 추출 중..."
},
"bootstrap_failed": {
"es": "Error en Bootstrap", "de": "Bootstrap fehlgeschlagen", "fr": "Échec du Bootstrap",
"pt": "Falha no Bootstrap", "ru": "Ошибка бутстрапа", "zh": "引导程序失败",
"ja": "ブートストラップ失敗", "ko": "부트스트랩 실패"
},
"bootstrap_mirror": {
"es": "Espejo", "de": "Spiegel", "fr": "Miroir",
"pt": "Espelho", "ru": "Зеркало", "zh": "镜像",
"ja": "ミラー", "ko": "미러"
},
"bootstrap_mirror_tooltip": {
"es": "Descargar desde espejo (bootstrap2.dragonx.is).\nUsa esto si la descarga principal es lenta o falla.",
"de": "Vom Spiegel herunterladen (bootstrap2.dragonx.is).\nVerwenden Sie dies, wenn der Hauptdownload langsam ist oder fehlschlägt.",
"fr": "Télécharger depuis le miroir (bootstrap2.dragonx.is).\nUtilisez ceci si le téléchargement principal est lent ou échoue.",
"pt": "Baixar do espelho (bootstrap2.dragonx.is).\nUse isto se o download principal estiver lento ou falhando.",
"ru": "Скачать с зеркала (bootstrap2.dragonx.is).\nИспользуйте, если основная загрузка медленная или не работает.",
"zh": "从镜像下载 (bootstrap2.dragonx.is)。\n如果主下载速度慢或失败,请使用此选项。",
"ja": "ミラーからダウンロード (bootstrap2.dragonx.is)。\nメインのダウンロードが遅い場合や失敗する場合に使用してください。",
"ko": "미러에서 다운로드 (bootstrap2.dragonx.is).\n메인 다운로드가 느리거나 실패할 경우 사용하세요."
},
"bootstrap_restart_daemon": {
"es": "Reiniciar Daemon", "de": "Daemon neu starten", "fr": "Redémarrer le Daemon",
"pt": "Reiniciar Daemon", "ru": "Перезапустить демон", "zh": "重启守护进程",
"ja": "デーモンを再起動", "ko": "데몬 재시작"
},
"bootstrap_success": {
"es": "Bootstrap Completado", "de": "Bootstrap abgeschlossen", "fr": "Bootstrap terminé",
"pt": "Bootstrap Completo", "ru": "Бутстрап завершён", "zh": "引导程序完成",
"ja": "ブートストラップ完了", "ko": "부트스트랩 완료"
},
"bootstrap_success_desc": {
"es": "Los datos de la blockchain se han extraído correctamente. Inicie el daemon para comenzar a sincronizar desde el punto del bootstrap.",
"de": "Blockchain-Daten wurden erfolgreich extrahiert. Starten Sie den Daemon, um ab dem Bootstrap-Punkt zu synchronisieren.",
"fr": "Les données de la blockchain ont été extraites avec succès. Démarrez le daemon pour commencer la synchronisation à partir du point de bootstrap.",
"pt": "Os dados da blockchain foram extraídos com sucesso. Inicie o daemon para começar a sincronizar a partir do ponto do bootstrap.",
"ru": "Данные блокчейна успешно извлечены. Запустите демон для начала синхронизации с точки бутстрапа.",
"zh": "区块链数据已成功提取。启动守护进程以从引导点开始同步。",
"ja": "ブロックチェーンデータが正常に展開されました。デーモンを起動してブートストラップポイントから同期を開始してください。",
"ko": "블록체인 데이터가 성공적으로 추출되었습니다. 데몬을 시작하여 부트스트랩 지점부터 동기화를 시작하세요."
},
"bootstrap_trust_warning": {
"es": "Solo use bootstrap.dragonx.is o bootstrap2.dragonx.is. Usar archivos de fuentes no confiables podría comprometer su nodo.",
"de": "Verwenden Sie nur bootstrap.dragonx.is oder bootstrap2.dragonx.is. Die Verwendung von Dateien aus nicht vertrauenswürdigen Quellen könnte Ihren Knoten gefährden.",
"fr": "N'utilisez que bootstrap.dragonx.is ou bootstrap2.dragonx.is. L'utilisation de fichiers provenant de sources non fiables pourrait compromettre votre nœud.",
"pt": "Use apenas bootstrap.dragonx.is ou bootstrap2.dragonx.is. Usar arquivos de fontes não confiáveis pode comprometer seu nó.",
"ru": "Используйте только bootstrap.dragonx.is или bootstrap2.dragonx.is. Использование файлов из ненадёжных источников может скомпрометировать ваш узел.",
"zh": "仅使用 bootstrap.dragonx.is 或 bootstrap2.dragonx.is。使用不受信任来源的文件可能会危及您的节点。",
"ja": "bootstrap.dragonx.is または bootstrap2.dragonx.is のみを使用してください。信頼できないソースのファイルを使用するとノードが危険にさらされる可能性があります。",
"ko": "bootstrap.dragonx.is 또는 bootstrap2.dragonx.is만 사용하세요. 신뢰할 수 없는 출처의 파일을 사용하면 노드가 손상될 수 있습니다."
},
"bootstrap_verifying": {
"es": "Verificando sumas de comprobación...", "de": "Prüfsummen werden überprüft...", "fr": "Vérification des sommes de contrôle...",
"pt": "Verificando somas de verificação...", "ru": "Проверка контрольных сумм...", "zh": "正在验证校验和...",
"ja": "チェックサムを検証中...", "ko": "체크섬 확인 중..."
},
"bootstrap_wallet_protected": {
"es": "(wallet.dat está protegido)", "de": "(wallet.dat ist geschützt)", "fr": "(wallet.dat est protégé)",
"pt": "(wallet.dat está protegido)", "ru": "(wallet.dat защищён)", "zh": "(wallet.dat 已受保护)",
"ja": "(wallet.dat は保護されています)", "ko": "(wallet.dat 보호됨)"
},
"bootstrap_warning": {
"es": "Los datos de bloques existentes (blocks, chainstate, notarizations) se eliminarán y reemplazarán. Su wallet.dat NO será modificado ni eliminado.",
"de": "Vorhandene Blockdaten (blocks, chainstate, notarizations) werden gelöscht und ersetzt. Ihre wallet.dat wird NICHT verändert oder gelöscht.",
"fr": "Les données de blocs existantes (blocks, chainstate, notarizations) seront supprimées et remplacées. Votre wallet.dat ne sera PAS modifié ni supprimé.",
"pt": "Os dados de blocos existentes (blocks, chainstate, notarizations) serão excluídos e substituídos. Seu wallet.dat NÃO será modificado ou excluído.",
"ru": "Существующие данные блоков (blocks, chainstate, notarizations) будут удалены и заменены. Ваш wallet.dat НЕ будет изменён или удалён.",
"zh": "现有区块数据blocks、chainstate、notarizations将被删除并替换。您的 wallet.dat 不会被修改或删除。",
"ja": "既存のブロックデータblocks、chainstate、notarizationsは削除され置き換えられます。wallet.dat は変更・削除されません。",
"ko": "기존 블록 데이터(blocks, chainstate, notarizations)가 삭제되고 교체됩니다. wallet.dat는 수정되거나 삭제되지 않습니다."
},
"choose_icon": {
"es": "Elegir Icono", "de": "Symbol wählen", "fr": "Choisir une icône",
"pt": "Escolher Ícone", "ru": "Выбрать иконку", "zh": "选择图标",
"ja": "アイコンを選択", "ko": "아이콘 선택"
},
"clear_icon": {
"es": "Borrar Icono", "de": "Symbol entfernen", "fr": "Effacer l'icône",
"pt": "Limpar Ícone", "ru": "Удалить иконку", "zh": "清除图标",
"ja": "アイコンをクリア", "ko": "아이콘 지우기"
},
"confirm_delete_blockchain_msg": {
"es": "Esto detendrá el daemon, eliminará todos los datos de la blockchain (blocks, chainstate, peers) y comenzará una nueva sincronización desde cero. Esto puede tardar varias horas.",
"de": "Dies stoppt den Daemon, löscht alle Blockchain-Daten (blocks, chainstate, peers) und startet eine neue Synchronisierung. Dies kann mehrere Stunden dauern.",
"fr": "Cela arrêtera le daemon, supprimera toutes les données de la blockchain (blocks, chainstate, peers) et démarrera une nouvelle synchronisation. Cela peut prendre plusieurs heures.",
"pt": "Isso irá parar o daemon, excluir todos os dados da blockchain (blocks, chainstate, peers) e iniciar uma nova sincronização do zero. Isso pode levar várias horas.",
"ru": "Это остановит демон, удалит все данные блокчейна (blocks, chainstate, peers) и начнёт синхронизацию с нуля. Это может занять несколько часов.",
"zh": "这将停止守护进程删除所有区块链数据blocks、chainstate、peers并从头开始重新同步。这可能需要几个小时。",
"ja": "デーモンを停止し、すべてのブロックチェーンデータblocks、chainstate、peersを削除して、最初から再同期を開始します。数時間かかる場合があります。",
"ko": "데몬을 중지하고 모든 블록체인 데이터(blocks, chainstate, peers)를 삭제한 후 처음부터 다시 동기화합니다. 몇 시간이 걸릴 수 있습니다."
},
"confirm_delete_blockchain_safe": {
"es": "Su wallet.dat, configuración e historial de transacciones están seguros y no se eliminarán.",
"de": "Ihre wallet.dat, Konfiguration und Transaktionshistorie sind sicher und werden nicht gelöscht.",
"fr": "Votre wallet.dat, votre configuration et votre historique de transactions sont en sécurité et ne seront pas supprimés.",
"pt": "Seu wallet.dat, configuração e histórico de transações estão seguros e não serão excluídos.",
"ru": "Ваш wallet.dat, конфигурация и история транзакций в безопасности и не будут удалены.",
"zh": "您的 wallet.dat、配置和交易历史是安全的不会被删除。",
"ja": "wallet.dat、設定、トランザクション履歴は安全で削除されません。",
"ko": "wallet.dat, 설정 및 거래 내역은 안전하며 삭제되지 않습니다."
},
"confirm_delete_blockchain_title": {
"es": "Eliminar Datos de Blockchain", "de": "Blockchain-Daten löschen", "fr": "Supprimer les données Blockchain",
"pt": "Excluir Dados da Blockchain", "ru": "Удалить данные блокчейна", "zh": "删除区块链数据",
"ja": "ブロックチェーンデータを削除", "ko": "블록체인 데이터 삭제"
},
"confirm_transfer": {
"es": "Confirmar Transferencia", "de": "Überweisung bestätigen", "fr": "Confirmer le transfert",
"pt": "Confirmar Transferência", "ru": "Подтвердить перевод", "zh": "确认转账",
"ja": "送金を確認", "ko": "이체 확인"
},
"copied": {
"es": "¡Copiado!", "de": "Kopiert!", "fr": "Copié !",
"pt": "Copiado!", "ru": "Скопировано!", "zh": "已复制!",
"ja": "コピーしました!", "ko": "복사됨!"
},
"daemon_version": {
"es": "Daemon", "de": "Daemon", "fr": "Daemon",
"pt": "Daemon", "ru": "Демон", "zh": "守护进程",
"ja": "デーモン", "ko": "데몬"
},
"delete_blockchain": {
"es": "Eliminar Blockchain", "de": "Blockchain löschen", "fr": "Supprimer Blockchain",
"pt": "Excluir Blockchain", "ru": "Удалить блокчейн", "zh": "删除区块链",
"ja": "ブロックチェーンを削除", "ko": "블록체인 삭제"
},
"delete_blockchain_confirm": {
"es": "Eliminar y Resincronizar", "de": "Löschen & Neu synchronisieren", "fr": "Supprimer & Resynchroniser",
"pt": "Excluir e Ressincronizar", "ru": "Удалить и пересинхронизировать", "zh": "删除并重新同步",
"ja": "削除して再同期", "ko": "삭제 후 재동기화"
},
"deshielding_warning": {
"es": "Advertencia: Esto des-protegerá fondos de una dirección privada (Z) a una dirección transparente (T).",
"de": "Warnung: Dies wird Gelder von einer privaten (Z) Adresse auf eine transparente (T) Adresse ent-schirmen.",
"fr": "Attention : Cela va déblinder des fonds d'une adresse privée (Z) vers une adresse transparente (T).",
"pt": "Aviso: Isso irá des-blindar fundos de um endereço privado (Z) para um endereço transparente (T).",
"ru": "Внимание: это переведёт средства из приватного (Z) адреса на прозрачный (T) адрес.",
"zh": "警告:这将把资金从隐私 (Z) 地址转移到透明 (T) 地址。",
"ja": "警告:プライベート (Z) アドレスからトランスペアレント (T) アドレスへ資金をデシールドします。",
"ko": "경고: 프라이빗 (Z) 주소에서 투명 (T) 주소로 자금을 디실딩합니다."
},
"download": {
"es": "Descargar", "de": "Herunterladen", "fr": "Télécharger",
"pt": "Baixar", "ru": "Скачать", "zh": "下载",
"ja": "ダウンロード", "ko": "다운로드"
},
"download_bootstrap": {
"es": "Descargar Bootstrap", "de": "Bootstrap herunterladen", "fr": "Télécharger Bootstrap",
"pt": "Baixar Bootstrap", "ru": "Скачать бутстрап", "zh": "下载引导程序",
"ja": "ブートストラップをダウンロード", "ko": "부트스트랩 다운로드"
},
"explorer_block_detail": {
"es": "Bloque", "de": "Block", "fr": "Bloc",
"pt": "Bloco", "ru": "Блок", "zh": "区块",
"ja": "ブロック", "ko": "블록"
},
"explorer_block_hash": {
"es": "Hash", "de": "Hash", "fr": "Hash",
"pt": "Hash", "ru": "Хеш", "zh": "哈希",
"ja": "ハッシュ", "ko": "해시"
},
"explorer_block_height": {
"es": "Altura", "de": "Höhe", "fr": "Hauteur",
"pt": "Altura", "ru": "Высота", "zh": "高度",
"ja": "高さ", "ko": "높이"
},
"explorer_block_merkle": {
"es": "Raíz Merkle", "de": "Merkle-Wurzel", "fr": "Racine Merkle",
"pt": "Raiz Merkle", "ru": "Корень Меркла", "zh": "Merkle 根",
"ja": "マークルルート", "ko": "머클 루트"
},
"explorer_block_size": {
"es": "Tamaño", "de": "Größe", "fr": "Taille",
"pt": "Tamanho", "ru": "Размер", "zh": "大小",
"ja": "サイズ", "ko": "크기"
},
"explorer_block_time": {
"es": "Hora", "de": "Zeit", "fr": "Heure",
"pt": "Hora", "ru": "Время", "zh": "时间",
"ja": "時刻", "ko": "시간"
},
"explorer_block_txs": {
"es": "Transacciones", "de": "Transaktionen", "fr": "Transactions",
"pt": "Transações", "ru": "Транзакции", "zh": "交易",
"ja": "トランザクション", "ko": "트랜잭션"
},
"explorer_chain_stats": {
"es": "Cadena", "de": "Kette", "fr": "Chaîne",
"pt": "Cadeia", "ru": "Цепочка", "zh": "",
"ja": "チェーン", "ko": "체인"
},
"explorer_invalid_query": {
"es": "Ingrese una altura de bloque o un hash de 64 caracteres",
"de": "Geben Sie eine Blockhöhe oder einen 64-stelligen Hash ein",
"fr": "Entrez une hauteur de bloc ou un hash de 64 caractères",
"pt": "Insira uma altura de bloco ou um hash de 64 caracteres",
"ru": "Введите высоту блока или 64-символьный хеш",
"zh": "输入区块高度或64位哈希",
"ja": "ブロック高さまたは64文字のハッシュを入力してください",
"ko": "블록 높이 또는 64자 해시를 입력하세요"
},
"explorer_mempool": {
"es": "Mempool", "de": "Mempool", "fr": "Mempool",
"pt": "Mempool", "ru": "Мемпул", "zh": "内存池",
"ja": "メモリプール", "ko": "멤풀"
},
"explorer_mempool_size": {
"es": "Tamaño", "de": "Größe", "fr": "Taille",
"pt": "Tamanho", "ru": "Размер", "zh": "大小",
"ja": "サイズ", "ko": "크기"
},
"explorer_mempool_txs": {
"es": "Transacciones", "de": "Transaktionen", "fr": "Transactions",
"pt": "Transações", "ru": "Транзакции", "zh": "交易",
"ja": "トランザクション", "ko": "트랜잭션"
},
"explorer_recent_blocks": {
"es": "Bloques Recientes", "de": "Letzte Blöcke", "fr": "Blocs récents",
"pt": "Blocos Recentes", "ru": "Последние блоки", "zh": "最近区块",
"ja": "最近のブロック", "ko": "최근 블록"
},
"explorer_search": {
"es": "Buscar", "de": "Suchen", "fr": "Rechercher",
"pt": "Pesquisar", "ru": "Поиск", "zh": "搜索",
"ja": "検索", "ko": "검색"
},
"explorer_tx_outputs": {
"es": "Salidas", "de": "Ausgaben", "fr": "Sorties",
"pt": "Saídas", "ru": "Выходы", "zh": "输出",
"ja": "出力", "ko": "출력"
},
"explorer_tx_size": {
"es": "Tamaño", "de": "Größe", "fr": "Taille",
"pt": "Tamanho", "ru": "Размер", "zh": "大小",
"ja": "サイズ", "ko": "크기"
},
"filter": {
"es": "Filtrar...", "de": "Filtern...", "fr": "Filtrer...",
"pt": "Filtrar...", "ru": "Фильтр...", "zh": "筛选...",
"ja": "フィルター...", "ko": "필터..."
},
"force_quit": {
"es": "Forzar Salida", "de": "Sofort beenden", "fr": "Forcer la fermeture",
"pt": "Forçar Saída", "ru": "Принудительный выход", "zh": "强制退出",
"ja": "強制終了", "ko": "강제 종료"
},
"force_quit_confirm_msg": {
"es": "Esto matará inmediatamente el daemon sin un apagado limpio.\nEsto puede corromper el índice de la blockchain y requerir una resincronización.",
"de": "Dies wird den Daemon sofort beenden ohne sauberes Herunterfahren.\nDies kann den Blockchain-Index beschädigen und eine Neusynchronisierung erfordern.",
"fr": "Cela tuera immédiatement le daemon sans arrêt propre.\nCela peut corrompre l'index de la blockchain et nécessiter une resynchronisation.",
"pt": "Isso matará imediatamente o daemon sem um desligamento limpo.\nIsso pode corromper o índice da blockchain e exigir uma ressincronização.",
"ru": "Это немедленно завершит демон без корректного завершения.\nЭто может повредить индекс блокчейна и потребовать повторной синхронизации.",
"zh": "这将立即终止守护进程而不进行正常关闭。\n这可能会损坏区块链索引并需要重新同步。",
"ja": "クリーンシャットダウンなしでデーモンを即座に終了します。\nブロックチェーンインデックスが破損し、再同期が必要になる可能性があります。",
"ko": "정상 종료 없이 데몬을 즉시 종료합니다.\n블록체인 인덱스가 손상되어 재동기화가 필요할 수 있습니다."
},
"force_quit_confirm_title": {
"es": "¿Forzar Salida?", "de": "Sofort beenden?", "fr": "Forcer la fermeture ?",
"pt": "Forçar Saída?", "ru": "Принудительный выход?", "zh": "强制退出?",
"ja": "強制終了しますか?", "ko": "강제 종료하시겠습니까?"
},
"force_quit_warning": {
"es": "Esto matará inmediatamente el daemon sin un apagado limpio. Puede requerir una resincronización de la blockchain.",
"de": "Dies wird den Daemon sofort beenden ohne sauberes Herunterfahren. Kann eine Neusynchronisierung der Blockchain erfordern.",
"fr": "Cela tuera immédiatement le daemon sans arrêt propre. Peut nécessiter une resynchronisation de la blockchain.",
"pt": "Isso matará imediatamente o daemon sem um desligamento limpo. Pode exigir uma ressincronização da blockchain.",
"ru": "Это немедленно завершит демон без корректного завершения. Может потребоваться повторная синхронизация блокчейна.",
"zh": "这将立即终止守护进程而不进行正常关闭。可能需要重新同步区块链。",
"ja": "クリーンシャットダウンなしでデーモンを即座に終了します。ブロックチェーンの再同期が必要になる場合があります。",
"ko": "정상 종료 없이 데몬을 즉시 종료합니다. 블록체인 재동기화가 필요할 수 있습니다."
},
"force_quit_yes": {
"es": "Forzar Salida", "de": "Sofort beenden", "fr": "Forcer la fermeture",
"pt": "Forçar Saída", "ru": "Принудительный выход", "zh": "强制退出",
"ja": "強制終了", "ko": "강제 종료"
},
"generating": {
"es": "Generando", "de": "Wird generiert", "fr": "Génération",
"pt": "Gerando", "ru": "Генерация", "zh": "正在生成",
"ja": "生成中", "ko": "생성 중"
},
"hidden_tag": {
"es": " (oculto)", "de": " (versteckt)", "fr": " (masqué)",
"pt": " (oculto)", "ru": " (скрыт)", "zh": " (已隐藏)",
"ja": " (非表示)", "ko": " (숨김)"
},
"incorrect_passphrase": {
"es": "Contraseña incorrecta", "de": "Falsches Passwort", "fr": "Mot de passe incorrect",
"pt": "Senha incorreta", "ru": "Неверный пароль", "zh": "密码错误",
"ja": "パスフレーズが正しくありません", "ko": "잘못된 암호"
},
"incorrect_pin": {
"es": "PIN incorrecto", "de": "Falsche PIN", "fr": "PIN incorrect",
"pt": "PIN incorreto", "ru": "Неверный PIN", "zh": "PIN 错误",
"ja": "PINが正しくありません", "ko": "잘못된 PIN"
},
"insufficient_funds": {
"es": "Fondos insuficientes para este monto más la comisión.", "de": "Unzureichendes Guthaben für diesen Betrag plus Gebühr.",
"fr": "Fonds insuffisants pour ce montant plus les frais.", "pt": "Fundos insuficientes para este valor mais taxa.",
"ru": "Недостаточно средств для этой суммы плюс комиссия.", "zh": "余额不足以支付此金额加手续费。",
"ja": "この金額と手数料に対して残高が不足しています。", "ko": "이 금액과 수수료를 위한 잔액이 부족합니다."
},
"label_placeholder": {
"es": "ej. Ahorros, Minería...", "de": "z.B. Ersparnisse, Mining...", "fr": "ex. Épargne, Minage...",
"pt": "ex. Poupança, Mineração...", "ru": "напр. Накопления, Майнинг...", "zh": "例如 储蓄、挖矿...",
"ja": "例: 貯金、マイニング...", "ko": "예: 저축, 채굴..."
},
"mining_benchmark_cancel": {
"es": "Cancelar benchmark", "de": "Benchmark abbrechen", "fr": "Annuler le benchmark",
"pt": "Cancelar benchmark", "ru": "Отменить тест", "zh": "取消基准测试",
"ja": "ベンチマークをキャンセル", "ko": "벤치마크 취소"
},
"mining_benchmark_cooling": {
"es": "Enfriando", "de": "Abkühlen", "fr": "Refroidissement",
"pt": "Resfriando", "ru": "Охлаждение", "zh": "冷却中",
"ja": "クーリング", "ko": "쿨링"
},
"mining_benchmark_dismiss": {
"es": "Cerrar", "de": "Schließen", "fr": "Fermer",
"pt": "Fechar", "ru": "Закрыть", "zh": "关闭",
"ja": "閉じる", "ko": "닫기"
},
"mining_benchmark_result": {
"es": "Óptimo", "de": "Optimal", "fr": "Optimal",
"pt": "Ótimo", "ru": "Оптимально", "zh": "最佳",
"ja": "最適", "ko": "최적"
},
"mining_benchmark_stabilizing": {
"es": "Estabilizando", "de": "Stabilisierung", "fr": "Stabilisation",
"pt": "Estabilizando", "ru": "Стабилизация", "zh": "稳定中",
"ja": "安定化中", "ko": "안정화 중"
},
"mining_benchmark_testing": {
"es": "Probando", "de": "Testen", "fr": "Test",
"pt": "Testando", "ru": "Тестирование", "zh": "测试中",
"ja": "テスト中", "ko": "테스트 중"
},
"mining_benchmark_tooltip": {
"es": "Encontrar el número óptimo de hilos para esta CPU",
"de": "Optimale Thread-Anzahl für diese CPU finden",
"fr": "Trouver le nombre optimal de threads pour ce CPU",
"pt": "Encontrar o número ideal de threads para esta CPU",
"ru": "Найти оптимальное количество потоков для этого процессора",
"zh": "为此 CPU 找到最佳线程数",
"ja": "このCPUに最適なスレッド数を検出",
"ko": "이 CPU에 최적의 스레드 수 찾기"
},
"mining_generate_z_address_hint": {
"es": "Genere una dirección Z en la pestaña Recibir para usarla como dirección de pago",
"de": "Generieren Sie eine Z-Adresse im Empfangen-Tab als Auszahlungsadresse",
"fr": "Générez une adresse Z dans l'onglet Recevoir pour l'utiliser comme adresse de paiement",
"pt": "Gere um endereço Z na aba Receber para usar como endereço de pagamento",
"ru": "Создайте Z-адрес на вкладке «Получить» для использования в качестве адреса выплат",
"zh": "在接收标签页生成一个 Z 地址作为支付地址",
"ja": "受信タブでZアドレスを生成して支払いアドレスとして使用してください",
"ko": "수신 탭에서 Z 주소를 생성하여 지급 주소로 사용하세요"
},
"mining_idle_gpu_off_tooltip": {
"es": "Sin restricción: ACTIVADO\nSolo la entrada de teclado/ratón determina el estado inactivo\nClic para activar detección de GPU",
"de": "Uneingeschränkt: EIN\nNur Tastatur-/Mauseingabe bestimmt den Leerlauf\nKlicken für GPU-bewusste Erkennung",
"fr": "Sans restriction : ACTIVÉ\nSeule l'entrée clavier/souris détermine l'inactivité\nCliquez pour activer la détection GPU",
"pt": "Sem restrição: ATIVADO\nApenas entrada de teclado/mouse determina o estado ocioso\nClique para ativar detecção de GPU",
"ru": "Без ограничений: ВКЛ\nТолько ввод с клавиатуры/мыши определяет состояние простоя\nНажмите для GPU-контроля",
"zh": "无限制:开启\n仅键盘/鼠标输入决定空闲状态\n点击启用GPU感知检测",
"ja": "制限なし: オン\nキーボード/マウス入力のみがアイドル状態を決定\nGPU検出を有効にするにはクリック",
"ko": "무제한: 켜짐\n키보드/마우스 입력만 유휴 상태를 결정\nGPU 감지를 활성화하려면 클릭"
},
"mining_idle_gpu_on_tooltip": {
"es": "GPU-consciente: ACTIVADO\nLa actividad de GPU (video, juegos) previene la minería inactiva\nClic para modo sin restricción",
"de": "GPU-bewusst: EIN\nGPU-Aktivität (Video, Spiele) verhindert Leerlauf-Mining\nKlicken für uneingeschränkten Modus",
"fr": "GPU-conscient : ACTIVÉ\nL'activité GPU (vidéo, jeux) empêche le minage inactif\nCliquez pour le mode sans restriction",
"pt": "GPU-consciente: ATIVADO\nAtividade de GPU (vídeo, jogos) impede mineração ociosa\nClique para modo sem restrição",
"ru": "GPU-контроль: ВКЛ\nАктивность GPU (видео, игры) предотвращает майнинг в простое\nНажмите для режима без ограничений",
"zh": "GPU感知开启\nGPU活动视频、游戏阻止空闲挖矿\n点击切换到无限制模式",
"ja": "GPU対応: オン\nGPUアクティビティ動画、ゲームがアイドルマイニングを防止\n制限なしモードに切り替えるにはクリック",
"ko": "GPU 감지: 켜짐\nGPU 활동(비디오, 게임)이 유휴 채굴을 방지\n무제한 모드로 전환하려면 클릭"
},
"mining_idle_scale_off_tooltip": {
"es": "Modo inicio/parada: ACTIVADO\nClic para cambiar al modo de escala de hilos",
"de": "Start/Stopp-Modus: EIN\nKlicken zum Wechsel auf Thread-Skalierung",
"fr": "Mode démarrage/arrêt : ACTIVÉ\nCliquez pour passer au mode mise à l'échelle des threads",
"pt": "Modo iniciar/parar: ATIVADO\nClique para mudar para modo de escala de threads",
"ru": "Режим старт/стоп: ВКЛ\nНажмите для переключения на масштабирование потоков",
"zh": "启动/停止模式:开启\n点击切换到线程缩放模式",
"ja": "開始/停止モード: オン\nスレッドスケーリングモードに切り替えるにはクリック",
"ko": "시작/중지 모드: 켜짐\n스레드 스케일링 모드로 전환하려면 클릭"
},
"mining_idle_scale_on_tooltip": {
"es": "Escala de hilos: ACTIVADO\nClic para cambiar al modo de inicio/parada",
"de": "Thread-Skalierung: EIN\nKlicken zum Wechsel auf Start/Stopp-Modus",
"fr": "Mise à l'échelle des threads : ACTIVÉ\nCliquez pour passer au mode démarrage/arrêt",
"pt": "Escala de threads: ATIVADO\nClique para mudar para modo iniciar/parar",
"ru": "Масштабирование потоков: ВКЛ\nНажмите для переключения на режим старт/стоп",
"zh": "线程缩放:开启\n点击切换到启动/停止模式",
"ja": "スレッドスケーリング: オン\n開始/停止モードに切り替えるにはクリック",
"ko": "스레드 스케일링: 켜짐\n시작/중지 모드로 전환하려면 클릭"
},
"mining_idle_threads_active_tooltip": {
"es": "Hilos cuando el usuario está activo", "de": "Threads bei Benutzeraktivität",
"fr": "Threads quand l'utilisateur est actif", "pt": "Threads quando o usuário está ativo",
"ru": "Потоки при активности пользователя", "zh": "用户活跃时的线程数",
"ja": "ユーザーアクティブ時のスレッド数", "ko": "사용자 활성 시 스레드"
},
"mining_idle_threads_idle_tooltip": {
"es": "Hilos cuando el sistema está inactivo", "de": "Threads im Leerlauf",
"fr": "Threads quand le système est inactif", "pt": "Threads quando o sistema está ocioso",
"ru": "Потоки при простое системы", "zh": "系统空闲时的线程数",
"ja": "システムアイドル時のスレッド数", "ko": "시스템 유휴 시 스레드"
},
"no_addresses_yet": {
"es": "Aún no hay direcciones", "de": "Noch keine Adressen", "fr": "Pas encore d'adresses",
"pt": "Nenhum endereço ainda", "ru": "Пока нет адресов", "zh": "暂无地址",
"ja": "アドレスがまだありません", "ko": "아직 주소가 없습니다"
},
"pin_not_set": {
"es": "PIN no configurado. Use la contraseña para desbloquear.",
"de": "PIN nicht gesetzt. Verwenden Sie das Passwort zum Entsperren.",
"fr": "PIN non défini. Utilisez le mot de passe pour déverrouiller.",
"pt": "PIN não definido. Use a senha para desbloquear.",
"ru": "PIN не установлен. Используйте пароль для разблокировки.",
"zh": "未设置 PIN。使用密码解锁。",
"ja": "PINが設定されていません。パスフレーズで解除してください。",
"ko": "PIN이 설정되지 않았습니다. 암호를 사용하여 잠금 해제하세요."
},
"recipient_balance": {
"es": "Destinatario: %.8f%.8f DRGX", "de": "Empfänger: %.8f%.8f DRGX",
"fr": "Destinataire : %.8f%.8f DRGX", "pt": "Destinatário: %.8f%.8f DRGX",
"ru": "Получатель: %.8f%.8f DRGX", "zh": "接收方: %.8f%.8f DRGX",
"ja": "受取人: %.8f%.8f DRGX", "ko": "수신자: %.8f%.8f DRGX"
},
"reduce_motion": {
"es": "Reducir Movimiento", "de": "Bewegung reduzieren", "fr": "Réduire les animations",
"pt": "Reduzir Movimento", "ru": "Уменьшить анимацию", "zh": "减少动画",
"ja": "モーションを減らす", "ko": "모션 줄이기"
},
"restarting_after_encryption": {
"es": "Reiniciando daemon después del cifrado...", "de": "Daemon wird nach Verschlüsselung neu gestartet...",
"fr": "Redémarrage du daemon après chiffrement...", "pt": "Reiniciando daemon após criptografia...",
"ru": "Перезапуск демона после шифрования...", "zh": "加密后重启守护进程...",
"ja": "暗号化後にデーモンを再起動中...", "ko": "암호화 후 데몬 재시작 중..."
},
"result_preview": {
"es": "Vista previa del resultado", "de": "Ergebnisvorschau", "fr": "Aperçu du résultat",
"pt": "Pré-visualização do resultado", "ru": "Предпросмотр результата", "zh": "结果预览",
"ja": "結果プレビュー", "ko": "결과 미리보기"
},
"retry": {
"es": "Reintentar", "de": "Wiederholen", "fr": "Réessayer",
"pt": "Tentar novamente", "ru": "Повторить", "zh": "重试",
"ja": "再試行", "ko": "재시도"
},
"sender_balance": {
"es": "Remitente: %.8f%.8f DRGX", "de": "Absender: %.8f%.8f DRGX",
"fr": "Expéditeur : %.8f%.8f DRGX", "pt": "Remetente: %.8f%.8f DRGX",
"ru": "Отправитель: %.8f%.8f DRGX", "zh": "发送方: %.8f%.8f DRGX",
"ja": "送信者: %.8f%.8f DRGX", "ko": "발신자: %.8f%.8f DRGX"
},
"sends_full_balance_warning": {
"es": "Esto envía el saldo completo. La dirección de envío tendrá saldo cero.",
"de": "Dies sendet das gesamte Guthaben. Die Sendeadresse wird ein Nullguthaben haben.",
"fr": "Cela envoie le solde complet. L'adresse d'envoi aura un solde nul.",
"pt": "Isso envia o saldo total. O endereço de envio terá saldo zero.",
"ru": "Это отправит весь баланс. Адрес отправителя останется с нулевым балансом.",
"zh": "这将发送全部余额。发送地址将变为零余额。",
"ja": "全残高を送信します。送信アドレスの残高はゼロになります。",
"ko": "전체 잔액을 전송합니다. 보내는 주소의 잔액이 0이 됩니다."
},
"set_label": {
"es": "Establecer Etiqueta...", "de": "Label setzen...", "fr": "Définir le libellé...",
"pt": "Definir Rótulo...", "ru": "Установить метку...", "zh": "设置标签...",
"ja": "ラベルを設定...", "ko": "라벨 설정..."
},
"shielded_address": {
"es": "Dirección Protegida", "de": "Geschirmte Adresse", "fr": "Adresse protégée",
"pt": "Endereço Blindado", "ru": "Экранированный адрес", "zh": "隐蔽地址",
"ja": "シールドアドレス", "ko": "보호 주소"
},
"shielding_notice": {
"es": "Nota: Esto blindará fondos de una dirección transparente (T) a una dirección privada (Z).",
"de": "Hinweis: Dies wird Gelder von einer transparenten (T) Adresse auf eine private (Z) Adresse schirmen.",
"fr": "Note : Cela blindera des fonds d'une adresse transparente (T) vers une adresse privée (Z).",
"pt": "Nota: Isso blindará fundos de um endereço transparente (T) para um endereço privado (Z).",
"ru": "Примечание: это переведёт средства с прозрачного (T) адреса на приватный (Z) адрес.",
"zh": "注意:这将把资金从透明 (T) 地址转移到隐私 (Z) 地址。",
"ja": "注意:トランスペアレント (T) アドレスからプライベート (Z) アドレスへ資金をシールドします。",
"ko": "참고: 투명 (T) 주소에서 프라이빗 (Z) 주소로 자금을 실딩합니다."
},
"showing_x_of_y": {
"es": "Mostrando %d de %d direcciones", "de": "%d von %d Adressen angezeigt",
"fr": "Affichage de %d sur %d adresses", "pt": "Mostrando %d de %d endereços",
"ru": "Показано %d из %d адресов", "zh": "显示 %d / %d 个地址",
"ja": "%d / %d アドレスを表示", "ko": "%d / %d 주소 표시"
},
"t_address": {
"es": "Dirección T", "de": "T-Adresse", "fr": "Adresse T",
"pt": "Endereço T", "ru": "T-адрес", "zh": "T 地址",
"ja": "Tアドレス", "ko": "T 주소"
},
"theme_language": {
"es": "TEMA E IDIOMA", "de": "THEMA & SPRACHE", "fr": "THÈME & LANGUE",
"pt": "TEMA E IDIOMA", "ru": "ТЕМА И ЯЗЫК", "zh": "主题与语言",
"ja": "テーマと言語", "ko": "테마 및 언어"
},
"tools_actions": {
"es": "Herramientas y Acciones...", "de": "Werkzeuge & Aktionen...", "fr": "Outils & Actions...",
"pt": "Ferramentas e Ações...", "ru": "Инструменты и действия...", "zh": "工具与操作...",
"ja": "ツールとアクション...", "ko": "도구 및 작업..."
},
"transfer_failed": {
"es": "Transferencia Fallida", "de": "Überweisung fehlgeschlagen", "fr": "Échec du transfert",
"pt": "Transferência Falhou", "ru": "Ошибка перевода", "zh": "转账失败",
"ja": "送金失敗", "ko": "이체 실패"
},
"transfer_funds": {
"es": "Transferir Fondos", "de": "Geld überweisen", "fr": "Transférer des fonds",
"pt": "Transferir Fundos", "ru": "Перевести средства", "zh": "转账",
"ja": "資金を送金", "ko": "자금 이체"
},
"transfer_sent": {
"es": "Transferencia Enviada", "de": "Überweisung gesendet", "fr": "Transfert envoyé",
"pt": "Transferência Enviada", "ru": "Перевод отправлен", "zh": "转账已发送",
"ja": "送金完了", "ko": "이체 전송됨"
},
"transfer_sent_desc": {
"es": "Su transferencia ha sido enviada a la red.",
"de": "Ihre Überweisung wurde an das Netzwerk gesendet.",
"fr": "Votre transfert a été soumis au réseau.",
"pt": "Sua transferência foi enviada à rede.",
"ru": "Ваш перевод отправлен в сеть.",
"zh": "您的转账已提交到网络。",
"ja": "送金がネットワークに送信されました。",
"ko": "이체가 네트워크에 제출되었습니다."
},
"transfer_to": {
"es": "Transferir a:", "de": "Überweisen an:", "fr": "Transférer à :",
"pt": "Transferir para:", "ru": "Перевести на:", "zh": "转账至:",
"ja": "送金先:", "ko": "이체 대상:"
},
"transparent_address": {
"es": "Dirección Transparente", "de": "Transparente Adresse", "fr": "Adresse transparente",
"pt": "Endereço Transparente", "ru": "Прозрачный адрес", "zh": "透明地址",
"ja": "トランスペアレントアドレス", "ko": "투명 주소"
},
"tt_delete_blockchain": {
"es": "Eliminar todos los datos de la blockchain e iniciar una nueva sincronización. Se preservan wallet.dat y la configuración.",
"de": "Alle Blockchain-Daten löschen und neu synchronisieren. wallet.dat und Konfiguration bleiben erhalten.",
"fr": "Supprimer toutes les données de la blockchain et démarrer une nouvelle synchronisation. wallet.dat et la configuration sont préservés.",
"pt": "Excluir todos os dados da blockchain e iniciar uma nova sincronização. wallet.dat e configuração são preservados.",
"ru": "Удалить все данные блокчейна и начать синхронизацию заново. wallet.dat и конфигурация сохраняются.",
"zh": "删除所有区块链数据并重新同步。wallet.dat 和配置将被保留。",
"ja": "すべてのブロックチェーンデータを削除して新規同期を開始します。wallet.dat と設定は保持されます。",
"ko": "모든 블록체인 데이터를 삭제하고 새로 동기화합니다. wallet.dat 및 설정은 보존됩니다."
},
"tt_download_bootstrap": {
"es": "Descargar bootstrap de blockchain para acelerar la sincronización\nLos datos de bloques existentes serán reemplazados",
"de": "Blockchain-Bootstrap herunterladen, um die Synchronisierung zu beschleunigen\nVorhandene Blockdaten werden ersetzt",
"fr": "Télécharger le bootstrap blockchain pour accélérer la synchronisation\nLes données de blocs existantes seront remplacées",
"pt": "Baixar bootstrap da blockchain para acelerar a sincronização\nDados de blocos existentes serão substituídos",
"ru": "Скачать бутстрап блокчейна для ускорения синхронизации\nСуществующие данные блоков будут заменены",
"zh": "下载区块链引导程序以加速同步\n现有区块数据将被替换",
"ja": "ブロックチェーンブートストラップをダウンロードして同期を高速化\n既存のブロックデータは置き換えられます",
"ko": "블록체인 부트스트랩을 다운로드하여 동기화 가속\n기존 블록 데이터가 교체됩니다"
},
"tt_reduce_motion": {
"es": "Desactivar transiciones animadas y lerp de saldo para accesibilidad",
"de": "Animierte Übergänge und Saldo-Lerp für Barrierefreiheit deaktivieren",
"fr": "Désactiver les transitions animées et le lerp de solde pour l'accessibilité",
"pt": "Desativar transições animadas e lerp de saldo para acessibilidade",
"ru": "Отключить анимированные переходы и плавное изменение баланса для доступности",
"zh": "禁用动画过渡和余额渐变以提高无障碍性",
"ja": "アクセシビリティのためにアニメーション遷移と残高補間を無効にする",
"ko": "접근성을 위해 애니메이션 전환 및 잔액 보간 비활성화"
},
"wizard_daemon_start_failed": {
"es": "Error al iniciar el daemon — se reintentará automáticamente",
"de": "Daemon-Start fehlgeschlagen — wird automatisch wiederholt",
"fr": "Échec du démarrage du daemon — sera réessayé automatiquement",
"pt": "Falha ao iniciar o daemon — será tentado novamente automaticamente",
"ru": "Не удалось запустить демон — будет повторено автоматически",
"zh": "启动守护进程失败 — 将自动重试",
"ja": "デーモンの起動に失敗しました — 自動的に再試行されます",
"ko": "데몬 시작 실패 — 자동으로 재시도됩니다"
},
"z_address": {
"es": "Dirección Z", "de": "Z-Adresse", "fr": "Adresse Z",
"pt": "Endereço Z", "ru": "Z-адрес", "zh": "Z 地址",
"ja": "Zアドレス", "ko": "Z 주소"
},
# ── Status bar strings ───────────────────────────────────────────────
"sb_warming_up": {
"es": "Calentando...", "de": "Aufwärmen...", "fr": "Démarrage...",
"pt": "Aquecendo...", "ru": "Прогрев...", "zh": "正在预热...",
"ja": "ウォームアップ中...", "ko": "워밍업 중..."
},
"sb_block": {
"es": "Bloque: %d", "de": "Block: %d", "fr": "Bloc : %d",
"pt": "Bloco: %d", "ru": "Блок: %d", "zh": "区块: %d",
"ja": "ブロック: %d", "ko": "블록: %d"
},
"sb_peers": {
"es": "Pares: %zu", "de": "Peers: %zu", "fr": "Pairs : %zu",
"pt": "Pares: %zu", "ru": "Пиры: %zu", "zh": "节点: %zu",
"ja": "ピア: %zu", "ko": "피어: %zu"
},
"sb_net_ghs": {
"es": "Red: %.2f GH/s", "de": "Netz: %.2f GH/s", "fr": "Rés: %.2f GH/s",
"pt": "Rede: %.2f GH/s", "ru": "Сеть: %.2f GH/s", "zh": "网络: %.2f GH/s",
"ja": "ネット: %.2f GH/s", "ko": "네트: %.2f GH/s"
},
"sb_net_mhs": {
"es": "Red: %.2f MH/s", "de": "Netz: %.2f MH/s", "fr": "Rés: %.2f MH/s",
"pt": "Rede: %.2f MH/s", "ru": "Сеть: %.2f MH/s", "zh": "网络: %.2f MH/s",
"ja": "ネット: %.2f MH/s", "ko": "네트: %.2f MH/s"
},
"sb_net_khs": {
"es": "Red: %.2f KH/s", "de": "Netz: %.2f KH/s", "fr": "Rés: %.2f KH/s",
"pt": "Rede: %.2f KH/s", "ru": "Сеть: %.2f KH/s", "zh": "网络: %.2f KH/s",
"ja": "ネット: %.2f KH/s", "ko": "네트: %.2f KH/s"
},
"sb_net_hs": {
"es": "Red: %.1f H/s", "de": "Netz: %.1f H/s", "fr": "Rés: %.1f H/s",
"pt": "Rede: %.1f H/s", "ru": "Сеть: %.1f H/s", "zh": "网络: %.1f H/s",
"ja": "ネット: %.1f H/s", "ko": "네트: %.1f H/s"
},
"sb_mining_hs": {
"es": "%.1f H/s", "de": "%.1f H/s", "fr": "%.1f H/s",
"pt": "%.1f H/s", "ru": "%.1f H/s", "zh": "%.1f H/s",
"ja": "%.1f H/s", "ko": "%.1f H/s"
},
"sb_syncing_eta": {
"es": "Sincronizando %.1f%% (%d restantes, %.0f blk/s, ~%s)",
"de": "Synchronisierung %.1f%% (%d übrig, %.0f Blk/s, ~%s)",
"fr": "Synchronisation %.1f%% (%d restants, %.0f blk/s, ~%s)",
"pt": "Sincronizando %.1f%% (%d restantes, %.0f blk/s, ~%s)",
"ru": "Синхронизация %.1f%% (%d осталось, %.0f блк/с, ~%s)",
"zh": "同步中 %.1f%% (剩余 %d, %.0f 块/秒, ~%s)",
"ja": "同期中 %.1f%% (残り %d, %.0f ブロック/秒, ~%s)",
"ko": "동기화 %.1f%% (%d 남음, %.0f 블록/초, ~%s)"
},
"sb_syncing_basic": {
"es": "Sincronizando %.1f%% (%d restantes)",
"de": "Synchronisierung %.1f%% (%d übrig)",
"fr": "Synchronisation %.1f%% (%d restants)",
"pt": "Sincronizando %.1f%% (%d restantes)",
"ru": "Синхронизация %.1f%% (%d осталось)",
"zh": "同步中 %.1f%% (剩余 %d)",
"ja": "同期中 %.1f%% (残り %d)",
"ko": "동기화 %.1f%% (%d 남음)"
},
"sb_rescanning_pct": {
"es": "Reescaneando %.0f%%", "de": "Neuscan %.0f%%", "fr": "Rescan %.0f%%",
"pt": "Reescaneando %.0f%%", "ru": "Пересканирование %.0f%%", "zh": "重新扫描 %.0f%%",
"ja": "再スキャン %.0f%%", "ko": "재스캔 %.0f%%"
},
"sb_rescanning": {
"es": "Reescaneando", "de": "Neuscan", "fr": "Rescan",
"pt": "Reescaneando", "ru": "Пересканирование", "zh": "重新扫描",
"ja": "再スキャン", "ko": "재스캔"
},
"sb_importing_keys": {
"es": "Importando claves", "de": "Schlüssel importieren", "fr": "Importation des clés",
"pt": "Importando chaves", "ru": "Импорт ключей", "zh": "正在导入密钥",
"ja": "鍵をインポート中", "ko": "키 가져오기 중"
},
"sb_daemon_not_found": {
"es": "Daemon no encontrado", "de": "Daemon nicht gefunden", "fr": "Daemon introuvable",
"pt": "Daemon não encontrado", "ru": "Демон не найден", "zh": "未找到守护进程",
"ja": "デーモンが見つかりません", "ko": "데몬을 찾을 수 없음"
},
"sb_loading_config": {
"es": "Cargando configuración...", "de": "Konfiguration laden...", "fr": "Chargement de la configuration...",
"pt": "Carregando configuração...", "ru": "Загрузка конфигурации...", "zh": "正在加载配置...",
"ja": "設定を読み込み中...", "ko": "설정 불러오는 중..."
},
"sb_waiting_config": {
"es": "Esperando configuración del daemon...", "de": "Warten auf Daemon-Konfiguration...", "fr": "En attente de la configuration du daemon...",
"pt": "Aguardando configuração do daemon...", "ru": "Ожидание конфигурации демона...", "zh": "等待守护进程配置...",
"ja": "デーモン設定を待機中...", "ko": "데몬 설정 대기 중..."
},
"sb_no_conf": {
"es": "DRAGONX.conf no encontrado", "de": "DRAGONX.conf nicht gefunden", "fr": "DRAGONX.conf introuvable",
"pt": "DRAGONX.conf não encontrado", "ru": "DRAGONX.conf не найден", "zh": "未找到 DRAGONX.conf",
"ja": "DRAGONX.conf が見つかりません", "ko": "DRAGONX.conf를 찾을 수 없음"
},
"sb_starting_daemon": {
"es": "Iniciando dragonxd...", "de": "dragonxd wird gestartet...", "fr": "Démarrage de dragonxd...",
"pt": "Iniciando dragonxd...", "ru": "Запуск dragonxd...", "zh": "正在启动 dragonxd...",
"ja": "dragonxd を起動中...", "ko": "dragonxd 시작 중..."
},
"sb_connecting_daemon": {
"es": "Conectando a dragonxd...", "de": "Verbindung zu dragonxd...", "fr": "Connexion à dragonxd...",
"pt": "Conectando ao dragonxd...", "ru": "Подключение к dragonxd...", "zh": "正在连接 dragonxd...",
"ja": "dragonxd に接続中...", "ko": "dragonxd에 연결 중..."
},
"sb_auth_failed": {
"es": "Autenticación fallida — verifique rpcuser/rpcpassword",
"de": "Authentifizierung fehlgeschlagen — rpcuser/rpcpassword prüfen",
"fr": "Authentification échouée — vérifiez rpcuser/rpcpassword",
"pt": "Autenticação falhou — verifique rpcuser/rpcpassword",
"ru": "Ошибка авторизации — проверьте rpcuser/rpcpassword",
"zh": "认证失败 — 请检查 rpcuser/rpcpassword",
"ja": "認証失敗 — rpcuser/rpcpassword を確認してください",
"ko": "인증 실패 — rpcuser/rpcpassword를 확인하세요"
},
"sb_waiting_daemon": {
"es": "Esperando a dragonxd...", "de": "Warten auf dragonxd...", "fr": "En attente de dragonxd...",
"pt": "Aguardando dragonxd...", "ru": "Ожидание dragonxd...", "zh": "等待 dragonxd 启动...",
"ja": "dragonxd を待機中...", "ko": "dragonxd 대기 중..."
},
"sb_waiting_daemon_err": {
"es": "Esperando a dragonxd — %s", "de": "Warten auf dragonxd — %s", "fr": "En attente de dragonxd — %s",
"pt": "Aguardando dragonxd — %s", "ru": "Ожидание dragonxd — %s", "zh": "等待 dragonxd — %s",
"ja": "dragonxd を待機中 — %s", "ko": "dragonxd 대기 중 — %s"
},
"sb_connecting_external": {
"es": "Conectando a daemon externo...", "de": "Verbindung zu externem Daemon...", "fr": "Connexion au daemon externe...",
"pt": "Conectando ao daemon externo...", "ru": "Подключение к внешнему демону...", "zh": "正在连接外部守护进程...",
"ja": "外部デーモンに接続中...", "ko": "외부 데몬에 연결 중..."
},
"sb_connecting_generic": {
"es": "Conectando al daemon...", "de": "Verbindung zum Daemon...", "fr": "Connexion au daemon...",
"pt": "Conectando ao daemon...", "ru": "Подключение к демону...", "zh": "正在连接守护进程...",
"ja": "デーモンに接続中...", "ko": "데몬에 연결 중..."
},
"sb_connecting_err": {
"es": "Conectando al daemon — %s", "de": "Verbindung zum Daemon — %s", "fr": "Connexion au daemon — %s",
"pt": "Conectando ao daemon — %s", "ru": "Подключение к демону — %s", "zh": "连接守护进程 — %s",
"ja": "デーモンに接続中 — %s", "ko": "데몬 연결 중 — %s"
},
"sb_daemon_crashed": {
"es": "El daemon se bloqueó %d veces", "de": "Daemon ist %d mal abgestürzt", "fr": "Le daemon a planté %d fois",
"pt": "O daemon travou %d vezes", "ru": "Демон упал %d раз", "zh": "守护进程崩溃 %d",
"ja": "デーモンが %d 回クラッシュしました", "ko": "데몬이 %d회 충돌함"
},
"sb_extracting_sapling": {
"es": "Extrayendo parámetros Sapling...", "de": "Sapling-Parameter werden extrahiert...", "fr": "Extraction des paramètres Sapling...",
"pt": "Extraindo parâmetros Sapling...", "ru": "Извлечение параметров Sapling...", "zh": "正在提取 Sapling 参数...",
"ja": "Sapling パラメータを展開中...", "ko": "Sapling 매개변수 추출 중..."
},
"sb_sapling_failed": {
"es": "Error al extraer parámetros Sapling.", "de": "Sapling-Parameter-Extraktion fehlgeschlagen.", "fr": "Échec de l'extraction des paramètres Sapling.",
"pt": "Falha ao extrair parâmetros Sapling.", "ru": "Ошибка извлечения параметров Sapling.", "zh": "提取 Sapling 参数失败。",
"ja": "Sapling パラメータの展開に失敗しました。", "ko": "Sapling 매개변수 추출 실패."
},
"sb_sapling_not_found": {
"es": "Parámetros Sapling no encontrados.", "de": "Sapling-Parameter nicht gefunden.", "fr": "Paramètres Sapling introuvables.",
"pt": "Parâmetros Sapling não encontrados.", "ru": "Параметры Sapling не найдены.", "zh": "未找到 Sapling 参数。",
"ja": "Sapling パラメータが見つかりません。", "ko": "Sapling 매개변수를 찾을 수 없음."
},
"sb_dragonxd_running": {
"es": "dragonxd ejecutándose", "de": "dragonxd läuft", "fr": "dragonxd en cours",
"pt": "dragonxd em execução", "ru": "dragonxd запущен", "zh": "dragonxd 运行中",
"ja": "dragonxd 実行中", "ko": "dragonxd 실행 중"
},
"sb_dragonxd_stopping": {
"es": "Deteniendo dragonxd...", "de": "dragonxd wird gestoppt...", "fr": "Arrêt de dragonxd...",
"pt": "Parando dragonxd...", "ru": "Остановка dragonxd...", "zh": "正在停止 dragonxd...",
"ja": "dragonxd を停止中...", "ko": "dragonxd 중지 중..."
},
"sb_dragonxd_stopped": {
"es": "dragonxd detenido", "de": "dragonxd gestoppt", "fr": "dragonxd arrêté",
"pt": "dragonxd parado", "ru": "dragonxd остановлен", "zh": "dragonxd 已停止",
"ja": "dragonxd 停止", "ko": "dragonxd 중지됨"
},
"sb_restarting_daemon": {
"es": "Reiniciando daemon...", "de": "Daemon wird neu gestartet...", "fr": "Redémarrage du daemon...",
"pt": "Reiniciando daemon...", "ru": "Перезапуск демона...", "zh": "正在重启守护进程...",
"ja": "デーモンを再起動中...", "ko": "데몬 재시작 중..."
},
# --- Sidebar / section label fixes ---
"explorer_section": {
"es": "EXPLORADOR", "de": "EXPLORER", "fr": "EXPLORATEUR",
"pt": "EXPLORADOR", "ru": "ОБОЗРЕВАТЕЛЬ", "zh": "浏览器",
"ja": "エクスプローラー", "ko": "탐색기"
},
} }
def main(): def main():

131
scripts/build_cjk_subset.py Normal file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""
Build a NotoSansCJK subset font containing all characters used by
the zh, ja, and ko translation files, plus common CJK punctuation
and symbols.
Usage:
python3 scripts/build_cjk_subset.py
Requires: pip install fonttools brotli
"""
import json
import os
from fontTools.ttLib import TTFont
from fontTools import subset as ftsubset
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
SOURCE_FONT = '/tmp/NotoSansCJKsc-Regular.otf'
OUTPUT_FONT = os.path.join(ROOT, 'res', 'fonts', 'NotoSansCJK-Subset.ttf')
# Collect all characters used in CJK translation files
needed = set()
for lang in ['zh', 'ja', 'ko']:
path = os.path.join(LANG_DIR, f'{lang}.json')
if not os.path.exists(path):
continue
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
for v in data.values():
if isinstance(v, str):
for c in v:
cp = ord(c)
if cp > 0x7F: # non-ASCII only (ASCII handled by Ubuntu font)
needed.add(cp)
# Also add common CJK ranges that future translations might use:
# - CJK punctuation and symbols (3000-303F)
# - Hiragana (3040-309F)
# - Katakana (30A0-30FF)
# - Bopomofo (3100-312F)
# - CJK quotation marks, brackets
for cp in range(0x3000, 0x3100):
needed.add(cp)
for cp in range(0x3100, 0x3130):
needed.add(cp)
# Fullwidth ASCII variants (commonly mixed in CJK text)
for cp in range(0xFF01, 0xFF5F):
needed.add(cp)
print(f"Total non-ASCII characters to include: {len(needed)}")
# Check which of these the source font supports
font = TTFont(SOURCE_FONT)
cmap = font.getBestCmap()
supportable = needed & set(cmap.keys())
unsupported = needed - set(cmap.keys())
print(f"Supported by source font: {len(supportable)}")
if unsupported:
print(f"Not in source font (will use fallback): {len(unsupported)}")
for cp in sorted(unsupported)[:10]:
print(f" U+{cp:04X} {chr(cp)}")
# Build the subset using pyftsubset CLI-style API
args = [
SOURCE_FONT,
f'--output-file={OUTPUT_FONT}',
f'--unicodes={",".join(f"U+{cp:04X}" for cp in sorted(supportable))}',
'--no-hinting',
'--desubroutinize',
]
ftsubset.main(args)
# Convert CFF outlines to TrueType (glyf) outlines.
# stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly.
from fontTools.pens.cu2quPen import Cu2QuPen
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.ttLib import newTable
tmp_otf = OUTPUT_FONT + '.tmp.otf'
os.rename(OUTPUT_FONT, tmp_otf)
conv = TTFont(tmp_otf)
if 'CFF ' in conv:
print("Converting CFF -> TrueType outlines...")
glyphOrder = conv.getGlyphOrder()
glyphSet = conv.getGlyphSet()
glyf_table = newTable("glyf")
glyf_table.glyphs = {}
glyf_table.glyphOrder = glyphOrder
loca_table = newTable("loca")
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
for gname in glyphOrder:
try:
ttPen = TTGlyphPen(glyphSet)
cu2quPen = Cu2QuPen(ttPen, max_err=1.0, reverse_direction=True)
glyphSet[gname].draw(cu2quPen)
glyf_table.glyphs[gname] = ttPen.glyph()
except Exception:
glyf_table.glyphs[gname] = TTGlyph()
del conv['CFF ']
if 'VORG' in conv:
del conv['VORG']
conv['glyf'] = glyf_table
conv['loca'] = loca_table
conv['head'].indexToLocFormat = 1
if 'maxp' in conv:
conv['maxp'].version = 0x00010000
conv.sfntVersion = "\x00\x01\x00\x00"
conv.save(OUTPUT_FONT)
conv.close()
os.remove(tmp_otf)
size = os.path.getsize(OUTPUT_FONT)
print(f"\nOutput: {OUTPUT_FONT}")
print(f"Size: {size / 1024:.0f} KB")
# Verify
verify = TTFont(OUTPUT_FONT)
verify_cmap = set(verify.getBestCmap().keys())
still_missing = needed - verify_cmap
print(f"Verified glyphs in subset: {len(verify_cmap)}")
if still_missing:
# These are chars not in the source font - expected for some Hangul/Hiragana
print(f"Not coverable by this font: {len(still_missing)} (need additional font)")
for cp in sorted(still_missing)[:10]:
print(f" U+{cp:04X} {chr(cp)}")
else:
print("All needed characters are covered!")

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Check which characters in translation files fall outside the font glyph ranges."""
import json
import unicodedata
import glob
import os
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LANG_DIR = os.path.join(SCRIPT_DIR, '..', 'res', 'lang')
# Glyph ranges from typography.cpp (regular font + CJK merge)
RANGES = [
# Regular font ranges
(0x0020, 0x00FF), # Basic Latin + Latin-1 Supplement
(0x0100, 0x024F), # Latin Extended-A + B
(0x0370, 0x03FF), # Greek and Coptic
(0x0400, 0x04FF), # Cyrillic
(0x0500, 0x052F), # Cyrillic Supplement
(0x2000, 0x206F), # General Punctuation
(0x2190, 0x21FF), # Arrows
(0x2200, 0x22FF), # Mathematical Operators
(0x2600, 0x26FF), # Miscellaneous Symbols
# CJK ranges
(0x2E80, 0x2FDF), # CJK Radicals
(0x3000, 0x30FF), # CJK Symbols, Hiragana, Katakana
(0x3100, 0x312F), # Bopomofo
(0x31F0, 0x31FF), # Katakana Extensions
(0x3400, 0x4DBF), # CJK Extension A
(0x4E00, 0x9FFF), # CJK Unified Ideographs
(0xAC00, 0xD7AF), # Hangul Syllables
(0xFF00, 0xFFEF), # Fullwidth Forms
]
def in_ranges(cp):
return any(lo <= cp <= hi for lo, hi in RANGES)
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
lang = os.path.basename(path).replace('.json', '')
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
missing = {}
for key, val in data.items():
if not isinstance(val, str):
continue
for c in val:
cp = ord(c)
if cp > 0x7F and not in_ranges(cp):
if c not in missing:
missing[c] = []
missing[c].append(key)
if missing:
print(f"\n=== {lang}.json: {len(missing)} missing characters ===")
for c in sorted(missing, key=lambda x: ord(x)):
cp = ord(c)
name = unicodedata.name(c, 'UNKNOWN')
keys = missing[c][:3]
key_str = ', '.join(keys)
if len(missing[c]) > 3:
key_str += f' (+{len(missing[c])-3} more)'
print(f" U+{cp:04X} {c} ({name}) — used in: {key_str}")
else:
print(f"=== {lang}.json: OK (all characters covered) ===")

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Check which characters needed by translations are missing from bundled fonts."""
import json
import os
from fontTools.ttLib import TTFont
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FONTS_DIR = os.path.join(ROOT, 'res', 'fonts')
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
# Load font cmaps
cjk = TTFont(os.path.join(FONTS_DIR, 'NotoSansCJK-Subset.ttf'))
cjk_cmap = set(cjk.getBestCmap().keys())
ubuntu = TTFont(os.path.join(FONTS_DIR, 'Ubuntu-R.ttf'))
ubuntu_cmap = set(ubuntu.getBestCmap().keys())
combined = cjk_cmap | ubuntu_cmap
print(f"CJK subset font glyphs: {len(cjk_cmap)}")
print(f"Ubuntu font glyphs: {len(ubuntu_cmap)}")
print(f"Combined: {len(combined)}")
print()
for lang in ['zh', 'ja', 'ko', 'ru', 'de', 'es', 'fr', 'pt']:
path = os.path.join(LANG_DIR, f'{lang}.json')
if not os.path.exists(path):
continue
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
needed = set()
for v in data.values():
if isinstance(v, str):
for c in v:
needed.add(ord(c))
missing = sorted(needed - combined)
if missing:
print(f"{lang}.json: {len(needed)} chars needed, {len(missing)} MISSING")
for cp in missing[:20]:
c = chr(cp)
print(f" U+{cp:04X} {c}")
if len(missing) > 20:
print(f" ... and {len(missing) - 20} more")
else:
print(f"{lang}.json: OK ({len(needed)} chars, all covered)")

View File

@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Convert CJK subset from CID-keyed CFF/OTF to TrueType/TTF.
stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly,
so we need glyf-based TrueType outlines instead.
Two approaches:
1. Direct CFF->TTF conversion via cu2qu (fontTools)
2. Download NotoSansSC-Regular.ttf (already TTF) and re-subset
This script tries approach 1 first, falls back to approach 2.
"""
import os
import sys
import json
import glob
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
FONT_DIR = os.path.join(PROJECT_ROOT, "res", "fonts")
LANG_DIR = os.path.join(PROJECT_ROOT, "res", "lang")
SRC_OTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.otf")
DST_TTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.ttf")
def get_needed_codepoints():
"""Collect all unique codepoints from CJK translation files."""
codepoints = set()
for lang_file in glob.glob(os.path.join(LANG_DIR, "*.json")):
with open(lang_file, "r", encoding="utf-8") as f:
data = json.load(f)
for value in data.values():
if isinstance(value, str):
for ch in value:
cp = ord(ch)
# Include CJK + Hangul + fullwidth + CJK symbols/kana
if cp >= 0x2E80:
codepoints.add(cp)
return codepoints
def convert_cff_to_ttf():
"""Convert existing OTF/CFF font to TTF using fontTools cu2qu."""
from fontTools.ttLib import TTFont
from fontTools.pens.cu2quPen import Cu2QuPen
from fontTools.pens.ttGlyphPen import TTGlyphPen
print(f"Loading {SRC_OTF}...")
font = TTFont(SRC_OTF)
# Verify it's CFF
if "CFF " not in font:
print("Font is not CFF, skipping conversion")
return False
cff = font["CFF "]
top = cff.cff.topDictIndex[0]
print(f"ROS: {getattr(top, 'ROS', None)}")
print(f"CID-keyed: {getattr(top, 'FDSelect', None) is not None}")
glyphOrder = font.getGlyphOrder()
print(f"Glyphs: {len(glyphOrder)}")
# Use fontTools' built-in otf2ttf if available
try:
from fontTools.otf2ttf import otf_to_ttf
otf_to_ttf(font)
font.save(DST_TTF)
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
font.close()
return True
except ImportError:
pass
# Manual conversion using cu2qu
print("Using manual CFF->TTF conversion with cu2qu...")
from fontTools.pens.recordingPen import RecordingPen
from fontTools.pens.pointPen import SegmentToPointPen
from fontTools import ttLib
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
import struct
# Get glyph set
glyphSet = font.getGlyphSet()
# Create new glyf table
from fontTools.ttLib import newTable
glyf_table = newTable("glyf")
glyf_table.glyphs = {}
glyf_table.glyphOrder = glyphOrder
loca_table = newTable("loca")
max_error = 1.0 # em-units tolerance for cubic->quadratic
for gname in glyphOrder:
try:
ttPen = TTGlyphPen(glyphSet)
cu2quPen = Cu2QuPen(ttPen, max_err=max_error, reverse_direction=True)
glyphSet[gname].draw(cu2quPen)
glyf_table.glyphs[gname] = ttPen.glyph()
except Exception as e:
# Fallback: empty glyph
glyf_table.glyphs[gname] = TTGlyph()
# Replace CFF with glyf
del font["CFF "]
if "VORG" in font:
del font["VORG"]
font["glyf"] = glyf_table
font["loca"] = loca_table
# Add required tables for TTF
# head table needs indexToLocFormat
font["head"].indexToLocFormat = 1 # long format
# Create maxp for TrueType
if "maxp" in font:
font["maxp"].version = 0x00010000
# Update sfntVersion
font.sfntVersion = "\x00\x01\x00\x00" # TrueType
font.save(DST_TTF)
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
font.close()
return True
def download_and_subset():
"""Download NotoSansSC-Regular.ttf and subset it."""
import urllib.request
from fontTools.ttLib import TTFont
from fontTools import subset
# Google Fonts provides static TTF files
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/SubsetOTF/SC/NotoSansSC-Regular.otf"
# Actually, we want TTF. Let's try the variable font approach.
# Or better: use google-fonts API for static TTF
# NotoSansSC static TTF from Google Fonts CDN
tmp_font = "/tmp/NotoSansSC-Regular.ttf"
if not os.path.exists(tmp_font):
print(f"Downloading NotoSansSC-Regular.ttf...")
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTC/NotoSansCJK-Regular.ttc"
# This is a TTC (font collection), too large.
# Use the OTF we already have and convert it.
return False
print(f"Using {tmp_font}")
font = TTFont(tmp_font)
cmap = font.getBestCmap()
print(f"Source has {len(cmap)} cmap entries")
needed = get_needed_codepoints()
print(f"Need {len(needed)} CJK codepoints")
# Subset
subsetter = subset.Subsetter()
subsetter.populate(unicodes=needed)
subsetter.subset(font)
font.save(DST_TTF)
print(f"Saved: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
font.close()
return True
def verify_result():
"""Verify the output TTF has glyf outlines and correct characters."""
from fontTools.ttLib import TTFont
font = TTFont(DST_TTF)
cmap = font.getBestCmap()
print(f"\n--- Verification ---")
print(f"Format: {font.sfntVersion!r}")
print(f"Has glyf: {'glyf' in font}")
print(f"Has CFF: {'CFF ' in font}")
print(f"Cmap entries: {len(cmap)}")
# Check key characters
test_chars = {
"": 0x5386, "": 0x53F2, # Chinese: history
"": 0x6982, "": 0x8FF0, # Chinese: overview
"": 0x8BBE, "": 0x7F6E, # Chinese: settings
}
for name, cp in test_chars.items():
status = "YES" if cp in cmap else "NO"
print(f" {name} (U+{cp:04X}): {status}")
size = os.path.getsize(DST_TTF)
print(f"File size: {size} bytes ({size/1024:.1f} KB)")
font.close()
if __name__ == "__main__":
print("=== CJK Font CFF -> TTF Converter ===\n")
if convert_cff_to_ttf():
verify_result()
else:
print("Direct conversion failed, trying download approach...")
if download_and_subset():
verify_result()
else:
print("ERROR: Could not convert font")
sys.exit(1)

View File

@@ -8,7 +8,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="${SCRIPT_DIR}/build/linux" BUILD_DIR="${SCRIPT_DIR}/build/linux"
APPDIR="${BUILD_DIR}/AppDir" APPDIR="${BUILD_DIR}/AppDir"
VERSION="1.1.1" VERSION="1.2.0"
# Colors # Colors
GREEN='\033[0;32m' GREEN='\033[0;32m'

View File

@@ -50,12 +50,20 @@ if [[ ! -f "$TARBALL" ]]; then
curl -fSL -o "$TARBALL" "$SODIUM_URL" curl -fSL -o "$TARBALL" "$SODIUM_URL"
fi fi
# Verify checksum # Verify checksum (sha256sum on Linux, shasum on macOS)
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || { if command -v sha256sum &>/dev/null; then
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download." echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
rm -f "$TARBALL" echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
exit 1 rm -f "$TARBALL"
} exit 1
}
elif command -v shasum &>/dev/null; then
echo "$SODIUM_SHA256 $TARBALL" | shasum -a 256 -c - || {
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
rm -f "$TARBALL"
exit 1
}
fi
# ── Extract ───────────────────────────────────────────────────────────────── # ── Extract ─────────────────────────────────────────────────────────────────
if [[ ! -d "$SRC_DIR" ]]; then if [[ ! -d "$SRC_DIR" ]]; then
@@ -115,6 +123,69 @@ case "$TARGET" in
;; ;;
esac esac
# ── Native macOS: build universal binary (arm64 + x86_64) ───────────────────
IS_MACOS_NATIVE=false
if [[ "$TARGET" == "native" && "$(uname -s)" == "Darwin" ]]; then
IS_MACOS_NATIVE=true
fi
if $IS_MACOS_NATIVE; then
echo "[fetch-libsodium] Building universal (arm64 + x86_64) for macOS..."
export MACOSX_DEPLOYMENT_TARGET="11.0"
INSTALL_ARM64="$PROJECT_DIR/libs/libsodium-arm64"
INSTALL_X86_64="$PROJECT_DIR/libs/libsodium-x86_64"
for ARCH in arm64 x86_64; do
echo "[fetch-libsodium] Building for $ARCH..."
cd "$SRC_DIR"
make clean 2>/dev/null || true
make distclean 2>/dev/null || true
if [[ "$ARCH" == "arm64" ]]; then
ARCH_INSTALL="$INSTALL_ARM64"
HOST_TRIPLE="aarch64-apple-darwin"
else
ARCH_INSTALL="$INSTALL_X86_64"
HOST_TRIPLE="x86_64-apple-darwin"
fi
ARCH_CFLAGS="-arch $ARCH -mmacosx-version-min=11.0"
./configure \
--prefix="$ARCH_INSTALL" \
--disable-shared \
--enable-static \
--with-pic \
--host="$HOST_TRIPLE" \
CFLAGS="$ARCH_CFLAGS" \
LDFLAGS="-arch $ARCH" \
> /dev/null
make -j"$(sysctl -n hw.ncpu 2>/dev/null || echo 4)" > /dev/null 2>&1
make install > /dev/null
done
# Merge with lipo
echo "[fetch-libsodium] Creating universal binary with lipo..."
mkdir -p "$INSTALL_DIR/lib" "$INSTALL_DIR/include"
lipo -create \
"$INSTALL_ARM64/lib/libsodium.a" \
"$INSTALL_X86_64/lib/libsodium.a" \
-output "$INSTALL_DIR/lib/libsodium.a"
cp -R "$INSTALL_ARM64/include/"* "$INSTALL_DIR/include/"
# Clean up per-arch builds
rm -rf "$INSTALL_ARM64" "$INSTALL_X86_64"
cd "$PROJECT_DIR"
rm -rf "$SRC_DIR"
rm -f "$TARBALL"
echo "[fetch-libsodium] Done (universal): $INSTALL_DIR/lib/libsodium.a"
lipo -info "$INSTALL_DIR/lib/libsodium.a"
exit 0
fi
echo "[fetch-libsodium] Configuring for target: $TARGET ..." echo "[fetch-libsodium] Configuring for target: $TARGET ..."
./configure "${CONFIGURE_ARGS[@]}" > /dev/null ./configure "${CONFIGURE_ARGS[@]}" > /dev/null

36
scripts/fix_mojibake.py Normal file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Fix mojibake en-dash (and other common patterns) in translation JSON files."""
import os
import glob
LANG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'res', 'lang')
# Common mojibake patterns: UTF-8 bytes interpreted as Latin-1
MOJIBAKE_FIXES = {
'\u00e2\u0080\u0093': '\u2013', # en dash
'\u00e2\u0080\u0094': '\u2014', # em dash
'\u00e2\u0080\u0099': '\u2019', # right single quote
'\u00e2\u0080\u009c': '\u201c', # left double quote
'\u00e2\u0080\u009d': '\u201d', # right double quote
'\u00e2\u0080\u00a6': '\u2026', # ellipsis
}
total_fixed = 0
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
with open(path, 'r', encoding='utf-8') as f:
raw = f.read()
original = raw
for bad, good in MOJIBAKE_FIXES.items():
if bad in raw:
count = raw.count(bad)
raw = raw.replace(bad, good)
lang = os.path.basename(path)
print(f" {lang}: fixed {count} x {repr(good)}")
total_fixed += count
if raw != original:
with open(path, 'w', encoding='utf-8') as f:
f.write(raw)
print(f"\nTotal fixes: {total_fixed}")

View File

@@ -7,7 +7,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="${SCRIPT_DIR}/build/linux" BUILD_DIR="${SCRIPT_DIR}/build/linux"
VERSION="1.1.1" VERSION="1.2.0"
# Colors for output # Colors for output
RED='\033[0;31m' RED='\033[0;31m'

View File

@@ -120,20 +120,20 @@ pkgs_core_debian="build-essential cmake git pkg-config
libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev
libxinerama-dev libxi-dev libxkbcommon-dev libwayland-dev libxinerama-dev libxi-dev libxkbcommon-dev libwayland-dev
libsodium-dev libcurl4-openssl-dev libsodium-dev libcurl4-openssl-dev
autoconf automake libtool wget" autoconf automake libtool wget python3 xxd"
pkgs_core_fedora="gcc gcc-c++ cmake git pkg-config pkgs_core_fedora="gcc gcc-c++ cmake git pkg-config
mesa-libGL-devel libX11-devel libXcursor-devel libXrandr-devel mesa-libGL-devel libX11-devel libXcursor-devel libXrandr-devel
libXinerama-devel libXi-devel libxkbcommon-devel wayland-devel libXinerama-devel libXi-devel libxkbcommon-devel wayland-devel
libsodium-devel libcurl-devel libsodium-devel libcurl-devel
autoconf automake libtool wget" autoconf automake libtool wget python3 vim-common"
pkgs_core_arch="base-devel cmake git pkg-config pkgs_core_arch="base-devel cmake git pkg-config
mesa libx11 libxcursor libxrandr libxinerama libxi mesa libx11 libxcursor libxrandr libxinerama libxi
libxkbcommon wayland libsodium curl libxkbcommon wayland libsodium curl
autoconf automake libtool wget" autoconf automake libtool wget python xxd"
pkgs_core_macos="cmake" pkgs_core_macos="cmake python xxd"
# Windows cross-compile (from Linux) # Windows cross-compile (from Linux)
pkgs_win_debian="mingw-w64 zip" pkgs_win_debian="mingw-w64 zip"
@@ -245,7 +245,7 @@ if [[ -z "$core_pkgs" ]]; then
else else
# Check if key tools are already present # Check if key tools are already present
NEED_CORE=false NEED_CORE=false
has_cmd cmake && has_cmd g++ && has_cmd pkg-config || NEED_CORE=true has_cmd cmake && has_cmd g++ && has_cmd pkg-config && has_cmd python3 && has_cmd xxd || NEED_CORE=true
if $NEED_CORE; then if $NEED_CORE; then
install_pkgs "$core_pkgs" "core build" install_pkgs "$core_pkgs" "core build"
@@ -258,6 +258,8 @@ check_tool cmake "cmake"
check_tool g++ "g++ (C++ compiler)" check_tool g++ "g++ (C++ compiler)"
check_tool git "git" check_tool git "git"
check_tool make "make" check_tool make "make"
check_tool python3 "python3 (theme expansion)"
check_tool xxd "xxd (embedded language headers)"
# ── 2. libsodium ──────────────────────────────────────────────────────────── # ── 2. libsodium ────────────────────────────────────────────────────────────
header "libsodium" header "libsodium"

File diff suppressed because it is too large Load Diff

174
src/app.h
View File

@@ -12,8 +12,13 @@
#include <chrono> #include <chrono>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include "data/transaction_history_cache.h"
#include "data/wallet_state.h" #include "data/wallet_state.h"
#include "rpc/connection.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 "ui/sidebar.h" #include "ui/sidebar.h"
#include "ui/windows/console_tab.h" #include "ui/windows/console_tab.h"
#include "imgui.h" #include "imgui.h"
@@ -25,7 +30,7 @@ namespace dragonx {
class RPCWorker; class RPCWorker;
} }
namespace config { class Settings; } namespace config { class Settings; }
namespace daemon { class EmbeddedDaemon; class XmrigManager; } namespace daemon { class DaemonController; class EmbeddedDaemon; class XmrigManager; }
namespace util { class Bootstrap; class SecureVault; } namespace util { class Bootstrap; class SecureVault; }
} }
@@ -173,6 +178,9 @@ public:
// Pool mining (xmrig) // Pool mining (xmrig)
void startPoolMining(int threads); void startPoolMining(int threads);
void stopPoolMining(); void stopPoolMining();
int getXmrigRequestedThreads() const {
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
}
// Mine-when-idle state query // Mine-when-idle state query
bool isIdleMiningActive() const { return idle_mining_active_; } bool isIdleMiningActive() const { return idle_mining_active_; }
@@ -180,7 +188,9 @@ public:
// Peers // Peers
const std::vector<PeerInfo>& getPeers() const { return state_.peers; } const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; } const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; }
bool isPeerRefreshInProgress() const { return peer_refresh_in_progress_.load(std::memory_order_relaxed); } bool isPeerRefreshInProgress() const {
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Peers);
}
void banPeer(const std::string& ip, int duration_seconds = 86400); void banPeer(const std::string& ip, int duration_seconds = 86400);
void unbanPeer(const std::string& ip); void unbanPeer(const std::string& ip);
void clearBans(); void clearBans();
@@ -199,6 +209,19 @@ public:
void unfavoriteAddress(const std::string& addr); void unfavoriteAddress(const std::string& addr);
bool isAddressFavorite(const std::string& addr) const; bool isAddressFavorite(const std::string& addr) const;
// Address metadata (labels, icons, custom ordering)
void setAddressLabel(const std::string& addr, const std::string& label);
void setAddressIcon(const std::string& addr, const std::string& icon);
std::string getAddressLabel(const std::string& addr) const;
std::string getAddressIcon(const std::string& addr) const;
int getAddressSortOrder(const std::string& addr) const;
void setAddressSortOrder(const std::string& addr, int order);
int getNextSortOrder() const;
void swapAddressOrder(const std::string& a, const std::string& b);
bool isMiningAddress(const std::string& addr) const;
void setMiningAddress(const std::string& addr, bool mining);
void invalidateAddressValidationCache();
// Key export/import // Key export/import
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback); void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
void exportAllKeys(std::function<void(const std::string&)> callback); void exportAllKeys(std::function<void(const std::string&)> callback);
@@ -218,13 +241,14 @@ public:
void refreshPeerInfo(); void refreshPeerInfo();
void refreshMarketData(); void refreshMarketData();
/// @brief Per-category refresh intervals, adjusted by active tab
using RefreshIntervals = services::NetworkRefreshService::Intervals;
/// @brief Get recommended refresh intervals for a given page
static RefreshIntervals getIntervalsForPage(ui::NavPage page);
// UI navigation // UI navigation
void setCurrentPage(ui::NavPage page) { void setCurrentPage(ui::NavPage page);
if (page != current_page_) {
current_page_ = page;
if (page == ui::NavPage::Peers) refreshPeerInfo();
}
}
ui::NavPage getCurrentPage() const { return current_page_; } ui::NavPage getCurrentPage() const { return current_page_; }
// Dialog triggers (used by settings page to open modal dialogs) // Dialog triggers (used by settings page to open modal dialogs)
@@ -254,6 +278,9 @@ public:
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; } void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
void rescanBlockchain(); // restart daemon with -rescan flag void rescanBlockchain(); // restart daemon with -rescan flag
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh 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_; }
void setBootstrapDownloading(bool v) { bootstrap_downloading_ = v; }
// Get daemon memory usage in MB (uses embedded daemon handle if available, // Get daemon memory usage in MB (uses embedded daemon handle if available,
// falls back to platform-level process scan for external daemons) // falls back to platform-level process scan for external daemons)
@@ -318,10 +345,7 @@ public:
void showChangePassphraseDialog() { show_change_passphrase_ = true; } void showChangePassphraseDialog() { show_change_passphrase_ = true; }
void showDecryptDialog() { void showDecryptDialog() {
show_decrypt_dialog_ = true; show_decrypt_dialog_ = true;
decrypt_phase_ = 0; // passphrase entry wallet_security_workflow_.reset();
decrypt_step_ = 0;
decrypt_status_.clear();
decrypt_in_progress_ = false;
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_)); memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
} }
@@ -333,8 +357,39 @@ public:
/// @brief Check if RPC worker has queued results waiting to be processed /// @brief Check if RPC worker has queued results waiting to be processed
bool hasPendingRPCResults() const; 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;
bool isTransactionRefreshInProgress() const {
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
}
private: private:
friend class AppDaemonLifecycleRuntime;
friend class AppDaemonLifecycleTaskContext;
bool sendStopCommandSafely(rpc::RPCClient& client, const char* context);
void maybeFinishTransactionSendProgress();
void upsertPendingSendTransaction(const std::string& opid,
const std::string& from,
const std::string& to,
double amount,
const std::string& memo);
void markPendingSendTransactionSucceeded(const std::string& opid,
const std::string& txid);
void removePendingSendTransactions(const std::vector<std::string>& opids,
bool restoreBalances);
void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
std::string transactionHistoryCacheWalletIdentity() const;
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
void loadTransactionHistoryCacheIfAvailable();
void storeTransactionHistoryCacheIfAvailable();
void wipePendingTransactionHistoryCachePassphrase();
void resetTransactionHistoryCacheSession();
void pruneShieldedHistoryScanProgress();
void invalidateShieldedHistoryScanProgress(bool persistCache);
// Subsystems // Subsystems
std::unique_ptr<rpc::RPCClient> rpc_; std::unique_ptr<rpc::RPCClient> rpc_;
std::unique_ptr<rpc::RPCWorker> worker_; std::unique_ptr<rpc::RPCWorker> worker_;
@@ -349,8 +404,9 @@ private:
rpc::ConnectionConfig saved_config_; rpc::ConnectionConfig saved_config_;
std::unique_ptr<config::Settings> settings_; std::unique_ptr<config::Settings> settings_;
std::unique_ptr<daemon::EmbeddedDaemon> embedded_daemon_; std::unique_ptr<daemon::DaemonController> daemon_controller_;
std::unique_ptr<daemon::XmrigManager> xmrig_manager_; std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
util::AsyncTaskManager async_tasks_;
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
// Wallet state // Wallet state
@@ -359,16 +415,18 @@ private:
// Shutdown state // Shutdown state
std::atomic<bool> shutting_down_{false}; std::atomic<bool> shutting_down_{false};
std::atomic<bool> shutdown_complete_{false}; std::atomic<bool> shutdown_complete_{false};
std::atomic<bool> refresh_in_progress_{false};
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
std::string shutdown_status_; std::string shutdown_status_;
std::thread shutdown_thread_; std::thread shutdown_thread_;
float shutdown_timer_ = 0.0f; float shutdown_timer_ = 0.0f;
bool force_quit_confirm_ = false;
std::chrono::steady_clock::time_point shutdown_start_time_; std::chrono::steady_clock::time_point shutdown_start_time_;
// Daemon restart (e.g. after changing debug log categories) // Daemon restart (e.g. after changing debug log categories)
std::atomic<bool> daemon_restarting_{false}; std::atomic<bool> daemon_restarting_{false};
std::thread daemon_restart_thread_;
// Encryption state check timeout
float encryption_check_timer_ = 0.0f;
// UI State // UI State
bool quit_requested_ = false; bool quit_requested_ = false;
@@ -398,6 +456,7 @@ private:
// Connection // Connection
std::string connection_status_ = "Disconnected"; std::string connection_status_ = "Disconnected";
bool connection_in_progress_ = false; bool connection_in_progress_ = false;
bool remote_rpc_plaintext_warning_shown_ = false;
float loading_timer_ = 0.0f; // spinner animation for loading overlay float loading_timer_ = 0.0f; // spinner animation for loading overlay
// Current page (sidebar navigation) // Current page (sidebar navigation)
@@ -435,47 +494,27 @@ private:
std::string pending_memo_; std::string pending_memo_;
std::string pending_label_; std::string pending_label_;
// Timers (in seconds since last update) // Per-category refresh timers, policy, and worker queue guards.
float refresh_timer_ = 0.0f; services::NetworkRefreshService network_refresh_;
float price_timer_ = 0.0f;
float fast_refresh_timer_ = 0.0f; // For mining stats
// Refresh intervals (seconds)
static constexpr float REFRESH_INTERVAL = 5.0f;
static constexpr float PRICE_INTERVAL = 60.0f;
static constexpr float FAST_REFRESH_INTERVAL = 1.0f;
// Mining refresh guard (prevents worker queue pileup)
std::atomic<bool> mining_refresh_in_progress_{false};
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
// Mining toggle guard (prevents concurrent setgenerate calls) // Mining toggle guard (prevents concurrent setgenerate calls)
std::atomic<bool> mining_toggle_in_progress_{false}; std::atomic<bool> mining_toggle_in_progress_{false};
// Peer refresh guard (visual feedback for refresh button)
std::atomic<bool> peer_refresh_in_progress_{false};
// Auto-shield guard (prevents concurrent auto-shield operations) // Auto-shield guard (prevents concurrent auto-shield operations)
std::atomic<bool> auto_shield_pending_{false}; std::atomic<bool> auto_shield_pending_{false};
// P4: Incremental transaction cache // P4: Incremental transaction cache
int last_tx_block_height_ = -1; // block height at last full tx fetch int last_tx_block_height_ = -1; // block height at last full tx fetch
float tx_age_timer_ = 0.0f; // seconds since last tx fetch
static constexpr float TX_MAX_AGE = 15.0f; // force tx refresh every N seconds even without new blocks
static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh 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;
std::unordered_map<std::string, int> shielded_history_scan_heights_;
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for // P4b: z_viewtransaction result cache — avoids re-calling the RPC for
// txids we've already enriched. Keyed by txid. // txids we've already enriched. Keyed by txid.
struct ViewTxCacheEntry { using ViewTxCacheEntry = services::NetworkRefreshService::TransactionViewCacheEntry;
std::string from_address; // first spend address services::NetworkRefreshService::TransactionViewCache viewtx_cache_;
struct Output {
std::string address;
double value = 0.0;
std::string memo;
};
std::vector<Output> outgoing_outputs;
};
std::unordered_map<std::string, ViewTxCacheEntry> viewtx_cache_;
// P4c: Confirmed transaction cache — deeply-confirmed txns (>= 10 confs) // P4c: Confirmed transaction cache — deeply-confirmed txns (>= 10 confs)
// are accumulated here and reused across refresh cycles. Only // are accumulated here and reused across refresh cycles. Only
@@ -486,14 +525,24 @@ private:
// Dirty flags for demand-driven refresh // Dirty flags for demand-driven refresh
bool addresses_dirty_ = true; // true → refreshAddresses() will run bool addresses_dirty_ = true; // true → refreshAddresses() will run
bool address_validation_cache_dirty_ = true;
bool transactions_dirty_ = false; // true → force tx refresh regardless of block height bool transactions_dirty_ = false; // true → force tx refresh regardless of block height
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
bool rescan_status_poll_in_progress_ = false;
bool opid_poll_in_progress_ = false;
// Pending z_sendmany operation tracking // Pending z_sendmany operation tracking
bool send_progress_active_ = false;
int send_submissions_in_flight_ = 0;
std::vector<std::string> pending_opids_; // opids to poll for completion std::vector<std::string> pending_opids_; // opids to poll for completion
float opid_poll_timer_ = 0.0f; struct PendingSendInfo {
static constexpr float OPID_POLL_INTERVAL = 2.0f; std::string from;
std::string to;
std::string memo;
double amount = 0.0;
std::int64_t timestamp = 0;
};
std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
// Txids from completed z_sendmany operations. // Txids from completed z_sendmany operations.
// Ensures shielded sends are discoverable by z_viewtransaction // Ensures shielded sends are discoverable by z_viewtransaction
// even when they don't appear in listtransactions or // even when they don't appear in listtransactions or
@@ -503,21 +552,23 @@ private:
// First-run wizard state // First-run wizard state
WizardPhase wizard_phase_ = WizardPhase::None; WizardPhase wizard_phase_ = WizardPhase::None;
std::unique_ptr<util::Bootstrap> bootstrap_; std::unique_ptr<util::Bootstrap> bootstrap_;
bool bootstrap_downloading_ = false; // true while settings bootstrap dialog is active
std::string wizard_pending_passphrase_; // held until daemon connects std::string wizard_pending_passphrase_; // held until daemon connects
std::string wizard_saved_passphrase_; // held until PinSetup completes/skipped std::string wizard_saved_passphrase_; // held until PinSetup completes/skipped
// Deferred encryption (wizard background task) // Wallet security flow state shared by wizard/settings encryption paths.
std::string deferred_encrypt_passphrase_; services::WalletSecurityController wallet_security_;
std::string deferred_encrypt_pin_; services::WalletSecurityWorkflow wallet_security_workflow_;
bool deferred_encrypt_pending_ = false;
// Wizard: stopping an external daemon before bootstrap // Wizard: stopping an external daemon before bootstrap
bool wizard_stopping_external_ = false; bool wizard_stopping_external_ = false;
std::string wizard_stop_status_; std::string wizard_stop_status_;
std::thread wizard_stop_thread_;
// PIN vault // PIN vault
std::unique_ptr<util::SecureVault> vault_; std::unique_ptr<util::SecureVault> vault_;
data::TransactionHistoryCache transaction_history_cache_;
std::string pending_transaction_history_cache_passphrase_;
bool transaction_history_cache_loaded_ = false;
// Lock screen state // Lock screen state
bool lock_screen_was_visible_ = false; // tracks lock→unlock transitions for auto-focus bool lock_screen_was_visible_ = false; // tracks lock→unlock transitions for auto-focus
@@ -559,14 +610,7 @@ private:
// Decrypt wallet dialog state // Decrypt wallet dialog state
bool show_decrypt_dialog_ = false; bool show_decrypt_dialog_ = false;
int decrypt_phase_ = 0; // 0=passphrase, 1=working, 2=done, 3=error
int decrypt_step_ = 0; // 0=unlock, 1=export, 2=stop, 3=rename, 4=restart, 5=import
char decrypt_pass_buf_[256] = {}; char decrypt_pass_buf_[256] = {};
std::string decrypt_status_;
bool decrypt_in_progress_ = false;
std::chrono::steady_clock::time_point decrypt_step_start_time_{};
std::chrono::steady_clock::time_point decrypt_overall_start_time_{};
std::atomic<bool> decrypt_import_active_{false}; // background z_importwallet running
// Wizard PIN setup state // Wizard PIN setup state
char wizard_pin_buf_[16] = {}; char wizard_pin_buf_[16] = {};
@@ -601,11 +645,21 @@ private:
void applyDefaultBanlist(); void applyDefaultBanlist();
// Private methods - data refresh // Private methods - data refresh
void refreshData(); void refreshData(); // Orchestrator: dispatches per-category refreshes
void refreshBalance(); void refreshCoreData(); // Balance + blockchain info (can use fast_worker_)
void refreshAddresses(); void refreshAddressData(); // Address lists + balances
void refreshTransactionData(); // Transaction list + z_viewtransaction enrichment
void refreshRecentTransactionData(); // Lightweight recent/unconfirmed tx poll
bool refreshEncryptionState(); // Wallet encryption/lock state
void refreshBalance(); // Legacy: balance-only refresh (used by specific callers)
void refreshAddresses(); // Legacy: standalone address refresh
void refreshPrice(); void refreshPrice();
void refreshWalletEncryptionState(); void refreshWalletEncryptionState();
void applyRefreshPolicy(ui::NavPage page);
bool currentPageNeedsWalletDataRefresh() const;
bool shouldRunWalletTransactionRefresh() const;
bool shouldRefreshTransactions() const;
bool shouldRefreshRecentTransactions() const;
void checkAutoLock(); void checkAutoLock();
void checkIdleMining(); void checkIdleMining();
}; };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@
#include "rpc/rpc_worker.h" #include "rpc/rpc_worker.h"
#include "rpc/connection.h" #include "rpc/connection.h"
#include "config/settings.h" #include "config/settings.h"
#include "daemon/daemon_controller.h"
#include "daemon/embedded_daemon.h" #include "daemon/embedded_daemon.h"
#include "ui/notifications.h" #include "ui/notifications.h"
#include "ui/material/color_theme.h" #include "ui/material/color_theme.h"
@@ -39,13 +40,43 @@ namespace dragonx {
using json = nlohmann::json; using json = nlohmann::json;
namespace {
struct WizardLowSpecSnapshot {
bool valid = false;
float blur = 0.0f;
float uiOp = 0.0f;
bool fx = false;
bool scanline = false;
};
struct WizardUiState {
float blur_amount = 1.5f;
bool theme_effects = true;
float ui_opacity = 1.0f;
bool low_spec = false;
bool scanline = true;
std::string balance_layout = "classic";
int language_index = 0;
bool appearance_init = false;
WizardLowSpecSnapshot low_spec_snapshot;
float card0_max_h = 0.0f;
float card1_max_h = 0.0f;
double external_last_check = -10.0;
bool daemon_prestarted = false;
};
WizardUiState s_wizardUi;
} // namespace
void App::restartWizard() void App::restartWizard()
{ {
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n"); DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
// Reset crash counter for fresh wizard attempt // Reset crash counter for fresh wizard attempt
if (embedded_daemon_) { if (daemon_controller_) {
embedded_daemon_->resetCrashCount(); daemon_controller_->resetCrashCount();
} }
// Disconnect RPC // Disconnect RPC
@@ -56,10 +87,11 @@ void App::restartWizard()
// Stop the embedded daemon in a background thread to avoid // Stop the embedded daemon in a background thread to avoid
// blocking the UI for up to 32 seconds (RPC stop + process wait). // blocking the UI for up to 32 seconds (RPC stop + process wait).
if (embedded_daemon_ && isEmbeddedDaemonRunning()) { if (daemon_controller_ && isEmbeddedDaemonRunning()) {
std::thread([this]() { async_tasks_.submit("wizard-restart-stop-daemon", [this](const util::AsyncTaskManager::Token& token) {
if (token.cancelled()) return;
stopEmbeddedDaemon(); stopEmbeddedDaemon();
}).detach(); });
} }
// Enter wizard — the wizard completion handler already calls // Enter wizard — the wizard completion handler already calls
@@ -73,6 +105,7 @@ void App::restartWizard()
// =========================================================================== // ===========================================================================
void App::renderFirstRunWizard() { void App::renderFirstRunWizard() {
auto& wizardUi = s_wizardUi;
ImGuiViewport* viewport = ImGui::GetMainViewport(); ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos); ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize); ImGui::SetNextWindowSize(viewport->WorkSize);
@@ -243,15 +276,14 @@ void App::renderFirstRunWizard() {
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp); (textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
cy += 14.0f * dp; cy += 14.0f * dp;
// Statics for appearance settings float& wiz_blur_amount = wizardUi.blur_amount;
static float wiz_blur_amount = 1.5f; bool& wiz_theme_effects = wizardUi.theme_effects;
static bool wiz_theme_effects = true; float& wiz_ui_opacity = wizardUi.ui_opacity;
static float wiz_ui_opacity = 1.0f; bool& wiz_low_spec = wizardUi.low_spec;
static bool wiz_low_spec = false; bool& wiz_scanline = wizardUi.scanline;
static bool wiz_scanline = true; std::string& wiz_balance_layout = wizardUi.balance_layout;
static std::string wiz_balance_layout = "classic"; int& wiz_language_index = wizardUi.language_index;
static int wiz_language_index = 0; bool& wiz_appearance_init = wizardUi.appearance_init;
static bool wiz_appearance_init = false;
if (!wiz_appearance_init) { if (!wiz_appearance_init) {
wiz_blur_amount = settings_->getBlurMultiplier(); wiz_blur_amount = settings_->getBlurMultiplier();
wiz_theme_effects = settings_->getThemeEffectsEnabled(); wiz_theme_effects = settings_->getThemeEffectsEnabled();
@@ -398,7 +430,7 @@ void App::renderFirstRunWizard() {
// --- Low-spec mode checkbox --- // --- Low-spec mode checkbox ---
// Snapshot for restoring settings when low-spec is turned off // Snapshot for restoring settings when low-spec is turned off
static struct { bool valid; float blur; float uiOp; bool fx; bool scanline; } wiz_lsSnap = {}; WizardLowSpecSnapshot& wiz_lsSnap = wizardUi.low_spec_snapshot;
ImGui::SetCursorScreenPos(ImVec2(cx, cy)); ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
@@ -596,7 +628,7 @@ void App::renderFirstRunWizard() {
cy += cardPad; cy += cardPad;
// Lock card height to the tallest content ever seen // Lock card height to the tallest content ever seen
static float card0MaxH = 0.0f; float& card0MaxH = wizardUi.card0_max_h;
card0MaxH = std::max(card0MaxH, cy - card0Top); card0MaxH = std::max(card0MaxH, cy - card0Top);
card0Bot = card0Top + card0MaxH; card0Bot = card0Top + card0MaxH;
@@ -774,14 +806,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) { if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption // Stop embedded daemon before bootstrap to avoid chain data corruption
if (isEmbeddedDaemonRunning()) { stopDaemonForBootstrap();
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap retry...\n");
if (rpc_ && rpc_->isConnected()) {
try { rpc_->call("stop"); } catch (...) {}
rpc_->disconnect();
}
onDisconnected("Bootstrap retry");
}
bootstrap_ = std::make_unique<util::Bootstrap>(); bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir(); std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir); bootstrap_->start(dataDir);
@@ -808,17 +833,23 @@ void App::renderFirstRunWizard() {
if (isFocused) { if (isFocused) {
static std::atomic<bool> s_extCached{false}; static std::atomic<bool> s_extCached{false};
static std::atomic<bool> s_checkInFlight{false}; static std::atomic<bool> s_checkInFlight{false};
static double s_extLastCheck = -10.0; double& s_extLastCheck = wizardUi.external_last_check;
double now = ImGui::GetTime(); double now = ImGui::GetTime();
if (now - s_extLastCheck >= 2.0 && !s_checkInFlight.load()) { if (now - s_extLastCheck >= 2.0 && !s_checkInFlight.load()) {
s_extLastCheck = now; s_extLastCheck = now;
bool embeddedRunning = isEmbeddedDaemonRunning(); bool embeddedRunning = isEmbeddedDaemonRunning();
s_checkInFlight.store(true); s_checkInFlight.store(true);
std::thread([embeddedRunning]() { async_tasks_.submit("wizard-external-daemon-check", [embeddedRunning](const util::AsyncTaskManager::Token& token) {
if (token.cancelled()) {
s_checkInFlight.store(false);
return;
}
bool inUse = daemon::EmbeddedDaemon::isRpcPortInUse(); bool inUse = daemon::EmbeddedDaemon::isRpcPortInUse();
s_extCached.store(inUse && !embeddedRunning); if (!token.cancelled()) {
s_extCached.store(inUse && !embeddedRunning);
}
s_checkInFlight.store(false); s_checkInFlight.store(false);
}).detach(); });
} }
externalRunning = s_extCached.load(); externalRunning = s_extCached.load();
} }
@@ -859,19 +890,19 @@ void App::renderFirstRunWizard() {
if (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) { if (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
wizard_stopping_external_ = true; wizard_stopping_external_ = true;
wizard_stop_status_ = "Sending stop command..."; wizard_stop_status_ = "Sending stop command...";
if (wizard_stop_thread_.joinable()) wizard_stop_thread_.join(); async_tasks_.submit("wizard-stop-external-daemon", [this](const util::AsyncTaskManager::Token& token) {
wizard_stop_thread_ = std::thread([this]() {
auto config = rpc::Connection::autoDetectConfig(); auto config = rpc::Connection::autoDetectConfig();
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) { if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
auto tmp_rpc = std::make_unique<rpc::RPCClient>(); auto tmp_rpc = std::make_unique<rpc::RPCClient>();
if (tmp_rpc->connect(config.host, config.port, if (tmp_rpc->connect(config.host, config.port,
config.rpcuser, config.rpcpassword)) { config.rpcuser, config.rpcpassword,
try { tmp_rpc->call("stop"); } catch (...) {} config.use_tls)) {
sendStopCommandSafely(*tmp_rpc, "Wizard external daemon stop");
tmp_rpc->disconnect(); tmp_rpc->disconnect();
} }
} }
wizard_stop_status_ = "Waiting for daemon to shut down..."; wizard_stop_status_ = "Waiting for daemon to shut down...";
for (int i = 0; i < 60; i++) { for (int i = 0; i < 60 && !token.cancelled(); i++) {
std::this_thread::sleep_for(std::chrono::seconds(1)); std::this_thread::sleep_for(std::chrono::seconds(1));
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) { if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
wizard_stop_status_ = "Daemon stopped."; wizard_stop_status_ = "Daemon stopped.";
@@ -879,6 +910,7 @@ void App::renderFirstRunWizard() {
return; return;
} }
} }
if (token.cancelled()) return;
wizard_stop_status_ = "Daemon did not stop — try manually."; wizard_stop_status_ = "Daemon did not stop — try manually.";
wizard_stopping_external_ = false; wizard_stopping_external_ = false;
}); });
@@ -955,14 +987,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) { if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption // Stop embedded daemon before bootstrap to avoid chain data corruption
if (isEmbeddedDaemonRunning()) { stopDaemonForBootstrap();
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap...\n");
if (rpc_ && rpc_->isConnected()) {
try { rpc_->call("stop"); } catch (...) {}
rpc_->disconnect();
}
onDisconnected("Bootstrap");
}
bootstrap_ = std::make_unique<util::Bootstrap>(); bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir(); std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir); bootstrap_->start(dataDir);
@@ -978,14 +1003,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface())); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) { if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
if (isEmbeddedDaemonRunning()) { stopDaemonForBootstrap();
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap (mirror)...\n");
if (rpc_ && rpc_->isConnected()) {
try { rpc_->call("stop"); } catch (...) {}
rpc_->disconnect();
}
onDisconnected("Bootstrap");
}
bootstrap_ = std::make_unique<util::Bootstrap>(); bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir(); std::string dataDir = util::Platform::getDragonXDataDir();
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName; std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
@@ -1012,7 +1030,7 @@ void App::renderFirstRunWizard() {
cy += cardPad; cy += cardPad;
// Lock card height to the tallest content ever seen (but not when collapsed) // Lock card height to the tallest content ever seen (but not when collapsed)
static float card1MaxH = 0.0f; float& card1MaxH = wizardUi.card1_max_h;
if (isCollapsed) { if (isCollapsed) {
card1Bot = card1Top + (cy - card1Top); card1Bot = card1Top + (cy - card1Top);
} else { } else {
@@ -1037,7 +1055,7 @@ void App::renderFirstRunWizard() {
// Pre-start daemon when encrypt card becomes focused so it's ready // Pre-start daemon when encrypt card becomes focused so it's ready
// by the time the user finishes typing their passphrase // by the time the user finishes typing their passphrase
if (isFocused) { if (isFocused) {
static bool wiz_daemon_prestarted = false; bool& wiz_daemon_prestarted = wizardUi.daemon_prestarted;
if (!wiz_daemon_prestarted) { if (!wiz_daemon_prestarted) {
wiz_daemon_prestarted = true; wiz_daemon_prestarted = true;
if (!state_.connected && isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) { if (!state_.connected && isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
@@ -1281,10 +1299,9 @@ void App::renderFirstRunWizard() {
ImGui::BeginDisabled(!canEncrypt); ImGui::BeginDisabled(!canEncrypt);
if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) { if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
// Save passphrase + optional PIN for background processing // Save passphrase + optional PIN for background processing
deferred_encrypt_passphrase_ = std::string(encrypt_pass_buf_); wallet_security_.beginDeferredEncryption(
if (pinEntered && pinOk) std::string(encrypt_pass_buf_),
deferred_encrypt_pin_ = pinStr; (pinEntered && pinOk) ? pinStr : std::string());
deferred_encrypt_pending_ = true;
// Clear sensitive buffers // Clear sensitive buffers
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_)); memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
@@ -1294,7 +1311,10 @@ void App::renderFirstRunWizard() {
// Start daemon + finish wizard immediately // Start daemon + finish wizard immediately
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) { if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
startEmbeddedDaemon(); if (!startEmbeddedDaemon()) {
ui::Notifications::instance().warning(
TR("wizard_daemon_start_failed"));
}
} }
tryConnect(); tryConnect();
wizard_phase_ = WizardPhase::Done; wizard_phase_ = WizardPhase::Done;
@@ -1313,7 +1333,10 @@ void App::renderFirstRunWizard() {
settings_->setWizardCompleted(true); settings_->setWizardCompleted(true);
settings_->save(); settings_->save();
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) { if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
startEmbeddedDaemon(); if (!startEmbeddedDaemon()) {
ui::Notifications::instance().warning(
TR("wizard_daemon_start_failed"));
}
} }
tryConnect(); tryConnect();
} }

View File

@@ -1,6 +1,9 @@
// DragonX Wallet - ImGui Edition // DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers // Copyright 2024-2026 The Hush Developers
// Released under the GPLv3 // Released under the GPLv3
//
// settings.cpp — JSON settings persistence. Loads/saves user preferences
// to ~/.config/ObsidianDragon/settings.json (Linux/macOS) or %APPDATA% (Windows).
#include "settings.h" #include "settings.h"
#include "version.h" #include "version.h"
@@ -123,12 +126,28 @@ bool Settings::load(const std::string& path)
for (const auto& a : j["favorite_addresses"]) for (const auto& a : j["favorite_addresses"])
if (a.is_string()) favorite_addresses_.insert(a.get<std::string>()); if (a.is_string()) favorite_addresses_.insert(a.get<std::string>());
} }
if (j.contains("address_meta") && j["address_meta"].is_object()) {
address_meta_.clear();
for (auto& [addr, meta] : j["address_meta"].items()) {
AddressMeta m;
if (meta.contains("label") && meta["label"].is_string())
m.label = meta["label"].get<std::string>();
if (meta.contains("icon") && meta["icon"].is_string())
m.icon = meta["icon"].get<std::string>();
if (meta.contains("order") && meta["order"].is_number_integer())
m.sortOrder = meta["order"].get<int>();
if (meta.contains("mining") && meta["mining"].is_boolean())
m.mining = meta["mining"].get<bool>();
address_meta_[addr] = m;
}
}
if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get<bool>(); 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("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("unlock_duration")) unlock_duration_ = j["unlock_duration"].get<int>();
if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get<bool>(); 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("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("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("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
if (j.contains("debug_categories") && j["debug_categories"].is_array()) { if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
debug_categories_.clear(); debug_categories_.clear();
@@ -137,6 +156,7 @@ bool Settings::load(const std::string& path)
} }
if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get<bool>(); if (j.contains("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("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_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("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>(); if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
@@ -153,6 +173,7 @@ bool Settings::load(const std::string& path)
if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get<bool>(); if (j.contains("idle_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_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_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()) { if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
saved_pool_urls_.clear(); saved_pool_urls_.clear();
for (const auto& u : j["saved_pool_urls"]) for (const auto& u : j["saved_pool_urls"])
@@ -224,18 +245,33 @@ bool Settings::save(const std::string& path)
j["favorite_addresses"] = json::array(); j["favorite_addresses"] = json::array();
for (const auto& addr : favorite_addresses_) for (const auto& addr : favorite_addresses_)
j["favorite_addresses"].push_back(addr); j["favorite_addresses"].push_back(addr);
{
json meta_obj = json::object();
for (const auto& [addr, m] : address_meta_) {
if (m.label.empty() && m.icon.empty() && m.sortOrder < 0 && !m.mining) continue;
json entry = json::object();
if (!m.label.empty()) entry["label"] = m.label;
if (!m.icon.empty()) entry["icon"] = m.icon;
if (m.sortOrder >= 0) entry["order"] = m.sortOrder;
if (m.mining) entry["mining"] = true;
meta_obj[addr] = entry;
}
j["address_meta"] = meta_obj;
}
j["wizard_completed"] = wizard_completed_; j["wizard_completed"] = wizard_completed_;
j["auto_lock_timeout"] = auto_lock_timeout_; j["auto_lock_timeout"] = auto_lock_timeout_;
j["unlock_duration"] = unlock_duration_; j["unlock_duration"] = unlock_duration_;
j["pin_enabled"] = pin_enabled_; j["pin_enabled"] = pin_enabled_;
j["keep_daemon_running"] = keep_daemon_running_; j["keep_daemon_running"] = keep_daemon_running_;
j["stop_external_daemon"] = stop_external_daemon_; j["stop_external_daemon"] = stop_external_daemon_;
j["max_connections"] = max_connections_;
j["verbose_logging"] = verbose_logging_; j["verbose_logging"] = verbose_logging_;
j["debug_categories"] = json::array(); j["debug_categories"] = json::array();
for (const auto& cat : debug_categories_) for (const auto& cat : debug_categories_)
j["debug_categories"].push_back(cat); j["debug_categories"].push_back(cat);
j["theme_effects_enabled"] = theme_effects_enabled_; j["theme_effects_enabled"] = theme_effects_enabled_;
j["low_spec_mode"] = low_spec_mode_; j["low_spec_mode"] = low_spec_mode_;
j["reduce_motion"] = reduce_motion_;
j["selected_exchange"] = selected_exchange_; j["selected_exchange"] = selected_exchange_;
j["selected_pair"] = selected_pair_; j["selected_pair"] = selected_pair_;
j["pool_url"] = pool_url_; j["pool_url"] = pool_url_;
@@ -250,6 +286,7 @@ bool Settings::save(const std::string& path)
j["idle_thread_scaling"] = idle_thread_scaling_; j["idle_thread_scaling"] = idle_thread_scaling_;
j["idle_threads_active"] = idle_threads_active_; j["idle_threads_active"] = idle_threads_active_;
j["idle_threads_idle"] = idle_threads_idle_; j["idle_threads_idle"] = idle_threads_idle_;
j["idle_gpu_aware"] = idle_gpu_aware_;
j["saved_pool_urls"] = json::array(); j["saved_pool_urls"] = json::array();
for (const auto& u : saved_pool_urls_) for (const auto& u : saved_pool_urls_)
j["saved_pool_urls"].push_back(u); j["saved_pool_urls"].push_back(u);

View File

@@ -5,6 +5,7 @@
#pragma once #pragma once
#include <algorithm> #include <algorithm>
#include <map>
#include <string> #include <string>
#include <set> #include <set>
#include <vector> #include <vector>
@@ -141,6 +142,54 @@ public:
void unfavoriteAddress(const std::string& addr) { favorite_addresses_.erase(addr); } void unfavoriteAddress(const std::string& addr) { favorite_addresses_.erase(addr); }
int getFavoriteAddressCount() const { return (int)favorite_addresses_.size(); } int getFavoriteAddressCount() const { return (int)favorite_addresses_.size(); }
// Address metadata (labels, icons, custom ordering)
struct AddressMeta {
std::string label;
std::string icon; // material icon name, e.g. "savings"
int sortOrder = -1; // -1 = auto (use default sort)
bool mining = false;
};
const AddressMeta& getAddressMeta(const std::string& addr) const {
static const AddressMeta empty{};
auto it = address_meta_.find(addr);
return it != address_meta_.end() ? it->second : empty;
}
void setAddressLabel(const std::string& addr, const std::string& label) {
address_meta_[addr].label = label;
}
void setAddressIcon(const std::string& addr, const std::string& icon) {
address_meta_[addr].icon = icon;
}
void setAddressSortOrder(const std::string& addr, int order) {
address_meta_[addr].sortOrder = order;
}
bool isMiningAddress(const std::string& addr) const {
auto it = address_meta_.find(addr);
return it != address_meta_.end() && it->second.mining;
}
void setMiningAddress(const std::string& addr, bool mining) {
address_meta_[addr].mining = mining;
}
std::set<std::string> getMiningAddresses() const {
std::set<std::string> addresses;
for (const auto& [addr, meta] : address_meta_) {
if (meta.mining) addresses.insert(addr);
}
return addresses;
}
int getNextSortOrder() const {
int mx = -1;
for (const auto& [k, v] : address_meta_)
if (v.sortOrder > mx) mx = v.sortOrder;
return mx + 1;
}
void swapAddressOrder(const std::string& a, const std::string& b) {
int oa = address_meta_[a].sortOrder;
int ob = address_meta_[b].sortOrder;
address_meta_[a].sortOrder = ob;
address_meta_[b].sortOrder = oa;
}
// First-run wizard // First-run wizard
bool getWizardCompleted() const { return wizard_completed_; } bool getWizardCompleted() const { return wizard_completed_; }
void setWizardCompleted(bool v) { wizard_completed_ = v; } void setWizardCompleted(bool v) { wizard_completed_ = v; }
@@ -165,6 +214,10 @@ public:
bool getStopExternalDaemon() const { return stop_external_daemon_; } bool getStopExternalDaemon() const { return stop_external_daemon_; }
void setStopExternalDaemon(bool v) { stop_external_daemon_ = v; } void setStopExternalDaemon(bool v) { stop_external_daemon_ = v; }
// Daemon — maximum peer connections (0 = daemon default)
int getMaxConnections() const { return max_connections_; }
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.) // Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
bool getVerboseLogging() const { return verbose_logging_; } bool getVerboseLogging() const { return verbose_logging_; }
void setVerboseLogging(bool v) { verbose_logging_ = v; } void setVerboseLogging(bool v) { verbose_logging_ = v; }
@@ -186,6 +239,10 @@ public:
bool getLowSpecMode() const { return low_spec_mode_; } bool getLowSpecMode() const { return low_spec_mode_; }
void setLowSpecMode(bool v) { low_spec_mode_ = v; } void setLowSpecMode(bool v) { low_spec_mode_ = v; }
// Reduce motion — disables animated transitions for accessibility
bool getReduceMotion() const { return reduce_motion_; }
void setReduceMotion(bool v) { reduce_motion_ = v; }
// Market — last selected exchange + pair // Market — last selected exchange + pair
std::string getSelectedExchange() const { return selected_exchange_; } std::string getSelectedExchange() const { return selected_exchange_; }
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; } void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
@@ -221,6 +278,8 @@ public:
void setIdleThreadsActive(int v) { idle_threads_active_ = std::max(0, v); } void setIdleThreadsActive(int v) { idle_threads_active_ = std::max(0, v); }
int getIdleThreadsIdle() const { return idle_threads_idle_; } int getIdleThreadsIdle() const { return idle_threads_idle_; }
void setIdleThreadsIdle(int v) { idle_threads_idle_ = std::max(0, v); } void setIdleThreadsIdle(int v) { idle_threads_idle_ = std::max(0, v); }
bool getIdleGpuAware() const { return idle_gpu_aware_; }
void setIdleGpuAware(bool v) { idle_gpu_aware_ = v; }
// Saved pool URLs (user-managed favorites dropdown) // Saved pool URLs (user-managed favorites dropdown)
const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; } const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; }
@@ -291,16 +350,19 @@ private:
bool scanline_enabled_ = true; bool scanline_enabled_ = true;
std::set<std::string> hidden_addresses_; std::set<std::string> hidden_addresses_;
std::set<std::string> favorite_addresses_; std::set<std::string> favorite_addresses_;
std::map<std::string, AddressMeta> address_meta_;
bool wizard_completed_ = false; bool wizard_completed_ = false;
int auto_lock_timeout_ = 900; // 15 minutes int auto_lock_timeout_ = 900; // 15 minutes
int unlock_duration_ = 600; // 10 minutes int unlock_duration_ = 600; // 10 minutes
bool pin_enabled_ = false; bool pin_enabled_ = false;
bool keep_daemon_running_ = false; bool keep_daemon_running_ = false;
bool stop_external_daemon_ = false; bool stop_external_daemon_ = false;
int max_connections_ = 0; // 0 = daemon default
bool verbose_logging_ = false; bool verbose_logging_ = false;
std::set<std::string> debug_categories_; std::set<std::string> debug_categories_;
bool theme_effects_enabled_ = true; bool theme_effects_enabled_ = true;
bool low_spec_mode_ = false; bool low_spec_mode_ = false;
bool reduce_motion_ = false;
std::string selected_exchange_ = "TradeOgre"; std::string selected_exchange_ = "TradeOgre";
std::string selected_pair_ = "DRGX/BTC"; std::string selected_pair_ = "DRGX/BTC";
@@ -317,6 +379,7 @@ private:
bool idle_thread_scaling_ = false; // scale threads instead of start/stop bool idle_thread_scaling_ = false; // scale threads instead of start/stop
int idle_threads_active_ = 0; // threads when user active (0 = auto) int idle_threads_active_ = 0; // threads when user active (0 = auto)
int idle_threads_idle_ = 0; // threads when idle (0 = auto = all) int idle_threads_idle_ = 0; // threads when idle (0 = auto = all)
bool idle_gpu_aware_ = true; // treat GPU activity as non-idle
std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites

View File

@@ -7,10 +7,10 @@
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake. // !! 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 ...) // !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
#define DRAGONX_VERSION "1.1.1" #define DRAGONX_VERSION "1.2.0-rc1"
#define DRAGONX_VERSION_MAJOR 1 #define DRAGONX_VERSION_MAJOR 1
#define DRAGONX_VERSION_MINOR 1 #define DRAGONX_VERSION_MINOR 2
#define DRAGONX_VERSION_PATCH 1 #define DRAGONX_VERSION_PATCH 0
#define DRAGONX_APP_NAME "ObsidianDragon" #define DRAGONX_APP_NAME "ObsidianDragon"
#define DRAGONX_ORG_NAME "Hush" #define DRAGONX_ORG_NAME "Hush"

View File

@@ -7,7 +7,7 @@
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake. // !! 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 ...) // !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
#define DRAGONX_VERSION "@PROJECT_VERSION@" #define DRAGONX_VERSION "@PROJECT_VERSION@@DRAGONX_VERSION_SUFFIX@"
#define DRAGONX_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ #define DRAGONX_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define DRAGONX_VERSION_MINOR @PROJECT_VERSION_MINOR@ #define DRAGONX_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define DRAGONX_VERSION_PATCH @PROJECT_VERSION_PATCH@ #define DRAGONX_VERSION_PATCH @PROJECT_VERSION_PATCH@

View File

@@ -0,0 +1,117 @@
#include "daemon_controller.h"
#include "../config/settings.h"
#include <algorithm>
namespace dragonx {
namespace daemon {
DaemonController::DaemonController()
: daemon_(std::make_unique<EmbeddedDaemon>())
{
}
DaemonController::~DaemonController() = default;
void DaemonController::setStateCallback(StateCallback callback)
{
daemon_->setStateCallback(std::move(callback));
}
void DaemonController::syncSettings(const config::Settings* settings)
{
if (!settings) return;
daemon_->setDebugCategories(settings->getDebugCategories());
daemon_->setMaxConnections(settings->getMaxConnections());
}
bool DaemonController::start(const config::Settings* settings)
{
syncSettings(settings);
return daemon_->start();
}
void DaemonController::stop(int waitMs)
{
daemon_->stop(waitMs);
}
bool DaemonController::isRunning() const
{
return daemon_->isRunning();
}
bool DaemonController::externalDaemonDetected() const
{
return daemon_->externalDaemonDetected();
}
DaemonController::State DaemonController::state() const
{
return daemon_->getState();
}
const std::string& DaemonController::lastError() const
{
return daemon_->getLastError();
}
int DaemonController::crashCount() const
{
return daemon_->getCrashCount();
}
int DaemonController::lastBlockHeight() const
{
return daemon_ ? daemon_->getLastBlockHeight() : 0;
}
double DaemonController::memoryUsageMB() const
{
return daemon_ ? daemon_->getMemoryUsageMB() : 0.0;
}
std::vector<std::string> DaemonController::recentLines(std::size_t count) const
{
return daemon_ ? daemon_->getRecentLines(count) : std::vector<std::string>{};
}
std::string DaemonController::outputSince(std::size_t& offset) const
{
return daemon_ ? daemon_->getOutputSince(offset) : std::string{};
}
void DaemonController::resetCrashCount()
{
daemon_->resetCrashCount();
}
void DaemonController::setRescanOnNextStart(bool enabled)
{
daemon_->setRescanOnNextStart(enabled);
}
bool DaemonController::rescanOnNextStart() const
{
return daemon_->rescanOnNextStart();
}
void DaemonController::prepareLifecycleOperation(const LifecycleDecision& decision,
const config::Settings* settings)
{
if (settings) syncSettings(settings);
if (decision.resetCrashCount) resetCrashCount();
if (decision.setRescanOnNextStart) setRescanOnNextStart(true);
}
DaemonController::ShutdownDecision DaemonController::shutdownDecision(
bool keepDaemonRunning, bool stopExternalDaemon) const
{
return evaluateShutdownPolicy(static_cast<bool>(daemon_),
daemon_ && daemon_->externalDaemonDetected(),
keepDaemonRunning,
stopExternalDaemon);
}
} // namespace daemon
} // namespace dragonx

View File

@@ -0,0 +1,225 @@
#pragma once
#include "embedded_daemon.h"
#include <algorithm>
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
namespace dragonx {
namespace config { class Settings; }
namespace daemon {
class DaemonController {
public:
using State = EmbeddedDaemon::State;
using StateCallback = EmbeddedDaemon::StateCallback;
enum class ShutdownAction {
DisconnectOnly,
StopDaemon
};
struct ShutdownDecision {
ShutdownAction action = ShutdownAction::DisconnectOnly;
const char* logReason = "no embedded daemon";
const char* status = "Disconnecting...";
};
enum class LifecycleOperation {
ManualRestart,
Rescan,
DeleteBlockchainData,
BootstrapStop
};
struct LifecycleDecision {
LifecycleOperation operation = LifecycleOperation::ManualRestart;
bool allowed = false;
bool wasRunning = false;
const char* taskName = "";
const char* status = "";
const char* warning = "";
bool resetCrashCount = false;
bool setRescanOnNextStart = false;
bool disconnectRpc = false;
int restartDelayMs = 0;
};
class LifecycleTaskContext {
public:
virtual ~LifecycleTaskContext() = default;
virtual bool cancelled() const = 0;
virtual bool shuttingDown() const = 0;
virtual void sleepForMs(int milliseconds) = 0;
};
class LifecycleRuntime {
public:
virtual ~LifecycleRuntime() = default;
virtual void stopDaemonWithPolicy() = 0;
virtual bool startDaemon() = 0;
virtual int deleteBlockchainData() = 0;
virtual void resetOutputOffset() = 0;
virtual void requestRpcStopAndDisconnect(const char* context, const char* reason) = 0;
};
struct LifecycleExecutionResult {
bool completed = false;
bool cancelled = false;
bool stopped = false;
bool started = false;
int deletedItems = 0;
};
DaemonController();
~DaemonController();
DaemonController(const DaemonController&) = delete;
DaemonController& operator=(const DaemonController&) = delete;
EmbeddedDaemon* daemon() { return daemon_.get(); }
const EmbeddedDaemon* daemon() const { return daemon_.get(); }
void setStateCallback(StateCallback callback);
void syncSettings(const config::Settings* settings);
bool start(const config::Settings* settings);
void stop(int waitMs);
bool isRunning() const;
bool externalDaemonDetected() const;
State state() const;
const std::string& lastError() const;
int crashCount() const;
int lastBlockHeight() const;
double memoryUsageMB() const;
std::vector<std::string> recentLines(std::size_t count) const;
std::string outputSince(std::size_t& offset) const;
void resetCrashCount();
void setRescanOnNextStart(bool enabled);
bool rescanOnNextStart() const;
static ShutdownDecision evaluateShutdownPolicy(bool hasDaemon,
bool externalDaemonDetected,
bool keepDaemonRunning,
bool stopExternalDaemon) {
if (!hasDaemon) {
return {};
}
if (keepDaemonRunning) {
return {ShutdownAction::DisconnectOnly,
"keep_daemon_running enabled",
"Disconnecting (daemon stays running)..."};
}
if (externalDaemonDetected && !stopExternalDaemon) {
return {ShutdownAction::DisconnectOnly,
"external daemon (not ours to stop)",
"Disconnecting (daemon stays running)..."};
}
return {ShutdownAction::StopDaemon,
"stopping managed daemon",
"Sending stop command to daemon..."};
}
static LifecycleDecision evaluateLifecycleOperation(LifecycleOperation operation,
bool usingEmbeddedDaemon,
bool hasDaemon,
bool daemonRunning,
bool restartInProgress = false) {
switch (operation) {
case LifecycleOperation::ManualRestart:
if (!usingEmbeddedDaemon || restartInProgress) return {};
return {operation, true, daemonRunning, "daemon-restart", "Restarting daemon...", "",
true, false, true, 500};
case LifecycleOperation::Rescan:
if (!usingEmbeddedDaemon || !hasDaemon) {
return {operation, false, daemonRunning, "", "",
"Rescan requires embedded daemon. Restart your daemon with -rescan manually."};
}
return {operation, true, daemonRunning, "rescan-blockchain", "Starting rescan...", "",
false, true, false, 3000};
case LifecycleOperation::DeleteBlockchainData:
if (!usingEmbeddedDaemon || !hasDaemon) {
return {operation, false, daemonRunning, "", "",
"Delete blockchain requires embedded daemon. Stop your daemon manually and delete the data directory."};
}
return {operation, true, daemonRunning, "delete-blockchain-data", "Deleting blockchain data...", "",
false, false, false, 3000};
case LifecycleOperation::BootstrapStop:
return {operation, true, daemonRunning, "bootstrap-stop-daemon", "Stopping daemon for bootstrap...", "",
false, false, true, 0};
}
return {};
}
void prepareLifecycleOperation(const LifecycleDecision& decision,
const config::Settings* settings = nullptr);
static inline LifecycleExecutionResult executeLifecycleOperation(const LifecycleDecision& decision,
LifecycleRuntime& runtime,
LifecycleTaskContext& task)
{
LifecycleExecutionResult result;
if (!decision.allowed) return result;
auto waitForDelay = [&]() {
int waitTicks = std::max(0, decision.restartDelayMs / 100);
for (int i = 0; i < waitTicks && !task.cancelled() && !task.shuttingDown(); ++i) {
task.sleepForMs(100);
}
};
auto cancelled = [&]() {
result.cancelled = task.cancelled() || task.shuttingDown();
return result.cancelled;
};
switch (decision.operation) {
case LifecycleOperation::BootstrapStop:
if (decision.wasRunning) {
runtime.requestRpcStopAndDisconnect("Bootstrap daemon stop", "Bootstrap");
result.stopped = true;
}
result.completed = true;
return result;
case LifecycleOperation::ManualRestart:
if (decision.wasRunning) {
runtime.stopDaemonWithPolicy();
result.stopped = true;
}
break;
case LifecycleOperation::Rescan:
case LifecycleOperation::DeleteBlockchainData:
runtime.stopDaemonWithPolicy();
result.stopped = true;
break;
}
if (cancelled()) return result;
waitForDelay();
if (cancelled()) return result;
if (decision.operation == LifecycleOperation::DeleteBlockchainData) {
result.deletedItems = runtime.deleteBlockchainData();
if (cancelled()) return result;
}
if (decision.operation == LifecycleOperation::Rescan ||
decision.operation == LifecycleOperation::DeleteBlockchainData) {
runtime.resetOutputOffset();
}
result.started = runtime.startDaemon();
result.completed = !cancelled();
return result;
}
ShutdownDecision shutdownDecision(bool keepDaemonRunning,
bool stopExternalDaemon) const;
private:
std::unique_ptr<EmbeddedDaemon> daemon_;
};
} // namespace daemon
} // namespace dragonx

View File

@@ -1,6 +1,9 @@
// DragonX Wallet - ImGui Edition // DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers // Copyright 2024-2026 The Hush Developers
// Released under the GPLv3 // Released under the GPLv3
//
// embedded_daemon.cpp — Manages the dragonxd child process lifecycle:
// binary discovery, process spawn, stdout/stderr monitoring, crash recovery.
#include "embedded_daemon.h" #include "embedded_daemon.h"
#include "../config/version.h" #include "../config/version.h"
@@ -202,7 +205,11 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
"-ac_reward=300000000", "-ac_reward=300000000",
"-ac_blocktime=36", "-ac_blocktime=36",
"-ac_private=1", "-ac_private=1",
"-addnode=176.126.87.241", "-addnode=node.dragonx.is",
"-addnode=node1.dragonx.is",
"-addnode=node2.dragonx.is",
"-addnode=node3.dragonx.is",
"-addnode=node4.dragonx.is",
"-experimentalfeatures", "-experimentalfeatures",
"-developerencryptwallet", "-developerencryptwallet",
dbcache_arg dbcache_arg
@@ -470,6 +477,11 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
args.push_back("-debug=" + cat); args.push_back("-debug=" + cat);
} }
// Append max connections if configured (0 = daemon default)
if (max_connections_ > 0) {
args.push_back("-maxconnections=" + std::to_string(max_connections_));
}
// Add -rescan flag if requested (one-shot) // Add -rescan flag if requested (one-shot)
if (rescan_on_next_start_.exchange(false)) { if (rescan_on_next_start_.exchange(false)) {
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n"); DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");

View File

@@ -117,6 +117,26 @@ public:
*/ */
std::vector<std::string> getRecentLines(int maxLines = 8) const; std::vector<std::string> getRecentLines(int maxLines = 8) const;
/**
* @brief Extract the latest block height from daemon output (thread-safe).
* Parses the last "height=N" from UpdateTip lines without copying
* the entire output buffer. Returns -1 if no UpdateTip found.
*/
int getLastBlockHeight() const {
std::lock_guard<std::mutex> lk(output_mutex_);
// Search backwards from the end for "height="
size_t pos = process_output_.rfind("height=");
if (pos == std::string::npos) return -1;
pos += 7; // skip "height="
int h = 0;
for (size_t i = pos; i < process_output_.size(); ++i) {
char c = process_output_[i];
if (c >= '0' && c <= '9') h = h * 10 + (c - '0');
else break;
}
return h > 0 ? h : -1;
}
/** /**
* @brief Whether start() detected an existing daemon on the RPC port. * @brief Whether start() detected an existing daemon on the RPC port.
* When true the wallet should connect to it instead of showing an error. * When true the wallet should connect to it instead of showing an error.
@@ -152,6 +172,11 @@ public:
void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; } void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
const std::set<std::string>& getDebugCategories() const { return debug_categories_; } const std::set<std::string>& getDebugCategories() const { return debug_categories_; }
/**
* @brief Set maximum peer connections (0 = use daemon default)
*/
void setMaxConnections(int v) { max_connections_ = v; }
/** /**
* @brief Request a blockchain rescan on the next daemon start * @brief Request a blockchain rescan on the next daemon start
*/ */
@@ -194,6 +219,7 @@ private:
std::thread monitor_thread_; std::thread monitor_thread_;
std::atomic<bool> should_stop_{false}; std::atomic<bool> should_stop_{false};
std::set<std::string> debug_categories_; std::set<std::string> debug_categories_;
int max_connections_ = 0; // 0 = daemon default
std::atomic<int> crash_count_{0}; // consecutive crash counter 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> rescan_on_next_start_{false}; // -rescan flag for next start
}; };

View File

@@ -0,0 +1,93 @@
#include "lifecycle_adapters.h"
#include "../util/logger.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <thread>
namespace dragonx {
namespace daemon {
AsyncLifecycleTaskContext::AsyncLifecycleTaskContext(
const util::AsyncTaskManager::Token& token,
const std::atomic<bool>& shuttingDown)
: token_(token), shuttingDown_(shuttingDown)
{
}
bool AsyncLifecycleTaskContext::cancelled() const
{
return token_.cancelled();
}
bool AsyncLifecycleTaskContext::shuttingDown() const
{
return shuttingDown_.load(std::memory_order_relaxed);
}
void AsyncLifecycleTaskContext::sleepForMs(int milliseconds)
{
std::this_thread::sleep_for(std::chrono::milliseconds(std::max(0, milliseconds)));
}
bool ImmediateLifecycleTaskContext::cancelled() const
{
return false;
}
bool ImmediateLifecycleTaskContext::shuttingDown() const
{
return false;
}
void ImmediateLifecycleTaskContext::sleepForMs(int)
{
}
int BlockchainDataCleaner::removeBlockchainData(const std::filesystem::path& dataDir)
{
namespace fs = std::filesystem;
static constexpr std::array<const char*, 4> directories = {
"blocks", "chainstate", "database", "notarizations"
};
static constexpr std::array<const char*, 5> files = {
"peers.dat", "fee_estimates.dat", "banlist.dat", "db.log", ".lock"
};
int removed = 0;
for (const char* directoryName : directories) {
fs::path path = dataDir / directoryName;
std::error_code existsError;
if (!fs::exists(path, existsError)) continue;
std::error_code removeError;
auto count = fs::remove_all(path, removeError);
if (!removeError) {
removed += static_cast<int>(count);
DEBUG_LOGF("[DaemonLifecycle] Removed %s (%d entries)\n",
directoryName, static_cast<int>(count));
} else {
DEBUG_LOGF("[DaemonLifecycle] Failed to remove %s: %s\n",
directoryName, removeError.message().c_str());
}
}
for (const char* fileName : files) {
fs::path path = dataDir / fileName;
std::error_code removeError;
if (fs::remove(path, removeError)) {
++removed;
DEBUG_LOGF("[DaemonLifecycle] Removed %s\n", fileName);
} else if (removeError) {
DEBUG_LOGF("[DaemonLifecycle] Failed to remove %s: %s\n",
fileName, removeError.message().c_str());
}
}
return removed;
}
} // namespace daemon
} // namespace dragonx

View File

@@ -0,0 +1,39 @@
#pragma once
#include "daemon_controller.h"
#include "../util/async_task_manager.h"
#include <atomic>
#include <filesystem>
namespace dragonx {
namespace daemon {
class AsyncLifecycleTaskContext final : public DaemonController::LifecycleTaskContext {
public:
AsyncLifecycleTaskContext(const util::AsyncTaskManager::Token& token,
const std::atomic<bool>& shuttingDown);
bool cancelled() const override;
bool shuttingDown() const override;
void sleepForMs(int milliseconds) override;
private:
const util::AsyncTaskManager::Token& token_;
const std::atomic<bool>& shuttingDown_;
};
class ImmediateLifecycleTaskContext final : public DaemonController::LifecycleTaskContext {
public:
bool cancelled() const override;
bool shuttingDown() const override;
void sleepForMs(int milliseconds) override;
};
class BlockchainDataCleaner final {
public:
static int removeBlockchainData(const std::filesystem::path& dataDir);
};
} // namespace daemon
} // namespace dragonx

View File

@@ -1,6 +1,9 @@
// DragonX Wallet - ImGui Edition // DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers // Copyright 2024-2026 The Hush Developers
// Released under the GPLv3 // Released under the GPLv3
//
// xmrig_manager.cpp — Pool mining process management via xmrig-hac.
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
#include "xmrig_manager.h" #include "xmrig_manager.h"
#include "../resources/embedded_resources.h" #include "../resources/embedded_resources.h"

View File

@@ -88,6 +88,10 @@ public:
const PoolStats& getStats() const { return stats_; } const PoolStats& getStats() const { return stats_; }
const std::string& getLastError() const { return last_error_; } const std::string& getLastError() const { return last_error_; }
/// Thread count requested at start() — available immediately, unlike
/// PoolStats::threads_active which requires an API response.
int getRequestedThreads() const { return threads_; }
/** /**
* @brief Get last N lines of xmrig stdout (thread-safe snapshot). * @brief Get last N lines of xmrig stdout (thread-safe snapshot).
*/ */

View File

@@ -0,0 +1,510 @@
#include "transaction_history_cache.h"
#include "../util/logger.h"
#include "../util/platform.h"
#include <nlohmann/json.hpp>
#include <sqlite3.h>
#include <sodium.h>
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <utility>
namespace fs = std::filesystem;
using json = nlohmann::json;
namespace dragonx {
namespace data {
namespace {
constexpr int kSchemaVersion = 1;
constexpr std::size_t kKeyBytes = 32;
struct Statement {
sqlite3_stmt* handle = nullptr;
Statement(sqlite3* db, const char* sql)
{
if (sqlite3_prepare_v2(db, sql, -1, &handle, nullptr) != SQLITE_OK) {
handle = nullptr;
}
}
~Statement()
{
if (handle) sqlite3_finalize(handle);
}
Statement(const Statement&) = delete;
Statement& operator=(const Statement&) = delete;
};
bool bindText(sqlite3_stmt* statement, int index, const std::string& value)
{
return sqlite3_bind_text(statement, index, value.c_str(), -1, SQLITE_TRANSIENT) == SQLITE_OK;
}
bool bindBlob(sqlite3_stmt* statement, int index, const std::vector<unsigned char>& value)
{
return sqlite3_bind_blob(statement, index, value.data(), static_cast<int>(value.size()), SQLITE_TRANSIENT) == SQLITE_OK;
}
std::vector<unsigned char> readBlob(sqlite3_stmt* statement, int index)
{
const void* data = sqlite3_column_blob(statement, index);
int bytes = sqlite3_column_bytes(statement, index);
if (!data || bytes <= 0) return {};
const auto* begin = static_cast<const unsigned char*>(data);
return std::vector<unsigned char>(begin, begin + bytes);
}
std::string hexEncode(const unsigned char* bytes, std::size_t length)
{
static constexpr char kHex[] = "0123456789abcdef";
std::string output;
output.resize(length * 2);
for (std::size_t index = 0; index < length; ++index) {
output[index * 2] = kHex[(bytes[index] >> 4) & 0x0F];
output[index * 2 + 1] = kHex[bytes[index] & 0x0F];
}
return output;
}
json transactionToJson(const TransactionInfo& transaction)
{
return json{
{"txid", transaction.txid},
{"type", transaction.type},
{"amount", transaction.amount},
{"timestamp", transaction.timestamp},
{"confirmations", transaction.confirmations},
{"address", transaction.address},
{"from_address", transaction.from_address},
{"memo", transaction.memo}
};
}
TransactionInfo transactionFromJson(const json& source)
{
TransactionInfo transaction;
transaction.txid = source.value("txid", std::string());
transaction.type = source.value("type", std::string());
transaction.amount = source.value("amount", 0.0);
transaction.timestamp = source.value("timestamp", static_cast<std::int64_t>(0));
transaction.confirmations = source.value("confirmations", 0);
transaction.address = source.value("address", std::string());
transaction.from_address = source.value("from_address", std::string());
transaction.memo = source.value("memo", std::string());
return transaction;
}
std::string associatedDataForWallet(const std::string& walletHash)
{
return std::string("obsidian-dragon-tx-history-v1:") + walletHash;
}
} // namespace
TransactionHistoryCache::TransactionHistoryCache()
: TransactionHistoryCache(defaultDatabasePath())
{
}
TransactionHistoryCache::TransactionHistoryCache(std::string databasePath)
: database_path_(std::move(databasePath))
{
if (sodium_init() < 0) {
DEBUG_LOGF("Failed to initialize libsodium for transaction history cache\n");
}
}
TransactionHistoryCache::~TransactionHistoryCache()
{
lockKey();
close();
}
std::string TransactionHistoryCache::defaultDatabasePath()
{
return (fs::path(util::Platform::getConfigDir()) / "transaction_history.sqlite").string();
}
std::string TransactionHistoryCache::walletIdentityFromAddresses(
const std::vector<std::string>& shieldedAddresses,
const std::vector<std::string>& transparentAddresses)
{
std::vector<std::string> addresses;
addresses.reserve(shieldedAddresses.size() + transparentAddresses.size());
for (const auto& address : shieldedAddresses) {
if (!address.empty()) addresses.push_back("z:" + address);
}
for (const auto& address : transparentAddresses) {
if (!address.empty()) addresses.push_back("t:" + address);
}
if (addresses.empty()) return {};
std::sort(addresses.begin(), addresses.end());
std::string identity = "wallet-addresses-v1\n";
for (const auto& address : addresses) {
identity += address;
identity += '\n';
}
return identity;
}
std::string TransactionHistoryCache::walletIdentityHash(const std::string& walletIdentity)
{
unsigned char digest[crypto_generichash_BYTES];
crypto_generichash(digest, sizeof(digest),
reinterpret_cast<const unsigned char*>(walletIdentity.data()),
walletIdentity.size(), nullptr, 0);
return hexEncode(digest, sizeof(digest));
}
bool TransactionHistoryCache::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 transaction history cache 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 transaction history cache: %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");
if (!createSchema()) {
close();
return false;
}
return true;
}
bool TransactionHistoryCache::unlockWithPassphrase(const std::string& walletIdentity,
const std::string& passphrase)
{
if (walletIdentity.empty() || passphrase.empty() || !ensureOpen()) return false;
std::string walletHash = walletIdentityHash(walletIdentity);
std::vector<unsigned char> salt = getOrCreateSalt(walletHash);
if (salt.empty()) return false;
if (!deriveKey(passphrase, salt)) return false;
unlocked_wallet_hash_ = std::move(walletHash);
key_ready_ = true;
return true;
}
void TransactionHistoryCache::lockKey()
{
if (key_ready_) sodium_memzero(key_.data(), key_.size());
key_ready_ = false;
unlocked_wallet_hash_.clear();
}
bool TransactionHistoryCache::isUnlockedFor(const std::string& walletIdentity) const
{
return key_ready_ && !walletIdentity.empty() &&
unlocked_wallet_hash_ == walletIdentityHash(walletIdentity);
}
TransactionHistoryCache::LoadResult TransactionHistoryCache::load(
const std::string& walletIdentity,
int currentTipHeight,
const std::string& currentTipHash)
{
LoadResult result;
if (!isUnlockedFor(walletIdentity) || !ensureOpen()) return result;
std::string walletHash = walletIdentityHash(walletIdentity);
int tipHeight = 0;
std::string tipHash;
std::time_t updatedAt = 0;
std::vector<unsigned char> nonce;
std::vector<unsigned char> cipherText;
if (!readSnapshot(walletHash, tipHeight, tipHash, updatedAt, nonce, cipherText)) return result;
if ((currentTipHeight > 0 && tipHeight > currentTipHeight) ||
(currentTipHeight > 0 && tipHeight == currentTipHeight &&
!currentTipHash.empty() && !tipHash.empty() && tipHash != currentTipHash)) {
clearWalletByHash(walletHash);
result.invalidated = true;
return result;
}
std::string plainText;
if (!decryptPayload(walletHash, nonce, cipherText, plainText)) return result;
try {
json payload = json::parse(plainText);
if (payload.value("schema_version", 0) != kSchemaVersion) return result;
if (payload.value("wallet_hash", std::string()) != walletHash) return result;
if (!payload.contains("transactions") || !payload["transactions"].is_array()) return result;
result.transactions.reserve(payload["transactions"].size());
for (const auto& transactionJson : payload["transactions"]) {
if (transactionJson.is_object()) {
result.transactions.push_back(transactionFromJson(transactionJson));
}
}
if (payload.contains("shielded_scan_heights") && payload["shielded_scan_heights"].is_object()) {
for (auto it = payload["shielded_scan_heights"].begin();
it != payload["shielded_scan_heights"].end(); ++it) {
if (!it.key().empty() && it.value().is_number_integer()) {
result.shieldedScanHeights[it.key()] = it.value().get<int>();
}
}
}
result.tipHeight = tipHeight;
result.tipHash = tipHash;
result.updatedAt = updatedAt;
result.loaded = true;
} catch (...) {
result.transactions.clear();
}
sodium_memzero(plainText.data(), plainText.size());
return result;
}
bool TransactionHistoryCache::replace(const std::string& walletIdentity,
int tipHeight,
const std::string& tipHash,
const std::vector<TransactionInfo>& transactions,
std::time_t updatedAt,
const std::unordered_map<std::string, int>& shieldedScanHeights)
{
if (!isUnlockedFor(walletIdentity) || !ensureOpen()) return false;
std::string walletHash = walletIdentityHash(walletIdentity);
json payload;
payload["schema_version"] = kSchemaVersion;
payload["wallet_hash"] = walletHash;
payload["tip_height"] = tipHeight;
payload["tip_hash"] = tipHash;
payload["updated_at"] = static_cast<std::int64_t>(updatedAt);
payload["transactions"] = json::array();
for (const auto& transaction : transactions) {
payload["transactions"].push_back(transactionToJson(transaction));
}
payload["shielded_scan_heights"] = json::object();
for (const auto& [address, height] : shieldedScanHeights) {
if (!address.empty() && height >= 0) {
payload["shielded_scan_heights"][address] = height;
}
}
std::string plainText = payload.dump();
std::vector<unsigned char> nonce;
std::vector<unsigned char> cipherText;
bool encrypted = encryptPayload(walletHash, plainText, nonce, cipherText);
sodium_memzero(plainText.data(), plainText.size());
if (!encrypted) return false;
Statement statement(db_,
"INSERT OR REPLACE INTO transaction_history_snapshots "
"(wallet_hash, schema_version, tip_height, tip_hash, updated_at, nonce, ciphertext) "
"VALUES (?, ?, ?, ?, ?, ?, ?)");
if (!statement.handle) return false;
if (!bindText(statement.handle, 1, walletHash)) return false;
sqlite3_bind_int(statement.handle, 2, kSchemaVersion);
sqlite3_bind_int(statement.handle, 3, std::max(0, tipHeight));
if (!bindText(statement.handle, 4, tipHash)) return false;
sqlite3_bind_int64(statement.handle, 5, static_cast<sqlite3_int64>(updatedAt));
if (!bindBlob(statement.handle, 6, nonce)) return false;
if (!bindBlob(statement.handle, 7, cipherText)) return false;
return sqlite3_step(statement.handle) == SQLITE_DONE;
}
void TransactionHistoryCache::clearWallet(const std::string& walletIdentity)
{
if (walletIdentity.empty()) return;
clearWalletByHash(walletIdentityHash(walletIdentity));
}
int TransactionHistoryCache::snapshotCount()
{
if (!ensureOpen()) return 0;
Statement statement(db_, "SELECT COUNT(*) FROM transaction_history_snapshots");
if (!statement.handle || sqlite3_step(statement.handle) != SQLITE_ROW) return 0;
return sqlite3_column_int(statement.handle, 0);
}
bool TransactionHistoryCache::exec(const char* sql)
{
if (!db_) return false;
char* error = nullptr;
int result = sqlite3_exec(db_, sql, nullptr, nullptr, &error);
if (result != SQLITE_OK) {
DEBUG_LOGF("Transaction history cache SQL error: %s\n", error ? error : sqlite3_errmsg(db_));
if (error) sqlite3_free(error);
return false;
}
return true;
}
bool TransactionHistoryCache::createSchema()
{
return exec("CREATE TABLE IF NOT EXISTS transaction_history_keys ("
"wallet_hash TEXT PRIMARY KEY,"
"salt BLOB NOT NULL)") &&
exec("CREATE TABLE IF NOT EXISTS transaction_history_snapshots ("
"wallet_hash TEXT PRIMARY KEY,"
"schema_version INTEGER NOT NULL,"
"tip_height INTEGER NOT NULL,"
"tip_hash TEXT NOT NULL,"
"updated_at INTEGER NOT NULL,"
"nonce BLOB NOT NULL,"
"ciphertext BLOB NOT NULL)");
}
std::vector<unsigned char> TransactionHistoryCache::getOrCreateSalt(const std::string& walletHash)
{
if (!ensureOpen()) return {};
{
Statement statement(db_, "SELECT salt FROM transaction_history_keys WHERE wallet_hash = ?");
if (!statement.handle) return {};
if (!bindText(statement.handle, 1, walletHash)) return {};
if (sqlite3_step(statement.handle) == SQLITE_ROW) {
auto salt = readBlob(statement.handle, 0);
if (salt.size() == crypto_pwhash_SALTBYTES) return salt;
}
}
std::vector<unsigned char> salt(crypto_pwhash_SALTBYTES);
randombytes_buf(salt.data(), salt.size());
Statement insert(db_,
"INSERT OR REPLACE INTO transaction_history_keys (wallet_hash, salt) VALUES (?, ?)");
if (!insert.handle) return {};
if (!bindText(insert.handle, 1, walletHash)) return {};
if (!bindBlob(insert.handle, 2, salt)) return {};
if (sqlite3_step(insert.handle) != SQLITE_DONE) return {};
return salt;
}
bool TransactionHistoryCache::deriveKey(const std::string& passphrase,
const std::vector<unsigned char>& salt)
{
if (salt.size() != crypto_pwhash_SALTBYTES) return false;
unsigned char derived[kKeyBytes];
int result = crypto_pwhash(derived, sizeof(derived),
passphrase.c_str(), passphrase.size(),
salt.data(),
crypto_pwhash_OPSLIMIT_INTERACTIVE,
crypto_pwhash_MEMLIMIT_INTERACTIVE,
crypto_pwhash_ALG_ARGON2ID13);
if (result != 0) return false;
std::copy(derived, derived + sizeof(derived), key_.begin());
sodium_memzero(derived, sizeof(derived));
return true;
}
bool TransactionHistoryCache::encryptPayload(const std::string& walletHash,
const std::string& plainText,
std::vector<unsigned char>& nonce,
std::vector<unsigned char>& cipherText) const
{
if (!key_ready_) return false;
nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
randombytes_buf(nonce.data(), nonce.size());
std::string associatedData = associatedDataForWallet(walletHash);
cipherText.resize(plainText.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES);
unsigned long long cipherLength = 0;
int result = crypto_aead_xchacha20poly1305_ietf_encrypt(
cipherText.data(), &cipherLength,
reinterpret_cast<const unsigned char*>(plainText.data()), plainText.size(),
reinterpret_cast<const unsigned char*>(associatedData.data()), associatedData.size(),
nullptr, nonce.data(), key_.data());
if (result != 0) return false;
cipherText.resize(static_cast<std::size_t>(cipherLength));
return true;
}
bool TransactionHistoryCache::decryptPayload(const std::string& walletHash,
const std::vector<unsigned char>& nonce,
const std::vector<unsigned char>& cipherText,
std::string& plainText) const
{
if (!key_ready_ || nonce.size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES ||
cipherText.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) {
return false;
}
std::string associatedData = associatedDataForWallet(walletHash);
std::vector<unsigned char> plain(cipherText.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES);
unsigned long long plainLength = 0;
int result = crypto_aead_xchacha20poly1305_ietf_decrypt(
plain.data(), &plainLength, nullptr,
cipherText.data(), cipherText.size(),
reinterpret_cast<const unsigned char*>(associatedData.data()), associatedData.size(),
nonce.data(), key_.data());
if (result != 0) return false;
plainText.assign(reinterpret_cast<const char*>(plain.data()), static_cast<std::size_t>(plainLength));
sodium_memzero(plain.data(), plain.size());
return true;
}
bool TransactionHistoryCache::readSnapshot(const std::string& walletHash,
int& tipHeight,
std::string& tipHash,
std::time_t& updatedAt,
std::vector<unsigned char>& nonce,
std::vector<unsigned char>& cipherText)
{
Statement statement(db_,
"SELECT tip_height, tip_hash, updated_at, nonce, ciphertext "
"FROM transaction_history_snapshots WHERE wallet_hash = ?");
if (!statement.handle) return false;
if (!bindText(statement.handle, 1, walletHash)) return false;
if (sqlite3_step(statement.handle) != SQLITE_ROW) return false;
tipHeight = sqlite3_column_int(statement.handle, 0);
const unsigned char* tipHashText = sqlite3_column_text(statement.handle, 1);
tipHash = tipHashText ? reinterpret_cast<const char*>(tipHashText) : std::string();
updatedAt = static_cast<std::time_t>(sqlite3_column_int64(statement.handle, 2));
nonce = readBlob(statement.handle, 3);
cipherText = readBlob(statement.handle, 4);
return !nonce.empty() && !cipherText.empty();
}
void TransactionHistoryCache::clearWalletByHash(const std::string& walletHash)
{
if (!ensureOpen()) return;
Statement statement(db_, "DELETE FROM transaction_history_snapshots WHERE wallet_hash = ?");
if (!statement.handle) return;
if (!bindText(statement.handle, 1, walletHash)) return;
sqlite3_step(statement.handle);
}
void TransactionHistoryCache::close()
{
if (!db_) return;
sqlite3_close(db_);
db_ = nullptr;
}
} // namespace data
} // namespace dragonx

View File

@@ -0,0 +1,90 @@
#pragma once
#include "wallet_state.h"
#include <array>
#include <cstdint>
#include <ctime>
#include <string>
#include <unordered_map>
#include <vector>
struct sqlite3;
namespace dragonx {
namespace data {
class TransactionHistoryCache {
public:
struct LoadResult {
bool loaded = false;
bool invalidated = false;
int tipHeight = 0;
std::string tipHash;
std::time_t updatedAt = 0;
std::vector<TransactionInfo> transactions;
std::unordered_map<std::string, int> shieldedScanHeights;
};
TransactionHistoryCache();
explicit TransactionHistoryCache(std::string databasePath);
~TransactionHistoryCache();
TransactionHistoryCache(const TransactionHistoryCache&) = delete;
TransactionHistoryCache& operator=(const TransactionHistoryCache&) = delete;
static std::string defaultDatabasePath();
static std::string walletIdentityFromAddresses(const std::vector<std::string>& shieldedAddresses,
const std::vector<std::string>& transparentAddresses);
static std::string walletIdentityHash(const std::string& walletIdentity);
bool ensureOpen();
bool unlockWithPassphrase(const std::string& walletIdentity, const std::string& passphrase);
void lockKey();
bool hasKey() const { return key_ready_; }
bool isUnlockedFor(const std::string& walletIdentity) const;
LoadResult load(const std::string& walletIdentity,
int currentTipHeight,
const std::string& currentTipHash);
bool replace(const std::string& walletIdentity,
int tipHeight,
const std::string& tipHash,
const std::vector<TransactionInfo>& transactions,
std::time_t updatedAt,
const std::unordered_map<std::string, int>& shieldedScanHeights = {});
void clearWallet(const std::string& walletIdentity);
int snapshotCount();
private:
bool exec(const char* sql);
bool createSchema();
std::vector<unsigned char> getOrCreateSalt(const std::string& walletHash);
bool deriveKey(const std::string& passphrase,
const std::vector<unsigned char>& salt);
bool encryptPayload(const std::string& walletHash,
const std::string& plainText,
std::vector<unsigned char>& nonce,
std::vector<unsigned char>& cipherText) const;
bool decryptPayload(const std::string& walletHash,
const std::vector<unsigned char>& nonce,
const std::vector<unsigned char>& cipherText,
std::string& plainText) const;
bool readSnapshot(const std::string& walletHash,
int& tipHeight,
std::string& tipHash,
std::time_t& updatedAt,
std::vector<unsigned char>& nonce,
std::vector<unsigned char>& cipherText);
void clearWalletByHash(const std::string& walletHash);
void close();
sqlite3* db_ = nullptr;
std::string database_path_;
std::array<unsigned char, 32> key_{};
bool key_ready_ = false;
std::string unlocked_wallet_hash_;
};
} // namespace data
} // namespace dragonx

View File

@@ -3,12 +3,44 @@
// Released under the GPLv3 // Released under the GPLv3
#include "wallet_state.h" #include "wallet_state.h"
#include <algorithm>
#include <ctime> #include <ctime>
#include <sstream> #include <sstream>
#include <iomanip> #include <iomanip>
namespace dragonx { namespace dragonx {
std::vector<size_t> sortedSpendableAddressIndices(const std::vector<AddressInfo>& addresses,
bool requirePositiveBalance)
{
std::vector<size_t> indices;
indices.reserve(addresses.size());
for (size_t i = 0; i < addresses.size(); ++i) {
const auto& address = addresses[i];
if (!address.isSpendable()) continue;
if (requirePositiveBalance && address.balance <= 0.0) continue;
indices.push_back(i);
}
std::sort(indices.begin(), indices.end(), [&](size_t lhs, size_t rhs) {
return addresses[lhs].balance > addresses[rhs].balance;
});
return indices;
}
int bestSpendableAddressIndex(const std::vector<AddressInfo>& addresses)
{
int bestIndex = -1;
double bestBalance = 0.0;
for (size_t i = 0; i < addresses.size(); ++i) {
if (addresses[i].isSpendable() && addresses[i].balance > bestBalance) {
bestBalance = addresses[i].balance;
bestIndex = static_cast<int>(i);
}
}
return bestIndex;
}
std::string TransactionInfo::getTimeString() const std::string TransactionInfo::getTimeString() const
{ {
if (timestamp == 0) return "Unknown"; if (timestamp == 0) return "Unknown";

View File

@@ -18,6 +18,7 @@ struct AddressInfo {
std::string address; std::string address;
double balance = 0.0; double balance = 0.0;
std::string type; // "shielded" or "transparent" std::string type; // "shielded" or "transparent"
bool has_spending_key = true; // false for view-only (imported via z_importviewingkey)
// For display // For display
std::string label; std::string label;
@@ -25,8 +26,13 @@ struct AddressInfo {
// Derived // Derived
bool isZAddr() const { return !address.empty() && address[0] == 'z'; } bool isZAddr() const { return !address.empty() && address[0] == 'z'; }
bool isShielded() const { return type == "shielded"; } bool isShielded() const { return type == "shielded"; }
bool isSpendable() const { return has_spending_key; }
}; };
std::vector<size_t> sortedSpendableAddressIndices(const std::vector<AddressInfo>& addresses,
bool requirePositiveBalance = true);
int bestSpendableAddressIndex(const std::vector<AddressInfo>& addresses);
/** /**
* @brief Represents a wallet transaction * @brief Represents a wallet transaction
*/ */
@@ -134,6 +140,9 @@ struct MarketInfo {
double change_24h = 0.0; double change_24h = 0.0;
double market_cap = 0.0; double market_cap = 0.0;
std::string last_updated; std::string last_updated;
std::chrono::steady_clock::time_point last_fetch_time{};
bool price_loading = false;
std::string price_error;
// Price history for chart // Price history for chart
std::vector<double> price_history; std::vector<double> price_history;
@@ -179,6 +188,9 @@ struct PoolMiningState {
struct WalletState { struct WalletState {
// Connection // Connection
bool connected = false; bool connected = false;
bool warming_up = false; // daemon reachable but in RPC warmup (error -28)
std::string warmup_status; // user-friendly title, e.g. "Processing blocks..."
std::string warmup_description; // subtitle explaining the stage
int daemon_version = 0; int daemon_version = 0;
std::string daemon_subversion; std::string daemon_subversion;
int protocol_version = 0; int protocol_version = 0;
@@ -249,6 +261,9 @@ struct WalletState {
void clear() { void clear() {
connected = false; connected = false;
warming_up = false;
warmup_status.clear();
warmup_description.clear();
daemon_version = 0; daemon_version = 0;
daemon_subversion.clear(); daemon_subversion.clear();
protocol_version = 0; protocol_version = 0;

View File

@@ -12,4 +12,5 @@ INCBIN(ubuntu_regular, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-R.ttf");
INCBIN(ubuntu_light, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Light.ttf"); INCBIN(ubuntu_light, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Light.ttf");
INCBIN(ubuntu_medium, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Medium.ttf"); INCBIN(ubuntu_medium, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Medium.ttf");
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf"); INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf");
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.otf"); INCBIN(mdi_pickaxe_subset, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf");
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf");

View File

@@ -27,6 +27,9 @@ extern "C" {
extern const unsigned char g_material_icons_data[]; extern const unsigned char g_material_icons_data[];
extern const unsigned int g_material_icons_size; extern const unsigned int g_material_icons_size;
extern const unsigned char g_mdi_pickaxe_subset_data[];
extern const unsigned int g_mdi_pickaxe_subset_size;
extern const unsigned char g_noto_cjk_subset_data[]; extern const unsigned char g_noto_cjk_subset_data[];
extern const unsigned int g_noto_cjk_subset_size; extern const unsigned int g_noto_cjk_subset_size;
} }

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

@@ -1224,6 +1224,8 @@ int main(int argc, char* argv[])
// Immediate triggers: async RPC results or visible notifications // Immediate triggers: async RPC results or visible notifications
bool hasImmediateWork = app.hasPendingRPCResults() bool hasImmediateWork = app.hasPendingRPCResults()
|| app.hasTransactionSendProgress()
|| app.isTransactionRefreshInProgress()
|| dragonx::ui::Notifications::instance().hasActive(); || dragonx::ui::Notifications::instance().hasActive();
// Periodic maintenance: fire refresh timers in app.update() // Periodic maintenance: fire refresh timers in app.update()
@@ -1801,6 +1803,8 @@ int main(int argc, char* argv[])
&& !opaqueBackground; && !opaqueBackground;
bool animating = app.isShuttingDown() bool animating = app.isShuttingDown()
|| backdropNeedsFrames || backdropNeedsFrames
|| app.hasTransactionSendProgress()
|| app.isTransactionRefreshInProgress()
|| dragonx::ui::effects::ThemeEffects::instance().hasActiveAnimation() || dragonx::ui::effects::ThemeEffects::instance().hasActiveAnimation()
|| dragonx::ui::Notifications::instance().hasActive() || dragonx::ui::Notifications::instance().hasActive()
|| dragonx::ui::material::SmoothScrollAnimating(); || dragonx::ui::material::SmoothScrollAnimating();

View File

@@ -6,11 +6,14 @@
#include "../config/version.h" #include "../config/version.h"
#include "../resources/embedded_resources.h" #include "../resources/embedded_resources.h"
#include <sodium.h>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <cstdlib> #include <cstdlib>
#include <ctime>
#include <filesystem> #include <filesystem>
#include <algorithm>
#include <cctype>
#include "../util/logger.h" #include "../util/logger.h"
@@ -26,6 +29,56 @@ namespace fs = std::filesystem;
namespace dragonx { namespace dragonx {
namespace rpc { namespace rpc {
namespace {
std::string generateSecureRandomString(size_t length)
{
static constexpr char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
static constexpr uint32_t charsetSize = static_cast<uint32_t>(sizeof(charset) - 1);
if (sodium_init() < 0) {
DEBUG_LOGF("Failed to initialize libsodium for RPC credential generation\n");
return {};
}
std::string result;
result.reserve(length);
for (size_t i = 0; i < length; ++i) {
result.push_back(charset[randombytes_uniform(charsetSize)]);
}
return result;
}
std::string lowercase(std::string value)
{
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return value;
}
bool parseBoolValue(const std::string& value)
{
std::string lowered = lowercase(value);
return lowered == "1" || lowered == "true" || lowered == "yes" || lowered == "on";
}
bool applyCookieAuth(ConnectionConfig& config, const std::string& dataDir)
{
std::string cookieUser, cookiePass;
if (!Connection::readAuthCookie(dataDir, cookieUser, cookiePass)) {
return false;
}
config.rpcuser = cookieUser;
config.rpcpassword = cookiePass;
config.auth_source = AuthSource::Cookie;
if (config.hush_dir.empty()) config.hush_dir = dataDir;
return true;
}
} // namespace
Connection::Connection() = default; Connection::Connection() = default;
Connection::~Connection() = default; Connection::~Connection() = default;
@@ -140,9 +193,15 @@ ConnectionConfig Connection::parseConfFile(const std::string& path)
config.host = value; config.host = value;
} else if (key == "proxy") { } else if (key == "proxy") {
config.proxy = value; config.proxy = value;
} else if (key == "rpctls" || key == "rpcssl" || key == "use_tls" || key == "rpcuse_tls") {
config.use_tls = parseBoolValue(value);
} }
} }
if (!config.rpcuser.empty() || !config.rpcpassword.empty()) {
config.auth_source = AuthSource::ConfigFile;
}
return config; return config;
} }
@@ -177,10 +236,7 @@ ConnectionConfig Connection::autoDetectConfig()
// If rpcpassword is empty, the daemon may be using .cookie auth // If rpcpassword is empty, the daemon may be using .cookie auth
if (config.rpcpassword.empty()) { if (config.rpcpassword.empty()) {
std::string cookieUser, cookiePass; if (applyCookieAuth(config, data_dir)) {
if (readAuthCookie(data_dir, cookieUser, cookiePass)) {
config.rpcuser = cookieUser;
config.rpcpassword = cookiePass;
DEBUG_LOGF("Using .cookie authentication (no rpcpassword in config)\n"); DEBUG_LOGF("Using .cookie authentication (no rpcpassword in config)\n");
} }
} }
@@ -196,23 +252,57 @@ ConnectionConfig Connection::autoDetectConfig()
return config; return config;
} }
bool Connection::buildCookieAuthConfig(const ConnectionConfig& base, ConnectionConfig& cookieConfig)
{
if (base.auth_source == AuthSource::Cookie) {
return false;
}
std::string dataDir = base.hush_dir.empty() ? getDefaultDataDir() : base.hush_dir;
ConnectionConfig fallback = base;
if (!applyCookieAuth(fallback, dataDir)) {
return false;
}
cookieConfig = std::move(fallback);
return true;
}
bool Connection::isLocalHost(const std::string& host)
{
std::string lowered = lowercase(host);
if (!lowered.empty() && lowered.front() == '[' && lowered.back() == ']') {
lowered = lowered.substr(1, lowered.size() - 2);
}
return lowered == "localhost" || lowered == "localhost." ||
lowered == "::1" || lowered == "0:0:0:0:0:0:0:1" ||
lowered == "127.0.0.1" || lowered.rfind("127.", 0) == 0;
}
bool Connection::usesPlaintextRemote(const ConnectionConfig& config)
{
return !config.use_tls && !isLocalHost(config.host);
}
const char* Connection::authSourceName(AuthSource source)
{
switch (source) {
case AuthSource::ConfigFile: return "config";
case AuthSource::Cookie: return "cookie";
case AuthSource::Missing: return "missing";
}
return "unknown";
}
bool Connection::createDefaultConfig(const std::string& path) bool Connection::createDefaultConfig(const std::string& path)
{ {
// Generate random rpcuser/rpcpassword std::string rpcuser = generateSecureRandomString(16);
auto generateRandomString = [](int length) -> std::string { std::string rpcpassword = generateSecureRandomString(32);
const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; if (rpcuser.empty() || rpcpassword.empty()) {
std::string result; DEBUG_LOGF("Failed to generate secure RPC credentials for config file: %s\n", path.c_str());
result.reserve(length); return false;
}
std::srand(static_cast<unsigned>(std::time(nullptr)));
for (int i = 0; i < length; i++) {
result += charset[std::rand() % (sizeof(charset) - 1)];
}
return result;
};
std::string rpcuser = generateRandomString(16);
std::string rpcpassword = generateRandomString(32);
std::ofstream file(path); std::ofstream file(path);
if (!file.is_open()) { if (!file.is_open()) {
@@ -234,8 +324,11 @@ bool Connection::createDefaultConfig(const std::string& path)
file << "exportdir=" << dataDir << "\n"; file << "exportdir=" << dataDir << "\n";
file << "experimentalfeatures=1\n"; file << "experimentalfeatures=1\n";
file << "developerencryptwallet=1\n"; file << "developerencryptwallet=1\n";
file << "addnode=195.201.20.230\n"; file << "addnode=node.dragonx.is\n";
file << "addnode=195.201.137.219\n"; file << "addnode=node1.dragonx.is\n";
file << "addnode=node2.dragonx.is\n";
file << "addnode=node3.dragonx.is\n";
file << "addnode=node4.dragonx.is\n";
file.close(); file.close();

View File

@@ -12,6 +12,12 @@ namespace rpc {
/** /**
* @brief Connection configuration * @brief Connection configuration
*/ */
enum class AuthSource {
Missing,
ConfigFile,
Cookie
};
struct ConnectionConfig { struct ConnectionConfig {
std::string host = "127.0.0.1"; std::string host = "127.0.0.1";
std::string port = "21769"; std::string port = "21769";
@@ -20,6 +26,8 @@ struct ConnectionConfig {
std::string hush_dir; std::string hush_dir;
std::string proxy; // SOCKS5 proxy for Tor std::string proxy; // SOCKS5 proxy for Tor
bool use_embedded = true; bool use_embedded = true;
bool use_tls = false;
AuthSource auth_source = AuthSource::Missing;
}; };
/** /**
@@ -96,6 +104,23 @@ public:
*/ */
static bool readAuthCookie(const std::string& dataDir, std::string& user, std::string& password); static bool readAuthCookie(const std::string& dataDir, std::string& user, std::string& password);
/**
* @brief Build a cookie-auth retry config from a failed config-auth attempt
*/
static bool buildCookieAuthConfig(const ConnectionConfig& base, ConnectionConfig& cookieConfig);
/**
* @brief Whether a host is local enough for plaintext HTTP RPC
*/
static bool isLocalHost(const std::string& host);
/**
* @brief Whether this config would send RPC credentials over plaintext to a remote host
*/
static bool usesPlaintextRemote(const ConnectionConfig& config);
static const char* authSourceName(AuthSource source);
private: private:
}; };

View File

@@ -1,19 +1,85 @@
// DragonX Wallet - ImGui Edition // DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers // Copyright 2024-2026 The Hush Developers
// Released under the GPLv3 // Released under the GPLv3
//
// rpc_client.cpp — JSON-RPC client over HTTPS using libcurl.
// All calls are blocking; run on RPCWorker threads, never on main thread.
#include "rpc_client.h" #include "rpc_client.h"
#include "../config/version.h" #include "../config/version.h"
#include "../util/base64.h" #include "../util/base64.h"
#include <curl/curl.h> #include <curl/curl.h>
#include <atomic>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <utility>
#include "../util/logger.h" #include "../util/logger.h"
namespace dragonx { namespace dragonx {
namespace rpc { namespace rpc {
namespace {
std::mutex g_trace_mutex;
RPCClient::TraceCallback g_trace_callback;
std::atomic_bool g_trace_enabled{false};
thread_local std::string g_trace_source;
void emitRpcTrace(const std::string& method)
{
if (!g_trace_enabled.load(std::memory_order_relaxed)) return;
RPCClient::TraceCallback callback;
{
std::lock_guard<std::mutex> lock(g_trace_mutex);
callback = g_trace_callback;
}
if (!callback) return;
std::string source = g_trace_source.empty() ? std::string("App") : g_trace_source;
callback(source, method);
}
} // namespace
RPCClient::TraceScope::TraceScope(std::string source)
: previous_(RPCClient::currentTraceSource())
{
RPCClient::setTraceSource(std::move(source));
}
RPCClient::TraceScope::~TraceScope()
{
RPCClient::setTraceSource(std::move(previous_));
}
void RPCClient::setTraceCallback(TraceCallback callback)
{
std::lock_guard<std::mutex> lock(g_trace_mutex);
g_trace_callback = std::move(callback);
}
void RPCClient::setTraceEnabled(bool enabled)
{
g_trace_enabled.store(enabled, std::memory_order_relaxed);
}
bool RPCClient::isTraceEnabled()
{
return g_trace_enabled.load(std::memory_order_relaxed);
}
std::string RPCClient::currentTraceSource()
{
return g_trace_source;
}
void RPCClient::setTraceSource(std::string source)
{
g_trace_source = std::move(source);
}
// Callback for libcurl to write response data // Callback for libcurl to write response data
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) { static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
size_t totalSize = size * nmemb; size_t totalSize = size * nmemb;
@@ -57,19 +123,36 @@ RPCClient::~RPCClient() = default;
bool RPCClient::connect(const std::string& host, const std::string& port, bool RPCClient::connect(const std::string& host, const std::string& port,
const std::string& user, const std::string& password) const std::string& user, const std::string& password)
{
return connect(host, port, user, password, false);
}
bool RPCClient::connect(const std::string& host, const std::string& port,
const std::string& user, const std::string& password,
bool useTls)
{ {
std::lock_guard<std::recursive_mutex> lk(curl_mutex_); std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
host_ = host; host_ = host;
port_ = port; port_ = port;
last_connect_info_ = json();
// Create Basic auth header with proper base64 encoding // Create Basic auth header with proper base64 encoding
std::string credentials = user + ":" + password; std::string credentials = user + ":" + password;
auth_ = util::base64_encode(credentials); auth_ = util::base64_encode(credentials);
// Build URL - use HTTP for localhost RPC (TLS not always enabled) impl_->url = std::string(useTls ? "https://" : "http://") + host + ":" + port + "/";
impl_->url = "http://" + host + ":" + port + "/";
VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str()); VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str());
// Clean up previous curl handle/headers to avoid leaks on retries
if (impl_->headers) {
curl_slist_free_all(impl_->headers);
impl_->headers = nullptr;
}
if (impl_->curl) {
curl_easy_cleanup(impl_->curl);
impl_->curl = nullptr;
}
// Initialize curl handle // Initialize curl handle
impl_->curl = curl_easy_init(); impl_->curl = curl_easy_init();
if (!impl_->curl) { if (!impl_->curl) {
@@ -78,7 +161,7 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
} }
// Set up headers - daemon expects text/plain, not application/json // Set up headers - daemon expects text/plain, not application/json
impl_->headers = curl_slist_append(impl_->headers, "Content-Type: text/plain"); impl_->headers = curl_slist_append(nullptr, "Content-Type: text/plain");
std::string auth_header = "Authorization: Basic " + auth_; std::string auth_header = "Authorization: Basic " + auth_;
impl_->headers = curl_slist_append(impl_->headers, auth_header.c_str()); impl_->headers = curl_slist_append(impl_->headers, auth_header.c_str());
@@ -94,14 +177,19 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
json result = call("getinfo"); json result = call("getinfo");
if (result.contains("version")) { if (result.contains("version")) {
connected_ = true; connected_ = true;
warming_up_ = false;
warmup_status_.clear();
last_connect_error_.clear(); last_connect_error_.clear();
last_connect_info_ = result;
DEBUG_LOGF("Connected to dragonxd v%d\n", result["version"].get<int>()); DEBUG_LOGF("Connected to dragonxd v%d\n", result["version"].get<int>());
return true; return true;
} }
} catch (const std::exception& e) { } catch (const std::exception& e) {
last_connect_error_ = e.what(); last_connect_error_ = e.what();
// Daemon warmup messages (Loading block index, Verifying blocks, etc.) // Daemon warmup messages (Loading block index, Verifying blocks, etc.)
// are normal startup progress — don't label them "Connection failed". // are normal startup progress — the daemon is reachable and auth works,
// it just hasn't finished initializing yet. Mark as connected+warmup
// so the wallet can show the UI instead of a blocking overlay.
std::string msg = e.what(); std::string msg = e.what();
bool isWarmup = (msg.find("Loading") != std::string::npos || bool isWarmup = (msg.find("Loading") != std::string::npos ||
msg.find("Verifying") != std::string::npos || msg.find("Verifying") != std::string::npos ||
@@ -110,20 +198,36 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
msg.find("Rescanning") != std::string::npos || msg.find("Rescanning") != std::string::npos ||
msg.find("Pruning") != std::string::npos); msg.find("Pruning") != std::string::npos);
if (isWarmup) { if (isWarmup) {
DEBUG_LOGF("Daemon starting: %s\n", msg.c_str()); connected_ = true;
warming_up_ = true;
warmup_status_ = msg;
DEBUG_LOGF("Daemon warming up: %s\n", msg.c_str());
return true;
} else { } else {
DEBUG_LOGF("Connection failed: %s\n", msg.c_str()); DEBUG_LOGF("Connection failed: %s\n", msg.c_str());
} }
} }
connected_ = false; connected_ = false;
warming_up_ = false;
warmup_status_.clear();
last_connect_info_ = json();
return false; return false;
} }
json RPCClient::getLastConnectInfo() const
{
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
return last_connect_info_;
}
void RPCClient::disconnect() void RPCClient::disconnect()
{ {
std::lock_guard<std::recursive_mutex> lk(curl_mutex_); std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
connected_ = false; connected_ = false;
warming_up_ = false;
warmup_status_.clear();
last_connect_info_ = json();
if (impl_->curl) { if (impl_->curl) {
curl_easy_cleanup(impl_->curl); curl_easy_cleanup(impl_->curl);
impl_->curl = nullptr; impl_->curl = nullptr;
@@ -151,6 +255,8 @@ json RPCClient::call(const std::string& method, const json& params)
throw std::runtime_error("Not connected"); throw std::runtime_error("Not connected");
} }
emitRpcTrace(method);
json payload = makePayload(method, params); json payload = makePayload(method, params);
std::string body = payload.dump(); std::string body = payload.dump();
std::string response_data; std::string response_data;
@@ -204,6 +310,8 @@ json RPCClient::call(const std::string& method, const json& params, long timeout
throw std::runtime_error("Not connected"); throw std::runtime_error("Not connected");
} }
emitRpcTrace(method);
// Temporarily override timeout // Temporarily override timeout
long prevTimeout = 30L; long prevTimeout = 30L;
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec); curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec);
@@ -263,6 +371,8 @@ std::string RPCClient::callRaw(const std::string& method, const json& params)
throw std::runtime_error("Not connected"); throw std::runtime_error("Not connected");
} }
emitRpcTrace(method);
json payload = makePayload(method, params); json payload = makePayload(method, params);
std::string body = payload.dump(); std::string body = payload.dump();
std::string response_data; std::string response_data;

View File

@@ -25,6 +25,20 @@ using ErrorCallback = std::function<void(const std::string&)>;
*/ */
class RPCClient { class RPCClient {
public: public:
using TraceCallback = std::function<void(const std::string& source, const std::string& method)>;
class TraceScope {
public:
explicit TraceScope(std::string source);
~TraceScope();
TraceScope(const TraceScope&) = delete;
TraceScope& operator=(const TraceScope&) = delete;
private:
std::string previous_;
};
RPCClient(); RPCClient();
~RPCClient(); ~RPCClient();
@@ -43,6 +57,10 @@ public:
bool connect(const std::string& host, const std::string& port, bool connect(const std::string& host, const std::string& port,
const std::string& user, const std::string& password); const std::string& user, const std::string& password);
bool connect(const std::string& host, const std::string& port,
const std::string& user, const std::string& password,
bool useTls);
/** /**
* @brief Disconnect from dragonxd * @brief Disconnect from dragonxd
*/ */
@@ -53,10 +71,30 @@ public:
*/ */
bool isConnected() const { return connected_; } bool isConnected() const { return connected_; }
/**
* @brief True if the last connect() succeeded but daemon returned a warmup error.
* The curl handle is valid and auth succeeded — RPC calls will throw warmup errors
* until the daemon finishes initializing.
*/
bool isWarmingUp() const { return warming_up_; }
/**
* @brief The warmup status message (e.g. "Activating best chain...").
* Empty when not in warmup.
*/
const std::string& getWarmupStatus() const { return warmup_status_; }
/** /**
* @brief Get the error message from the last failed connect() attempt. * @brief Get the error message from the last failed connect() attempt.
*/ */
const std::string& getLastConnectError() const { return last_connect_error_; } const std::string& getLastConnectError() const { return last_connect_error_; }
json getLastConnectInfo() const;
static void setTraceCallback(TraceCallback callback);
static void setTraceEnabled(bool enabled);
static bool isTraceEnabled();
static std::string currentTraceSource();
static void setTraceSource(std::string source);
/** /**
* @brief Make a raw RPC call * @brief Make a raw RPC call
@@ -182,7 +220,10 @@ private:
std::string port_; std::string port_;
std::string auth_; // Base64 encoded "user:password" std::string auth_; // Base64 encoded "user:password"
bool connected_ = false; bool connected_ = false;
bool warming_up_ = false;
std::string warmup_status_;
std::string last_connect_error_; std::string last_connect_error_;
json last_connect_info_;
mutable std::recursive_mutex curl_mutex_; // serializes all curl handle access mutable std::recursive_mutex curl_mutex_; // serializes all curl handle access
// HTTP client (implementation hidden) // HTTP client (implementation hidden)

View File

@@ -1,6 +1,9 @@
// DragonX Wallet - ImGui Edition // DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers // Copyright 2024-2026 The Hush Developers
// Released under the GPLv3 // Released under the GPLv3
//
// rpc_worker.cpp — Background work queue. Executes WorkFn on its own thread,
// returns MainCb callbacks drained each frame on the main thread.
#include "rpc_worker.h" #include "rpc_worker.h"
#include <cstdio> #include <cstdio>
@@ -94,6 +97,18 @@ bool RPCWorker::hasPendingResults() const
return !results_.empty(); return !results_.empty();
} }
std::size_t RPCWorker::pendingTaskCount() const
{
std::lock_guard<std::mutex> lk(taskMtx_);
return tasks_.size();
}
std::size_t RPCWorker::pendingResultCount() const
{
std::lock_guard<std::mutex> lk(resultMtx_);
return results_.size();
}
void RPCWorker::run() void RPCWorker::run()
{ {
while (true) { while (true) {

View File

@@ -6,6 +6,7 @@
#include <atomic> #include <atomic>
#include <condition_variable> #include <condition_variable>
#include <cstddef>
#include <deque> #include <deque>
#include <functional> #include <functional>
#include <mutex> #include <mutex>
@@ -69,6 +70,8 @@ public:
/// True when there are completed results waiting for the main thread. /// True when there are completed results waiting for the main thread.
bool hasPendingResults() const; bool hasPendingResults() const;
std::size_t pendingTaskCount() const;
std::size_t pendingResultCount() const;
/// True when the worker thread is running. /// True when the worker thread is running.
bool isRunning() const { return running_.load(std::memory_order_relaxed); } bool isRunning() const { return running_.load(std::memory_order_relaxed); }
@@ -80,7 +83,7 @@ private:
std::atomic<bool> running_{false}; std::atomic<bool> running_{false};
// ---- Task queue (produced by main thread, consumed by worker) ---- // ---- Task queue (produced by main thread, consumed by worker) ----
std::mutex taskMtx_; mutable std::mutex taskMtx_;
std::condition_variable taskCv_; std::condition_variable taskCv_;
std::deque<WorkFn> tasks_; std::deque<WorkFn> tasks_;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,382 @@
#pragma once
#include "data/wallet_state.h"
#include "refresh_scheduler.h"
#include "rpc/rpc_worker.h"
#include <nlohmann/json.hpp>
#include <array>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <ctime>
#include <optional>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
namespace dragonx {
namespace services {
class NetworkRefreshService {
public:
using Timer = RefreshScheduler::Timer;
using Intervals = RefreshScheduler::Intervals;
class RefreshRpcGateway {
public:
virtual ~RefreshRpcGateway() = default;
virtual nlohmann::json call(const std::string& method,
const nlohmann::json& params) = 0;
};
enum class Job {
Core,
Addresses,
Transactions,
Mining,
Peers,
Price,
Encryption,
ConnectionInit,
Count
};
struct DispatchTicket {
Job job = Job::Core;
std::uint64_t generation = 0;
bool accepted = false;
};
struct JobStats {
std::uint64_t started = 0;
std::uint64_t finished = 0;
std::uint64_t skippedInFlight = 0;
std::uint64_t skippedQueuePressure = 0;
std::uint64_t staleCallbacks = 0;
std::size_t lastQueueDepth = 0;
};
struct EnqueueResult {
DispatchTicket ticket;
bool enqueued = false;
std::size_t queueDepth = 0;
};
struct ConnectionInfoResult {
bool ok = false;
std::optional<int> daemonVersion;
std::optional<int> protocolVersion;
std::optional<int> p2pPort;
std::optional<int> longestChain;
std::optional<int> notarized;
std::optional<int> blocks;
};
struct WalletEncryptionResult {
bool ok = false;
bool encrypted = false;
std::int64_t unlockedUntil = 0;
};
struct WarmupPollResult {
bool ready = false;
ConnectionInfoResult info;
std::string errorMessage;
};
struct ConnectionInitResult {
ConnectionInfoResult info;
WalletEncryptionResult encryption;
};
struct CoreRefreshResult {
bool balanceOk = false;
std::optional<double> shieldedBalance;
std::optional<double> transparentBalance;
std::optional<double> totalBalance;
bool blockchainOk = false;
std::optional<int> blocks;
std::optional<int> headers;
std::optional<std::string> bestBlockHash;
std::optional<double> verificationProgress;
std::optional<int> longestChain;
std::optional<int> notarized;
};
struct MiningRefreshResult {
std::optional<double> localHashrate;
bool miningOk = false;
std::optional<bool> generate;
std::optional<int> genproclimit;
std::optional<int> blocks;
std::optional<double> difficulty;
std::optional<double> networkHashrate;
std::optional<std::string> chain;
double daemonMemoryMb = 0.0;
};
struct PeerRefreshResult {
std::vector<PeerInfo> peers;
std::vector<BannedPeer> bannedPeers;
};
struct PriceRefreshResult {
MarketInfo market;
};
struct PriceHttpResponse {
bool transportOk = false;
long httpStatus = 0;
std::string body;
std::string transportError;
};
struct PriceHttpResult {
std::optional<PriceRefreshResult> price;
std::string errorMessage;
};
struct AddressRefreshResult {
std::vector<AddressInfo> shieldedAddresses;
std::vector<AddressInfo> transparentAddresses;
};
struct AddressRefreshSnapshot {
std::unordered_map<std::string, bool> shieldedSpendingKeys;
};
struct TransactionViewCacheEntry {
std::string from_address;
std::int64_t timestamp = 0;
int confirmations = 0;
struct Output {
std::string address;
double value = 0.0;
std::string memo;
};
std::vector<Output> outgoing_outputs;
};
using TransactionViewCache = std::unordered_map<std::string, TransactionViewCacheEntry>;
struct TransactionRefreshSnapshot {
std::vector<std::string> shieldedAddresses;
std::unordered_set<std::string> fullyEnrichedTxids;
TransactionViewCache viewTxCache;
std::unordered_set<std::string> sendTxids;
std::unordered_set<std::string> pendingOpids;
std::vector<TransactionInfo> previousTransactions;
std::set<std::string> miningAddresses;
std::unordered_map<std::string, int> shieldedScanHeights;
std::size_t shieldedScanStartIndex = 0;
std::size_t maxShieldedReceiveScans = 0;
};
struct TransactionRefreshResult {
std::vector<TransactionInfo> transactions;
int blockHeight = -1;
TransactionViewCache newViewTxEntries;
std::size_t nextShieldedScanStartIndex = 0;
std::size_t shieldedAddressesScanned = 0;
std::size_t shieldedAddressCount = 0;
std::unordered_map<std::string, int> shieldedScanHeights;
bool shieldedScanComplete = true;
};
struct OperationStatusPollResult {
std::vector<std::string> doneOpids;
std::vector<std::string> staleOpids;
std::vector<std::string> successTxids;
std::unordered_map<std::string, std::string> successTxidsByOpid;
std::vector<std::string> failureMessages;
bool anySuccess = false;
};
struct TransactionCacheUpdate {
TransactionViewCache& viewTxCache;
std::unordered_set<std::string>& sendTxids;
std::vector<TransactionInfo>& confirmedTxCache;
std::unordered_set<std::string>& confirmedTxIds;
int& confirmedCacheBlock;
int& lastTxBlockHeight;
};
static Intervals intervalsForPage(ui::NavPage page) { return RefreshScheduler::intervalsForPage(page); }
static ConnectionInfoResult parseConnectionInfoResult(const nlohmann::json& info);
static WalletEncryptionResult parseWalletEncryptionResult(const nlohmann::json& walletInfo);
static WarmupPollResult collectWarmupPollResult(RefreshRpcGateway& rpc);
static ConnectionInitResult collectConnectionInitResult(
RefreshRpcGateway& rpc,
const std::optional<ConnectionInfoResult>& prefetchedInfo = std::nullopt);
static CoreRefreshResult parseCoreRefreshResult(const nlohmann::json& totalBalance,
bool balanceOk,
const nlohmann::json& blockInfo,
bool blockOk);
static CoreRefreshResult collectCoreRefreshResult(RefreshRpcGateway& rpc);
static MiningRefreshResult parseMiningRefreshResult(const nlohmann::json& miningInfo,
bool miningOk,
const nlohmann::json& localHashrate,
bool hashrateOk,
double daemonMemoryMb);
static MiningRefreshResult collectMiningRefreshResult(RefreshRpcGateway& rpc,
double daemonMemoryMb,
bool includeSlowRefresh,
bool includeLocalHashrate = true);
static PeerRefreshResult parsePeerRefreshResult(const nlohmann::json& peers,
const nlohmann::json& bannedPeers);
static PeerRefreshResult collectPeerRefreshResult(RefreshRpcGateway& rpc);
static std::optional<PriceRefreshResult> parseCoinGeckoPriceResponse(const std::string& response,
std::time_t fetchedAt);
static PriceHttpResult parsePriceHttpResponse(const PriceHttpResponse& response,
std::time_t fetchedAt);
static AddressInfo buildShieldedAddressInfo(const std::string& address,
const nlohmann::json& validation,
bool validationSucceeded);
static AddressInfo buildTransparentAddressInfo(const std::string& address);
static std::vector<AddressInfo> parseTransparentAddressList(const nlohmann::json& addressList);
static void applyShieldedBalancesFromUnspent(std::vector<AddressInfo>& addresses,
const nlohmann::json& unspent);
static void applyTransparentBalancesFromUnspent(std::vector<AddressInfo>& addresses,
const nlohmann::json& unspent);
static AddressRefreshSnapshot buildAddressRefreshSnapshot(const WalletState& state);
static AddressRefreshResult collectAddressRefreshResult(
RefreshRpcGateway& rpc,
const AddressRefreshSnapshot& snapshot = {});
static TransactionRefreshSnapshot buildTransactionRefreshSnapshot(const WalletState& state,
const TransactionViewCache& viewTxCache,
const std::unordered_set<std::string>& sendTxids);
static void appendTransparentTransactions(std::vector<TransactionInfo>& transactions,
std::set<std::string>& knownTxids,
const nlohmann::json& result,
const std::set<std::string>& miningAddresses = {});
static void appendShieldedReceivedTransactions(std::vector<TransactionInfo>& transactions,
std::set<std::string>& knownTxids,
const std::string& address,
const nlohmann::json& received,
const std::set<std::string>& miningAddresses = {});
static TransactionViewCacheEntry parseViewTransactionCacheEntry(const nlohmann::json& viewTransaction);
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
const std::string& txid,
const TransactionViewCacheEntry& entry);
static void sortTransactionsNewestFirst(std::vector<TransactionInfo>& transactions);
static TransactionRefreshResult collectTransactionRefreshResult(RefreshRpcGateway& rpc,
const TransactionRefreshSnapshot& snapshot,
int currentBlockHeight,
int maxViewTransactionsPerCycle);
static TransactionRefreshResult collectRecentTransactionRefreshResult(
RefreshRpcGateway& rpc,
const TransactionRefreshSnapshot& snapshot,
int currentBlockHeight,
int pageSize = 100);
static OperationStatusPollResult parseOperationStatusPoll(const nlohmann::json& result,
const std::vector<std::string>& requestedOpids);
static void applyConnectionInfoResult(WalletState& state, const ConnectionInfoResult& result);
static void applyWalletEncryptionResult(WalletState& state, const WalletEncryptionResult& result);
static void applyConnectionInitResult(WalletState& state, const ConnectionInitResult& result);
static void applyCoreRefreshResult(WalletState& state,
const CoreRefreshResult& result,
std::time_t updatedAt);
static void applyMiningRefreshResult(WalletState& state,
const MiningRefreshResult& result,
std::time_t updatedAt);
static void applyPeerRefreshResult(WalletState& state,
PeerRefreshResult&& result,
std::time_t updatedAt);
static void markPriceRefreshStarted(WalletState& state);
static void applyPriceRefreshResult(WalletState& state,
const PriceRefreshResult& result,
std::chrono::steady_clock::time_point fetchedAt);
static void applyPriceRefreshFailure(WalletState& state,
const std::string& errorMessage);
static void applyAddressRefreshResult(WalletState& state,
AddressRefreshResult&& result);
static void applyTransactionRefreshResult(WalletState& state,
TransactionCacheUpdate cacheUpdate,
TransactionRefreshResult&& result,
std::time_t updatedAt);
void applyPage(ui::NavPage page) { scheduler_.applyPage(page); }
void setIntervals(Intervals intervals) { scheduler_.setIntervals(intervals); }
const Intervals& intervals() const { return scheduler_.intervals(); }
void tick(float deltaSeconds) { scheduler_.tick(deltaSeconds); }
bool isDue(Timer timer) const { return scheduler_.isDue(timer); }
bool consumeDue(Timer timer) { return scheduler_.consumeDue(timer); }
void reset(Timer timer) { scheduler_.reset(timer); }
void markDue(Timer timer) { scheduler_.markDue(timer); }
void setTimer(Timer timer, float seconds) { scheduler_.setTimer(timer, seconds); }
float timer(Timer timer) const { return scheduler_.timer(timer); }
float interval(Timer timer) const { return scheduler_.interval(timer); }
void markImmediateRefresh() { scheduler_.markImmediateRefresh(); }
void markWalletMutationRefresh() { scheduler_.markWalletMutationRefresh(); }
void resetTxAge() { scheduler_.resetTxAge(); }
bool shouldRefreshTransactions(int lastTxBlockHeight,
int currentBlockHeight,
bool transactionsDirty) const;
bool beginJob(Job job);
bool beginJob(Job job, std::size_t queuedWork, std::size_t maxQueuedWork);
void finishJob(Job job);
bool jobInProgress(Job job) const;
void resetJobs();
DispatchTicket beginDispatch(Job job, std::size_t queuedWork = 0, std::size_t maxQueuedWork = 0);
bool completeDispatch(const DispatchTicket& ticket);
void cancelDispatch(const DispatchTicket& ticket);
JobStats stats(Job job) const;
template <typename Worker, typename WorkFn>
EnqueueResult enqueue(Job job, Worker& worker, WorkFn&& work, std::size_t maxQueuedWork = 0)
{
std::size_t queueDepth = worker.pendingTaskCount();
auto ticket = beginDispatch(job, queueDepth, maxQueuedWork);
if (!ticket.accepted) return {ticket, false, queueDepth};
worker.post([this, ticket, work = std::forward<WorkFn>(work)]() mutable -> rpc::RPCWorker::MainCb {
rpc::RPCWorker::MainCb mainCallback;
try {
mainCallback = work();
} catch (...) {
mainCallback = nullptr;
}
return [this, ticket, mainCallback = std::move(mainCallback)]() mutable {
if (!completeDispatch(ticket)) return;
if (mainCallback) mainCallback();
};
});
return {ticket, true, queueDepth};
}
private:
std::atomic<bool>& jobFlag(Job job);
const std::atomic<bool>& jobFlag(Job job) const;
static std::size_t jobIndex(Job job);
RefreshScheduler scheduler_;
std::atomic<bool> coreInProgress_{false};
std::atomic<bool> addressesInProgress_{false};
std::atomic<bool> transactionsInProgress_{false};
std::atomic<bool> miningInProgress_{false};
std::atomic<bool> peersInProgress_{false};
std::atomic<bool> priceInProgress_{false};
std::atomic<bool> encryptionInProgress_{false};
std::atomic<bool> connectionInitInProgress_{false};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> generations_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> started_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> finished_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> skippedInFlight_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> skippedQueuePressure_{};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> staleCallbacks_{};
std::array<std::atomic<std::size_t>, static_cast<std::size_t>(Job::Count)> lastQueueDepth_{};
};
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,156 @@
#include "refresh_scheduler.h"
#include <algorithm>
namespace dragonx {
namespace services {
RefreshScheduler::Intervals RefreshScheduler::intervalsForPage(ui::NavPage page)
{
using NP = ui::NavPage;
switch (page) {
case NP::Overview: return {2.0f, 10.0f, 15.0f, 0.0f};
case NP::Send: return {3.0f, 10.0f, 5.0f, 0.0f};
case NP::Receive: return {5.0f, 15.0f, 5.0f, 0.0f};
case NP::History: return {5.0f, 3.0f, 15.0f, 0.0f};
case NP::Mining: return {5.0f, 15.0f, 15.0f, 0.0f};
case NP::Peers: return {5.0f, 15.0f, 15.0f, 5.0f};
case NP::Market: return {5.0f, 15.0f, 15.0f, 0.0f};
case NP::Console: return {10.0f, 30.0f, 30.0f, 0.0f};
default: return {5.0f, 15.0f, 15.0f, 0.0f};
}
}
void RefreshScheduler::applyPage(ui::NavPage page)
{
setIntervals(intervalsForPage(page));
}
void RefreshScheduler::setIntervals(Intervals intervals)
{
intervals_ = intervals;
}
void RefreshScheduler::tick(float deltaSeconds)
{
float delta = std::max(0.0f, deltaSeconds);
timers_.core += delta;
timers_.transactions += delta;
timers_.addresses += delta;
timers_.peers += delta;
timers_.price += delta;
timers_.fast += delta;
timers_.txAge += delta;
timers_.opid += delta;
}
bool RefreshScheduler::isDue(Timer timer) const
{
float timerInterval = interval(timer);
return timerInterval > 0.0f && timerRef(timer) >= timerInterval;
}
bool RefreshScheduler::consumeDue(Timer timer)
{
if (!isDue(timer)) return false;
reset(timer);
return true;
}
void RefreshScheduler::reset(Timer timer)
{
timerRef(timer) = 0.0f;
}
void RefreshScheduler::markDue(Timer timer)
{
float timerInterval = interval(timer);
timerRef(timer) = timerInterval > 0.0f ? timerInterval : 0.0f;
}
void RefreshScheduler::setTimer(Timer timer, float seconds)
{
timerRef(timer) = std::max(0.0f, seconds);
}
float RefreshScheduler::timer(Timer timer) const
{
return timerRef(timer);
}
float RefreshScheduler::interval(Timer timer) const
{
switch (timer) {
case Timer::Core: return intervals_.core;
case Timer::Transactions: return intervals_.transactions;
case Timer::Addresses: return intervals_.addresses;
case Timer::Peers: return intervals_.peers;
case Timer::Price: return kPrice;
case Timer::Fast: return kFast;
case Timer::TxAge: return kTxMaxAge;
case Timer::Opid: return kOpidPoll;
}
return 0.0f;
}
void RefreshScheduler::markImmediateRefresh()
{
markDue(Timer::Core);
markDue(Timer::Transactions);
markDue(Timer::Addresses);
markDue(Timer::Peers);
}
void RefreshScheduler::markWalletMutationRefresh()
{
markDue(Timer::Core);
markDue(Timer::Transactions);
markDue(Timer::Addresses);
}
void RefreshScheduler::resetTxAge()
{
reset(Timer::TxAge);
}
bool RefreshScheduler::shouldRefreshTransactions(int lastTxBlockHeight,
int currentBlockHeight,
bool transactionsDirty) const
{
return lastTxBlockHeight < 0
|| currentBlockHeight != lastTxBlockHeight
|| transactionsDirty;
}
float& RefreshScheduler::timerRef(Timer timer)
{
switch (timer) {
case Timer::Core: return timers_.core;
case Timer::Transactions: return timers_.transactions;
case Timer::Addresses: return timers_.addresses;
case Timer::Peers: return timers_.peers;
case Timer::Price: return timers_.price;
case Timer::Fast: return timers_.fast;
case Timer::TxAge: return timers_.txAge;
case Timer::Opid: return timers_.opid;
}
return timers_.core;
}
const float& RefreshScheduler::timerRef(Timer timer) const
{
switch (timer) {
case Timer::Core: return timers_.core;
case Timer::Transactions: return timers_.transactions;
case Timer::Addresses: return timers_.addresses;
case Timer::Peers: return timers_.peers;
case Timer::Price: return timers_.price;
case Timer::Fast: return timers_.fast;
case Timer::TxAge: return timers_.txAge;
case Timer::Opid: return timers_.opid;
}
return timers_.core;
}
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,80 @@
#pragma once
#include "ui/sidebar.h"
namespace dragonx {
namespace services {
class RefreshScheduler {
public:
enum class Timer {
Core,
Transactions,
Addresses,
Peers,
Price,
Fast,
TxAge,
Opid
};
struct Intervals {
float core;
float transactions;
float addresses;
float peers;
};
static constexpr float kCoreDefault = 5.0f;
static constexpr float kAddressDefault = 15.0f;
static constexpr float kTransactionDefault = 10.0f;
static constexpr float kPeerDefault = 10.0f;
static constexpr float kPrice = 60.0f;
static constexpr float kFast = 1.0f;
static constexpr float kTxMaxAge = 15.0f;
static constexpr float kOpidPoll = 2.0f;
static Intervals intervalsForPage(ui::NavPage page);
void applyPage(ui::NavPage page);
void setIntervals(Intervals intervals);
const Intervals& intervals() const { return intervals_; }
void tick(float deltaSeconds);
bool isDue(Timer timer) const;
bool consumeDue(Timer timer);
void reset(Timer timer);
void markDue(Timer timer);
void setTimer(Timer timer, float seconds);
float timer(Timer timer) const;
float interval(Timer timer) const;
void markImmediateRefresh();
void markWalletMutationRefresh();
void resetTxAge();
bool shouldRefreshTransactions(int lastTxBlockHeight,
int currentBlockHeight,
bool transactionsDirty) const;
private:
struct Timers {
float core = 0.0f;
float transactions = 0.0f;
float addresses = 0.0f;
float peers = 0.0f;
float price = 0.0f;
float fast = 0.0f;
float txAge = 0.0f;
float opid = 0.0f;
};
float& timerRef(Timer timer);
const float& timerRef(Timer timer) const;
Intervals intervals_{kCoreDefault, kTransactionDefault, kAddressDefault, kPeerDefault};
Timers timers_;
};
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,127 @@
#include "wallet_security_controller.h"
#include "../util/secure_vault.h"
#include <cctype>
#include <cstdio>
#include <utility>
namespace dragonx {
namespace services {
WalletSecurityController::~WalletSecurityController()
{
clearDeferredEncryption();
}
void WalletSecurityController::beginDeferredEncryption(std::string passphrase, std::string pin)
{
clearDeferredEncryption();
deferred_.passphrase = std::move(passphrase);
deferred_.pin = std::move(pin);
deferred_.pending = true;
deferred_.lastConnectAttempt = -10.0;
}
WalletSecurityController::DeferredEncryptionSnapshot WalletSecurityController::deferredEncryption() const
{
return {deferred_.passphrase, deferred_.pin};
}
bool WalletSecurityController::shouldAttemptDeferredConnect(double nowSeconds, double minIntervalSeconds)
{
if (!deferred_.pending) return false;
if (nowSeconds - deferred_.lastConnectAttempt < minIntervalSeconds) return false;
deferred_.lastConnectAttempt = nowSeconds;
return true;
}
void WalletSecurityController::clearDeferredEncryption()
{
secureClear(deferred_.passphrase);
secureClear(deferred_.pin);
deferred_.pending = false;
deferred_.lastConnectAttempt = -10.0;
}
WalletSecurityController::DeferredEncryptionResult WalletSecurityController::runDeferredEncryption(
DeferredEncryptionSnapshot request, RpcGateway& rpc, VaultGateway* vault)
{
DeferredEncryptionResult result;
result.pinProvided = !request.pin.empty();
std::string error;
if (!rpc.encryptWallet(request.passphrase, error)) {
result.error = error.empty() ? "encryptwallet failed" : error;
secureClear(request.passphrase);
secureClear(request.pin);
return result;
}
result.encrypted = true;
result.restartRequired = true;
if (result.pinProvided && vault) {
result.pinStored = vault->storePin(request.pin, request.passphrase);
}
secureClear(request.passphrase);
secureClear(request.pin);
return result;
}
WalletSecurityController::PinValidationResult WalletSecurityController::validatePinSetup(
const std::string& pin, const std::string& confirmation, bool allowEmpty, std::size_t minLength)
{
if (pin.empty() && confirmation.empty()) {
return allowEmpty
? PinValidationResult{true, PinValidationError::None, ""}
: PinValidationResult{false, PinValidationError::Empty, "PIN is required"};
}
if (pin != confirmation) {
return {false, PinValidationError::Mismatch, "PINs do not match"};
}
if (pin.size() < minLength) {
return {false, PinValidationError::TooShort, "PIN is too short"};
}
for (unsigned char c : pin) {
if (!std::isdigit(c)) {
return {false, PinValidationError::NonDigit, "PIN must contain only digits"};
}
}
return {true, PinValidationError::None, ""};
}
WalletSecurityController::KeyKind WalletSecurityController::classifyAddress(const std::string& address)
{
return !address.empty() && address[0] == 'z' ? KeyKind::Shielded : KeyKind::Transparent;
}
WalletSecurityController::KeyKind WalletSecurityController::classifyPrivateKey(const std::string& key)
{
return !key.empty() && key[0] == 's' ? KeyKind::Shielded : KeyKind::Transparent;
}
const char* WalletSecurityController::importSuccessMessage(KeyKind kind)
{
return kind == KeyKind::Shielded
? "Z-address key imported successfully. Wallet is rescanning."
: "T-address key imported successfully. Wallet is rescanning.";
}
std::string WalletSecurityController::decryptExportFileName(std::uint64_t timestampSeconds)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "obsidiandecryptexport%llu",
static_cast<unsigned long long>(timestampSeconds));
return std::string(buffer);
}
void WalletSecurityController::secureClear(std::string& value)
{
if (!value.empty()) {
util::SecureVault::secureZero(&value[0], value.size());
value.clear();
}
}
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,93 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
namespace dragonx {
namespace services {
class WalletSecurityController {
public:
enum class PinValidationError {
None,
Empty,
Mismatch,
TooShort,
NonDigit
};
struct PinValidationResult {
bool ok = false;
PinValidationError error = PinValidationError::None;
const char* message = "";
};
struct DeferredEncryptionSnapshot {
std::string passphrase;
std::string pin;
};
class RpcGateway {
public:
virtual ~RpcGateway() = default;
virtual bool encryptWallet(const std::string& passphrase, std::string& error) = 0;
virtual bool unlockWallet(const std::string& passphrase, int timeoutSeconds, std::string& error) = 0;
virtual bool exportWallet(const std::string& fileName, long timeoutSeconds, std::string& error) = 0;
virtual bool importWallet(const std::string& filePath, long timeoutSeconds, std::string& error) = 0;
};
class VaultGateway {
public:
virtual ~VaultGateway() = default;
virtual bool storePin(const std::string& pin, const std::string& passphrase) = 0;
};
enum class KeyKind {
Transparent,
Shielded
};
struct DeferredEncryptionResult {
bool encrypted = false;
bool pinProvided = false;
bool pinStored = false;
bool restartRequired = false;
std::string error;
};
~WalletSecurityController();
void beginDeferredEncryption(std::string passphrase, std::string pin = {});
bool hasDeferredEncryption() const { return deferred_.pending; }
DeferredEncryptionSnapshot deferredEncryption() const;
bool shouldAttemptDeferredConnect(double nowSeconds, double minIntervalSeconds = 3.0);
void clearDeferredEncryption();
DeferredEncryptionResult runDeferredEncryption(DeferredEncryptionSnapshot request,
RpcGateway& rpc,
VaultGateway* vault);
static PinValidationResult validatePinSetup(const std::string& pin,
const std::string& confirmation,
bool allowEmpty = false,
std::size_t minLength = 4);
static KeyKind classifyAddress(const std::string& address);
static KeyKind classifyPrivateKey(const std::string& key);
static const char* importSuccessMessage(KeyKind kind);
static std::string decryptExportFileName(std::uint64_t timestampSeconds);
static void secureClear(std::string& value);
private:
struct DeferredEncryptionState {
std::string passphrase;
std::string pin;
bool pending = false;
double lastConnectAttempt = -10.0;
};
DeferredEncryptionState deferred_;
};
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,114 @@
#include "wallet_security_workflow.h"
#include <utility>
namespace dragonx {
namespace services {
void WalletSecurityWorkflow::reset()
{
state_ = {};
}
void WalletSecurityWorkflow::start(std::chrono::steady_clock::time_point now)
{
state_.phase = DecryptPhase::Working;
state_.step = DecryptStep::Unlock;
state_.status = stepStatus(DecryptStep::Unlock);
state_.inProgress = true;
state_.stepStarted = now;
state_.overallStarted = now;
}
void WalletSecurityWorkflow::advanceTo(DecryptStep step, std::string status,
std::chrono::steady_clock::time_point now)
{
state_.phase = DecryptPhase::Working;
state_.step = step;
state_.status = std::move(status);
state_.inProgress = true;
state_.stepStarted = now;
}
void WalletSecurityWorkflow::failEntry(std::string status)
{
state_.phase = DecryptPhase::PassphraseEntry;
state_.step = DecryptStep::Unlock;
state_.status = std::move(status);
state_.inProgress = false;
}
void WalletSecurityWorkflow::fail(std::string status)
{
state_.phase = DecryptPhase::Error;
state_.status = std::move(status);
state_.inProgress = false;
}
void WalletSecurityWorkflow::closeDialogForImport()
{
state_.inProgress = false;
state_.importActive = true;
}
void WalletSecurityWorkflow::finishImport()
{
state_.importActive = false;
}
WalletSecurityWorkflow::WalletFilePlan WalletSecurityWorkflow::planWalletFiles(
const std::string& dataDir,
std::uint64_t timestampSeconds)
{
WalletFilePlan plan;
plan.dataDir = dataDir;
plan.exportFile = WalletSecurityController::decryptExportFileName(timestampSeconds);
plan.exportPath = dataDir + plan.exportFile;
plan.walletPath = dataDir + "wallet.dat";
plan.backupPath = dataDir + "wallet.dat.encrypted.bak";
return plan;
}
const char* WalletSecurityWorkflow::stepStatus(DecryptStep step)
{
switch (step) {
case DecryptStep::Unlock: return "Unlocking wallet...";
case DecryptStep::ExportKeys: return "Exporting wallet keys...";
case DecryptStep::StopDaemon: return "Stopping daemon...";
case DecryptStep::BackupWallet: return "Backing up encrypted wallet...";
case DecryptStep::RestartDaemon: return "Restarting daemon...";
case DecryptStep::ImportKeys: return "Importing wallet keys...";
}
return "";
}
const char* WalletSecurityWorkflow::stepLabel(DecryptStep step)
{
switch (step) {
case DecryptStep::Unlock: return "Unlocking wallet";
case DecryptStep::ExportKeys: return "Exporting wallet keys";
case DecryptStep::StopDaemon: return "Stopping daemon";
case DecryptStep::BackupWallet: return "Backing up encrypted wallet";
case DecryptStep::RestartDaemon: return "Restarting daemon";
case DecryptStep::ImportKeys: return "Importing wallet keys";
}
return "";
}
WalletSecurityWorkflow::DecryptStep WalletSecurityWorkflow::stepFromIndex(int step)
{
if (step <= 0) return DecryptStep::Unlock;
if (step == 1) return DecryptStep::ExportKeys;
if (step == 2) return DecryptStep::StopDaemon;
if (step == 3) return DecryptStep::BackupWallet;
if (step == 4) return DecryptStep::RestartDaemon;
return DecryptStep::ImportKeys;
}
bool WalletSecurityWorkflow::stepIsComplete(DecryptStep current, DecryptStep candidate)
{
return stepIndex(candidate) < stepIndex(current);
}
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,78 @@
#pragma once
#include "wallet_security_controller.h"
#include <chrono>
#include <cstdint>
#include <string>
namespace dragonx {
namespace services {
class WalletSecurityWorkflow {
public:
enum class DecryptPhase {
PassphraseEntry = 0,
Working = 1,
Success = 2,
Error = 3
};
enum class DecryptStep {
Unlock = 0,
ExportKeys = 1,
StopDaemon = 2,
BackupWallet = 3,
RestartDaemon = 4,
ImportKeys = 5
};
struct DecryptSnapshot {
DecryptPhase phase = DecryptPhase::PassphraseEntry;
DecryptStep step = DecryptStep::Unlock;
std::string status;
bool inProgress = false;
bool importActive = false;
std::chrono::steady_clock::time_point stepStarted{};
std::chrono::steady_clock::time_point overallStarted{};
};
struct WalletFilePlan {
std::string dataDir;
std::string exportFile;
std::string exportPath;
std::string walletPath;
std::string backupPath;
};
void reset();
void start(std::chrono::steady_clock::time_point now);
void advanceTo(DecryptStep step, std::string status,
std::chrono::steady_clock::time_point now);
void failEntry(std::string status);
void fail(std::string status);
void closeDialogForImport();
void finishImport();
DecryptSnapshot snapshot() const { return state_; }
DecryptPhase phase() const { return state_.phase; }
DecryptStep step() const { return state_.step; }
const std::string& status() const { return state_.status; }
bool inProgress() const { return state_.inProgress; }
bool importActive() const { return state_.importActive; }
bool canClose() const { return state_.phase != DecryptPhase::Working; }
static WalletFilePlan planWalletFiles(const std::string& dataDir,
std::uint64_t timestampSeconds);
static const char* stepStatus(DecryptStep step);
static const char* stepLabel(DecryptStep step);
static int stepIndex(DecryptStep step) { return static_cast<int>(step); }
static DecryptStep stepFromIndex(int step);
static bool stepIsComplete(DecryptStep current, DecryptStep candidate);
private:
DecryptSnapshot state_;
};
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,104 @@
#include "wallet_security_workflow_executor.h"
namespace dragonx {
namespace services {
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::unlockWallet(
const std::string& passphrase, RpcGateway& rpc, int timeoutSeconds)
{
std::string error;
if (!rpc.unlockWallet(passphrase, timeoutSeconds, error)) {
return {false, error.empty() ? "Incorrect passphrase" : error, true};
}
return {true, {}, false};
}
WalletSecurityWorkflowExecutor::ExportOutcome WalletSecurityWorkflowExecutor::exportWallet(
RpcGateway& rpc, FileGateway& files, std::uint64_t timestampSeconds, long timeoutSeconds)
{
ExportOutcome outcome;
outcome.filePlan = WalletSecurityWorkflow::planWalletFiles(files.dataDir(), timestampSeconds);
std::string error;
if (!rpc.exportWallet(outcome.filePlan.exportFile, timeoutSeconds, error)) {
outcome.ok = false;
outcome.error = error.empty() ? "Export failed" : "Export failed: " + error;
return outcome;
}
outcome.ok = true;
return outcome;
}
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::stopDaemon(RpcGateway& rpc)
{
std::string error;
(void)rpc.requestDaemonStop(error);
return {true, {}, false};
}
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::backupEncryptedWallet(
FileGateway& files, const WalletFilePlan& filePlan)
{
std::string error;
if (!files.backupEncryptedWallet(filePlan, error)) {
return {false, error.empty() ? "Failed to rename wallet.dat" : "Failed to rename wallet.dat: " + error, false};
}
return {true, {}, false};
}
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::restartDaemonAndWait(
DaemonGateway& daemon, RpcGateway& rpc, int preRestartDelayMs,
int embeddedRestartSettleMs, int maxProbeSeconds)
{
auto waitForMs = [&](int milliseconds) -> bool {
int remaining = milliseconds;
while (remaining > 0 && !daemon.cancelled() && !daemon.shuttingDown()) {
int slice = remaining >= 100 ? 100 : remaining;
daemon.sleepForMs(slice);
remaining -= slice;
}
return !daemon.cancelled() && !daemon.shuttingDown();
};
if (!waitForMs(preRestartDelayMs)) return {false, "", false};
if (daemon.isUsingEmbeddedDaemon()) {
daemon.stopEmbeddedDaemon();
if (daemon.cancelled() || daemon.shuttingDown()) return {false, "", false};
if (!waitForMs(embeddedRestartSettleMs)) return {false, "", false};
daemon.startEmbeddedDaemon();
}
bool daemonUp = false;
std::string lastError;
for (int i = 0; i < maxProbeSeconds && !daemon.cancelled() && !daemon.shuttingDown(); ++i) {
daemon.sleepForMs(1000);
if (rpc.probeDaemon(lastError)) {
daemonUp = true;
break;
}
}
if (daemon.cancelled() || daemon.shuttingDown()) return {false, "", false};
if (!daemonUp) return {false, "Daemon failed to restart", false};
return {true, {}, false};
}
WalletSecurityWorkflowExecutor::Outcome WalletSecurityWorkflowExecutor::importWallet(
ImportGateway& importer, const std::string& exportPath, long timeoutSeconds)
{
std::string error;
if (!importer.importWallet(exportPath, timeoutSeconds, error)) {
return {false, error.empty() ? "Key import failed" : "Key import failed: " + error, false};
}
return {true, {}, false};
}
void WalletSecurityWorkflowExecutor::cleanupVaultAndPin(const VaultCleanupGateway& cleanup)
{
if (cleanup) cleanup();
}
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,83 @@
#pragma once
#include "wallet_security_workflow.h"
#include <cstdint>
#include <functional>
#include <string>
namespace dragonx {
namespace services {
class WalletSecurityWorkflowExecutor {
public:
using WalletFilePlan = WalletSecurityWorkflow::WalletFilePlan;
struct Outcome {
bool ok = false;
std::string error;
bool passphraseRejected = false;
};
struct ExportOutcome : Outcome {
WalletFilePlan filePlan;
};
class RpcGateway {
public:
virtual ~RpcGateway() = default;
virtual bool unlockWallet(const std::string& passphrase, int timeoutSeconds, std::string& error) = 0;
virtual bool exportWallet(const std::string& fileName, long timeoutSeconds, std::string& error) = 0;
virtual bool requestDaemonStop(std::string& error) = 0;
virtual bool probeDaemon(std::string& error) = 0;
};
class ImportGateway {
public:
virtual ~ImportGateway() = default;
virtual bool importWallet(const std::string& exportPath, long timeoutSeconds, std::string& error) = 0;
};
class FileGateway {
public:
virtual ~FileGateway() = default;
virtual std::string dataDir() = 0;
virtual bool backupEncryptedWallet(const WalletFilePlan& filePlan, std::string& error) = 0;
};
class DaemonGateway {
public:
virtual ~DaemonGateway() = default;
virtual bool isUsingEmbeddedDaemon() const = 0;
virtual void stopEmbeddedDaemon() = 0;
virtual bool startEmbeddedDaemon() = 0;
virtual bool cancelled() const = 0;
virtual bool shuttingDown() const = 0;
virtual void sleepForMs(int milliseconds) = 0;
};
using VaultCleanupGateway = std::function<void()>;
static Outcome unlockWallet(const std::string& passphrase,
RpcGateway& rpc,
int timeoutSeconds = 600);
static ExportOutcome exportWallet(RpcGateway& rpc,
FileGateway& files,
std::uint64_t timestampSeconds,
long timeoutSeconds = 300L);
static Outcome stopDaemon(RpcGateway& rpc);
static Outcome backupEncryptedWallet(FileGateway& files,
const WalletFilePlan& filePlan);
static Outcome restartDaemonAndWait(DaemonGateway& daemon,
RpcGateway& rpc,
int preRestartDelayMs = 2000,
int embeddedRestartSettleMs = 1000,
int maxProbeSeconds = 60);
static Outcome importWallet(ImportGateway& importer,
const std::string& exportPath,
long timeoutSeconds = 1200L);
static void cleanupVaultAndPin(const VaultCleanupGateway& cleanup);
};
} // namespace services
} // namespace dragonx

View File

@@ -0,0 +1,362 @@
#include "explorer_block_cache.h"
#include "../../util/logger.h"
#include "../../util/platform.h"
#include <nlohmann/json.hpp>
#include <sqlite3.h>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <utility>
namespace fs = std::filesystem;
using json = nlohmann::json;
namespace dragonx {
namespace ui {
namespace {
constexpr int kCacheSchemaVersion = 1;
struct Statement {
sqlite3_stmt* handle = nullptr;
Statement(sqlite3* db, const char* sql)
{
if (sqlite3_prepare_v2(db, sql, -1, &handle, nullptr) != SQLITE_OK) {
handle = nullptr;
}
}
~Statement()
{
if (handle) sqlite3_finalize(handle);
}
Statement(const Statement&) = delete;
Statement& operator=(const Statement&) = delete;
};
bool bindText(sqlite3_stmt* statement, int index, const std::string& value)
{
return sqlite3_bind_text(statement, index, value.c_str(), -1, SQLITE_TRANSIENT) == SQLITE_OK;
}
bool blockSummaryFromJson(const json& source, ExplorerBlockSummary& block)
{
if (!source.is_object()) return false;
try {
block.height = source.value("height", 0);
block.hash = source.value("hash", std::string());
block.tx_count = source.value("tx_count", 0);
block.size = source.value("size", 0);
block.time = source.value("time", static_cast<std::int64_t>(0));
block.difficulty = source.value("difficulty", 0.0);
} catch (...) {
return false;
}
return block.height > 0 && !block.hash.empty();
}
} // namespace
ExplorerBlockCache::ExplorerBlockCache()
: ExplorerBlockCache(defaultDatabasePath(), defaultLegacyJsonPath())
{
}
ExplorerBlockCache::ExplorerBlockCache(std::string databasePath, std::string legacyJsonPath)
: database_path_(std::move(databasePath)), legacy_json_path_(std::move(legacyJsonPath))
{
}
ExplorerBlockCache::~ExplorerBlockCache()
{
close();
}
std::string ExplorerBlockCache::defaultDatabasePath()
{
return (fs::path(util::Platform::getConfigDir()) / "explorer_blocks.sqlite").string();
}
std::string ExplorerBlockCache::defaultLegacyJsonPath()
{
return (fs::path(util::Platform::getConfigDir()) / "explorer_blocks_cache.json").string();
}
bool ExplorerBlockCache::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& e) {
DEBUG_LOGF("Failed to create explorer cache directory: %s\n", e.what());
return false;
}
sqlite3* openedDb = nullptr;
if (sqlite3_open(database_path_.c_str(), &openedDb) != SQLITE_OK) {
DEBUG_LOGF("Failed to open explorer block cache: %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");
if (!createSchema()) {
close();
return false;
}
migrateLegacyJsonIfNeeded();
return true;
}
std::map<int, ExplorerBlockSummary> ExplorerBlockCache::loadRange(int minHeight, int maxHeight)
{
std::map<int, ExplorerBlockSummary> blocks;
if (minHeight > maxHeight) std::swap(minHeight, maxHeight);
if (minHeight < 1 || maxHeight < 1 || !ensureOpen()) return blocks;
Statement statement(db_,
"SELECT height, hash, tx_count, size, time, difficulty "
"FROM explorer_blocks WHERE height BETWEEN ? AND ? ORDER BY height DESC");
if (!statement.handle) return blocks;
sqlite3_bind_int(statement.handle, 1, minHeight);
sqlite3_bind_int(statement.handle, 2, maxHeight);
while (sqlite3_step(statement.handle) == SQLITE_ROW) {
ExplorerBlockSummary block;
block.height = sqlite3_column_int(statement.handle, 0);
const unsigned char* hashText = sqlite3_column_text(statement.handle, 1);
if (hashText) block.hash = reinterpret_cast<const char*>(hashText);
block.tx_count = sqlite3_column_int(statement.handle, 2);
block.size = sqlite3_column_int(statement.handle, 3);
block.time = static_cast<std::int64_t>(sqlite3_column_int64(statement.handle, 4));
block.difficulty = sqlite3_column_double(statement.handle, 5);
if (block.height > 0 && !block.hash.empty()) {
blocks[block.height] = std::move(block);
}
}
return blocks;
}
bool ExplorerBlockCache::storeBlock(const ExplorerBlockSummary& block)
{
if (block.height < 1 || block.hash.empty() || !ensureOpen()) return false;
Statement statement(db_,
"INSERT OR REPLACE INTO explorer_blocks "
"(height, hash, tx_count, size, time, difficulty) VALUES (?, ?, ?, ?, ?, ?)");
if (!statement.handle) return false;
sqlite3_bind_int(statement.handle, 1, block.height);
if (!bindText(statement.handle, 2, block.hash)) return false;
sqlite3_bind_int(statement.handle, 3, block.tx_count);
sqlite3_bind_int(statement.handle, 4, block.size);
sqlite3_bind_int64(statement.handle, 5, static_cast<sqlite3_int64>(block.time));
sqlite3_bind_double(statement.handle, 6, block.difficulty);
return sqlite3_step(statement.handle) == SQLITE_DONE;
}
int ExplorerBlockCache::cachedBlockCount()
{
if (!ensureOpen()) return 0;
Statement statement(db_, "SELECT COUNT(*) FROM explorer_blocks");
if (!statement.handle || sqlite3_step(statement.handle) != SQLITE_ROW) return 0;
return sqlite3_column_int(statement.handle, 0);
}
void ExplorerBlockCache::clearBlocks()
{
if (!ensureOpen()) return;
exec("DELETE FROM explorer_blocks");
}
ExplorerBlockCache::SavedTipValidation ExplorerBlockCache::prepareValidation(
int currentHeight, const std::string& currentBestHash)
{
SavedTipValidation validation;
if (currentHeight <= 0 || !ensureOpen()) return validation;
int savedHeight = getMetaInt("tip_height", 0);
std::string savedHash = getMetaValue("tip_hash");
if (savedHeight <= 0 || savedHash.empty()) {
if (!currentBestHash.empty()) updateTip(currentHeight, currentBestHash);
return validation;
}
if (currentHeight < savedHeight) {
clearBlocks();
if (!currentBestHash.empty()) updateTip(currentHeight, currentBestHash);
else updateTip(0, std::string());
return validation;
}
if (currentHeight == savedHeight) {
if (currentBestHash.empty()) return validation;
if (currentBestHash != savedHash) clearBlocks();
updateTip(currentHeight, currentBestHash);
return validation;
}
if (currentBestHash.empty()) return validation;
validation.needed = true;
validation.height = savedHeight;
validation.expectedHash = savedHash;
return validation;
}
void ExplorerBlockCache::applySavedTipValidation(const SavedTipValidation& validation,
const std::string& actualHash,
int currentHeight,
const std::string& currentBestHash)
{
if (!validation.needed || !ensureOpen()) return;
if (actualHash.empty()) return;
if (actualHash != validation.expectedHash) {
clearBlocks();
}
if (currentHeight > 0 && !currentBestHash.empty()) {
updateTip(currentHeight, currentBestHash);
}
}
void ExplorerBlockCache::updateTip(int height, const std::string& hash)
{
if (!ensureOpen()) return;
setMetaValue("tip_height", std::to_string(std::max(0, height)));
setMetaValue("tip_hash", hash);
}
bool ExplorerBlockCache::exec(const char* sql)
{
if (!db_) return false;
char* error = nullptr;
int result = sqlite3_exec(db_, sql, nullptr, nullptr, &error);
if (result != SQLITE_OK) {
DEBUG_LOGF("Explorer block cache SQL error: %s\n", error ? error : sqlite3_errmsg(db_));
if (error) sqlite3_free(error);
return false;
}
return true;
}
std::string ExplorerBlockCache::getMetaValue(const std::string& key)
{
if (!ensureOpen()) return {};
Statement statement(db_, "SELECT value FROM explorer_cache_meta WHERE key = ?");
if (!statement.handle) return {};
if (!bindText(statement.handle, 1, key)) return {};
if (sqlite3_step(statement.handle) != SQLITE_ROW) return {};
const unsigned char* valueText = sqlite3_column_text(statement.handle, 0);
return valueText ? reinterpret_cast<const char*>(valueText) : std::string();
}
int ExplorerBlockCache::getMetaInt(const std::string& key, int fallback)
{
std::string value = getMetaValue(key);
if (value.empty()) return fallback;
try {
return std::stoi(value);
} catch (...) {
return fallback;
}
}
void ExplorerBlockCache::setMetaValue(const std::string& key, const std::string& value)
{
if (!ensureOpen()) return;
Statement statement(db_,
"INSERT OR REPLACE INTO explorer_cache_meta (key, value) VALUES (?, ?)");
if (!statement.handle) return;
if (!bindText(statement.handle, 1, key)) return;
if (!bindText(statement.handle, 2, value)) return;
sqlite3_step(statement.handle);
}
bool ExplorerBlockCache::createSchema()
{
return exec("CREATE TABLE IF NOT EXISTS explorer_blocks ("
"height INTEGER PRIMARY KEY,"
"hash TEXT NOT NULL,"
"tx_count INTEGER NOT NULL,"
"size INTEGER NOT NULL,"
"time INTEGER NOT NULL,"
"difficulty REAL NOT NULL)") &&
exec("CREATE TABLE IF NOT EXISTS explorer_cache_meta ("
"key TEXT PRIMARY KEY,"
"value TEXT NOT NULL)") &&
exec("INSERT OR IGNORE INTO explorer_cache_meta (key, value) VALUES "
"('schema_version', '1')");
}
void ExplorerBlockCache::migrateLegacyJsonIfNeeded()
{
if (!db_ || getMetaValue("json_migrated") == "1") return;
bool migrated = false;
try {
if (!legacy_json_path_.empty() && fs::exists(legacy_json_path_)) {
std::ifstream file(legacy_json_path_);
json cache;
file >> cache;
if (cache.is_object() && cache.value("version", 0) == kCacheSchemaVersion) {
exec("BEGIN IMMEDIATE TRANSACTION");
if (cache.contains("blocks") && cache["blocks"].is_array()) {
for (const auto& entry : cache["blocks"]) {
ExplorerBlockSummary block;
if (blockSummaryFromJson(entry, block)) {
storeBlock(block);
}
}
}
exec("COMMIT");
int tipHeight = cache.value("tip_height", 0);
std::string tipHash = cache.value("tip_hash", std::string());
if (tipHeight > 0 && !tipHash.empty()) {
updateTip(tipHeight, tipHash);
}
}
}
migrated = true;
} catch (const std::exception& e) {
exec("ROLLBACK");
DEBUG_LOGF("Failed to migrate explorer JSON cache: %s\n", e.what());
migrated = true;
}
if (migrated) setMetaValue("json_migrated", "1");
}
void ExplorerBlockCache::close()
{
if (!db_) return;
sqlite3_close(db_);
db_ = nullptr;
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,70 @@
#pragma once
#include <cstdint>
#include <map>
#include <string>
struct sqlite3;
namespace dragonx {
namespace ui {
struct ExplorerBlockSummary {
int height = 0;
std::string hash;
int tx_count = 0;
int size = 0;
std::int64_t time = 0;
double difficulty = 0.0;
};
class ExplorerBlockCache {
public:
struct SavedTipValidation {
bool needed = false;
int height = 0;
std::string expectedHash;
};
ExplorerBlockCache();
ExplorerBlockCache(std::string databasePath, std::string legacyJsonPath);
~ExplorerBlockCache();
ExplorerBlockCache(const ExplorerBlockCache&) = delete;
ExplorerBlockCache& operator=(const ExplorerBlockCache&) = delete;
static std::string defaultDatabasePath();
static std::string defaultLegacyJsonPath();
bool ensureOpen();
bool isOpen() const { return db_ != nullptr; }
const std::string& databasePath() const { return database_path_; }
std::map<int, ExplorerBlockSummary> loadRange(int minHeight, int maxHeight);
bool storeBlock(const ExplorerBlockSummary& block);
int cachedBlockCount();
void clearBlocks();
SavedTipValidation prepareValidation(int currentHeight, const std::string& currentBestHash);
void applySavedTipValidation(const SavedTipValidation& validation,
const std::string& actualHash,
int currentHeight,
const std::string& currentBestHash);
void updateTip(int height, const std::string& hash);
private:
bool exec(const char* sql);
std::string getMetaValue(const std::string& key);
int getMetaInt(const std::string& key, int fallback);
void setMetaValue(const std::string& key, const std::string& value);
bool createSchema();
void migrateLegacyJsonIfNeeded();
void close();
sqlite3* db_ = nullptr;
std::string database_path_;
std::string legacy_json_path_;
};
} // namespace ui
} // namespace dragonx

View File

@@ -432,6 +432,20 @@ inline float columnOffset(float ratio, float availW) {
return availW * ratio; return availW * ratio;
} }
// ============================================================================
// Dialogs
// ============================================================================
inline float kDialogDefaultWidth() { return schema::UI().drawElement("dialog", "width-default").sizeOr(480.0f) * dpiScale(); }
inline float kDialogLargeWidth() { return schema::UI().drawElement("dialog", "width-lg").sizeOr(600.0f) * dpiScale(); }
inline float kDialogExtraLargeWidth() { return schema::UI().drawElement("dialog", "width-xl").sizeOr(660.0f) * dpiScale(); }
inline float kDialogMinWidth() { return schema::UI().drawElement("dialog", "min-width").sizeOr(280.0f) * dpiScale(); }
inline float kDialogFormWidth() { return schema::UI().drawElement("dialog", "form-width").sizeOr(400.0f) * dpiScale(); }
inline float kDialogActionWidth() { return schema::UI().drawElement("dialog", "action-width").sizeOr(100.0f) * dpiScale(); }
inline float kDialogActionGap() { return schema::UI().drawElement("dialog", "action-gap").sizeOr(8.0f) * dpiScale(); }
inline float kDialogMaxHeightRatio() { return schema::UI().drawElement("dialog", "max-height-ratio").sizeOr(0.94f); }
inline float kDialogCompactBottomRatio() { return schema::UI().drawElement("dialog", "compact-bottom-ratio").sizeOr(0.64f); }
// ============================================================================ // ============================================================================
// Buttons // Buttons
// ============================================================================ // ============================================================================
@@ -562,7 +576,7 @@ inline float mainCardTargetH(float formW, float vs) {
float innerW = formW - pad * 2; float innerW = formW - pad * 2;
float qrColW = innerW * 0.35f; float qrColW = innerW * 0.35f;
float qrPad = spacingMd(); float qrPad = spacingMd();
float maxQrSz = std::min(qrColW - qrPad * 2, 280.0f * dp); float maxQrSz = std::min(std::max(0.0f, qrColW - qrPad * 2), 280.0f * dp);
float qrSize = std::max(100.0f * dp, maxQrSz); float qrSize = std::max(100.0f * dp, maxQrSz);
float totalQr = qrSize + qrPad * 2; float totalQr = qrSize + qrPad * 2;
float innerGap = spacingLg(); float innerGap = spacingLg();

View File

@@ -21,6 +21,7 @@
#include <algorithm> #include <algorithm>
#include <unordered_map> #include <unordered_map>
#include <cmath> #include <cmath>
#include <string>
namespace dragonx { namespace dragonx {
namespace ui { namespace ui {
@@ -90,10 +91,40 @@ inline void DrawTextShadow(ImDrawList* dl, const ImVec2& pos, ImU32 col,
// and will return true even when a modal popup covers the rect, which // and will return true even when a modal popup covers the rect, which
// causes background elements to show hover highlights through dialogs. // causes background elements to show hover highlights through dialogs.
inline int& OverlayDialogActiveFrame()
{
static int s_frame = -1;
return s_frame;
}
inline void MarkOverlayDialogActive()
{
OverlayDialogActiveFrame() = ImGui::GetFrameCount();
}
inline bool IsCurrentWindowOverlayDialog()
{
ImGuiWindow* window = ImGui::GetCurrentWindow();
for (ImGuiWindow* node = window; node; node = node->ParentWindow) {
if (node->Name && strcmp(node->Name, "##OverlayScrim") == 0)
return true;
}
return false;
}
inline bool IsOverlayDialogBlockingInput()
{
int activeFrame = OverlayDialogActiveFrame();
int currentFrame = ImGui::GetFrameCount();
return activeFrame == currentFrame || activeFrame == (currentFrame - 1);
}
inline bool IsRectHovered(const ImVec2& r_min, const ImVec2& r_max, bool clip = true) inline bool IsRectHovered(const ImVec2& r_min, const ImVec2& r_max, bool clip = true)
{ {
if (!ImGui::IsMouseHoveringRect(r_min, r_max, clip)) if (!ImGui::IsMouseHoveringRect(r_min, r_max, clip))
return false; return false;
if (IsOverlayDialogBlockingInput() && !IsCurrentWindowOverlayDialog())
return false;
// If a modal popup is open and it is not the current window, treat // If a modal popup is open and it is not the current window, treat
// the content as non-hoverable (same logic ImGui uses internally // the content as non-hoverable (same logic ImGui uses internally
// inside IsWindowContentHoverable for modal blocking). // inside IsWindowContentHoverable for modal blocking).
@@ -885,8 +916,11 @@ inline bool DrawDialogTitleBar(const char* title, bool* p_open, ImU32 accent_col
// Creates a fullscreen semi-transparent overlay with a centered card dialog. // Creates a fullscreen semi-transparent overlay with a centered card dialog.
// Similar to the shutdown screen pattern but for interactive dialogs. // Similar to the shutdown screen pattern but for interactive dialogs.
inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth = 460.0f, float scrimOpacity = 0.92f) inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth = 460.0f, float scrimOpacity = 0.92f,
float cardBottomViewportRatio = 0.85f, const char* idSuffix = nullptr)
{ {
MarkOverlayDialogActive();
ImGuiViewport* vp = ImGui::GetMainViewport(); ImGuiViewport* vp = ImGui::GetMainViewport();
ImVec2 vp_pos = vp->Pos; ImVec2 vp_pos = vp->Pos;
ImVec2 vp_size = vp->Size; ImVec2 vp_size = vp->Size;
@@ -900,7 +934,16 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
bool opened = ImGui::Begin("##OverlayScrim", nullptr, std::string scrimId = "##OverlayScrim";
std::string childId = "##OverlayDialogContent";
if (idSuffix && idSuffix[0] != '\0') {
scrimId += "_";
scrimId += idSuffix;
childId += "_";
childId += idSuffix;
}
bool opened = ImGui::Begin(scrimId.c_str(), nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoNav |
@@ -915,13 +958,21 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
ImDrawList* dl = ImGui::GetWindowDrawList(); ImDrawList* dl = ImGui::GetWindowDrawList();
// Consume pointer input on the scrim so the overlay owns clicks and wheel
// events even when the click lands outside the card content.
ImGui::SetCursorScreenPos(vp_pos);
ImGui::InvisibleButton("##OverlayInputBlocker", vp_size,
ImGuiButtonFlags_MouseButtonLeft |
ImGuiButtonFlags_MouseButtonRight |
ImGuiButtonFlags_MouseButtonMiddle);
// Calculate card position (centered) // Calculate card position (centered)
float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f; float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
float cardY = vp_pos.y + vp_size.y * 0.15f; float cardY = vp_pos.y + vp_size.y * 0.15f;
// Draw glass card background // Draw glass card background
ImVec2 cardMin(cardX, cardY); ImVec2 cardMin(cardX, cardY);
ImVec2 cardMax(cardX + cardWidth, vp_pos.y + vp_size.y * 0.85f); ImVec2 cardMax(cardX + cardWidth, vp_pos.y + vp_size.y * cardBottomViewportRatio);
// Card background with glass effect // Card background with glass effect
GlassPanelSpec cardGlass; GlassPanelSpec cardGlass;
@@ -931,13 +982,18 @@ inline bool BeginOverlayDialog(const char* title, bool* p_open, float cardWidth
cardGlass.borderWidth = 1.0f; cardGlass.borderWidth = 1.0f;
DrawGlassPanel(dl, cardMin, cardMax, cardGlass); DrawGlassPanel(dl, cardMin, cardMax, cardGlass);
if (p_open && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!ImGui::IsMouseHoveringRect(cardMin, cardMax, false)) {
*p_open = false;
}
// Set up child region for card content // Set up child region for card content
ImGui::SetCursorScreenPos(ImVec2(cardX, cardY)); ImGui::SetCursorScreenPos(ImVec2(cardX, cardY));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 16.0f); ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 16.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28, 24)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28, 24));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // Transparent - glass already drawn ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // Transparent - glass already drawn
bool childVisible = ImGui::BeginChild("##OverlayDialogContent", bool childVisible = ImGui::BeginChild(childId.c_str(),
ImVec2(cardWidth, 0), // 0 height = auto-size ImVec2(cardWidth, 0), // 0 height = auto-size
ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysUseWindowPadding, ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysUseWindowPadding,
ImGuiWindowFlags_NoScrollbar); ImGuiWindowFlags_NoScrollbar);
@@ -961,6 +1017,23 @@ inline void EndOverlayDialog()
ImGui::PopStyleColor(); // WindowBg scrim ImGui::PopStyleColor(); // WindowBg scrim
} }
inline void PlaceOverlayDialogActions(float totalWidth)
{
float rowStartX = ImGui::GetCursorPosX();
float contentW = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalWidth) * 0.5f));
}
inline void BeginOverlayDialogFooter(float totalActionWidth, bool drawSeparator = true)
{
ImGui::Spacing();
if (drawSeparator) {
ImGui::Separator();
ImGui::Spacing();
}
PlaceOverlayDialogActions(totalActionWidth);
}
} // namespace material } // namespace material
} // namespace ui } // namespace ui
} // namespace dragonx } // namespace dragonx

View File

@@ -0,0 +1,115 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "typography.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <string>
namespace dragonx {
namespace ui {
namespace material {
namespace project_icons {
struct ProjectIcon {
const char* name;
const char* glyph;
};
inline constexpr const char* kPickaxeName = "pickaxe";
inline constexpr const char* kPickaxeGlyph = "\xEE\x80\x81";
inline constexpr ProjectIcon kWalletIcons[] = {
{"savings", ICON_MD_SAVINGS}, {"account_balance", ICON_MD_ACCOUNT_BALANCE}, {"account_balance_wallet", ICON_MD_ACCOUNT_BALANCE_WALLET}, {"wallet", ICON_MD_WALLET},
{"payments", ICON_MD_PAYMENTS}, {"credit_card", ICON_MD_CREDIT_CARD}, {"local_atm", ICON_MD_LOCAL_ATM}, {"diamond", ICON_MD_DIAMOND},
{"attach_money", ICON_MD_ATTACH_MONEY}, {"currency_bitcoin", ICON_MD_CURRENCY_BITCOIN}, {"currency_exchange", ICON_MD_CURRENCY_EXCHANGE}, {"balance", ICON_MD_BALANCE},
{"calculate", ICON_MD_CALCULATE}, {"trending_up", ICON_MD_TRENDING_UP}, {"euro", ICON_MD_EURO}, {"leaderboard", ICON_MD_LEADERBOARD},
{"paid", ICON_MD_PAID}, {"sell", ICON_MD_SELL}, {"receipt", ICON_MD_RECEIPT}, {"percent", ICON_MD_PERCENT},
{"price_change", ICON_MD_PRICE_CHANGE}, {"price_check", ICON_MD_PRICE_CHECK}, {"toll", ICON_MD_TOLL}, {"money", ICON_MD_MONEY},
{"show_chart", ICON_MD_SHOW_CHART}, {"candlestick_chart", ICON_MD_CANDLESTICK_CHART}, {"bar_chart", ICON_MD_BAR_CHART}, {"pie_chart", ICON_MD_PIE_CHART},
{"area_chart", ICON_MD_AREA_CHART}, {"stacked_bar_chart", ICON_MD_STACKED_BAR_CHART}, {"waterfall_chart", ICON_MD_WATERFALL_CHART}, {"scatter_plot", ICON_MD_SCATTER_PLOT},
{"query_stats", ICON_MD_QUERY_STATS}, {"speed", ICON_MD_SPEED}, {"donut_large", ICON_MD_DONUT_LARGE},
{kPickaxeName, nullptr},
{"hardware", ICON_MD_HARDWARE}, {"construction", ICON_MD_CONSTRUCTION}, {"handyman", ICON_MD_HANDYMAN}, {"build", ICON_MD_BUILD},
{"carpenter", ICON_MD_CARPENTER}, {"plumbing", ICON_MD_PLUMBING}, {"home_repair_service", ICON_MD_HOME_REPAIR_SERVICE}, {"precision_manufacturing", ICON_MD_PRECISION_MANUFACTURING},
{"factory", ICON_MD_FACTORY}, {"warehouse", ICON_MD_WAREHOUSE}, {"inventory", ICON_MD_INVENTORY}, {"recycling", ICON_MD_RECYCLING},
{"oil_barrel", ICON_MD_OIL_BARREL}, {"offline_bolt", ICON_MD_OFFLINE_BOLT}, {"thunderstorm", ICON_MD_THUNDERSTORM}, {"terminal", ICON_MD_TERMINAL},
{"storage", ICON_MD_STORAGE}, {"memory", ICON_MD_MEMORY}, {"developer_board", ICON_MD_DEVELOPER_BOARD},
{"shield", ICON_MD_SHIELD}, {"security", ICON_MD_SECURITY}, {"lock", ICON_MD_LOCK}, {"swap_horiz", ICON_MD_SWAP_HORIZ},
{"verified", ICON_MD_VERIFIED}, {"verified_user", ICON_MD_VERIFIED_USER}, {"key", ICON_MD_KEY}, {"badge", ICON_MD_BADGE},
{"store", ICON_MD_STORE}, {"storefront", ICON_MD_STOREFRONT}, {"shopping_bag", ICON_MD_SHOPPING_BAG}, {"business", ICON_MD_BUSINESS},
{"work", ICON_MD_WORK}, {"real_estate_agent", ICON_MD_REAL_ESTATE_AGENT}, {"gavel", ICON_MD_GAVEL}, {"local_shipping", ICON_MD_LOCAL_SHIPPING},
{"home", ICON_MD_HOME}, {"apartment", ICON_MD_APARTMENT}, {"cottage", ICON_MD_COTTAGE}, {"landscape", ICON_MD_LANDSCAPE},
{"account_circle", ICON_MD_ACCOUNT_CIRCLE}, {"face", ICON_MD_FACE}, {"manage_accounts", ICON_MD_MANAGE_ACCOUNTS}, {"groups", ICON_MD_GROUPS}, {"mood", ICON_MD_MOOD},
{"rocket_launch", ICON_MD_ROCKET_LAUNCH}, {"flight", ICON_MD_FLIGHT}, {"directions_car", ICON_MD_DIRECTIONS_CAR}, {"travel_explore", ICON_MD_TRAVEL_EXPLORE},
{"explore", ICON_MD_EXPLORE}, {"location_on", ICON_MD_LOCATION_ON}, {"map", ICON_MD_MAP}, {"luggage", ICON_MD_LUGGAGE}, {"anchor", ICON_MD_ANCHOR},
{"public", ICON_MD_PUBLIC}, {"language", ICON_MD_LANGUAGE}, {"forest", ICON_MD_FOREST}, {"park", ICON_MD_PARK},
{"water_drop", ICON_MD_WATER_DROP}, {"beach_access", ICON_MD_BEACH_ACCESS}, {"energy_savings_leaf", ICON_MD_ENERGY_SAVINGS_LEAF}, {"solar_power", ICON_MD_SOLAR_POWER},
{"favorite", ICON_MD_FAVORITE}, {"star", ICON_MD_STAR}, {"celebration", ICON_MD_CELEBRATION}, {"casino", ICON_MD_CASINO},
{"auto_awesome", ICON_MD_AUTO_AWESOME}, {"emoji_events", ICON_MD_EMOJI_EVENTS}, {"military_tech", ICON_MD_MILITARY_TECH}, {"flag", ICON_MD_FLAG},
{"bolt", ICON_MD_BOLT}, {"tungsten", ICON_MD_TUNGSTEN}, {"lightbulb", ICON_MD_LIGHTBULB}, {"insights", ICON_MD_INSIGHTS},
{"hub", ICON_MD_HUB}, {"token", ICON_MD_TOKEN}, {"electric_bolt", ICON_MD_ELECTRIC_BOLT}, {"science", ICON_MD_SCIENCE}, {"biotech", ICON_MD_BIOTECH},
{"category", ICON_MD_CATEGORY}, {"label", ICON_MD_LABEL}, {"school", ICON_MD_SCHOOL}, {"local_hospital", ICON_MD_LOCAL_HOSPITAL}, {"local_florist", ICON_MD_LOCAL_FLORIST},
{"coffee", ICON_MD_LOCAL_CAFE}, {"restaurant", ICON_MD_RESTAURANT}, {"wine_bar", ICON_MD_WINE_BAR}, {"liquor", ICON_MD_LIQUOR},
{"outdoor_grill", ICON_MD_OUTDOOR_GRILL}, {"nightlife", ICON_MD_NIGHTLIFE}, {"sports_bar", ICON_MD_SPORTS_BAR},
{"pets", ICON_MD_PETS}, {"fitness_center", ICON_MD_FITNESS_CENTER}, {"spa", ICON_MD_SPA}, {"self_improvement", ICON_MD_SELF_IMPROVEMENT},
{"psychology", ICON_MD_PSYCHOLOGY}, {"sports_soccer", ICON_MD_SPORTS_SOCCER}, {"sports_esports", ICON_MD_SPORTS_ESPORTS},
{"hiking", ICON_MD_HIKING}, {"palette", ICON_MD_PALETTE}, {"museum", ICON_MD_MUSEUM}, {"church", ICON_MD_CHURCH}, {"surfing", ICON_MD_SURFING},
{"redeem", ICON_MD_REDEEM}, {"handshake", ICON_MD_HANDSHAKE}, {"healing", ICON_MD_HEALING}, {"volunteer", ICON_MD_VOLUNTEER_ACTIVISM},
{"stadium", ICON_MD_STADIUM}, {"temple_buddhist", ICON_MD_TEMPLE_BUDDHIST}, {"theater_comedy", ICON_MD_THEATER_COMEDY}, {"watch", ICON_MD_WATCH},
};
inline int walletIconCount()
{
return static_cast<int>(sizeof(kWalletIcons) / sizeof(kWalletIcons[0]));
}
inline const char* walletIconName(int index)
{
return (index >= 0 && index < walletIconCount()) ? kWalletIcons[index].name : "";
}
inline const char* glyphForName(const std::string& name)
{
for (const auto& icon : kWalletIcons) {
if (name == icon.name) return icon.glyph;
}
return nullptr;
}
inline bool drawByName(ImDrawList* drawList,
const std::string& name,
ImVec2 center,
ImU32 color,
ImFont* iconFont,
float iconFontSize)
{
if (!drawList) return false;
if (name == kPickaxeName) {
ImFont* pickaxeFont = Typography::instance().pickaxeFontForSize(iconFontSize);
if (!pickaxeFont) return false;
ImVec2 iconSize = pickaxeFont->CalcTextSizeA(iconFontSize, 1000.0f, 0.0f, kPickaxeGlyph);
drawList->AddText(pickaxeFont, iconFontSize,
ImVec2(center.x - iconSize.x * 0.5f, center.y - iconSize.y * 0.5f), color, kPickaxeGlyph);
return true;
}
const char* glyph = glyphForName(name);
if (!glyph || !iconFont) return false;
ImVec2 iconSize = iconFont->CalcTextSizeA(iconFontSize, 1000.0f, 0.0f, glyph);
drawList->AddText(iconFont, iconFontSize,
ImVec2(center.x - iconSize.x * 0.5f, center.y - iconSize.y * 0.5f), color, glyph);
return true;
}
} // namespace project_icons
} // namespace material
} // namespace ui
} // namespace dragonx

View File

@@ -101,6 +101,7 @@ bool Typography::reload(ImGuiIO& io, float dpiScale)
loaded_ = false; loaded_ = false;
for (int i = 0; i < kNumStyles; ++i) fonts_[i] = nullptr; for (int i = 0; i < kNumStyles; ++i) fonts_[i] = nullptr;
for (int i = 0; i < kNumIconSizes; ++i) iconFonts_[i] = nullptr; for (int i = 0; i < kNumIconSizes; ++i) iconFonts_[i] = nullptr;
for (int i = 0; i < kNumIconSizes; ++i) pickaxeFonts_[i] = nullptr;
return load(io, dpiScale); return load(io, dpiScale);
} }
@@ -190,6 +191,13 @@ bool Typography::load(ImGuiIO& io, float dpiScale)
iconFonts_[2] = loadIconFont(io, 24.0f * scale, "IconLarge"); iconFonts_[2] = loadIconFont(io, 24.0f * scale, "IconLarge");
iconFonts_[3] = loadIconFont(io, 40.0f * scale, "IconXL"); iconFonts_[3] = loadIconFont(io, 40.0f * scale, "IconXL");
// Load a one-glyph MDI subset for pickaxe. The glyph is remapped onto a
// BMP private-use codepoint so it remains renderable with 16-bit ImWchar.
pickaxeFonts_[0] = loadPickaxeFont(io, 14.0f * scale, "PickaxeSmall");
pickaxeFonts_[1] = loadPickaxeFont(io, 18.0f * scale, "PickaxeMed");
pickaxeFonts_[2] = loadPickaxeFont(io, 24.0f * scale, "PickaxeLarge");
pickaxeFonts_[3] = loadPickaxeFont(io, 40.0f * scale, "PickaxeXL");
// Verify all fonts loaded // Verify all fonts loaded
bool allLoaded = true; bool allLoaded = true;
for (int i = 0; i < kNumStyles; ++i) { for (int i = 0; i < kNumStyles; ++i) {
@@ -300,7 +308,14 @@ ImFont* Typography::loadFont(ImGuiIO& io, int weight, float size, const char* na
cjkCfg.GlyphRanges = cjkRanges; cjkCfg.GlyphRanges = cjkRanges;
snprintf(cjkCfg.Name, sizeof(cjkCfg.Name), "NotoSansCJK %.0fpx (merge)", size); snprintf(cjkCfg.Name, sizeof(cjkCfg.Name), "NotoSansCJK %.0fpx (merge)", size);
io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg); ImFont* mergeResult = io.Fonts->AddFontFromMemoryTTF(cjkCopy, g_noto_cjk_subset_size, size, &cjkCfg);
if (mergeResult) {
DEBUG_LOGF("Typography: Merged CJK (%u bytes) into %s OK\n",
g_noto_cjk_subset_size, name);
} else {
DEBUG_LOGF("Typography: WARNING — CJK merge FAILED for %s (size=%u)\n",
name, g_noto_cjk_subset_size);
}
} }
} else { } else {
DEBUG_LOGF("Typography: Failed to load %s\n", name); DEBUG_LOGF("Typography: Failed to load %s\n", name);
@@ -339,6 +354,51 @@ ImFont* Typography::loadIconFont(ImGuiIO& io, float size, const char* name)
return font; return font;
} }
ImFont* Typography::loadPickaxeFont(ImGuiIO& io, float size, const char* name)
{
if (g_mdi_pickaxe_subset_size == 0) {
DEBUG_LOGF("Typography: Pickaxe subset font is empty\n");
return nullptr;
}
void* fontDataCopy = IM_ALLOC(g_mdi_pickaxe_subset_size);
memcpy(fontDataCopy, g_mdi_pickaxe_subset_data, g_mdi_pickaxe_subset_size);
ImFontConfig cfg;
cfg.FontDataOwnedByAtlas = true;
cfg.OversampleH = 2;
cfg.OversampleV = 1;
cfg.PixelSnapH = true;
cfg.MergeMode = false;
cfg.GlyphMinAdvanceX = size;
static const ImWchar pickaxeRange[] = {
Typography::kPickaxeCodepoint,
Typography::kPickaxeCodepoint,
0,
};
cfg.GlyphRanges = pickaxeRange;
snprintf(cfg.Name, sizeof(cfg.Name), "MDIPickaxe %.0fpx", size);
ImFont* font = io.Fonts->AddFontFromMemoryTTF(fontDataCopy, g_mdi_pickaxe_subset_size, size, &cfg);
if (font) {
DEBUG_LOGF("Typography: Loaded pickaxe font %s (%.0fpx)\n", name, size);
} else {
DEBUG_LOGF("Typography: Failed to load pickaxe font %s\n", name);
IM_FREE(fontDataCopy);
}
return font;
}
ImFont* Typography::pickaxeFontForSize(float size) const
{
if (size <= 15.0f) return pickaxeSmall();
if (size <= 20.0f) return pickaxeMed();
if (size <= 32.0f) return pickaxeLarge();
return pickaxeXL();
}
ImFont* Typography::getFont(TypeStyle style) const ImFont* Typography::getFont(TypeStyle style) const
{ {
int index = static_cast<int>(style); int index = static_cast<int>(style);

View File

@@ -175,6 +175,14 @@ public:
ImFont* iconMed() const { return iconFonts_[1] ? iconFonts_[1] : getFont(TypeStyle::Body1); } ImFont* iconMed() const { return iconFonts_[1] ? iconFonts_[1] : getFont(TypeStyle::Body1); }
ImFont* iconLarge() const { return iconFonts_[2] ? iconFonts_[2] : getFont(TypeStyle::H5); } ImFont* iconLarge() const { return iconFonts_[2] ? iconFonts_[2] : getFont(TypeStyle::H5); }
ImFont* iconXL() const { return iconFonts_[3] ? iconFonts_[3] : getFont(TypeStyle::H3); } ImFont* iconXL() const { return iconFonts_[3] ? iconFonts_[3] : getFont(TypeStyle::H3); }
ImFont* pickaxeSmall() const { return pickaxeFonts_[0] ? pickaxeFonts_[0] : iconSmall(); }
ImFont* pickaxeMed() const { return pickaxeFonts_[1] ? pickaxeFonts_[1] : iconMed(); }
ImFont* pickaxeLarge() const { return pickaxeFonts_[2] ? pickaxeFonts_[2] : iconLarge(); }
ImFont* pickaxeXL() const { return pickaxeFonts_[3] ? pickaxeFonts_[3] : iconXL(); }
ImFont* pickaxeFontForSize(float size) const;
static constexpr ImWchar kPickaxeCodepoint = 0xE001;
/** /**
* @brief Resolve a font name string to ImFont* * @brief Resolve a font name string to ImFont*
@@ -254,10 +262,12 @@ private:
// Icon fonts at different sizes: [0]=small(14), [1]=med(18), [2]=large(24), [3]=xl(40) // Icon fonts at different sizes: [0]=small(14), [1]=med(18), [2]=large(24), [3]=xl(40)
ImFont* iconFonts_[4] = {}; ImFont* iconFonts_[4] = {};
ImFont* pickaxeFonts_[4] = {};
static constexpr int kNumIconSizes = 4; static constexpr int kNumIconSizes = 4;
// Load an icon-only font at a specific pixel size // Load an icon-only font at a specific pixel size
ImFont* loadIconFont(ImGuiIO& io, float size, const char* name); ImFont* loadIconFont(ImGuiIO& io, float size, const char* name);
ImFont* loadPickaxeFont(ImGuiIO& io, float size, const char* name);
// Type specifications // Type specifications
static const TypeSpec* getTypeSpecs(); static const TypeSpec* getTypeSpecs();

File diff suppressed because it is too large Load Diff

View File

@@ -84,7 +84,7 @@ bool UISchema::loadFromFile(const std::string& path) {
for (const auto& flatSection : {"business", "animations", "console", for (const auto& flatSection : {"business", "animations", "console",
"backdrop", "shutdown", "notifications", "status-bar", "backdrop", "shutdown", "notifications", "status-bar",
"qr-code", "content-area", "style", "responsive", "qr-code", "content-area", "style", "responsive",
"spacing", "spacing-tokens", "button", "input", "fonts", "spacing", "spacing-tokens", "button", "dialog", "input", "fonts",
"inline-dialogs", "sidebar", "panels", "typography", "inline-dialogs", "sidebar", "panels", "typography",
"effects", "security"}) { "effects", "security"}) {
if (auto* sec = root[flatSection].as_table()) { if (auto* sec = root[flatSection].as_table()) {
@@ -172,7 +172,7 @@ bool UISchema::loadFromString(const std::string& tomlStr, const std::string& lab
for (const auto& flatSection : {"business", "animations", "console", for (const auto& flatSection : {"business", "animations", "console",
"backdrop", "shutdown", "notifications", "status-bar", "backdrop", "shutdown", "notifications", "status-bar",
"qr-code", "content-area", "style", "responsive", "qr-code", "content-area", "style", "responsive",
"spacing", "spacing-tokens", "button", "input", "fonts", "spacing", "spacing-tokens", "button", "dialog", "input", "fonts",
"inline-dialogs", "sidebar", "panels", "typography", "inline-dialogs", "sidebar", "panels", "typography",
"effects", "security"}) { "effects", "security"}) {
if (auto* sec = root[flatSection].as_table()) { if (auto* sec = root[flatSection].as_table()) {
@@ -251,7 +251,7 @@ bool UISchema::mergeOverlayFromFile(const std::string& path) {
for (const auto& flatSection : {"business", "animations", "console", for (const auto& flatSection : {"business", "animations", "console",
"backdrop", "shutdown", "notifications", "status-bar", "backdrop", "shutdown", "notifications", "status-bar",
"qr-code", "content-area", "style", "responsive", "qr-code", "content-area", "style", "responsive",
"spacing", "spacing-tokens", "button", "input", "fonts", "spacing", "spacing-tokens", "button", "dialog", "input", "fonts",
"inline-dialogs", "sidebar", "panels", "typography", "inline-dialogs", "sidebar", "panels", "typography",
"effects", "security"}) { "effects", "security"}) {
if (auto* sec = root[flatSection].as_table()) { if (auto* sec = root[flatSection].as_table()) {

View File

@@ -380,7 +380,7 @@ inline void DrawGlassBevelButton(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
// collapsed: when true, sidebar is in icon-only mode (narrow width). // collapsed: when true, sidebar is in icon-only mode (narrow width).
// The caller can toggle collapsed via a reference if a toggle button is desired. // The caller can toggle collapsed via a reference if a toggle button is desired.
inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHeight, inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHeight,
SidebarStatus& status, bool& collapsed) SidebarStatus& status, bool& collapsed, bool locked = false)
{ {
using namespace material; using namespace material;
bool changed = false; bool changed = false;
@@ -415,46 +415,13 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
const float bottomPadding = sde("bottom-padding", 6.0f); const float bottomPadding = sde("bottom-padding", 6.0f);
const float exitIconGap = sde("exit-icon-gap", 4.0f); const float exitIconGap = sde("exit-icon-gap", 4.0f);
const float sbSectionLabelPadLeft = sde("section-label-pad-left", 16.0f); const float sbSectionLabelPadLeft = sde("section-label-pad-left", 16.0f);
const float sbItemPadX = sde("item-pad-x", 8.0f);
// Base values for responsive scaling // Base values for responsive scaling
const float baseItemHeight = sde("item-height", 46.0f); const float baseItemHeight = sde("item-height", 46.0f);
const float baseNavGap = sde("nav-gap", 20.0f); const float baseNavGap = sde("nav-gap", 20.0f);
const float baseSectionGap = sde("section-gap", 12.0f); const float baseSectionGap = sde("section-gap", 8.0f);
const float baseButtonSpacing = sde("button-spacing", 3.0f); const float baseButtonSpacing = sde("button-spacing", 6.0f);
const float sectionLabelPadBot = sde("section-label-pad-bottom", 4.0f);
// Estimate total sidebar content height at base sizes to detect overflow.
// Fixed chrome: top margin + collapse strip + exit strip + bottom padding
const float fixedChrome = glassMarginY + stripH + stripH + bottomPadding;
// Section labels: 2 sections × (gap + label height + pad)
const float sectionLabelH = 2.0f * (baseSectionGap + 13.0f * dp);
// Nav items: 9 items × (height + spacing)
const float navItemsH = (float)(int)NavPage::Count_ * (baseItemHeight + baseButtonSpacing);
const float baseContentH = fixedChrome + baseNavGap + navItemsH + sectionLabelH;
// Responsive shrink: if content would overflow, scale down flexible sizes.
// Clamp scale so buttons never shrink below what fits in the minimum
// sidebar height defined in ui.toml ("min-height").
float sidebarMinHeight = sde("min-height", 360.0f);
float scaleFloor = 0.55f;
if (sidebarMinHeight > fixedChrome) {
float flexH = baseContentH - fixedChrome;
if (flexH > 0.0f) {
float minFlex = sidebarMinHeight - fixedChrome;
scaleFloor = std::max(0.55f, minFlex / flexH);
}
}
float sidebarScale = 1.0f;
if (baseContentH > contentHeight && contentHeight > fixedChrome) {
float flexH = baseContentH - fixedChrome;
float availFlex = contentHeight - fixedChrome;
sidebarScale = std::max(scaleFloor, availFlex / flexH);
}
const float sbItemHeight = baseItemHeight * sidebarScale;
const float navGap = baseNavGap * sidebarScale;
const float sbSectionGap = baseSectionGap * sidebarScale;
const float buttonSpacing = baseButtonSpacing * sidebarScale;
// How "expanded" are we? 0.0 = fully collapsed, 1.0 = fully expanded // How "expanded" are we? 0.0 = fully collapsed, 1.0 = fully expanded
float expandFrac = (sbWidth > sbCollapsedWidth) float expandFrac = (sbWidth > sbCollapsedWidth)
@@ -462,7 +429,10 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
: 1.0f; : 1.0f;
if (expandFrac < 0.0f) expandFrac = 0.0f; if (expandFrac < 0.0f) expandFrac = 0.0f;
if (expandFrac > 1.0f) expandFrac = 1.0f; if (expandFrac > 1.0f) expandFrac = 1.0f;
bool showLabels = expandFrac > 0.3f; // hide labels during early part of animation bool showLabels = expandFrac > 0.3f;
// Font size for section labels (fixed, doesn't scale)
float olFsz = ScaledFontSize(Type().overline());
// Glass panel rounding from responsive schema // Glass panel rounding from responsive schema
float glassRounding = [&]() { float glassRounding = [&]() {
@@ -470,128 +440,165 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
return (v >= 0 ? v : 8.0f) * dp; return (v >= 0 ? v : 8.0f) * dp;
}(); }();
// ===================================================================
// PASS 1: Compute layout — all Y positions relative to panel top
// ===================================================================
// Separate fixed parts (don't scale) from flex parts (scale when tight).
float fixedH = stripH; // collapse strip
for (int i = 0; i < (int)NavPage::Count_; ++i)
if (kNavItems[i].section_label && showLabels)
fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below
fixedH += bottomPadding + stripH; // exit area
float baseFlexH = baseNavGap;
for (int i = 0; i < (int)NavPage::Count_; ++i) {
if (kNavItems[i].section_label) {
if (showLabels) baseFlexH += baseSectionGap;
else baseFlexH += baseSectionGap * 0.8f;
}
baseFlexH += baseItemHeight + baseButtonSpacing;
}
// Responsive shrink (only scales flex parts)
float sidebarMinHeight = sde("min-height", 360.0f);
float scaleFloor = (sidebarMinHeight > fixedH && baseFlexH > 0.0f)
? std::max(0.55f, (sidebarMinHeight - fixedH) / baseFlexH) : 0.55f;
float sidebarScale = 1.0f;
if (fixedH + baseFlexH > contentHeight && baseFlexH > 0.0f)
sidebarScale = std::max(scaleFloor, (contentHeight - fixedH) / baseFlexH);
const float itemH = baseItemHeight * sidebarScale;
const float navGap = baseNavGap * sidebarScale;
const float sectionGap = baseSectionGap * sidebarScale;
const float btnSpacing = baseButtonSpacing * sidebarScale;
// Compute Y position for every element (relative to panel top)
float itemY[(int)NavPage::Count_];
float sectionLabelY[4] = {}; int nSectionLabels = 0;
float separatorY[4] = {}; int nSeparators = 0;
float curY = stripH + navGap; // after collapse strip + gap
for (int i = 0; i < (int)NavPage::Count_; ++i) {
if (kNavItems[i].section_label) {
if (showLabels) {
curY += sectionGap;
if (nSectionLabels < 4) sectionLabelY[nSectionLabels++] = curY;
curY += olFsz + 2.0f + sectionLabelPadBot;
} else {
curY += sectionGap * 0.4f;
if (nSeparators < 4) separatorY[nSeparators++] = curY;
curY += sectionGap * 0.4f;
}
}
itemY[i] = curY;
curY += itemH + btnSpacing;
}
float exitRelY = curY + bottomPadding;
float panelH = exitRelY + stripH;
// Vertical centering — offset so panel is centered in the child window
float centerOffset = std::max(glassMarginY, (contentHeight - panelH) * 0.5f);
if (centerOffset + panelH > contentHeight)
centerOffset = std::max(0.0f, contentHeight - panelH);
// ===================================================================
// PASS 2: Render using computed positions
// ===================================================================
ImGui::BeginChild("##Sidebar", ImVec2(sidebarWidth, contentHeight), false, ImGui::BeginChild("##Sidebar", ImVec2(sidebarWidth, contentHeight), false,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoBackground); ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoBackground);
ImDrawList* dl = ImGui::GetWindowDrawList(); ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 wp = ImGui::GetWindowPos(); ImVec2 wp = ImGui::GetWindowPos();
// Glass card background — inset with rounded corners, matching content cards float panelLeft = wp.x + glassMarginL;
float sidebarMarginY = glassMarginY; // top & bottom inset float panelRight = wp.x + sidebarWidth - glassMarginR;
float sidebarMarginL = glassMarginL; // left inset float panelTopY = wp.y + centerOffset;
float sidebarMarginR = glassMarginR; // right inset (tighter on content side) float panelBotY = std::min(panelTopY + panelH, wp.y + contentHeight - glassMarginY);
// Panel bounds
float panelLeft = wp.x + sidebarMarginL;
float panelRight = wp.x + sidebarWidth - sidebarMarginR;
// Defer glass panel drawing until we know content height (channel 0 = background) // Draw list splitter: channel 0 = glass background, channel 1 = content
ImDrawListSplitter splitter; ImDrawListSplitter splitter;
splitter.Split(dl, 2); splitter.Split(dl, 2);
splitter.SetCurrentChannel(dl, 1); splitter.SetCurrentChannel(dl, 1);
// Top padding (just the margin — collapse button sits at panel top) // ---- Collapse toggle strip (panel top) ----
ImGui::Dummy(ImVec2(0, sidebarMarginY));
// ---- Collapse toggle — flush strip at top of sidebar panel ----
{ {
ImVec2 savedCursor = ImGui::GetCursorScreenPos();
float rnd = glassRounding;
// Strip spans full panel width, sits inside top of panel
float stripX = panelLeft; float stripX = panelLeft;
float stripW = panelRight - panelLeft; float stripW = panelRight - panelLeft;
float stripY = wp.y + sidebarMarginY; ImVec2 stripMin(stripX, panelTopY);
ImVec2 stripMin(stripX, stripY); ImVec2 stripMax(stripX + stripW, panelTopY + stripH);
ImVec2 stripMax(stripX + stripW, stripY + stripH);
ImGui::SetCursorScreenPos(stripMin); ImGui::SetCursorScreenPos(stripMin);
if (ImGui::InvisibleButton("##SidebarCollapse", ImVec2(stripW, stripH))) { if (ImGui::InvisibleButton("##SidebarCollapse", ImVec2(stripW, stripH)))
collapsed = !collapsed; collapsed = !collapsed;
}
bool btnHover = ImGui::IsItemHovered(); bool btnHover = ImGui::IsItemHovered();
// Draw strip background — flush with panel top (no extra rounding, blends in)
ImU32 stripBg = btnHover ? schema::UI().resolveColor("var(--sidebar-hover)", IM_COL32(255, 255, 255, 25)) : IM_COL32(0, 0, 0, 0);
if (btnHover) { if (btnHover) {
dl->AddRectFilled(stripMin, stripMax, stripBg, dl->AddRectFilled(stripMin, stripMax,
rnd, ImDrawFlags_RoundCornersTop); schema::UI().resolveColor("var(--sidebar-hover)", IM_COL32(255, 255, 255, 25)),
glassRounding, ImDrawFlags_RoundCornersTop);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
} }
// Subtle bottom separator dl->AddLine(ImVec2(stripMin.x + glassRounding * 0.5f, stripMax.y),
dl->AddLine(ImVec2(stripMin.x + rnd * 0.5f, stripMax.y), ImVec2(stripMax.x - glassRounding * 0.5f, stripMax.y),
ImVec2(stripMax.x - rnd * 0.5f, stripMax.y),
schema::UI().resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15)), 1.0f); schema::UI().resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15)), 1.0f);
if (btnHover) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
// Chevron icon centered in strip
ImU32 iconCol = btnHover ? OnSurface() : schema::UI().resolveColor("var(--sidebar-icon)", IM_COL32(255, 255, 255, 60)); ImU32 iconCol = btnHover ? OnSurface() : schema::UI().resolveColor("var(--sidebar-icon)", IM_COL32(255, 255, 255, 60));
float cx = stripX + stripW * 0.5f; float cx = stripX + stripW * 0.5f;
float cy = stripY + stripH * 0.5f; float cy = panelTopY + stripH * 0.5f;
{ ImFont* iconFont = Type().iconSmall();
ImFont* iconFont = Type().iconSmall(); const char* chevIcon = collapsed ? ICON_MD_CHEVRON_RIGHT : ICON_MD_CHEVRON_LEFT;
const char* chevIcon = collapsed ? ICON_MD_CHEVRON_RIGHT : ICON_MD_CHEVRON_LEFT; float chevFsz = ScaledFontSize(iconFont);
float chevFsz = ScaledFontSize(iconFont); ImVec2 chevSz = iconFont->CalcTextSizeA(chevFsz, 1000.0f, 0.0f, chevIcon);
ImVec2 chevSz = iconFont->CalcTextSizeA(chevFsz, 1000.0f, 0.0f, chevIcon); dl->AddText(iconFont, chevFsz,
dl->AddText(iconFont, chevFsz, ImVec2(cx - chevSz.x * 0.5f, cy - chevSz.y * 0.5f), iconCol, chevIcon);
ImVec2(cx - chevSz.x * 0.5f, cy - chevSz.y * 0.5f), iconCol, chevIcon);
}
ImGui::SetCursorScreenPos(savedCursor); // restore — toggle is an overlay
} }
// Gap between collapse divider and first nav item // ---- Section labels / separators ----
ImGui::Dummy(ImVec2(0, navGap)); if (showLabels) {
ImFont* olFont = Type().overline();
float olFontSz = ScaledFontSize(olFont);
float maxSectionW = panelRight - wp.x - sbSectionLabelPadLeft;
int si = 0;
for (int i = 0; i < (int)NavPage::Count_; ++i) {
if (kNavItems[i].section_label && si < nSectionLabels) {
float ly = panelTopY + sectionLabelY[si++];
const char* sLabel = NavSectionLabel(kNavItems[i]);
float drawFsz = olFontSz;
ImVec2 sLabelSz = olFont->CalcTextSizeA(drawFsz, 1000.0f, 0.0f, sLabel);
if (sLabelSz.x > maxSectionW && maxSectionW > 0)
drawFsz *= maxSectionW / sLabelSz.x;
ImVec4 olCol = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium());
olCol.w *= expandFrac;
dl->AddText(olFont, drawFsz,
ImVec2(wp.x + sbSectionLabelPadLeft, ly),
ImGui::ColorConvertFloat4ToU32(olCol), sLabel);
}
}
} else {
for (int si = 0; si < nSeparators; ++si) {
float sy = panelTopY + separatorY[si];
dl->AddLine(ImVec2(wp.x + btnPadCollapsed, sy),
ImVec2(wp.x + sidebarWidth - btnPadCollapsed, sy),
Divider(), 1.0f);
}
}
// ---- Navigation items ---- // ---- Navigation items ----
float btnPadX = collapsed ? btnPadCollapsed : btnPadExpanded;
for (int i = 0; i < (int)NavPage::Count_; ++i) { for (int i = 0; i < (int)NavPage::Count_; ++i) {
const NavItem& item = kNavItems[i]; const NavItem& item = kNavItems[i];
// Section label (only when expanded)
if (item.section_label && showLabels) {
ImGui::Dummy(ImVec2(0, sbSectionGap));
ImFont* olFont = Type().overline();
float labelY = ImGui::GetCursorScreenPos().y;
ImVec4 olCol = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium());
olCol.w *= expandFrac;
float olFsz = ScaledFontSize(olFont);
dl->AddText(olFont, olFsz,
ImVec2(wp.x + sbSectionLabelPadLeft, labelY),
ImGui::ColorConvertFloat4ToU32(olCol), NavSectionLabel(item));
ImGui::Dummy(ImVec2(0, olFsz + 2.0f));
} else if (item.section_label && !showLabels) {
// Collapsed: thin separator instead of label
ImGui::Dummy(ImVec2(0, sbSectionGap * 0.4f));
float sepY2 = ImGui::GetCursorScreenPos().y;
dl->AddLine(ImVec2(wp.x + btnPadCollapsed, sepY2), ImVec2(wp.x + sidebarWidth - btnPadCollapsed, sepY2),
Divider(), 1.0f);
ImGui::Dummy(ImVec2(0, sbSectionGap * 0.4f));
}
bool selected = (current == item.page); bool selected = (current == item.page);
float btnY = panelTopY + itemY[i];
float btnRnd = itemH * 0.22f;
ImGui::PushID(i); ImVec2 indMin(panelLeft + btnPadX, btnY);
ImVec2 indMax(panelRight - btnPadX, btnY + itemH);
float itemH = sbItemHeight;
float btnRnd = itemH * 0.22f; // moderate rounding
float btnPadX = collapsed ? btnPadCollapsed : btnPadExpanded; // tighter padding when collapsed
ImVec2 cursor = ImGui::GetCursorScreenPos();
// Keep button height constant (itemH) so sidebar content height doesn't
// change during collapse animation, which would destabilize centering.
float btnH = itemH;
// Item bounds for icon/label placement (inset)
ImVec2 itemMin(wp.x + sbItemPadX, cursor.y);
ImVec2 itemMax(wp.x + sidebarWidth - sbItemPadX, cursor.y + btnH);
// Button bounds — inset from panel edges for spacing
ImVec2 indMin(panelLeft + btnPadX, cursor.y);
ImVec2 indMax(panelRight - btnPadX, cursor.y + btnH);
// All buttons are embossed; hover presses halfway, selected presses fully
bool hovered = material::IsRectHovered(indMin, indMax); bool hovered = material::IsRectHovered(indMin, indMax);
{
float btnDepth = 0.0f;
if (selected) btnDepth = 1.0f;
else if (hovered) btnDepth = 0.5f;
// Theme effects behind active button // Glass bevel + theme effects
{
float btnDepth = selected ? 1.0f : (hovered ? 0.5f : 0.0f);
if (selected) { if (selected) {
auto& fx = effects::ThemeEffects::instance(); auto& fx = effects::ThemeEffects::instance();
fx.drawGlowPulse(dl, indMin, indMax, btnRnd); fx.drawGlowPulse(dl, indMin, indMax, btnRnd);
@@ -600,92 +607,77 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
fx.drawShimmer(dl, indMin, indMax, btnRnd); fx.drawShimmer(dl, indMin, indMax, btnRnd);
fx.drawGradientBorderShift(dl, indMin, indMax, btnRnd); fx.drawGradientBorderShift(dl, indMin, indMax, btnRnd);
} }
DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f); DrawGlassCutout(dl, indMin, indMax, btnRnd, 1.5f);
DrawGlassBevelButton(dl, indMin, indMax, btnRnd, btnDepth, 18); DrawGlassBevelButton(dl, indMin, indMax, btnRnd, btnDepth, 18);
buttonRects.push_back({indMin, indMax, btnRnd}); buttonRects.push_back({indMin, indMax, btnRnd});
} }
if (hovered) { if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
// Click detection // Click detection
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { bool pageNeedsUnlock = locked &&
item.page != NavPage::Console &&
item.page != NavPage::Peers &&
item.page != NavPage::Settings;
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !pageNeedsUnlock) {
current = item.page; current = item.page;
changed = true; changed = true;
} }
// Icon + label — centered horizontally within button bounds // Icon + label
float iconS = iconHalfSize; // icon half-size in pixels float iconS = iconHalfSize;
float iconCY = cursor.y + btnH * 0.5f; float iconCY = btnY + itemH * 0.5f;
float textY = cursor.y + (btnH - ImGui::GetTextLineHeight()) * 0.5f; float textY = btnY + (itemH - ImGui::GetTextLineHeight()) * 0.5f;
ImU32 textCol = selected ? Primary() : OnSurfaceMedium(); ImU32 textCol = selected ? Primary() : (pageNeedsUnlock ? OnSurfaceDisabled() : OnSurfaceMedium());
if (showLabels) { if (showLabels) {
// Measure total width of icon + gap + label, then center.
// If the translated label is too wide, shrink the font to fit.
ImFont* font = selected ? Type().subtitle2() : Type().body2(); ImFont* font = selected ? Type().subtitle2() : Type().body2();
float gap = iconLabelGap;
float lblFsz = ScaledFontSize(font); float lblFsz = ScaledFontSize(font);
float btnW = indMax.x - indMin.x; float btnW = indMax.x - indMin.x;
float maxLabelW = btnW - iconS * 2.0f - gap - Layout::spacingXs() * 2; float maxLabelW = btnW - iconS * 2.0f - iconLabelGap - Layout::spacingXs() * 2;
ImVec2 labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item)); ImVec2 labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
if (labelSz.x > maxLabelW && maxLabelW > 0) { if (labelSz.x > maxLabelW && maxLabelW > 0) {
float shrink = maxLabelW / labelSz.x; lblFsz *= maxLabelW / labelSz.x;
lblFsz *= shrink;
labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item)); labelSz = font->CalcTextSizeA(lblFsz, 1000.0f, 0.0f, NavLabel(item));
} }
float totalW = iconS * 2.0f + gap + labelSz.x; float totalW = iconS * 2.0f + iconLabelGap + labelSz.x;
float btnCX = (indMin.x + indMax.x) * 0.5f; float btnCX = (indMin.x + indMax.x) * 0.5f;
float startX = btnCX - totalW * 0.5f; float startX = btnCX - totalW * 0.5f;
float iconCX = startX + iconS; DrawNavIcon(dl, item.page, startX + iconS, iconCY, iconS, textCol);
DrawNavIcon(dl, item.page, iconCX, iconCY, iconS, textCol);
float labelX = startX + iconS * 2.0f + gap;
ImVec4 lc = ImGui::ColorConvertU32ToFloat4(textCol); ImVec4 lc = ImGui::ColorConvertU32ToFloat4(textCol);
lc.w *= expandFrac; lc.w *= expandFrac;
dl->AddText(font, lblFsz, ImVec2(labelX, textY), dl->AddText(font, lblFsz,
ImGui::ColorConvertFloat4ToU32(lc), NavLabel(item)); ImVec2(startX + iconS * 2.0f + iconLabelGap, textY),
ImGui::ColorConvertFloat4ToU32(lc), NavLabel(item));
} else { } else {
float iconCX = (indMin.x + indMax.x) * 0.5f; float iconCX = (indMin.x + indMax.x) * 0.5f;
DrawNavIcon(dl, item.page, iconCX, iconCY, iconS, textCol); DrawNavIcon(dl, item.page, iconCX, iconCY, iconS, textCol);
if (hovered) ImGui::SetTooltip("%s", NavLabel(item));
} }
// Tooltip when collapsed + hovered // Badge indicator
if (!showLabels && hovered) {
ImGui::SetTooltip("%s", NavLabel(item));
}
// ---- Badge indicator ----
{ {
int badgeCount = 0; int badgeCount = 0;
bool dotOnly = false; bool dotOnly = false;
ImU32 badgeCol = Primary(); ImU32 badgeCol = Primary();
ImU32 badgeTextCol = OnPrimary(); ImU32 badgeTextCol = OnPrimary();
if (item.page == NavPage::History && status.unconfirmedTxCount > 0) { if (item.page == NavPage::History && status.unconfirmedTxCount > 0) {
badgeCount = status.unconfirmedTxCount; badgeCount = status.unconfirmedTxCount;
badgeCol = Warning(); badgeCol = Warning(); badgeTextCol = OnWarning();
badgeTextCol = OnWarning();
} else if (item.page == NavPage::Mining && status.miningActive) { } else if (item.page == NavPage::Mining && status.miningActive) {
dotOnly = true; dotOnly = true; badgeCol = Success();
badgeCol = Success();
} else if (item.page == NavPage::Peers && status.peerCount > 0) { } else if (item.page == NavPage::Peers && status.peerCount > 0) {
badgeCount = status.peerCount; badgeCount = status.peerCount;
} }
if (badgeCount > 0 || dotOnly) { if (badgeCount > 0 || dotOnly) {
float badgeR = dotOnly ? badgeRadiusDot : badgeRadiusNumber; float badgeR = dotOnly ? badgeRadiusDot : badgeRadiusNumber;
float badgeX, badgeY; float bx = indMax.x - badgeR - 6.0f;
float by = indMin.y + badgeR + 5.0f;
// Upper-right corner of button, offset from edges dl->AddCircleFilled(ImVec2(bx, by), badgeR, badgeCol);
badgeX = indMax.x - badgeR - 6.0f;
badgeY = indMin.y + badgeR + 5.0f;
dl->AddCircleFilled(ImVec2(badgeX, badgeY), badgeR, badgeCol);
if (!dotOnly && showLabels) { if (!dotOnly && showLabels) {
char buf[16]; char buf[16];
snprintf(buf, sizeof(buf), "%d", badgeCount > 99 ? 99 : badgeCount); snprintf(buf, sizeof(buf), "%d", badgeCount > 99 ? 99 : badgeCount);
@@ -693,120 +685,81 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
float capFsz = ScaledFontSize(capFont); float capFsz = ScaledFontSize(capFont);
ImVec2 ts = capFont->CalcTextSizeA(capFsz, 1000.0f, 0.0f, buf); ImVec2 ts = capFont->CalcTextSizeA(capFsz, 1000.0f, 0.0f, buf);
dl->AddText(capFont, capFsz, dl->AddText(capFont, capFsz,
ImVec2(badgeX - ts.x * 0.5f, badgeY - ts.y * 0.5f), ImVec2(bx - ts.x * 0.5f, by - ts.y * 0.5f), badgeTextCol, buf);
badgeTextCol, buf);
} }
} }
} }
ImGui::Dummy(ImVec2(sidebarWidth, btnH + buttonSpacing)); // extra vertical spacing between buttons
ImGui::PopID();
} }
// (Pill indicator removed — glass bevel on active button is sufficient)
// Reserve space for exit strip at panel bottom (drawn as overlay below)
ImGui::Dummy(ImVec2(0, stripH));
// ---- Keyboard navigation ---- // ---- Keyboard navigation ----
if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) { if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
int idx = static_cast<int>(current); int idx = static_cast<int>(current);
bool nav = false; bool nav = false;
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K))
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) {
if (idx > 0) { idx--; nav = true; } if (idx > 0) { idx--; nav = true; }
} if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J))
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J)) {
if (idx < (int)NavPage::Count_ - 1) { idx++; nav = true; } if (idx < (int)NavPage::Count_ - 1) { idx++; nav = true; }
} if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket))
// Toggle collapse with [ key
if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) {
collapsed = !collapsed; collapsed = !collapsed;
} if (nav) { current = static_cast<NavPage>(idx); changed = true; }
if (nav) {
current = static_cast<NavPage>(idx);
changed = true;
}
} }
// Bottom padding // ---- Glass panel background (channel 0) ----
ImGui::Dummy(ImVec2(0, bottomPadding));
// Measure total sidebar content height
float totalContentEndY = ImGui::GetCursorScreenPos().y;
// Draw glass panel background — capped to sidebar bounds so corners are visible
float panelBottomY = totalContentEndY;
float panelTopY = wp.y + sidebarMarginY;
// Clamp bottom so rounded corners are never clipped by the child window
float maxPanelBottom = wp.y + contentHeight - sidebarMarginY;
if (panelBottomY > maxPanelBottom) panelBottomY = maxPanelBottom;
splitter.SetCurrentChannel(dl, 0); splitter.SetCurrentChannel(dl, 0);
{ {
float rnd = glassRounding;
ImVec2 panelMin(panelLeft, panelTopY); ImVec2 panelMin(panelLeft, panelTopY);
ImVec2 panelMax(panelRight, panelBottomY); ImVec2 panelMax(panelRight, panelBotY);
GlassPanelSpec sidebarGlass; GlassPanelSpec glass;
sidebarGlass.rounding = rnd; glass.rounding = glassRounding;
sidebarGlass.fillAlpha = 14; glass.fillAlpha = 14;
sidebarGlass.borderAlpha = 25; glass.borderAlpha = 25;
sidebarGlass.borderWidth = 1.0f; glass.borderWidth = 1.0f;
// Record vertex range before drawing the glass panel fill int vtx0 = dl->VtxBuffer.Size;
int glassVtx0 = dl->VtxBuffer.Size; DrawGlassPanel(dl, panelMin, panelMax, glass);
DrawGlassPanel(dl, panelMin, panelMax, sidebarGlass); int vtx1 = dl->VtxBuffer.Size;
int glassVtx1 = dl->VtxBuffer.Size;
// Punch holes: zero out glass panel vertices that fall inside // Punch holes in glass where buttons are to avoid double opacity
// any button rect so the button area doesn't double-up opacity. for (int vi = vtx0; vi < vtx1; vi++) {
for (int vi = glassVtx0; vi < glassVtx1; vi++) {
ImDrawVert& v = dl->VtxBuffer[vi]; ImDrawVert& v = dl->VtxBuffer[vi];
for (int bi = 0; bi < buttonRects.Size; bi++) { for (int bi = 0; bi < buttonRects.Size; bi++) {
const Rect& r = buttonRects[bi]; const Rect& r = buttonRects[bi];
// Inset test slightly (0.5px) to avoid killing edge pixels
if (v.pos.x > r.mn.x + 0.5f && v.pos.x < r.mx.x - 0.5f && if (v.pos.x > r.mn.x + 0.5f && v.pos.x < r.mx.x - 0.5f &&
v.pos.y > r.mn.y + 0.5f && v.pos.y < r.mx.y - 0.5f) { v.pos.y > r.mn.y + 0.5f && v.pos.y < r.mx.y - 0.5f) {
v.col = (v.col & 0x00FFFFFFu); // zero alpha v.col = (v.col & 0x00FFFFFFu);
break; break;
} }
} }
} }
} }
// ---- Exit button — flush strip at bottom of sidebar panel ---- // ---- Exit button strip (panel bottom) ----
splitter.SetCurrentChannel(dl, 1); splitter.SetCurrentChannel(dl, 1);
{ {
float rnd = glassRounding; float exitY = panelTopY + exitRelY;
float exitStripH = stripH; if (exitY + stripH > panelBotY) exitY = panelBotY - stripH;
float exitStripX = panelLeft; float exitX = panelLeft;
float exitStripW = panelRight - panelLeft; float exitW = panelRight - panelLeft;
float exitStripY = panelBottomY - exitStripH; ImVec2 exitMin(exitX, exitY);
ImVec2 exitMin(exitStripX, exitStripY); ImVec2 exitMax(exitX + exitW, exitY + stripH);
ImVec2 exitMax(exitStripX + exitStripW, exitStripY + exitStripH);
ImGui::SetCursorScreenPos(exitMin); ImGui::SetCursorScreenPos(exitMin);
if (ImGui::InvisibleButton("##ExitBtn", ImVec2(exitStripW, exitStripH))) { if (ImGui::InvisibleButton("##ExitBtn", ImVec2(exitW, stripH)))
status.exitClicked = true; status.exitClicked = true;
}
bool exitHover = ImGui::IsItemHovered(); bool exitHover = ImGui::IsItemHovered();
// Hover highlight with rounded bottom corners (matching panel)
if (exitHover) { if (exitHover) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
dl->AddRectFilled(exitMin, exitMax, schema::UI().resolveColor("var(--sidebar-hover)", IM_COL32(255, 255, 255, 25)), dl->AddRectFilled(exitMin, exitMax,
rnd, ImDrawFlags_RoundCornersBottom); schema::UI().resolveColor("var(--sidebar-hover)", IM_COL32(255, 255, 255, 25)),
glassRounding, ImDrawFlags_RoundCornersBottom);
} }
dl->AddLine(ImVec2(exitMin.x + glassRounding * 0.5f, exitMin.y),
// Subtle top separator ImVec2(exitMax.x - glassRounding * 0.5f, exitMin.y),
dl->AddLine(ImVec2(exitMin.x + rnd * 0.5f, exitMin.y),
ImVec2(exitMax.x - rnd * 0.5f, exitMin.y),
schema::UI().resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15)), 1.0f); schema::UI().resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15)), 1.0f);
// Exit icon (+ label when expanded) centered in strip
ImU32 exitCol = exitHover ? Error() : schema::UI().resolveColor("var(--sidebar-icon)", IM_COL32(255, 255, 255, 60)); ImU32 exitCol = exitHover ? Error() : schema::UI().resolveColor("var(--sidebar-icon)", IM_COL32(255, 255, 255, 60));
float cx = exitStripX + exitStripW * 0.5f; float cx = exitX + exitW * 0.5f;
float cy = exitStripY + exitStripH * 0.5f; float cy = exitY + stripH * 0.5f;
if (showLabels) { if (showLabels) {
ImFont* iconFont = Type().iconSmall(); ImFont* iconFont = Type().iconSmall();
@@ -815,31 +768,25 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
float eIconFsz = ScaledFontSize(iconFont); float eIconFsz = ScaledFontSize(iconFont);
float eLblFsz = ScaledFontSize(font); float eLblFsz = ScaledFontSize(font);
ImVec2 iconSz = iconFont->CalcTextSizeA(eIconFsz, 1000.0f, 0.0f, exitIcon); ImVec2 iconSz = iconFont->CalcTextSizeA(eIconFsz, 1000.0f, 0.0f, exitIcon);
ImVec2 labelSz = font->CalcTextSizeA(eLblFsz, 1000.0f, 0.0f, "Exit"); const char* exitLabel = TR("exit");
float gap = exitIconGap; ImVec2 labelSz = font->CalcTextSizeA(eLblFsz, 1000.0f, 0.0f, exitLabel);
float totalW = iconSz.x + gap + labelSz.x; float totalW = iconSz.x + exitIconGap + labelSz.x;
float startX = cx - totalW * 0.5f; float startX = cx - totalW * 0.5f;
dl->AddText(iconFont, eIconFsz, dl->AddText(iconFont, eIconFsz,
ImVec2(startX, cy - iconSz.y * 0.5f), exitCol, exitIcon); ImVec2(startX, cy - iconSz.y * 0.5f), exitCol, exitIcon);
ImVec4 lc = ImGui::ColorConvertU32ToFloat4(exitCol); ImVec4 lc = ImGui::ColorConvertU32ToFloat4(exitCol);
lc.w *= expandFrac; lc.w *= expandFrac;
dl->AddText(font, eLblFsz, dl->AddText(font, eLblFsz,
ImVec2(startX + iconSz.x + gap, cy - labelSz.y * 0.5f), ImVec2(startX + iconSz.x + exitIconGap, cy - labelSz.y * 0.5f),
ImGui::ColorConvertFloat4ToU32(lc), "Exit"); ImGui::ColorConvertFloat4ToU32(lc), exitLabel);
} else { } else {
ImFont* iconFont = Type().iconSmall(); ImFont* iconFont = Type().iconSmall();
const char* exitIcon = ICON_MD_EXIT_TO_APP; const char* exitIcon = ICON_MD_EXIT_TO_APP;
float eIconFsz = ScaledFontSize(iconFont); float eIconFsz = ScaledFontSize(iconFont);
ImVec2 iconSz = iconFont->CalcTextSizeA(eIconFsz, 1000.0f, 0.0f, exitIcon); ImVec2 iconSz = iconFont->CalcTextSizeA(eIconFsz, 1000.0f, 0.0f, exitIcon);
dl->AddText(iconFont, eIconFsz, dl->AddText(iconFont, eIconFsz,
ImVec2(cx - iconSz.x * 0.5f, cy - iconSz.y * 0.5f), ImVec2(cx - iconSz.x * 0.5f, cy - iconSz.y * 0.5f), exitCol, exitIcon);
exitCol, exitIcon); if (exitHover) ImGui::SetTooltip("%s", TR("exit"));
}
if (!showLabels && exitHover) {
ImGui::SetTooltip("Exit");
} }
} }

View File

@@ -6,6 +6,7 @@
#include "../../app.h" #include "../../app.h"
#include "../../config/version.h" #include "../../config/version.h"
#include "../../util/i18n.h" #include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../schema/ui_schema.h" #include "../schema/ui_schema.h"
#include "../material/type.h" #include "../material/type.h"
#include "../material/draw_helpers.h" #include "../material/draw_helpers.h"
@@ -121,33 +122,15 @@ void RenderAboutDialog(App* app, bool* p_open)
// Links // Links
if (material::StyledButton(TR("about_website"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) { if (material::StyledButton(TR("about_website"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
#ifdef _WIN32 util::Platform::openUrl("https://dragonx.is");
system("start https://dragonx.is");
#elif __APPLE__
system("open https://dragonx.is");
#else
system("xdg-open https://dragonx.is &");
#endif
} }
ImGui::SameLine(); ImGui::SameLine();
if (material::StyledButton(TR("about_github"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) { if (material::StyledButton(TR("about_github"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
#ifdef _WIN32 util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon");
system("start https://git.dragonx.is/dragonx/ObsidianDragon");
#elif __APPLE__
system("open https://git.dragonx.is/dragonx/ObsidianDragon");
#else
system("xdg-open https://git.dragonx.is/dragonx/ObsidianDragon &");
#endif
} }
ImGui::SameLine(); ImGui::SameLine();
if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) { if (material::StyledButton(TR("about_block_explorer"), ImVec2(linkBtn.width, 0), S.resolveFont(linkBtn.font))) {
#ifdef _WIN32 util::Platform::openUrl("https://explorer.dragonx.is");
system("start https://explorer.dragonx.is");
#elif __APPLE__
system("open https://explorer.dragonx.is");
#else
system("xdg-open https://explorer.dragonx.is &");
#endif
} }
ImGui::Spacing(); ImGui::Spacing();

View File

@@ -10,6 +10,7 @@
#include "../schema/ui_schema.h" #include "../schema/ui_schema.h"
#include "../material/draw_helpers.h" #include "../material/draw_helpers.h"
#include "imgui.h" #include "imgui.h"
#include <cstring>
#include <memory> #include <memory>
namespace dragonx { namespace dragonx {
@@ -35,6 +36,12 @@ static data::AddressBook& getAddressBook() {
return *s_address_book; return *s_address_book;
} }
static void copyEditField(char* dest, size_t destSize, const std::string& source) {
if (destSize == 0) return;
std::strncpy(dest, source.c_str(), destSize - 1);
dest[destSize - 1] = '\0';
}
void AddressBookDialog::show() void AddressBookDialog::show()
{ {
s_open = true; s_open = true;
@@ -66,15 +73,101 @@ void AddressBookDialog::render(App* app)
auto notesInput = S.input("dialogs.address-book", "notes-input"); auto notesInput = S.input("dialogs.address-book", "notes-input");
auto actionBtn = S.button("dialogs.address-book", "action-button"); auto actionBtn = S.button("dialogs.address-book", "action-button");
auto clearEditFields = []() {
s_edit_label[0] = '\0';
s_edit_address[0] = '\0';
s_edit_notes[0] = '\0';
};
auto loadEditFields = [](const data::AddressBookEntry& entry) {
copyEditField(s_edit_label, sizeof(s_edit_label), entry.label);
copyEditField(s_edit_address, sizeof(s_edit_address), entry.address);
copyEditField(s_edit_notes, sizeof(s_edit_notes), entry.notes);
};
auto renderEntryDialog = [&]() {
bool isEdit = s_show_edit_dialog;
bool* open = isEdit ? &s_show_edit_dialog : &s_show_add_dialog;
if (!*open) return;
const char* title = isEdit ? TR("address_book_edit") : TR("address_book_add");
const char* id = isEdit ? "AddressBookEdit" : "AddressBookAdd";
float dialogW = std::max(Layout::kDialogMinWidth(), Layout::kDialogDefaultWidth());
float formW = addrInput.width > 0 ? addrInput.width : Layout::kDialogFormWidth();
float actionW = actionBtn.width > 0 ? actionBtn.width : Layout::kDialogActionWidth();
float actionGap = actionBtn.gap > 0 ? actionBtn.gap : Layout::kDialogActionGap();
float notesH = notesInput.height > 0 ? notesInput.height : 60.0f;
if (material::BeginOverlayDialog(title, open, dialogW, 0.94f,
Layout::kDialogCompactBottomRatio(), id)) {
ImGui::Text("%s", TR("label"));
ImGui::SetNextItemWidth(formW);
ImGui::InputText(isEdit ? "##EditLabel" : "##AddLabel", s_edit_label, sizeof(s_edit_label));
ImGui::Spacing();
ImGui::Text("%s", TR("address_label"));
ImGui::SetNextItemWidth(formW);
ImGui::InputText(isEdit ? "##EditAddress" : "##AddAddress", s_edit_address, sizeof(s_edit_address));
if (!isEdit) {
ImGui::SameLine();
if (material::StyledButton(TR("paste"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) copyEditField(s_edit_address, sizeof(s_edit_address), clipboard);
}
}
ImGui::Spacing();
ImGui::Text("%s", TR("notes_optional"));
ImGui::SetNextItemWidth(formW);
ImGui::InputTextMultiline(isEdit ? "##EditNotes" : "##AddNotes",
s_edit_notes, sizeof(s_edit_notes), ImVec2(formW, notesH));
bool canSubmit = std::strlen(s_edit_label) > 0 && std::strlen(s_edit_address) > 0;
float totalActionsW = actionW * 2.0f + actionGap;
material::BeginOverlayDialogFooter(totalActionsW);
if (!canSubmit) ImGui::BeginDisabled();
const char* primaryLabel = isEdit ? TR("save") : TR("add");
if (material::StyledButton(primaryLabel, ImVec2(actionW, 0), S.resolveFont(actionBtn.font))) {
data::AddressBookEntry entry(s_edit_label, s_edit_address, s_edit_notes);
if (isEdit) {
if (getAddressBook().updateEntry(s_selected_index, entry)) {
Notifications::instance().success(TR("address_book_updated"));
s_show_edit_dialog = false;
} else {
Notifications::instance().error(TR("address_book_update_failed"));
}
} else {
if (getAddressBook().addEntry(entry)) {
Notifications::instance().success(TR("address_book_added"));
s_show_add_dialog = false;
} else {
Notifications::instance().error(TR("address_book_exists"));
}
}
}
if (!canSubmit) ImGui::EndDisabled();
ImGui::SameLine(0, actionGap);
if (material::StyledButton(TR("cancel"), ImVec2(actionW, 0), S.resolveFont(actionBtn.font))) {
*open = false;
}
material::EndOverlayDialog();
}
};
if (material::BeginOverlayDialog(TR("address_book_title"), &s_open, win.width, 0.94f)) { if (material::BeginOverlayDialog(TR("address_book_title"), &s_open, win.width, 0.94f)) {
auto& book = getAddressBook(); auto& book = getAddressBook();
// Toolbar // Toolbar
if (material::StyledButton(TR("address_book_add_new"), ImVec2(0,0), S.resolveFont(actionBtn.font))) { if (material::StyledButton(TR("address_book_add_new"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
s_show_add_dialog = true; s_show_add_dialog = true;
s_edit_label[0] = '\0'; clearEditFields();
s_edit_address[0] = '\0';
s_edit_notes[0] = '\0';
} }
ImGui::SameLine(); ImGui::SameLine();
@@ -86,9 +179,7 @@ void AddressBookDialog::render(App* app)
if (material::StyledButton(TR("edit"), ImVec2(0,0), S.resolveFont(actionBtn.font))) { if (material::StyledButton(TR("edit"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
if (has_selection) { if (has_selection) {
const auto& entry = book.entries()[s_selected_index]; const auto& entry = book.entries()[s_selected_index];
strncpy(s_edit_label, entry.label.c_str(), sizeof(s_edit_label) - 1); loadEditFields(entry);
strncpy(s_edit_address, entry.address.c_str(), sizeof(s_edit_address) - 1);
strncpy(s_edit_notes, entry.notes.c_str(), sizeof(s_edit_notes) - 1);
s_show_edit_dialog = true; s_show_edit_dialog = true;
} }
} }
@@ -153,9 +244,7 @@ void AddressBookDialog::render(App* app)
// Double-click to edit // Double-click to edit
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
s_selected_index = static_cast<int>(i); s_selected_index = static_cast<int>(i);
strncpy(s_edit_label, entry.label.c_str(), sizeof(s_edit_label) - 1); loadEditFields(entry);
strncpy(s_edit_address, entry.address.c_str(), sizeof(s_edit_address) - 1);
strncpy(s_edit_notes, entry.notes.c_str(), sizeof(s_edit_notes) - 1);
s_show_edit_dialog = true; s_show_edit_dialog = true;
} }
@@ -187,122 +276,7 @@ void AddressBookDialog::render(App* app)
material::EndOverlayDialog(); material::EndOverlayDialog();
} }
// Add dialog renderEntryDialog();
if (s_show_add_dialog) {
ImGui::OpenPopup("Add Address");
}
// Re-use center for sub-dialogs
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Add Address", &s_show_add_dialog, ImGuiWindowFlags_AlwaysAutoResize)) {
material::Type().text(material::TypeStyle::H6, TR("address_book_add"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Text("%s", TR("label"));
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputText("##AddLabel", s_edit_label, sizeof(s_edit_label));
ImGui::Spacing();
ImGui::Text("%s", TR("address_label"));
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputText("##AddAddress", s_edit_address, sizeof(s_edit_address));
ImGui::SameLine();
if (material::StyledButton(TR("paste"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
const char* clipboard = ImGui::GetClipboardText();
if (clipboard) {
strncpy(s_edit_address, clipboard, sizeof(s_edit_address) - 1);
}
}
ImGui::Spacing();
ImGui::Text("%s", TR("notes_optional"));
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputTextMultiline("##AddNotes", s_edit_notes, sizeof(s_edit_notes), ImVec2(addrInput.width, notesInput.height > 0 ? notesInput.height : 60));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
bool can_add = strlen(s_edit_label) > 0 && strlen(s_edit_address) > 0;
if (!can_add) ImGui::BeginDisabled();
if (material::StyledButton(TR("add"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
data::AddressBookEntry entry(s_edit_label, s_edit_address, s_edit_notes);
if (getAddressBook().addEntry(entry)) {
Notifications::instance().success(TR("address_book_added"));
s_show_add_dialog = false;
} else {
Notifications::instance().error(TR("address_book_exists"));
}
}
if (!can_add) ImGui::EndDisabled();
ImGui::SameLine();
if (material::StyledButton(TR("cancel"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
s_show_add_dialog = false;
}
ImGui::EndPopup();
}
// Edit dialog
if (s_show_edit_dialog) {
ImGui::OpenPopup("Edit Address");
}
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Edit Address", &s_show_edit_dialog, ImGuiWindowFlags_AlwaysAutoResize)) {
material::Type().text(material::TypeStyle::H6, TR("address_book_edit"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Text("%s", TR("label"));
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputText("##EditLabel", s_edit_label, sizeof(s_edit_label));
ImGui::Spacing();
ImGui::Text("%s", TR("address_label"));
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputText("##EditAddress", s_edit_address, sizeof(s_edit_address));
ImGui::Spacing();
ImGui::Text("%s", TR("notes_optional"));
ImGui::SetNextItemWidth(addrInput.width);
ImGui::InputTextMultiline("##EditNotes", s_edit_notes, sizeof(s_edit_notes), ImVec2(addrInput.width, notesInput.height > 0 ? notesInput.height : 60));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
bool can_save = strlen(s_edit_label) > 0 && strlen(s_edit_address) > 0;
if (!can_save) ImGui::BeginDisabled();
if (material::StyledButton(TR("save"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
data::AddressBookEntry entry(s_edit_label, s_edit_address, s_edit_notes);
if (getAddressBook().updateEntry(s_selected_index, entry)) {
Notifications::instance().success(TR("address_book_updated"));
s_show_edit_dialog = false;
} else {
Notifications::instance().error(TR("address_book_update_failed"));
}
}
if (!can_save) ImGui::EndDisabled();
ImGui::SameLine();
if (material::StyledButton(TR("cancel"), ImVec2(actionBtn.width, 0), S.resolveFont(actionBtn.font))) {
s_show_edit_dialog = false;
}
ImGui::EndPopup();
}
} }
} // namespace ui } // namespace ui

View File

@@ -0,0 +1,254 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <vector>
#include <cctype>
#include <cstring>
#include <cstddef>
#include "../../app.h"
#include "../../util/i18n.h"
#include "../material/draw_helpers.h"
#include "../material/project_icons.h"
#include "../theme.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
class AddressLabelDialog {
public:
static bool drawIconByName(ImDrawList* dl,
const std::string& name,
ImVec2 center,
float /*emSize*/,
ImU32 color,
ImFont* iconFont,
float iconFontSize) {
return material::project_icons::drawByName(dl, name, center, color, iconFont, iconFontSize);
}
static void show(App* app, const std::string& address, bool isZ) {
s_open = true;
s_app = app;
s_address = address;
s_isZ = isZ;
s_selectedIcon = -1;
s_iconSearch[0] = '\0';
// Pre-fill from existing metadata
std::string existing = app->getAddressLabel(address);
std::strncpy(s_label, existing.c_str(), sizeof(s_label) - 1);
s_label[sizeof(s_label) - 1] = '\0';
std::string existingIcon = app->getAddressIcon(address);
for (int i = 0; i < material::project_icons::walletIconCount(); ++i) {
if (material::project_icons::walletIconName(i) == existingIcon) {
s_selectedIcon = i;
break;
}
}
}
static void render() {
if (!s_open || !s_app) return;
using namespace material;
constexpr float cardTopViewportRatio = 0.15f;
constexpr float cardBottomViewportRatio = 0.80f;
if (BeginOverlayDialog(TR("set_label"), &s_open, 660.0f, 0.92f, cardBottomViewportRatio)) {
float dp = Layout::dpiScale();
// Address preview
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
s_isZ ? TR("shielded_address") : TR("transparent_address"));
ImGui::Spacing();
{
std::string display = s_address;
if (display.size() > 42) display = display.substr(0, 20) + "..." + display.substr(display.size() - 16);
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), display.c_str());
}
ImGui::Spacing();
ImGui::Spacing();
// Label input
Type().text(TypeStyle::Subtitle2, TR("label"));
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##AddrLabel", TR("label_placeholder"), s_label, sizeof(s_label));
ImGui::Spacing();
ImGui::Spacing();
// Icon picker grid
Type().text(TypeStyle::Subtitle2, TR("choose_icon"));
ImGui::Spacing();
// Search bar
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##IconSearch", TR("search_icons"), s_iconSearch, sizeof(s_iconSearch));
ImGui::Spacing();
ImFont* iconFont = Type().iconMed();
float iconFsz = ScaledFontSize(iconFont);
float cellSz = iconFsz + 16.0f * dp;
float avail = ImGui::GetContentRegionAvail().x;
int cols = std::max(1, (int)(avail / (cellSz + 4.0f * dp)));
const char* cancelLabel = TR("cancel");
const char* saveLabel = TR("save");
ImFont* buttonFont = Type().button();
float buttonFontSize = ScaledFontSize(buttonFont);
const ImGuiStyle& style = ImGui::GetStyle();
const float cardHeight = ImGui::GetMainViewport()->Size.y * (cardBottomViewportRatio - cardTopViewportRatio);
const float bottomPadding = style.WindowPadding.y;
const bool showClearIcon = (s_selectedIcon >= 0);
const float clearRowH = showClearIcon ? ImGui::GetTextLineHeight() : 0.0f;
const float buttonRowH = ImGui::GetFrameHeight();
const float separatorH = 1.0f;
const float preButtonReserve =
(showClearIcon ? (style.ItemSpacing.y + clearRowH) : 0.0f) +
style.ItemSpacing.y * 3.0f + separatorH;
const float buttonY = cardHeight - bottomPadding - buttonRowH;
// Build filtered index list
std::vector<int> visible;
visible.reserve(material::project_icons::walletIconCount());
{
// Simple case-insensitive substring match on icon name
std::string needle(s_iconSearch);
for (char& c : needle) c = (char)std::tolower((unsigned char)c);
for (int i = 0; i < material::project_icons::walletIconCount(); ++i) {
const char* iconName = material::project_icons::walletIconName(i);
if (needle.empty() || std::strstr(iconName, needle.c_str()) != nullptr)
visible.push_back(i);
}
}
// The grid owns all vertical space above the bottom action band.
const float gridStartY = ImGui::GetCursorPosY();
const float controlsTopY = std::max(gridStartY + cellSz * 2.0f, buttonY - preButtonReserve);
const float gridMaxH = std::max(cellSz * 2.0f, controlsTopY - gridStartY);
ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 0));
ImGui::BeginChild("##IconGrid", ImVec2(avail, gridMaxH), ImGuiChildFlags_None,
ImGuiWindowFlags_NoScrollbar);
ImDrawList* dl = ImGui::GetWindowDrawList();
if (visible.empty()) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_icons_found"));
}
int col = 0;
for (int vi = 0; vi < (int)visible.size(); ++vi) {
int i = visible[vi];
const char* iconName = material::project_icons::walletIconName(i);
if (col != 0) ImGui::SameLine(0, 4.0f * dp);
ImVec2 pos = ImGui::GetCursorScreenPos();
ImVec2 mn = pos;
ImVec2 mx(pos.x + cellSz, pos.y + cellSz);
bool hov = ImGui::IsMouseHoveringRect(mn, mx);
bool sel = (s_selectedIcon == i);
// Background
if (sel) {
dl->AddRectFilled(mn, mx, WithAlpha(Primary(), 40), 6.0f * dp);
dl->AddRect(mn, mx, WithAlpha(Primary(), 120), 6.0f * dp, 0, 1.5f * dp);
} else if (hov) {
dl->AddRectFilled(mn, mx, IM_COL32(255, 255, 255, 20), 6.0f * dp);
}
// Icon centered in cell
drawIconByName(dl,
iconName,
ImVec2(mn.x + cellSz * 0.5f, mn.y + cellSz * 0.5f),
iconFsz,
sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()),
iconFont,
iconFsz);
ImGui::PushID(i);
ImGui::InvisibleButton("##icon", ImVec2(cellSz, cellSz));
if (ImGui::IsItemClicked()) s_selectedIcon = i;
if (hov) ImGui::SetTooltip("%s", iconName);
ImGui::PopID();
col = (col + 1) % cols;
}
ImGui::EndChild();
ImGui::PopStyleColor();
if (ImGui::GetCursorPosY() < controlsTopY) {
ImGui::SetCursorPosY(controlsTopY);
}
// "No icon" option
if (showClearIcon) {
ImGui::Spacing();
if (ImGui::SmallButton(TR("clear_icon"))) {
s_selectedIcon = -1;
}
}
ImGui::Spacing();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
if (ImGui::GetCursorPosY() < buttonY) {
ImGui::SetCursorPosY(buttonY);
}
// Buttons
float minBtnW = 120.0f * dp;
float buttonPadW = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp;
float cancelW = std::max(minBtnW,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, cancelLabel).x + buttonPadW);
float saveW = std::max(minBtnW,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, saveLabel).x + buttonPadW);
float totalW = cancelW + saveW + Layout::spacingMd();
float rowStartX = ImGui::GetCursorPosX();
float contentW = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f));
if (TactileButton(cancelLabel, ImVec2(cancelW, 0), buttonFont)) {
s_open = false;
}
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(saveLabel, ImVec2(saveW, 0), buttonFont)) {
// Apply changes
s_app->setAddressLabel(s_address, s_label);
if (s_selectedIcon >= 0)
s_app->setAddressIcon(s_address, material::project_icons::walletIconName(s_selectedIcon));
else
s_app->setAddressIcon(s_address, "");
s_open = false;
}
EndOverlayDialog();
}
}
static bool isOpen() { return s_open; }
private:
static inline bool s_open = false;
static inline App* s_app = nullptr;
static inline std::string s_address;
static inline bool s_isZ = false;
static inline char s_label[128] = {};
static inline int s_selectedIcon = -1;
static inline char s_iconSearch[64] = {};
public:
// Expose for the address list to look up icon glyphs by name
static const char* iconGlyphForName(const std::string& name) {
return material::project_icons::glyphForName(name);
}
};
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,334 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <cstdio>
#include <cstring>
#include <cmath>
#include "../../app.h"
#include "../../util/i18n.h"
#include "../material/draw_helpers.h"
#include "../notifications.h"
#include "../theme.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
/**
* @brief Modal dialog for transferring funds between two addresses.
*
* Shown when a user drags one address row onto another in the address list.
* Displays from/to addresses, balances, amount input, fee, and a preview of
* the resulting balances after transfer.
*/
struct AddressTransferInfo {
std::string fromAddr;
std::string toAddr;
double fromBalance = 0.0;
double toBalance = 0.0;
bool fromIsZ = false;
bool toIsZ = false;
};
class AddressTransferDialog {
public:
using TransferInfo = AddressTransferInfo;
static void show(App* app, const TransferInfo& info) {
s_open = true;
s_app = app;
s_info = info;
s_sending = false;
s_resultMsg.clear();
s_success = false;
// Default fee
s_fee = 0.0001;
// Pre-fill with the maximum sendable amount so the dialog is valid
// immediately without requiring the user to press Max.
snprintf(s_amount, sizeof(s_amount), "%.8f", maxSendableAmount(info.fromBalance, s_fee));
}
static void render() {
if (!s_open || !s_app) return;
using namespace material;
if (BeginOverlayDialog(TR("transfer_funds"), &s_open, 620.0f, 0.94f)) {
float dp = Layout::dpiScale();
ImDrawList* dl = ImGui::GetWindowDrawList();
// If we got a result, show it
if (!s_resultMsg.empty()) {
renderResult();
EndOverlayDialog();
return;
}
// Shielding / deshielding warning
if (s_info.fromIsZ && !s_info.toIsZ) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
ImGui::TextWrapped("%s", TR("deshielding_warning"));
ImGui::PopStyleColor();
ImGui::Spacing();
} else if (!s_info.fromIsZ && s_info.toIsZ) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Success()));
ImGui::TextWrapped("%s", TR("shielding_notice"));
ImGui::PopStyleColor();
ImGui::Spacing();
}
// From address
Type().text(TypeStyle::Overline, TR("from"));
renderAddressRow(s_info.fromAddr, s_info.fromBalance, s_info.fromIsZ, dp);
ImGui::Spacing();
// Arrow
{
float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f;
ImGui::SetCursorPosX(arrowCX - 8.0f);
ImFont* iconFont = Type().iconMed();
float fsz = ScaledFontSize(iconFont);
ImVec2 pos = ImGui::GetCursorScreenPos();
dl->AddText(iconFont, fsz, pos, OnSurfaceMedium(), ICON_MD_ARROW_DOWNWARD);
ImGui::Dummy(ImVec2(fsz, fsz));
}
ImGui::Spacing();
// To address
Type().text(TypeStyle::Overline, TR("to"));
renderAddressRow(s_info.toAddr, s_info.toBalance, s_info.toIsZ, dp);
ImGui::Spacing();
ImGui::Spacing();
// Amount input + Max button on same row without overflow
Type().text(TypeStyle::Subtitle2, TR("amount"));
{
float spacing = ImGui::GetStyle().ItemSpacing.x;
float maxBtnW = ImGui::CalcTextSize(TR("max")).x
+ ImGui::GetStyle().FramePadding.x * 2.0f;
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - maxBtnW - spacing);
}
ImGui::InputText("##TransferAmt", s_amount, sizeof(s_amount),
ImGuiInputTextFlags_CharsDecimal);
// Max button
ImGui::SameLine();
if (ImGui::SmallButton(TR("max"))) {
snprintf(s_amount, sizeof(s_amount), "%.8f",
maxSendableAmount(s_info.fromBalance, s_fee));
}
ImGui::Spacing();
// Fee display
{
char feeBuf[64];
snprintf(feeBuf, sizeof(feeBuf), "%s: %.4f DRGX", TR("fee"), s_fee);
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), feeBuf);
}
ImGui::Spacing();
// Preview after transfer
double amount = atof(s_amount);
double totalDeduct = amount + s_fee;
double newFromBal = s_info.fromBalance - totalDeduct;
double newToBal = s_info.toBalance + amount;
bool amountValid = amount > 0 && totalDeduct <= s_info.fromBalance;
ImGui::Separator();
ImGui::Spacing();
Type().text(TypeStyle::Subtitle2, TR("result_preview"));
ImGui::Spacing();
{
char buf[128];
snprintf(buf, sizeof(buf), TR("sender_balance"),
s_info.fromBalance, amountValid ? newFromBal : s_info.fromBalance);
Type().textColored(TypeStyle::Caption,
(amountValid && newFromBal < 1e-9) ? Warning() : OnSurfaceMedium(), buf);
}
{
char buf[128];
snprintf(buf, sizeof(buf), TR("recipient_balance"),
s_info.toBalance, amountValid ? newToBal : s_info.toBalance);
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), buf);
}
// Warnings
if (!amountValid && amount > 0) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Error()));
ImGui::TextWrapped("%s", TR("insufficient_funds"));
ImGui::PopStyleColor();
}
if (amountValid && newFromBal < 1e-9) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
ImGui::TextWrapped("%s", TR("sends_full_balance_warning"));
ImGui::PopStyleColor();
}
// Buttons
const char* cancelLabel = TR("cancel");
const char* confirmLabel = TR("confirm_transfer");
const char* sendingLabel = TR("sending");
ImFont* buttonFont = Type().button();
float buttonFontSize = ScaledFontSize(buttonFont);
float minBtnW = 120.0f * dp;
float confirmMinW = 160.0f * dp;
float buttonPadW = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp;
float cancelW = std::max(minBtnW,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, cancelLabel).x + buttonPadW);
float confirmTextW = std::max(
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, confirmLabel).x,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, sendingLabel).x);
float confirmW = std::max(confirmMinW, confirmTextW + buttonPadW);
float totalW = cancelW + confirmW + Layout::spacingMd();
float footerH = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y * 3.0f + 1.0f;
ImGuiViewport* vp = ImGui::GetMainViewport();
float cardBottomY = vp->Pos.y + vp->Size.y * 0.85f;
float footerTopY = cardBottomY - 24.0f * dp - footerH;
float currentY = ImGui::GetCursorScreenPos().y;
if (currentY < footerTopY) {
ImGui::Dummy(ImVec2(0, footerTopY - currentY));
} else {
ImGui::Spacing();
}
ImGui::Separator();
ImGui::Spacing();
float rowStartX = ImGui::GetCursorPosX();
float contentW = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f));
if (TactileButton(cancelLabel, ImVec2(cancelW, 0), buttonFont)) {
s_open = false;
}
ImGui::SameLine(0, Layout::spacingMd());
ImGui::BeginDisabled(!amountValid || s_sending);
if (TactileButton(s_sending ? sendingLabel : confirmLabel, ImVec2(confirmW, 0), buttonFont)) {
s_sending = true;
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
amount, s_fee, "",
[](bool ok, const std::string& result) {
s_sending = false;
s_success = ok;
s_resultMsg = result;
if (ok) {
Notifications::instance().success(TR("transfer_sent_desc"));
} else {
Notifications::instance().error(result.empty() ? TR("transfer_failed") : result);
}
});
s_open = false;
}
ImGui::EndDisabled();
EndOverlayDialog();
}
}
static bool isOpen() { return s_open; }
static void close() { s_open = false; }
private:
static double maxSendableAmount(double balance, double fee) {
double maxAmt = balance - fee;
return maxAmt > 0.0 ? maxAmt : 0.0;
}
static void renderAddressRow(const std::string& addr, double balance, bool isZ, float dp) {
using namespace material;
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 pos = ImGui::GetCursorScreenPos();
float w = ImGui::GetContentRegionAvail().x;
float h = 40.0f * dp;
ImVec2 mn = pos, mx(pos.x + w, pos.y + h);
GlassPanelSpec g;
g.rounding = 8.0f * dp;
g.fillAlpha = 18;
DrawGlassPanel(dl, mn, mx, g);
float pad = 12.0f * dp;
// Type icon
ImFont* iconFont = Type().iconSmall();
const char* icon = isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE;
ImU32 col = isZ ? Success() : Warning();
float iconFsz = ScaledFontSize(iconFont);
ImVec2 iSz = iconFont->CalcTextSizeA(iconFsz, 1000.0f, 0.0f, icon);
dl->AddText(iconFont, iconFsz, ImVec2(mn.x + pad, mn.y + (h - iSz.y) * 0.5f), col, icon);
// Address (truncated)
// Balance (right-aligned) — computed first so we know how much space address gets
char balBuf[32];
snprintf(balBuf, sizeof(balBuf), "%.8f DRGX", balance);
ImFont* body = Type().body2();
float bodyFsz = ScaledFontSize(body);
ImVec2 balSz = body->CalcTextSizeA(bodyFsz, 1000.0f, 0.0f, balBuf);
float balX = mx.x - pad - balSz.x;
dl->AddText(body, bodyFsz, ImVec2(balX, mn.y + (h - balSz.y) * 0.5f),
balance > 0 ? OnSurface() : OnSurfaceDisabled(), balBuf);
float textX = mn.x + pad + iSz.x + 8.0f * dp;
ImFont* capFont = Type().caption();
float capFsz = ScaledFontSize(capFont);
// Clip address text so it never overlaps the balance
float addrMaxW = balX - textX - 8.0f * dp;
dl->PushClipRect(ImVec2(textX, mn.y), ImVec2(textX + addrMaxW, mn.y + h), true);
dl->AddText(capFont, capFsz, ImVec2(textX, mn.y + (h * 0.5f - capFsz)), OnSurfaceMedium(), addr.c_str());
dl->PopClipRect();
ImGui::Dummy(ImVec2(w, h));
}
static void renderResult() {
using namespace material;
if (s_success) {
ImGui::Spacing();
Type().text(TypeStyle::H6, TR("transfer_sent"));
ImGui::Spacing();
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("transfer_sent_desc"));
ImGui::Spacing();
{
char buf[128];
snprintf(buf, sizeof(buf), "Operation ID: %s",
s_resultMsg.size() > 40
? (s_resultMsg.substr(0, 20) + "..." + s_resultMsg.substr(s_resultMsg.size() - 10)).c_str()
: s_resultMsg.c_str());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
}
} else {
ImGui::Spacing();
Type().textColored(TypeStyle::H6, Error(), TR("transfer_failed"));
ImGui::Spacing();
ImGui::TextWrapped("%s", s_resultMsg.c_str());
}
ImGui::Spacing();
ImGui::Spacing();
float btnW = 120.0f;
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f);
if (TactileButton(TR("close"), ImVec2(btnW, 0))) {
s_open = false;
}
}
static inline bool s_open = false;
static inline App* s_app = nullptr;
static inline TransferInfo s_info;
static inline char s_amount[64] = {};
static inline double s_fee = 0.0001;
static inline bool s_sending = false;
static inline bool s_success = false;
static inline std::string s_resultMsg;
};
} // namespace ui
} // namespace dragonx

View File

@@ -120,6 +120,7 @@ void BackupWalletDialog::render(App* app)
bool success = false; bool success = false;
std::string statusMsg; std::string statusMsg;
try { try {
rpc::RPCClient::TraceScope trace("Settings / Backup wallet");
rpc->call("backupwallet", json::array({dest})); rpc->call("backupwallet", json::array({dest}));
// Check if file was created // Check if file was created
if (fs::exists(dest)) { if (fs::exists(dest)) {
@@ -144,7 +145,7 @@ void BackupWalletDialog::render(App* app)
s_status = statusMsg; s_status = statusMsg;
s_backing_up = false; s_backing_up = false;
if (success) { if (success) {
Notifications::instance().success(TR("backup_created")); Notifications::instance().success(TR("backup_created"), 5.0f);
} else { } else {
Notifications::instance().warning(statusMsg); Notifications::instance().warning(statusMsg);
} }

View File

@@ -0,0 +1,106 @@
#include "balance_address_list.h"
#include <algorithm>
#include <cctype>
#include <cstdio>
namespace dragonx {
namespace ui {
namespace {
bool containsIgnoreCase(const std::string& haystack, const std::string& needle)
{
if (needle.empty()) return true;
auto it = std::search(haystack.begin(), haystack.end(),
needle.begin(), needle.end(),
[](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
});
return it != haystack.end();
}
} // namespace
bool AddressListMatchesFilter(const AddressListInput& input, const std::string& filter)
{
if (!input.info) return false;
return containsIgnoreCase(input.info->address, filter) ||
containsIgnoreCase(input.info->label, filter) ||
containsIgnoreCase(input.label, filter);
}
std::vector<AddressListRow> BuildAddressListRows(const std::vector<AddressListInput>& inputs,
const std::string& filter,
bool hideZeroBalances,
bool showHidden)
{
std::vector<AddressListRow> rows;
rows.reserve(inputs.size());
for (const auto& input : inputs) {
if (!input.info) continue;
if (!AddressListMatchesFilter(input, filter)) continue;
if (input.hidden && !showHidden) continue;
if (hideZeroBalances && input.info->balance < 1e-9 && !input.hidden && !input.favorite) continue;
rows.push_back({input.info, input.isZ, input.hidden, input.favorite, input.mining,
input.label, input.icon, input.sortOrder});
}
std::sort(rows.begin(), rows.end(),
[](const AddressListRow& a, const AddressListRow& b) {
bool aHasOrder = a.sortOrder >= 0;
bool bHasOrder = b.sortOrder >= 0;
if (aHasOrder && bHasOrder) return a.sortOrder < b.sortOrder;
if (aHasOrder != bHasOrder) return aHasOrder > bHasOrder;
if (a.favorite != b.favorite) return a.favorite > b.favorite;
if (a.isZ != b.isZ) return a.isZ > b.isZ;
return a.info->balance > b.info->balance;
});
return rows;
}
AddressRowLayout ComputeAddressRowLayout(float rowX,
float rowY,
float rowWidth,
float rowHeight,
float rowPadLeft,
float rowIconSize,
float spacingMd,
float spacingSm,
float spacingXs)
{
AddressRowLayout layout;
layout.contentStartX = rowX + rowPadLeft;
layout.contentStartY = rowY + spacingMd;
layout.buttonSize = rowHeight - spacingMd * 2.0f;
const float buttonY = rowY + (rowHeight - layout.buttonSize) * 0.5f;
const float rightEdge = rowX + rowWidth;
const float favoriteX = rightEdge - layout.buttonSize - spacingXs;
const float visibilityX = favoriteX - spacingSm - layout.buttonSize;
layout.favoriteButton = {favoriteX, buttonY, layout.buttonSize, layout.buttonSize};
layout.visibilityButton = {visibilityX, buttonY, layout.buttonSize, layout.buttonSize};
layout.contentRight = visibilityX - spacingSm;
layout.labelX = layout.contentStartX + rowIconSize * 2.0f + spacingMd;
return layout;
}
std::string FormatAddressUsdValue(double balance, double priceUsd)
{
if (priceUsd <= 0.0 || balance <= 0.0) return {};
char buffer[32];
const double usdValue = balance * priceUsd;
if (usdValue >= 1.0) {
std::snprintf(buffer, sizeof(buffer), "$%.2f", usdValue);
} else if (usdValue >= 0.01) {
std::snprintf(buffer, sizeof(buffer), "$%.4f", usdValue);
} else {
std::snprintf(buffer, sizeof(buffer), "$%.6f", usdValue);
}
return buffer;
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,67 @@
#pragma once
#include "data/wallet_state.h"
#include <string>
#include <vector>
namespace dragonx {
namespace ui {
struct AddressListInput {
const AddressInfo* info = nullptr;
bool isZ = false;
bool hidden = false;
bool favorite = false;
bool mining = false;
std::string label;
std::string icon;
int sortOrder = -1;
};
struct AddressListRow {
const AddressInfo* info = nullptr;
bool isZ = false;
bool hidden = false;
bool favorite = false;
bool mining = false;
std::string label;
std::string icon;
int sortOrder = -1;
};
struct AddressRowRect {
float x = 0.0f;
float y = 0.0f;
float width = 0.0f;
float height = 0.0f;
};
struct AddressRowLayout {
float contentStartX = 0.0f;
float contentStartY = 0.0f;
float labelX = 0.0f;
float contentRight = 0.0f;
float buttonSize = 0.0f;
AddressRowRect favoriteButton;
AddressRowRect visibilityButton;
};
bool AddressListMatchesFilter(const AddressListInput& input, const std::string& filter);
std::vector<AddressListRow> BuildAddressListRows(const std::vector<AddressListInput>& inputs,
const std::string& filter,
bool hideZeroBalances,
bool showHidden);
AddressRowLayout ComputeAddressRowLayout(float rowX,
float rowY,
float rowWidth,
float rowHeight,
float rowPadLeft,
float rowIconSize,
float spacingMd,
float spacingSm,
float spacingXs);
std::string FormatAddressUsdValue(double balance, double priceUsd);
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,52 @@
#include "balance_recent_tx.h"
#include "../../config/version.h"
#include <cmath>
#include <cstdio>
#include <ctime>
namespace dragonx {
namespace ui {
namespace {
std::string truncateRecentAddress(const std::string& address, int maxLen)
{
if (address.length() <= static_cast<size_t>(maxLen)) return address;
int half = (maxLen - 3) / 2;
return address.substr(0, half) + "..." + address.substr(address.length() - half);
}
std::string formatRecentAmount(const std::string& type, double amount)
{
char buffer[32];
snprintf(buffer, sizeof(buffer), "%s%.4f %s",
type == "send" ? "-" : "+",
std::abs(amount), DRAGONX_TICKER);
return std::string(buffer);
}
std::string recentTimeAgo(int64_t timestamp)
{
if (timestamp <= 0) return "";
int64_t now = static_cast<int64_t>(std::time(nullptr));
int64_t diff = now - timestamp;
if (diff < 0) diff = 0;
if (diff < 60) return std::to_string(diff) + "s ago";
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
return std::to_string(diff / 86400) + "d ago";
}
}
RecentTxDisplay buildRecentTxDisplay(const TransactionInfo& tx, int addressMaxLen)
{
return {
tx.getTypeDisplay(),
truncateRecentAddress(tx.address, addressMaxLen),
formatRecentAmount(tx.type, tx.amount),
recentTimeAgo(tx.timestamp)
};
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,20 @@
#pragma once
#include "../../data/wallet_state.h"
#include <string>
namespace dragonx {
namespace ui {
struct RecentTxDisplay {
std::string typeText;
std::string addressText;
std::string amountText;
std::string timeText;
};
RecentTxDisplay buildRecentTxDisplay(const TransactionInfo& tx, int addressMaxLen);
} // namespace ui
} // namespace dragonx

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,117 @@
#include "balance_tab_helpers.h"
#include "../../config/version.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../../util/i18n.h"
#include "../material/colors.h"
#include "../material/type.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <ctime>
namespace dragonx {
namespace ui {
std::string TrId(const char* trKey, const char* id)
{
std::string value = TR(trKey);
value += "##";
value += id;
return value;
}
bool containsIgnoreCase(const std::string& value, const std::string& search)
{
if (search.empty()) return true;
std::string haystack = value;
std::string needle = search;
std::transform(haystack.begin(), haystack.end(), haystack.begin(), ::tolower);
std::transform(needle.begin(), needle.end(), needle.begin(), ::tolower);
return haystack.find(needle) != std::string::npos;
}
std::string timeAgo(int64_t timestamp)
{
if (timestamp <= 0) return "";
int64_t now = static_cast<int64_t>(std::time(nullptr));
int64_t diff = now - timestamp;
if (diff < 0) diff = 0;
if (diff < 60) return std::to_string(diff) + "s ago";
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
return std::to_string(diff / 86400) + "d ago";
}
std::string truncateAddress(const std::string& address, int maxLen)
{
if (address.length() <= static_cast<size_t>(maxLen)) return address;
int half = (maxLen - 3) / 2;
return address.substr(0, half) + "..." + address.substr(address.length() - half);
}
ImU32 recentTxIconColor(const std::string& type)
{
using namespace material;
if (type == "send") return Error();
if (type == "receive") return Success();
return Warning();
}
ImU32 recentTxAmountColor(const std::string& type)
{
using namespace material;
return type == "send" ? Error() : Success();
}
std::string formatRecentTxAmount(const std::string& type, double amount)
{
char buffer[32];
snprintf(buffer, sizeof(buffer), "%s%.4f %s",
type == "send" ? "-" : "+",
std::abs(amount), DRAGONX_TICKER);
return std::string(buffer);
}
void DrawTxIcon(ImDrawList* drawList, const std::string& type,
float centerX, float centerY, float, ImU32 color)
{
using namespace material;
ImFont* iconFont = Type().iconSmall();
const char* icon = ICON_MD_CONSTRUCTION;
if (type == "send") {
icon = ICON_MD_CALL_MADE;
} else if (type == "receive") {
icon = ICON_MD_CALL_RECEIVED;
}
ImVec2 size = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
drawList->AddText(iconFont, iconFont->LegacySize,
ImVec2(centerX - size.x * 0.5f, centerY - size.y * 0.5f), color, icon);
}
void DrawSparkline(ImDrawList* drawList, const ImVec2& min, const ImVec2& max,
const std::vector<double>& data, ImU32 color, float thickness)
{
if (data.size() < 2) return;
double low = *std::min_element(data.begin(), data.end());
double high = *std::max_element(data.begin(), data.end());
double range = high - low;
if (range < 1e-12) range = 1.0;
float width = max.x - min.x;
float height = max.y - min.y;
int count = static_cast<int>(data.size());
std::vector<ImVec2> points;
points.reserve(count);
for (int i = 0; i < count; i++) {
float x = min.x + static_cast<float>(i) / static_cast<float>(count - 1) * width;
float y = max.y - static_cast<float>((data[i] - low) / range) * height;
points.push_back(ImVec2(x, y));
}
drawList->AddPolyline(points.data(), count, color, ImDrawFlags_None, thickness);
}
} // namespace ui
} // namespace dragonx

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