32 Commits

Author SHA1 Message Date
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
363 changed files with 57573 additions and 70157 deletions

16
.gitignore vendored
View File

@@ -37,20 +37,6 @@ asmap.dat
/memory
/todo.md
/.github/
/ObsidianDragon-agent/
# macOS
.DS_Store
# Local-only archive of superseded lite-wallet design/planning docs (untracked)
docs/_archive/
# ed25519 release-signing keys — the secret key must NEVER be committed
*.ed25519.key
*.ed25519.pub.b64
# Lite-backend deps are fetched (or `cargo vendor`-ed locally for offline); not committed.
third_party/silentdragonxlite/lib/vendor/
# Generated by configure_file from res/ObsidianDragon.manifest.in (do not track)
res/ObsidianDragon.manifest
.DS_Store

102
CLAUDE.md
View File

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

View File

@@ -14,21 +14,12 @@ if(APPLE)
endif()
endif()
project(ObsidianDragon
VERSION 2.0.0
project(ObsidianDragon
VERSION 1.1.1
LANGUAGES C CXX
DESCRIPTION "DragonX Cryptocurrency Wallet"
)
# Pre-release suffix (e.g. "-rc1", "-beta2"). Leave empty for stable releases.
set(DRAGONX_VERSION_SUFFIX "")
# ObsidianDragonLite is versioned INDEPENDENTLY of the full-node app above. The active variant's
# version flows to the generated header, the Windows .rc/manifest, and build.sh's release names via
# DRAGONX_APP_VERSION* (resolved in the lite/full block below).
set(DRAGONX_LITE_VERSION "1.0.0")
set(DRAGONX_LITE_VERSION_SUFFIX "")
# C++17 standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -42,132 +33,6 @@ endif()
# Options
option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON)
option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON)
option(DRAGONX_BUILD_LITE "Build ObsidianDragonLite variant without full-node features" OFF)
option(DRAGONX_ENABLE_LITE_BACKEND "Enable real lite wallet backend integration" OFF)
option(DRAGONX_ENABLE_CHAT "Enable the HushChat protocol/UI integration" ON)
set(DRAGONX_LITE_BACKEND_LIBRARY "" CACHE FILEPATH "Path to a prebuilt SDXL-compatible lite backend library")
set(DRAGONX_LITE_BACKEND_INCLUDE_DIR "" CACHE PATH "Optional include directory for SDXL-compatible lite backend headers")
set(DRAGONX_LITE_BACKEND_EXTRA_LIBS "" CACHE STRING "Additional libraries needed by the SDXL-compatible lite backend")
set(DRAGONX_LITE_BACKEND_LINK_MODE "imported" CACHE STRING "Lite backend link mode; Phase 1 supports imported only")
set_property(CACHE DRAGONX_LITE_BACKEND_LINK_MODE PROPERTY STRINGS imported)
set(DRAGONX_LITE_BACKEND_ABI "sdxl-c-v1" CACHE STRING "Expected lite backend C ABI version")
set(DRAGONX_LITE_BACKEND_SYMBOLS_FILE "" CACHE FILEPATH "Path to generated lite backend exported-symbol inventory")
set(DRAGONX_LITE_BACKEND_MANIFEST "" CACHE FILEPATH "Optional path to generated lite backend artifact manifest")
option(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE "Require verified signature metadata in the lite backend artifact manifest" OFF)
set(DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS
litelib_wallet_exists
litelib_initialize_new
litelib_initialize_new_from_phrase
litelib_initialize_existing
litelib_execute
litelib_rust_free_string
litelib_check_server_online
litelib_shutdown
)
if(DRAGONX_BUILD_LITE)
set(DRAGONX_APP_NAME "ObsidianDragonLite")
set(DRAGONX_BINARY_NAME "ObsidianDragonLite")
# NOTE: do NOT FORCE-write DRAGONX_ENABLE_EMBEDDED_DAEMON=OFF into the cache here. A forced
# cache write persists into a later full-node reconfigure of the same build dir and silently
# disables the embedded daemon — the binary still embeds/extracts, but isUsingEmbeddedDaemon()
# returns false, so it "unpacks dragonxd but never starts" (the 1.3.0 regression). It is also
# redundant: makeWalletCapabilities() already forces the embedded-daemon capability off for any
# lite build via `fullNodeBuild && embeddedDaemonCompiled`, so lite never launches a daemon
# regardless of this flag. build.sh sets the flag explicitly per variant to defeat stale caches.
set(DRAGONX_APP_VERSION "${DRAGONX_LITE_VERSION}")
set(DRAGONX_APP_VERSION_SUFFIX "${DRAGONX_LITE_VERSION_SUFFIX}")
else()
set(DRAGONX_APP_NAME "ObsidianDragon")
set(DRAGONX_BINARY_NAME "ObsidianDragon")
set(DRAGONX_APP_VERSION "${PROJECT_VERSION}")
set(DRAGONX_APP_VERSION_SUFFIX "${DRAGONX_VERSION_SUFFIX}")
endif()
# Split the active version into numeric components for the generated header + Windows VERSIONINFO.
string(REPLACE "." ";" _dragonx_ver_parts "${DRAGONX_APP_VERSION}")
list(GET _dragonx_ver_parts 0 DRAGONX_APP_VERSION_MAJOR)
list(GET _dragonx_ver_parts 1 DRAGONX_APP_VERSION_MINOR)
list(GET _dragonx_ver_parts 2 DRAGONX_APP_VERSION_PATCH)
set(DRAGONX_LITE_BACKEND_READY OFF)
if(DRAGONX_ENABLE_LITE_BACKEND)
if(NOT DRAGONX_BUILD_LITE)
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND is only supported with DRAGONX_BUILD_LITE=ON")
endif()
if(NOT DRAGONX_LITE_BACKEND_LINK_MODE STREQUAL "imported")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_LINK_MODE currently supports only 'imported'; runtime dynamic loading is a later bridge-runtime phase")
endif()
if(NOT DRAGONX_LITE_BACKEND_ABI STREQUAL "sdxl-c-v1")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_ABI must be sdxl-c-v1")
endif()
if(NOT DRAGONX_LITE_BACKEND_LIBRARY)
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND requires DRAGONX_LITE_BACKEND_LIBRARY to point at an SDXL-compatible artifact")
endif()
if(NOT EXISTS "${DRAGONX_LITE_BACKEND_LIBRARY}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_LIBRARY does not exist: ${DRAGONX_LITE_BACKEND_LIBRARY}")
endif()
if(NOT DRAGONX_LITE_BACKEND_SYMBOLS_FILE)
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND requires DRAGONX_LITE_BACKEND_SYMBOLS_FILE generated by scripts/build-lite-backend-artifact.sh")
endif()
if(NOT EXISTS "${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE does not exist: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
endif()
file(STRINGS "${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}" DRAGONX_LITE_BACKEND_SYMBOL_LINES)
if(NOT DRAGONX_LITE_BACKEND_SYMBOL_LINES)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE is empty: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
endif()
foreach(DRAGONX_LITE_REQUIRED_SYMBOL IN LISTS DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS)
list(FIND DRAGONX_LITE_BACKEND_SYMBOL_LINES "${DRAGONX_LITE_REQUIRED_SYMBOL}" DRAGONX_LITE_SYMBOL_INDEX)
if(DRAGONX_LITE_SYMBOL_INDEX EQUAL -1)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE is missing required symbol: ${DRAGONX_LITE_REQUIRED_SYMBOL}")
endif()
endforeach()
if(DRAGONX_LITE_BACKEND_MANIFEST AND NOT EXISTS "${DRAGONX_LITE_BACKEND_MANIFEST}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST does not exist: ${DRAGONX_LITE_BACKEND_MANIFEST}")
endif()
if(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE)
if(NOT DRAGONX_LITE_BACKEND_MANIFEST)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires DRAGONX_LITE_BACKEND_MANIFEST")
endif()
file(READ "${DRAGONX_LITE_BACKEND_MANIFEST}" DRAGONX_LITE_BACKEND_MANIFEST_JSON)
string(JSON DRAGONX_LITE_SIGNATURE_STATUS ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_STATUS_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_status)
if(DRAGONX_LITE_SIGNATURE_STATUS_ERROR)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing signature verification status")
endif()
if(NOT DRAGONX_LITE_SIGNATURE_STATUS STREQUAL "verified")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verified signature metadata")
endif()
string(JSON DRAGONX_LITE_SIGNATURE_VERIFIED_SHA ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verified_artifact_sha256)
string(JSON DRAGONX_LITE_ARTIFACT_SHA ERROR_VARIABLE DRAGONX_LITE_ARTIFACT_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" artifact sha256)
if(DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR OR DRAGONX_LITE_ARTIFACT_SHA_ERROR)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing artifact/signature SHA-256 metadata")
endif()
if(NOT DRAGONX_LITE_SIGNATURE_VERIFIED_SHA STREQUAL DRAGONX_LITE_ARTIFACT_SHA)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST signature metadata does not verify the artifact SHA-256")
endif()
string(JSON DRAGONX_LITE_SIGNATURE_PERFORMED ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_performed)
if(DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR OR NOT DRAGONX_LITE_SIGNATURE_PERFORMED)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verification_performed=true")
endif()
endif()
add_library(dragonx_lite_backend UNKNOWN IMPORTED)
set_target_properties(dragonx_lite_backend PROPERTIES
IMPORTED_LOCATION "${DRAGONX_LITE_BACKEND_LIBRARY}"
)
if(DRAGONX_LITE_BACKEND_INCLUDE_DIR)
if(NOT IS_DIRECTORY "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_INCLUDE_DIR does not exist: ${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
endif()
set_target_properties(dragonx_lite_backend PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}"
)
endif()
set(DRAGONX_LITE_BACKEND_READY ON)
endif()
include(CTest)
# Output directories
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
@@ -240,32 +105,6 @@ FetchContent_Declare(
)
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)
if(WIN32)
# For Windows cross-compilation, fetch and build libcurl statically
@@ -405,70 +244,22 @@ set(APP_SOURCES
src/app_network.cpp
src/app_security.cpp
src/app_wizard.cpp
src/services/network_refresh_service.cpp
src/services/refresh_scheduler.cpp
src/services/wallet_security_controller.cpp
src/services/wallet_security_workflow.cpp
src/services/wallet_security_workflow_executor.cpp
src/chat/chat_protocol.cpp
src/chat/chat_crypto.cpp
src/chat/chat_identity.cpp
src/chat/chat_store.cpp
src/chat/chat_service.cpp
src/chat/chat_database.cpp
src/chat/chat_outgoing.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_diagnostics.cpp
src/wallet/lite_wallet_controller.cpp
src/wallet/lite_result_parsers.cpp
src/wallet/lite_sync_service.cpp
src/wallet/lite_wallet_gateway.cpp
src/wallet/lite_wallet_state_mapper.cpp
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
src/wallet/lite_wallet_server_selection_adapter.cpp
src/wallet/lite_wallet_lifecycle_service.cpp
src/data/wallet_state.cpp
src/data/transaction_history_cache.cpp
src/ui/theme.cpp
src/ui/theme_loader.cpp
src/ui/explorer/explorer_block_cache.cpp
src/ui/material/color_theme.cpp
src/ui/material/typography.cpp
src/ui/notifications.cpp
src/ui/windows/main_window.cpp
src/ui/windows/balance_tab.cpp
src/ui/windows/balance_components.cpp
src/ui/windows/balance_address_list.cpp
src/ui/windows/balance_recent_tx.cpp
src/ui/windows/balance_tab_helpers.cpp
src/ui/windows/send_tab.cpp
src/ui/windows/receive_tab.cpp
src/ui/windows/transactions_tab.cpp
src/ui/windows/mining_tab.cpp
src/ui/windows/mining_earnings.cpp
src/ui/windows/mining_stats.cpp
src/ui/windows/mining_controls.cpp
src/ui/windows/mining_mode_toggle.cpp
src/ui/windows/mining_benchmark.cpp
src/ui/windows/mining_pool_panel.cpp
src/ui/windows/mining_tab_helpers.cpp
src/ui/windows/peers_tab.cpp
src/ui/windows/network_tab.cpp
src/ui/windows/explorer_tab.cpp
src/ui/windows/market_tab.cpp
src/ui/windows/console_tab.cpp
src/ui/windows/console_command_executor.cpp
src/ui/windows/console_command_reference.cpp
src/ui/windows/console_input_model.cpp
src/ui/windows/console_model.cpp
src/ui/windows/console_output_model.cpp
src/ui/windows/console_scroll_controller.cpp
src/ui/windows/console_selection_controller.cpp
src/ui/windows/console_tab_helpers.cpp
src/ui/windows/console_text_layout.cpp
src/ui/windows/settings_window.cpp
src/ui/pages/settings_page.cpp
src/ui/windows/about_dialog.cpp
@@ -476,13 +267,14 @@ set(APP_SOURCES
src/ui/windows/transaction_details_dialog.cpp
src/ui/windows/qr_popup_dialog.cpp
src/ui/windows/validate_address_dialog.cpp
src/ui/windows/contacts_tab.cpp
src/ui/windows/chat_tab.cpp
src/ui/windows/address_book_dialog.cpp
src/ui/windows/shield_dialog.cpp
src/ui/windows/request_payment_dialog.cpp
src/ui/windows/block_info_dialog.cpp
src/ui/windows/import_key_dialog.cpp
src/ui/windows/export_all_keys_dialog.cpp
src/ui/windows/export_transactions_dialog.cpp
src/ui/windows/backup_wallet_dialog.cpp
src/ui/widgets/qr_code.cpp
src/rpc/rpc_client.cpp
src/rpc/rpc_worker.cpp
@@ -491,31 +283,16 @@ set(APP_SOURCES
src/data/address_book.cpp
src/data/exchange_info.cpp
src/util/logger.cpp
src/util/async_task_manager.cpp
src/util/amount_format.cpp
src/util/address_validation.cpp
src/util/base64.cpp
src/util/single_instance.cpp
src/util/i18n.cpp
src/util/text_format.cpp
src/util/platform.cpp
src/util/payment_uri.cpp
src/util/texture_loader.cpp
src/util/noise_texture.cpp
src/daemon/embedded_daemon.cpp
src/daemon/seed_wallet_creator.cpp
src/daemon/daemon_controller.cpp
src/daemon/lifecycle_adapters.cpp
src/daemon/xmrig_manager.cpp
src/util/bootstrap.cpp
src/util/lite_server_probe.cpp
src/util/pool_registry_core.cpp
src/util/pool_stats_service.cpp
src/util/http_download.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
src/util/daemon_updater.cpp
src/util/daemon_updater_core.cpp
src/util/secure_vault.cpp
src/ui/effects/framebuffer.cpp
src/ui/effects/blur_shader.cpp
@@ -546,77 +323,34 @@ endif()
set(APP_HEADERS
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/wallet/wallet_capabilities.h
src/wallet/wallet_backend.h
src/wallet/lite_owned_string.h
src/wallet/lite_rollout_policy.h
src/wallet/lite_client_bridge.h
src/wallet/lite_connection_service.h
src/wallet/lite_result_parsers.h
src/wallet/lite_sync_service.h
src/wallet/lite_wallet_gateway.h
src/wallet/lite_wallet_state_mapper.h
src/wallet/lite_wallet_lifecycle_ui_adapter.h
src/wallet/lite_wallet_server_selection_adapter.h
src/wallet/lite_wallet_lifecycle_service.h
src/chat/chat_protocol.h
src/chat/chat_crypto.h
src/chat/chat_identity.h
src/chat/chat_message.h
src/chat/chat_store.h
src/chat/chat_service.h
src/chat/chat_database.h
src/chat/chat_outgoing.h
src/config/version.h
src/data/wallet_state.h
src/data/transaction_history_cache.h
src/ui/theme.h
src/ui/theme_loader.h
src/ui/explorer/explorer_block_cache.h
src/ui/notifications.h
src/ui/windows/main_window.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/receive_tab.h
src/ui/windows/transactions_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/explorer_tab.h
src/ui/windows/market_tab.h
src/ui/windows/console_channel.h
src/ui/windows/console_command_reference.h
src/ui/windows/console_input_model.h
src/ui/windows/console_model.h
src/ui/windows/console_output_model.h
src/ui/windows/console_scroll_controller.h
src/ui/windows/console_selection_controller.h
src/ui/windows/console_tab.h
src/ui/windows/console_tab_helpers.h
src/ui/windows/settings_window.h
src/ui/windows/about_dialog.h
src/ui/windows/key_export_dialog.h
src/ui/windows/transaction_details_dialog.h
src/ui/windows/qr_popup_dialog.h
src/ui/windows/validate_address_dialog.h
src/ui/windows/contacts_tab.h
src/ui/windows/chat_tab.h
src/ui/windows/contact_picker.h
src/ui/windows/address_book_dialog.h
src/ui/windows/shield_dialog.h
src/ui/windows/request_payment_dialog.h
src/ui/windows/block_info_dialog.h
src/ui/windows/import_key_dialog.h
src/ui/windows/export_all_keys_dialog.h
src/ui/windows/export_transactions_dialog.h
src/ui/windows/backup_wallet_dialog.h
src/ui/widgets/qr_code.h
src/rpc/rpc_client.h
src/rpc/rpc_worker.h
@@ -626,8 +360,6 @@ set(APP_HEADERS
src/data/address_book.h
src/data/exchange_info.h
src/util/logger.h
src/util/async_task_manager.h
src/util/amount_format.h
src/util/base64.h
src/util/single_instance.h
src/util/i18n.h
@@ -635,9 +367,6 @@ set(APP_HEADERS
src/util/payment_uri.h
src/util/secure_vault.h
src/daemon/embedded_daemon.h
src/daemon/seed_wallet_creator.h
src/daemon/daemon_controller.h
src/daemon/lifecycle_adapters.h
src/daemon/xmrig_manager.h
src/ui/effects/framebuffer.h
src/ui/effects/blur_shader.h
@@ -676,12 +405,10 @@ if(WIN32)
set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc)
endif()
# Generate version values from the single project(VERSION ...) declaration.
# Keep the build-specific app name in the build tree so full/lite configures do
# not rewrite a tracked source header.
# Generate version.h from the single project(VERSION ...) declaration
configure_file(
${CMAKE_SOURCE_DIR}/src/config/version.h.in
${CMAKE_BINARY_DIR}/generated/dragonx_generated_version.h
${CMAKE_SOURCE_DIR}/src/config/version.h
@ONLY
)
@@ -692,22 +419,6 @@ configure_file(
@ONLY
)
# INCBIN uses .incbin assembler directives that reference font files at
# assembly time — CMake doesn't track these implicit dependencies.
# Tell CMake that the generated source depends on the actual font binaries
# so a font file change triggers recompilation.
set_source_files_properties(
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
PROPERTIES OBJECT_DEPENDS
"${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-R.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/UbuntuMono-R.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
)
add_executable(ObsidianDragon
${APP_SOURCES}
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
@@ -718,8 +429,6 @@ add_executable(ObsidianDragon
${WIN_RC_FILE}
)
set_target_properties(ObsidianDragon PROPERTIES OUTPUT_NAME "${DRAGONX_BINARY_NAME}")
target_include_directories(ObsidianDragon PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/embedded
@@ -739,63 +448,10 @@ target_link_libraries(ObsidianDragon PRIVATE
SDL3::SDL3
nlohmann_json::nlohmann_json
tomlplusplus::tomlplusplus
sqlite3_amalgamation
${CURL_LIBRARIES}
${SODIUM_LIBRARY}
)
if(DRAGONX_LITE_BACKEND_READY)
target_link_libraries(ObsidianDragon PRIVATE dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS})
# Real-backend smoke tool (only built when a real lite backend is linked).
add_executable(lite_smoke
tools/lite_smoke.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_result_parsers.cpp
)
target_include_directories(lite_smoke PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/generated
${SODIUM_INCLUDE_DIR}
)
target_compile_definitions(lite_smoke PRIVATE DRAGONX_ENABLE_LITE_BACKEND=1)
target_link_libraries(lite_smoke PRIVATE
dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS}
nlohmann_json::nlohmann_json
${SODIUM_LIBRARY}
)
if(UNIX)
target_link_libraries(lite_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
endif()
# Real-backend SEND smoke tool — drives the exact GUI send path (bridge.execute("send", ...)).
add_executable(lite_send_smoke
tools/lite_send_smoke.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_result_parsers.cpp
)
target_include_directories(lite_send_smoke PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/generated
${SODIUM_INCLUDE_DIR}
)
target_compile_definitions(lite_send_smoke PRIVATE DRAGONX_ENABLE_LITE_BACKEND=1)
target_link_libraries(lite_send_smoke PRIVATE
dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS}
nlohmann_json::nlohmann_json
${SODIUM_LIBRARY}
)
if(UNIX)
target_link_libraries(lite_send_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
endif()
endif()
# Platform-specific settings
if(WIN32)
target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi iphlpapi d3d11 dxgi d3dcompiler dcomp)
@@ -824,10 +480,6 @@ endif()
# Compile definitions
target_compile_definitions(ObsidianDragon PRIVATE
DRAGONX_DEBUG
DRAGONX_LITE_BUILD=$<BOOL:${DRAGONX_BUILD_LITE}>
DRAGONX_ENABLE_EMBEDDED_DAEMON=$<BOOL:${DRAGONX_ENABLE_EMBEDDED_DAEMON}>
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
DRAGONX_ENABLE_CHAT=$<BOOL:${DRAGONX_ENABLE_CHAT}>
)
if(WIN32)
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11)
@@ -835,26 +487,6 @@ else()
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAS_GLAD)
endif()
add_executable(HushChatFixtureCheck
tools/hushchat_fixture_check.cpp
src/chat/chat_protocol.cpp
src/chat/chat_fixture_tooling.cpp
)
target_include_directories(HushChatFixtureCheck PRIVATE
${CMAKE_SOURCE_DIR}/src
${SODIUM_INCLUDE_DIR}
)
target_link_libraries(HushChatFixtureCheck PRIVATE
nlohmann_json::nlohmann_json
${SODIUM_LIBRARY}
)
target_compile_definitions(HushChatFixtureCheck PRIVATE
DRAGONX_ENABLE_CHAT=0
)
# -----------------------------------------------------------------------------
# Copy resources
# -----------------------------------------------------------------------------
@@ -878,12 +510,6 @@ endif()
# 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)
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})
get_filename_component(LANG_FILENAME ${LANG_FILE} NAME)
add_custom_command(
@@ -897,18 +523,16 @@ if(LANG_FILES)
list(APPEND LANG_OUTPUTS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME})
# Also regenerate the embedded header so the binary always has fresh translations
if(XXD_EXECUTABLE)
get_filename_component(LANG_CODE ${LANG_FILENAME} NAME_WE)
set(LANG_HEADER ${CMAKE_BINARY_DIR}/generated/embedded/lang_${LANG_CODE}.h)
add_custom_command(
OUTPUT ${LANG_HEADER}
COMMAND ${XXD_EXECUTABLE} -i "res/lang/${LANG_FILENAME}" > "${LANG_HEADER}"
DEPENDS ${LANG_FILE}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Embedding lang_${LANG_CODE}.h"
)
list(APPEND LANG_OUTPUTS ${LANG_HEADER})
endif()
get_filename_component(LANG_CODE ${LANG_FILENAME} NAME_WE)
set(LANG_HEADER ${CMAKE_SOURCE_DIR}/src/embedded/lang_${LANG_CODE}.h)
add_custom_command(
OUTPUT ${LANG_HEADER}
COMMAND xxd -i "res/lang/${LANG_FILENAME}" > "src/embedded/lang_${LANG_CODE}.h"
DEPENDS ${LANG_FILE}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Embedding lang_${LANG_CODE}.h"
)
list(APPEND LANG_OUTPUTS ${LANG_HEADER})
endforeach()
add_custom_target(copy_langs ALL DEPENDS ${LANG_OUTPUTS})
add_dependencies(ObsidianDragon copy_langs)
@@ -955,7 +579,6 @@ if(THEME_FILES AND Python3_EXECUTABLE)
message(STATUS " Theme files: ${THEME_FILES} (build-time expansion via Python)")
elseif(THEME_FILES)
# 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})
get_filename_component(THEME_FILENAME ${THEME_FILE} NAME)
add_custom_command(
@@ -1005,133 +628,20 @@ install(TARGETS ObsidianDragon
)
install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res
DESTINATION share/${DRAGONX_BINARY_NAME}
DESTINATION share/ObsidianDragon
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/chat/chat_protocol.cpp
src/chat/chat_crypto.cpp
src/chat/chat_identity.cpp
src/chat/chat_store.cpp
src/chat/chat_service.cpp
src/chat/chat_database.cpp
src/chat/chat_outgoing.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_diagnostics.cpp
src/wallet/lite_wallet_controller.cpp
src/wallet/lite_result_parsers.cpp
src/wallet/lite_sync_service.cpp
src/wallet/lite_wallet_gateway.cpp
src/wallet/lite_wallet_state_mapper.cpp
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
src/wallet/lite_wallet_server_selection_adapter.cpp
src/wallet/lite_wallet_lifecycle_service.cpp
src/ui/explorer/explorer_block_cache.cpp
src/ui/windows/balance_address_list.cpp
src/ui/windows/balance_recent_tx.cpp
src/ui/windows/console_input_model.cpp
src/ui/windows/console_model.cpp
src/ui/windows/console_output_model.cpp
src/ui/windows/console_scroll_controller.cpp
src/ui/windows/console_selection_controller.cpp
src/ui/windows/console_tab_helpers.cpp
src/ui/windows/console_text_layout.cpp
src/ui/windows/mining_benchmark.cpp
src/ui/windows/mining_pool_panel.cpp
src/ui/windows/mining_tab_helpers.cpp
src/util/payment_uri.cpp
src/util/amount_format.cpp
src/util/address_validation.cpp
src/util/i18n.cpp
src/util/text_format.cpp
src/data/wallet_state.cpp
src/data/transaction_history_cache.cpp
src/daemon/lifecycle_adapters.cpp
src/rpc/connection.cpp
src/config/settings.cpp
src/resources/embedded_resources.cpp
src/util/secure_vault.cpp
src/util/platform.cpp
src/util/logger.cpp
src/util/lite_server_probe.cpp
src/util/pool_registry_core.cpp
src/util/http_download.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
src/util/daemon_updater.cpp
src/util/daemon_updater_core.cpp
${MINIZ_SOURCES}
)
target_include_directories(ObsidianDragonTests PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/resources
${CMAKE_SOURCE_DIR}/libs
${CMAKE_BINARY_DIR}/generated
${IMGUI_DIR}
${SODIUM_INCLUDE_DIR}
${CURL_INCLUDE_DIRS}
${MINIZ_DIR}
)
target_link_libraries(ObsidianDragonTests PRIVATE
nlohmann_json::nlohmann_json
sqlite3_amalgamation
${SODIUM_LIBRARY}
${CURL_LIBRARIES}
)
target_compile_definitions(ObsidianDragonTests PRIVATE
DRAGONX_ENABLE_CHAT=$<BOOL:${DRAGONX_ENABLE_CHAT}>
DRAGONX_LITE_BUILD=$<BOOL:${DRAGONX_BUILD_LITE}>
DRAGONX_ENABLE_EMBEDDED_DAEMON=$<BOOL:${DRAGONX_ENABLE_EMBEDDED_DAEMON}>
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
DRAGONX_TEST_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures"
)
if(DRAGONX_LITE_BACKEND_READY)
target_link_libraries(ObsidianDragonTests PRIVATE dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS})
endif()
if(UNIX)
target_link_libraries(ObsidianDragonTests PRIVATE ${CMAKE_DL_LIBS})
endif()
add_test(NAME ObsidianDragonPhase4Tests COMMAND ObsidianDragonTests)
endif()
# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------
message(STATUS "")
message(STATUS "DragonX ImGui Wallet Configuration:")
message(STATUS " Version: ${DRAGONX_APP_VERSION}${DRAGONX_APP_VERSION_SUFFIX} (${DRAGONX_APP_NAME})")
message(STATUS " Version: ${PROJECT_VERSION}")
message(STATUS " Build type: ${CMAKE_BUILD_TYPE}")
message(STATUS " C++ Standard: ${CMAKE_CXX_STANDARD}")
message(STATUS " ImGui dir: ${IMGUI_DIR}")
message(STATUS " SDL3 found: ${SDL3_FOUND}")
message(STATUS " Sodium lib: ${SODIUM_LIBRARY}")
message(STATUS " Lite build: ${DRAGONX_BUILD_LITE}")
message(STATUS " Lite requested: ${DRAGONX_ENABLE_LITE_BACKEND}")
message(STATUS " Lite backend: ${DRAGONX_LITE_BACKEND_READY}")
message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}")
message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
message(STATUS " Lite manifest: ${DRAGONX_LITE_BACKEND_MANIFEST}")
message(STATUS " Lite signature: ${DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE}")
message(STATUS "")

View File

@@ -1,8 +1,6 @@
# ObsidianDragon - DragonX Wallet
# DragonX Wallet - ImGui Edition
A lightweight, portable full-node cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
Current pre-release: **1.2.0-rc1**.
A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
![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)
@@ -11,13 +9,10 @@ Current pre-release: **1.2.0-rc1**.
- **Full Node Support**: Connects to dragonxd for complete blockchain verification
- **Shielded Transactions**: Full z-address support with encrypted memos
- **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
- **Integrated Mining**: CPU mining controls with hashrate monitoring
- **Market Data**: Real-time price charts from CoinGecko
- **QR Codes**: Generate and display QR codes for receiving addresses
- **Multi-language**: i18n support for English, German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese
- **CJK Fonts**: Bundled CJK subset font for translated interfaces
- **Multi-language**: i18n support (English, Spanish, more coming)
- **Lightweight**: ~5-10MB binary vs ~50MB+ for Qt version
- **Fast Builds**: Compiles in seconds, not minutes
@@ -121,8 +116,7 @@ cd 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
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:
@@ -137,10 +131,9 @@ The wallet checks its **own directory first** when looking for DragonX node bina
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.
## Configuration
Configuration is stored in `~/.hush/DRAGONX/DRAGONX.conf`:
Configuration is stored in \`~/.hush/DRAGONX/DRAGONX.conf\`:
```
rpcuser=your_rpc_user
@@ -155,46 +148,44 @@ ObsidianDragon/
├── src/
│ ├── main.cpp # Entry point, SDL/ImGui setup
│ ├── app.cpp/h # Main application class
│ ├── data/ # WalletState, address book, exchange info
│ ├── config/ # Settings persistence and committed/generated version.h
│ ├── wallet_state.h # Wallet data structures
│ ├── version.h # Version definitions
│ ├── ui/
│ │ ├── schema/ # TOML UI schema and skin manager
│ │ ── material/ # Material components, typography, layout
│ │ ├── windows/ # Tabs and dialogs
│ │ └── pages/ # Multi-page screens such as Settings
│ │ ├── theme.cpp/h # DragonX theme
│ │ ── windows/ # UI tabs and dialogs
│ ├── rpc/
│ │ ├── rpc_client.cpp # JSON-RPC client
│ │ └── connection.cpp # Daemon connection
│ ├── resources/ # Embedded resource extraction
├── platform/ # Windows DX11/backdrop helpers
│ ├── config/
│ └── settings.cpp # Settings persistence
│ ├── util/
│ │ ├── i18n.cpp # Internationalization
│ │ └── ...
│ └── daemon/
│ └── embedded_daemon.cpp
├── res/
│ ├── fonts/ # Ubuntu, icon, and CJK fonts
│ ├── fonts/ # Ubuntu font
│ └── lang/ # Translation files
├── libs/
│ └── qrcode/ # QR code generation
├── CMakeLists.txt
├── build.sh # Release/cross-platform build script
└── scripts/create-appimage.sh # AppImage packaging
├── build-release.sh # Build script
└── create-appimage.sh # AppImage packaging
```
## Dependencies
Fetched or discovered by CMake:
Fetched automatically by CMake (no manual install needed):
- **[SDL3](https://github.com/libsdl-org/SDL)** — Cross-platform windowing/input
- **[nlohmann/json](https://github.com/nlohmann/json)** — JSON parsing
- **[toml++](https://github.com/marzer/tomlplusplus)** — TOML parsing (UI schema/themes)
- **[libcurl](https://curl.se/libcurl/)** — HTTP/HTTPS transport for daemon RPC and network calls (system on Linux/macOS, fetched on Windows)
- **[libcurl](https://curl.se/libcurl/)** — HTTPS RPC transport (system on Linux, fetched on Windows)
Bundled in `libs/`:
- **[Dear ImGui](https://github.com/ocornut/imgui)** — Immediate mode GUI
- **[libsodium](https://libsodium.org)** — Cryptographic operations (system on Linux or fetched by `scripts/fetch-libsodium.sh`)
- **[libsodium](https://libsodium.org)** — Cryptographic operations (fetched by `scripts/fetch-libsodium.sh`)
- **[QR-Code-generator](https://github.com/nayuki/QR-Code-generator)** — QR code rendering
- **[miniz](https://github.com/richgel999/miniz)** — ZIP compression
- **[GLAD](https://glad.dav1d.de/)** — OpenGL loader (Linux/macOS)
@@ -211,11 +202,9 @@ Bundled in `libs/`:
## 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:
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
3. The language will appear in Settings automatically

View File

@@ -361,22 +361,7 @@ https://www.apache.org/licenses/LICENSE-2.0
---
## 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
## 13. IconFontCppHeaders
- **Location:** `src/embedded/IconsMaterialDesign.h`
- **Source:** https://github.com/juliettef/IconFontCppHeaders
@@ -405,7 +390,7 @@ freely, subject to the following restrictions:
---
## 15. Ubuntu Font Family
## 14. Ubuntu Font Family
- **Location:** `res/fonts/Ubuntu-Light.ttf`, `Ubuntu-Medium.ttf`, `Ubuntu-R.ttf`
- **Source:** https://design.ubuntu.com/font

437
build.sh
View File

@@ -20,9 +20,7 @@
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# VERSION is resolved per-variant from CMakeLists.txt (the single source of truth) after arg
# parsing — see the APP_BASENAME block below. Placeholder until then.
VERSION=""
VERSION="1.1.1"
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
@@ -43,8 +41,6 @@ DO_DEV=false
DO_LINUX=false
DO_WIN=false
DO_MAC=false
DO_LITE=false
DO_LITE_BACKEND=false
CLEAN=false
BUILD_TYPE="Release"
@@ -58,10 +54,6 @@ Targets (at least one required, or none for dev build):
--linux-release Linux release (zip + AppImage) -> release/linux/
--win-release Windows cross-compile (mingw-w64) -> release/windows/
--mac-release macOS .app bundle + DMG -> release/mac/
--lite Build ObsidianDragonLite variant (no embedded daemon/full-node features)
--lite-backend Like --lite, and link the real SDXL litelib backend artifact
(auto-discovers build/lite-backend/<platform>/; build it with
scripts/build-lite-backend-artifact.sh, or set DRAGONX_LITE_BACKEND_DIR)
Build trees are stored under build/{linux,windows,mac}/
@@ -82,7 +74,6 @@ Examples:
$0 --linux-release # Linux release (zip + AppImage)
$0 --win-release # Windows cross-compile
$0 --mac-release # macOS bundle + DMG (native or osxcross)
$0 --lite-backend --mac-release # macOS ObsidianDragonLite.app + DMG (lite backend)
$0 --clean --linux-release --win-release # Clean + both
EOF
exit 0
@@ -94,8 +85,6 @@ while [[ $# -gt 0 ]]; do
--linux-release) DO_LINUX=true; shift ;;
--win-release) DO_WIN=true; shift ;;
--mac-release) DO_MAC=true; shift ;;
--lite) DO_LITE=true; shift ;;
--lite-backend) DO_LITE=true; DO_LITE_BACKEND=true; shift ;;
-c|--clean) CLEAN=true; shift ;;
-d|--debug) BUILD_TYPE="Debug"; shift ;;
-j) JOBS="$2"; shift 2 ;;
@@ -109,92 +98,6 @@ if ! $DO_LINUX && ! $DO_WIN && ! $DO_MAC; then
DO_DEV=true
fi
APP_BASENAME="ObsidianDragon"
CMAKE_LITE_ARGS=()
# Always set the variant flag EXPLICITLY (ON and OFF) so switching variants in a shared build dir
# can't reuse a stale cached value (e.g. a prior --lite build leaving DRAGONX_BUILD_LITE=ON).
if $DO_LITE; then
APP_BASENAME="ObsidianDragonLite"
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=ON")
# Lite never embeds/launches a daemon; set it explicitly too for cache hygiene.
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=OFF")
info "Lite mode enabled: building ${APP_BASENAME}"
else
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=OFF")
# Re-assert the embedded daemon ON for full-node builds, EXPLICITLY, so a build dir whose cache
# was poisoned OFF by a prior --lite configure (or any stale value) is healed — otherwise the
# full-node app extracts dragonxd but never launches it (isUsingEmbeddedDaemon() == false).
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=ON")
fi
# Resolve the release version string for the active variant from CMakeLists.txt (single source of
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
_cml="$SCRIPT_DIR/CMakeLists.txt"
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
_full_suffix=$(sed -n 's/^set(DRAGONX_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
_lite_ver=$(sed -n 's/^set(DRAGONX_LITE_VERSION[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
_lite_suffix=$(sed -n 's/^set(DRAGONX_LITE_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
if $DO_LITE; then
VERSION="${_lite_ver}${_lite_suffix}"
else
VERSION="${_full_ver}${_full_suffix}"
fi
[ -n "$_full_ver" ] && [ -n "$VERSION" ] || { err "Could not parse version from CMakeLists.txt"; exit 1; }
info "Release version: ${VERSION} (${APP_BASENAME})"
# ── Lite backend (real SDXL litelib) linking ─────────────────────────────────
# Enables DRAGONX_ENABLE_LITE_BACKEND with an imported artifact produced by
# scripts/build-lite-backend-artifact.sh. Auto-discovers build/lite-backend/<platform>/;
# override the directory with DRAGONX_LITE_BACKEND_DIR.
if $DO_LITE_BACKEND; then
# Artifact platform follows the cross target when exactly one non-host release is requested,
# so `--lite-backend --win-release` links the Windows backend (not the host's) automatically.
case "$(uname -s)" in
Linux) lb_platform="linux" ;;
Darwin) lb_platform="macos" ;;
*) lb_platform="linux" ;;
esac
if $DO_WIN && ! $DO_LINUX && ! $DO_MAC; then lb_platform="windows"; fi
if $DO_MAC && ! $DO_LINUX && ! $DO_WIN; then lb_platform="macos"; fi
lb_dir="${DRAGONX_LITE_BACKEND_DIR:-$SCRIPT_DIR/build/lite-backend/$lb_platform}"
lb_lib=""
for cand in "$lb_dir"/libsilentdragonxlite.a "$lb_dir"/libsilentdragonxlite.so "$lb_dir"/silentdragonxlite.lib; do
[[ -f "$cand" ]] && { lb_lib="$cand"; break; }
done
lb_symbols="$lb_dir/lite-backend-symbols.txt"
lb_manifest="$lb_dir/lite-backend-artifact-manifest.json"
if [[ -z "$lb_lib" || ! -f "$lb_symbols" ]]; then
err "Lite backend artifact not found under: $lb_dir"
err "Build it first: ./scripts/build-lite-backend-artifact.sh --platform $lb_platform"
err "Or set DRAGONX_LITE_BACKEND_DIR to an existing artifact directory."
exit 1
fi
CMAKE_LITE_ARGS+=(
"-DDRAGONX_ENABLE_LITE_BACKEND=ON"
"-DDRAGONX_LITE_BACKEND_LIBRARY=$lb_lib"
"-DDRAGONX_LITE_BACKEND_SYMBOLS_FILE=$lb_symbols"
"-DDRAGONX_LITE_BACKEND_LINK_MODE=imported"
"-DDRAGONX_LITE_BACKEND_ABI=sdxl-c-v1"
)
[[ -f "$lb_manifest" ]] && CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_MANIFEST=$lb_manifest")
# A Rust x86_64-pc-windows-gnu staticlib pulls in Win32 system libs (rustls/schannel, ring,
# dirs, std) that the app doesn't already link. The set is rustc's `--print native-static-libs`
# for the backend (winapi_* shims mapped to the real mingw import libs); all exist in mingw-w64.
if [[ "$lb_platform" == "windows" ]]; then
CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_EXTRA_LIBS=advapi32;ws2_32;kernel32;bcrypt;cfgmgr32;credui;crypt32;cryptnet;fwpuclnt;gdi32;msimg32;ncrypt;ntdll;ole32;opengl32;secur32;shell32;synchronization;user32;winspool;userenv")
fi
info "Lite backend enabled ($lb_platform): $lb_lib"
else
# Explicit OFF so a prior --lite-backend configure in a shared build dir can't leave it ON
# (which would then fail the BUILD_LITE=OFF guard in CMake).
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_LITE_BACKEND=OFF")
fi
should_bundle_full_node_assets() {
! $DO_LITE
}
# ── Helper: find resource files ──────────────────────────────────────────────
find_sapling_params() {
local dirs=(
@@ -312,14 +215,13 @@ build_dev() {
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=ON \
"${CMAKE_LITE_ARGS[@]}"
-DDRAGONX_USE_SYSTEM_SDL3=ON
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/${APP_BASENAME}" ]] || { err "Build failed"; exit 1; }
info "Dev binary: $bd/bin/${APP_BASENAME} ($(du -h "bin/${APP_BASENAME}" | cut -f1))"
[[ -f "bin/ObsidianDragon" ]] || { err "Build failed"; exit 1; }
info "Dev binary: $bd/bin/ObsidianDragon ($(du -h bin/ObsidianDragon | cut -f1))"
}
# ═══════════════════════════════════════════════════════════════════════════════
@@ -340,51 +242,42 @@ build_release_linux() {
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=ON \
"${CMAKE_LITE_ARGS[@]}"
-DDRAGONX_USE_SYSTEM_SDL3=ON
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; }
[[ -f "bin/ObsidianDragon" ]] || { err "Linux build failed"; exit 1; }
info "Stripping ..."
strip "bin/${APP_BASENAME}"
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
strip bin/ObsidianDragon
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
if should_bundle_full_node_assets; then
# ── Bundle daemon ────────────────────────────────────────────────────
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
# ── Bundle daemon ────────────────────────────────────────────────────────
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
# ── Bundle Sapling params ────────────────────────────────────────────
SAPLING_SPEND="" SAPLING_OUTPUT=""
find_sapling_params && {
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
info "Bundled Sapling params"
} || warn "Sapling params not found — not bundled"
else
info "Lite mode: skipping daemon and Sapling/asmap bundling"
fi
# ── Bundle Sapling params ────────────────────────────────────────────────
SAPLING_SPEND="" SAPLING_OUTPUT=""
find_sapling_params && {
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
info "Bundled Sapling params"
} || warn "Sapling params not found — not bundled"
# ── Package: release/linux/ ──────────────────────────────────────────────
# Remove only THIS variant's prior artifacts so full-node and lite releases can coexist in the
# same output dir (both ObsidianDragon* and ObsidianDragonLite* end up under release/linux/).
rm -rf "$out"
mkdir -p "$out"
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.AppImage"
local DIST="${APP_BASENAME}-${VERSION}-Linux-x64"
local DIST="ObsidianDragon-${VERSION}-Linux-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp "bin/${APP_BASENAME}" "$dist_dir/"
if should_bundle_full_node_assets; then
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
fi
cp bin/ObsidianDragon "$dist_dir/"
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
# Bundle xmrig for mining support
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
@@ -406,29 +299,27 @@ build_release_linux() {
"$APPDIR/usr/share/icons/hicolor/256x256/apps" \
"$APPDIR/usr/share/ObsidianDragon/res"
cp "bin/${APP_BASENAME}" "$APPDIR/usr/bin/"
cp bin/ObsidianDragon "$APPDIR/usr/bin/"
cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true
if should_bundle_full_node_assets; then
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$APPDIR/usr/bin/"
# Daemon data files must be alongside the daemon binary (usr/bin/)
# because dragonxd searches relative to its own directory.
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
fi
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$APPDIR/usr/bin/"
# Daemon data files must be alongside the daemon binary (usr/bin/)
# because dragonxd searches relative to its own directory.
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
# Bundle xmrig for mining support
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
# Desktop entry
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<DESK
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<'DESK'
[Desktop Entry]
Type=Application
Name=DragonX Wallet
Comment=DragonX Cryptocurrency Wallet
Exec=${APP_BASENAME}
Exec=ObsidianDragon
Icon=ObsidianDragon
Categories=Finance;Network;
Terminal=false
@@ -459,14 +350,14 @@ SVG
cp "$APPDIR/ObsidianDragon.svg" "$APPDIR/ObsidianDragon.png" 2>/dev/null || true
# AppRun
cat > "$APPDIR/AppRun" <<APPRUN
cat > "$APPDIR/AppRun" <<'APPRUN'
#!/bin/bash
SELF=\$(readlink -f "\$0")
HERE=\${SELF%/*}
export DRAGONX_RES_PATH="\${HERE}/usr/share/ObsidianDragon/res"
export LD_LIBRARY_PATH="\${HERE}/usr/lib:\${LD_LIBRARY_PATH}"
cd "\${HERE}/usr/share/ObsidianDragon"
exec "\${HERE}/usr/bin/${APP_BASENAME}" "\$@"
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
export DRAGONX_RES_PATH="${HERE}/usr/share/ObsidianDragon/res"
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}"
cd "${HERE}/usr/share/ObsidianDragon"
exec "${HERE}/usr/bin/ObsidianDragon" "$@"
APPRUN
chmod +x "$APPDIR/AppRun"
@@ -495,9 +386,9 @@ APPRUN
local ARCH
ARCH=$(uname -m)
cd "$bd"
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
cp "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" "$out/${APP_BASENAME}-${VERSION}.AppImage"
info "AppImage: $out/${APP_BASENAME}-${VERSION}.AppImage ($(du -h "$out/${APP_BASENAME}-${VERSION}.AppImage" | cut -f1))"
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
cp "ObsidianDragon-${VERSION}-${ARCH}.AppImage" "$out/ObsidianDragon-${VERSION}.AppImage"
info "AppImage: $out/ObsidianDragon-${VERSION}.AppImage ($(du -h "$out/ObsidianDragon-${VERSION}.AppImage" | cut -f1))"
} || warn "AppImage creation failed — binaries zip still in release/linux/"
info "Linux release artifacts: $out/"
@@ -606,48 +497,26 @@ HDR
# ── Daemon binaries ──────────────────────────────────────────────
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
if should_bundle_full_node_assets; then
if [[ -d "$DD" && -f "$DD/dragonxd.exe" ]]; then
info "Embedding daemon binaries ..."
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
local sym=$(echo "$f" | sed 's/[^a-zA-Z0-9]/_/g')
if [[ -f "$DD/$f" ]]; then
cp -f "$DD/$f" "$RES/$f"
info " Staged $f ($(du -h "$DD/$f" | cut -f1))"
echo "INCBIN(${sym}, \"$RES/$f\");" >> "$GEN/embedded_data.h"
else
echo "extern \"C\" { static const uint8_t* g_${sym}_data = nullptr; }" >> "$GEN/embedded_data.h"
echo "static const unsigned int g_${sym}_size = 0;" >> "$GEN/embedded_data.h"
fi
done
else
warn "prebuilt-binaries/dragonxd-win/ not found — wallet-only build"
fi
if [[ -d "$DD" && -f "$DD/dragonxd.exe" ]]; then
info "Embedding daemon binaries ..."
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
local sym=$(echo "$f" | sed 's/[^a-zA-Z0-9]/_/g')
if [[ -f "$DD/$f" ]]; then
cp -f "$DD/$f" "$RES/$f"
info " Staged $f ($(du -h "$DD/$f" | cut -f1))"
echo "INCBIN(${sym}, \"$RES/$f\");" >> "$GEN/embedded_data.h"
else
echo "extern \"C\" { static const uint8_t* g_${sym}_data = nullptr; }" >> "$GEN/embedded_data.h"
echo "static const unsigned int g_${sym}_size = 0;" >> "$GEN/embedded_data.h"
fi
done
else
info "Lite mode: skipping embedded daemon binaries"
warn "prebuilt-binaries/dragonxd-win/ not found — wallet-only build"
fi
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
# The published DRG-XMRig archives ship the binary inside a versioned subdir, not as a flat
# xmrig.exe. Extract it from the matching win-x64 zip if it isn't already staged — otherwise
# the embed below never fires (HAS_EMBEDDED_XMRIG stays undefined) and the wallet ships with
# no miner ("xmrig binary not found" at runtime).
if [[ ! -f "$XMRIG_DIR/xmrig.exe" ]]; then
local _xz; _xz=$(ls "$XMRIG_DIR"/drg-xmrig-*-win-x64.zip 2>/dev/null | head -1)
if [[ -n "$_xz" ]] && command -v unzip >/dev/null 2>&1; then
local _xtmp; _xtmp=$(mktemp -d)
# -j flattens the versioned subdir; check the file (not unzip's exit code, which is
# non-zero if a pattern matches nothing).
unzip -j -o "$_xz" '*xmrig.exe' -d "$_xtmp" >/dev/null 2>&1 || true
if [[ -f "$_xtmp/xmrig.exe" ]]; then
cp -f "$_xtmp/xmrig.exe" "$XMRIG_DIR/xmrig.exe"
info " Extracted xmrig.exe from $(basename "$_xz")"
fi
rm -rf "$_xtmp"
fi
fi
if [[ -f "$XMRIG_DIR/xmrig.exe" ]]; then
cp -f "$XMRIG_DIR/xmrig.exe" "$RES/xmrig.exe"
info " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
@@ -730,40 +599,34 @@ HDR
cmake "$SCRIPT_DIR" \
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
"${CMAKE_LITE_ARGS[@]}"
-DDRAGONX_USE_SYSTEM_SDL3=OFF
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; }
info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)"
[[ -f "bin/ObsidianDragon.exe" ]] || { err "Windows build failed"; exit 1; }
info "Binary: $(du -h bin/ObsidianDragon.exe | cut -f1)"
# ── Package: release/windows/ ────────────────────────────────────────────
# Remove only THIS variant's prior artifacts so full-node and lite releases coexist here.
rm -rf "$out"
mkdir -p "$out"
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.exe"
local DIST="${APP_BASENAME}-${VERSION}-Windows-x64"
local DIST="ObsidianDragon-${VERSION}-Windows-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp "bin/${APP_BASENAME}.exe" "$dist_dir/"
cp bin/ObsidianDragon.exe "$dist_dir/"
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
if should_bundle_full_node_assets; then
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
# Bundle Sapling params + asmap for the zip distribution
# (The single-file exe has these embedded via INCBIN, but the zip
# needs them on disk so the daemon can find them in its work dir.)
for f in sapling-spend.params sapling-output.params asmap.dat; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
else
info "Lite mode: skipping daemon and Sapling/asmap assets in Windows zip"
fi
# Bundle Sapling params + asmap for the zip distribution
# (The single-file exe has these embedded via INCBIN, but the zip
# needs them on disk so the daemon can find them in its work dir.)
for f in sapling-spend.params sapling-output.params asmap.dat; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
# Bundle xmrig for mining support
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
@@ -772,8 +635,8 @@ HDR
cp -r bin/res "$dist_dir/" 2>/dev/null || true
# ── Single-file exe (all resources embedded) ────────────────────────────
cp "bin/${APP_BASENAME}.exe" "$out/${APP_BASENAME}-${VERSION}.exe"
info "Single-file exe: $out/${APP_BASENAME}-${VERSION}.exe ($(du -h "$out/${APP_BASENAME}-${VERSION}.exe" | cut -f1))"
cp bin/ObsidianDragon.exe "$out/ObsidianDragon-${VERSION}.exe"
info "Single-file exe: $out/ObsidianDragon-${VERSION}.exe ($(du -h "$out/ObsidianDragon-${VERSION}.exe" | cut -f1))"
# ── Zip ──────────────────────────────────────────────────────────────────
if command -v zip &>/dev/null; then
@@ -955,8 +818,7 @@ TOOLCHAIN
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} \
"${CMAKE_LITE_ARGS[@]}"
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"}
else
# Build libsodium as universal if needed
local need_sodium=false
@@ -982,40 +844,39 @@ TOOLCHAIN
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
"${CMAKE_LITE_ARGS[@]}"
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
fi
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/${APP_BASENAME}" ]] || { err "macOS build failed"; exit 1; }
[[ -f "bin/ObsidianDragon" ]] || { err "macOS build failed"; exit 1; }
# Strip — use osxcross strip for cross-builds
if $IS_CROSS; then
local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip"
if [[ -x "$STRIP_CMD" ]]; then
info "Stripping (osxcross) ..."
"$STRIP_CMD" "bin/${APP_BASENAME}"
"$STRIP_CMD" bin/ObsidianDragon
else
warn "osxcross strip not found at $STRIP_CMD — skipping"
fi
else
info "Stripping ..."
strip "bin/${APP_BASENAME}"
strip bin/ObsidianDragon
# Verify universal binary
if command -v lipo &>/dev/null; then
info "Architecture info:"
lipo -info "bin/${APP_BASENAME}"
lipo -info bin/ObsidianDragon
fi
fi
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
# ── Create .app bundle ───────────────────────────────────────────────────
rm -rf "$out"
mkdir -p "$out"
local APP="$out/${APP_BASENAME}.app"
local APP="$out/ObsidianDragon.app"
local CONTENTS="$APP/Contents"
local MACOS="$CONTENTS/MacOS"
local RESOURCES="$CONTENTS/Resources"
@@ -1024,43 +885,39 @@ TOOLCHAIN
mkdir -p "$MACOS" "$RESOURCES/res" "$FRAMEWORKS"
# Main binary
cp "bin/${APP_BASENAME}" "$MACOS/"
chmod +x "$MACOS/${APP_BASENAME}"
cp bin/ObsidianDragon "$MACOS/"
chmod +x "$MACOS/ObsidianDragon"
# Resources
cp -r bin/res/* "$RESOURCES/res/" 2>/dev/null || true
if should_bundle_full_node_assets; then
# Daemon binaries (macOS native, from dragonxd-mac/)
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
if [[ -d "$daemon_dir" ]]; then
for f in dragonxd dragonx-cli dragonx-tx; do
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; chmod +x "$MACOS/$f"; info " Bundled $f"; }
done
for f in sapling-spend.params sapling-output.params; do
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; info " Bundled $f"; }
done
elif ! $IS_CROSS; then
# Native macOS: try standard paths
local daemon_paths=(
"$SCRIPT_DIR/../dragonxd"
"$HOME/dragonx/src/dragonxd"
)
for p in "${daemon_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonxd"; chmod +x "$MACOS/dragonxd"; info " Bundled dragonxd"; break; }
done
local cli_paths=(
"$SCRIPT_DIR/../dragonx-cli"
"$HOME/dragonx/src/dragonx-cli"
)
for p in "${cli_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonx-cli"; chmod +x "$MACOS/dragonx-cli"; info " Bundled dragonx-cli"; break; }
done
else
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
fi
# Daemon binaries (macOS native, from dragonxd-mac/)
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
if [[ -d "$daemon_dir" ]]; then
for f in dragonxd dragonx-cli dragonx-tx; do
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; chmod +x "$MACOS/$f"; info " Bundled $f"; }
done
for f in sapling-spend.params sapling-output.params; do
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; info " Bundled $f"; }
done
elif ! $IS_CROSS; then
# Native macOS: try standard paths
local daemon_paths=(
"$SCRIPT_DIR/../dragonxd"
"$HOME/dragonx/src/dragonxd"
)
for p in "${daemon_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonxd"; chmod +x "$MACOS/dragonxd"; info " Bundled dragonxd"; break; }
done
local cli_paths=(
"$SCRIPT_DIR/../dragonx-cli"
"$HOME/dragonx/src/dragonx-cli"
)
for p in "${cli_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonx-cli"; chmod +x "$MACOS/dragonx-cli"; info " Bundled dragonx-cli"; break; }
done
else
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
fi
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
@@ -1073,13 +930,11 @@ TOOLCHAIN
warn "xmrig not found — mining unavailable in .app"
fi
if should_bundle_full_node_assets; then
# asmap.dat — placed in MacOS/ so the daemon finds it next to its binary
find_asmap 2>/dev/null && {
cp "$ASMAP_DAT" "$MACOS/asmap.dat"
info " Bundled asmap.dat"
}
fi
# asmap.dat — placed in MacOS/ so the daemon finds it next to its binary
find_asmap 2>/dev/null && {
cp "$ASMAP_DAT" "$MACOS/asmap.dat"
info " Bundled asmap.dat"
}
# Bundle SDL3 dylib
local sdl_dylib=""
@@ -1099,34 +954,26 @@ TOOLCHAIN
# Fix the rpath so the binary finds SDL3 in Frameworks/
if $IS_CROSS; then
local INSTALL_NAME_TOOL="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-install_name_tool"
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/ObsidianDragon" 2>/dev/null || true
else
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/ObsidianDragon" 2>/dev/null || true
fi
info " Bundled $sdl_name"
fi
# Launcher script (ensures working dir + dylib path). Uses ${APP_BASENAME} so the lite
# variant (ObsidianDragonLite) gets a correctly-named launcher + .bin pair.
mv "$MACOS/${APP_BASENAME}" "$MACOS/${APP_BASENAME}.bin"
cat > "$MACOS/${APP_BASENAME}" <<LAUNCH
# Launcher script (ensures working dir + dylib path)
mv "$MACOS/ObsidianDragon" "$MACOS/ObsidianDragon.bin"
cat > "$MACOS/ObsidianDragon" <<'LAUNCH'
#!/bin/bash
DIR="\$(cd "\$(dirname "\$0")" && pwd)"
export DYLD_LIBRARY_PATH="\$DIR/../Frameworks:\$DYLD_LIBRARY_PATH"
export DRAGONX_RES_PATH="\$DIR/../Resources/res"
cd "\$DIR/../Resources"
exec "\$DIR/${APP_BASENAME}.bin" "\$@"
DIR="$(cd "$(dirname "$0")" && pwd)"
export DYLD_LIBRARY_PATH="$DIR/../Frameworks:$DYLD_LIBRARY_PATH"
export DRAGONX_RES_PATH="$DIR/../Resources/res"
cd "$DIR/../Resources"
exec "$DIR/ObsidianDragon.bin" "$@"
LAUNCH
chmod +x "$MACOS/${APP_BASENAME}"
chmod +x "$MACOS/ObsidianDragon"
# Info.plist — display name + bundle id differ per variant so lite and full-node .apps
# can coexist; the executable matches the launcher (${APP_BASENAME}); the icon is shared.
local APP_DISPLAY_NAME="DragonX Wallet"
local APP_BUNDLE_ID="is.hush.dragonx"
if $DO_LITE; then
APP_DISPLAY_NAME="DragonX Wallet Lite"
APP_BUNDLE_ID="is.hush.dragonx.lite"
fi
# Info.plist
cat > "$CONTENTS/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
@@ -1134,17 +981,17 @@ LAUNCH
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>${APP_DISPLAY_NAME}</string>
<string>DragonX Wallet</string>
<key>CFBundleDisplayName</key>
<string>${APP_DISPLAY_NAME}</string>
<string>DragonX Wallet</string>
<key>CFBundleIdentifier</key>
<string>${APP_BUNDLE_ID}</string>
<string>is.hush.dragonx</string>
<key>CFBundleVersion</key>
<string>${VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION}</string>
<key>CFBundleExecutable</key>
<string>${APP_BASENAME}</string>
<string>ObsidianDragon</string>
<key>CFBundleIconFile</key>
<string>ObsidianDragon</string>
<key>CFBundlePackageType</key>
@@ -1206,27 +1053,25 @@ PLIST
info ".app bundle created: $APP"
# ── Zip the .app bundle ──────────────────────────────────────────────────
local APP_ZIP="${APP_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.app.zip"
local APP_ZIP="ObsidianDragon-${VERSION}-macOS-${MAC_ARCH}.app.zip"
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$APP_ZIP" "${APP_BASENAME}.app")
(cd "$out" && zip -r "$APP_ZIP" "ObsidianDragon.app")
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
fi
# ── Create DMG ───────────────────────────────────────────────────────────
local DMG_BASENAME="DragonX_Wallet"
$DO_LITE && DMG_BASENAME="DragonX_Wallet_Lite"
local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg"
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"
if command -v create-dmg &>/dev/null; then
# create-dmg (works on macOS; also available on Linux via npm)
info "Creating DMG with create-dmg ..."
create-dmg \
--volname "${APP_DISPLAY_NAME}" \
--volname "DragonX Wallet" \
--volicon "$RESOURCES/ObsidianDragon.icns" \
--window-pos 200 120 \
--window-size 600 400 \
--icon-size 100 \
--icon "${APP_BASENAME}.app" 150 190 \
--icon "ObsidianDragon.app" 150 190 \
--app-drop-link 450 190 \
--no-internet-enable \
"$out/$DMG_NAME" \
@@ -1241,7 +1086,7 @@ PLIST
mkdir -p "$staging"
cp -a "$APP" "$staging/"
ln -s /Applications "$staging/Applications"
hdiutil create -volname "${APP_DISPLAY_NAME}" \
hdiutil create -volname "DragonX Wallet" \
-srcfolder "$staging" \
-ov -format UDZO \
"$out/$DMG_NAME" 2>/dev/null && {
@@ -1257,7 +1102,7 @@ PLIST
cp -a "$APP" "$staging/"
# Can't create a real symlink to /Applications in an ISO, but the .app
# is the important part — users drag it to Applications manually.
genisoimage -V "${APP_DISPLAY_NAME}" \
genisoimage -V "DragonX Wallet" \
-D -R -apple -no-pad \
-o "$out/$DMG_NAME" \
"$staging" 2>/dev/null && {

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -43,8 +43,6 @@
"address_url": "Adress-URL",
"addresses_appear_here": "Ihre Empfangsadressen erscheinen hier, sobald Sie verbunden sind.",
"advanced": "ERWEITERT",
"advanced_effects": "Erweiterte Effekte...",
"ago": "her",
"all_filter": "Alle",
"allow_custom_fees": "Benutzerdefinierte Gebühren erlauben",
"amount": "Betrag",
@@ -92,52 +90,12 @@
"block_timestamp": "Zeitstempel:",
"block_transactions": "Transaktionen:",
"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",
"characters": "Zeichen",
"chat": "Chat",
"chat_cancel": "Abbrechen",
"chat_contact_request": "kontaktanfrage",
"chat_empty_hint": "Noch keine Unterhaltungen. Nachrichten, die du erhältst, erscheinen hier.",
"chat_locked_hint": "Entsperre deine Wallet, um deine Chats zu laden.",
"chat_new_button": "Neue Unterhaltung",
"chat_new_message": "Nachricht",
"chat_new_send": "Anfrage senden",
"chat_new_title": "Neue Unterhaltung",
"chat_new_zaddr": "z-Adresse des Empfängers",
"chat_select_hint": "Wähle eine Unterhaltung aus, um sie anzuzeigen.",
"chat_send": "Senden",
"chat_send_failed": "nicht gesendet",
"chat_toast_compose_failed": "Nachricht konnte nicht erstellt werden (zu lang?).",
"chat_toast_lite_busy": "Es wird bereits gesendet, oder es ist keine Wallet geöffnet.",
"chat_toast_no_zaddr": "Keine z-Adresse verfügbar, um den Chat zu senden.",
"chat_toast_not_connected": "Nicht verbunden Nachricht nicht gesendet.",
"chat_toast_request_compose_failed": "Kontaktanfrage konnte nicht erstellt werden (ungültige Adresse / ungültiger Text?).",
"chat_toast_request_queued": "Kontaktanfrage in Warteschlange.",
"chat_toast_waiting_reply": "Warte auf die Antwort des Kontakts, bevor du ihm schreiben kannst.",
"chat_waiting_reply": "Warte auf die Antwort dieses Kontakts sobald er antwortet, kannst du ihm schreiben.",
"chat_you": "Du",
"choose_icon": "Symbol wählen",
"clear": "Leeren",
"clear_all_bans": "Alle Sperren aufheben",
"clear_anyway": "Trotzdem löschen",
"clear_form_confirm": "Alle Formularfelder leeren?",
"clear_icon": "Symbol entfernen",
"clear_request": "Anfrage leeren",
"click_copy_address": "Klicken zum Kopieren der Adresse",
"click_copy_uri": "Klicken zum Kopieren der URI",
@@ -148,12 +106,8 @@
"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_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_transaction": "Transaktion bestätigen",
"confirm_transfer": "Überweisung bestätigen",
"confirmations": "Bestätigungen",
"confirmations_display": "%d Bestätigungen | %s",
"confirmed": "Bestätigt",
@@ -218,7 +172,6 @@
"console_welcome": "Willkommen bei ObsidianDragon Konsole",
"console_zoom_in": "Vergrößern",
"console_zoom_out": "Verkleinern",
"copied": "Kopiert!",
"copy": "Kopieren",
"copy_address": "Vollständige Adresse kopieren",
"copy_error": "Fehler kopieren",
@@ -227,45 +180,22 @@
"copy_uri": "URI kopieren",
"current_price": "Aktueller Preis",
"custom_fees": "Benutzerdefinierte Gebühren",
"daemon_version": "Daemon",
"dark": "Dunkel",
"date": "Datum",
"date_label": "Datum:",
"debug_logging": "FEHLERPROTOKOLLIERUNG",
"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",
"disconnected": "Getrennt",
"dismiss": "Verwerfen",
"display": "Anzeige",
"download": "Herunterladen",
"download_bootstrap": "Bootstrap herunterladen",
"dragonx_green": "DragonX (Grün)",
"edit": "Bearbeiten",
"error": "Fehler",
"error_format": "Fehler: %s",
"est_time_to_block": "Gesch. Zeit bis Block",
"exit": "Beenden",
"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",
"explorer": "EXPLORER",
"export": "Exportieren",
"export_csv": "CSV exportieren",
"export_keys_btn": "Schlüssel exportieren",
@@ -294,22 +224,14 @@
"fetch_prices": "Preise abrufen",
"file": "Datei",
"file_save_location": "Datei wird gespeichert in: ~/.config/ObsidianDragon/",
"filter": "Filtern...",
"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_upper": "VON",
"full_details": "Alle Details",
"general": "Allgemein",
"generating": "Wird generiert",
"go_to_receive": "Zum Empfangen",
"height": "Höhe",
"help": "Hilfe",
"hidden_tag": " (versteckt)",
"hide": "Ausblenden",
"hide_address": "Adresse ausblenden",
"hide_zero_balances": "Nullsalden ausblenden",
@@ -331,9 +253,6 @@
"import_key_warning": "Warnung: Teilen Sie niemals Ihre privaten Schlüssel! Das Importieren von Schlüsseln aus nicht vertrauenswürdigen Quellen kann Ihr Wallet gefährden.",
"import_key_z_format": "Z-Adresse Ausgabeschlüssel (secret-extended-key-...)",
"import_private_key": "Privaten Schlüssel importieren...",
"incorrect_passphrase": "Falsches Passwort",
"incorrect_pin": "Falsche PIN",
"insufficient_funds": "Unzureichendes Guthaben für diesen Betrag plus Gebühr.",
"invalid_address": "Ungültiges Adressformat",
"ip_address": "IP-Adresse",
"keep": "Behalten",
@@ -347,7 +266,6 @@
"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.",
"label": "Bezeichnung:",
"label_placeholder": "z.B. Ersparnisse, Mining...",
"language": "Sprache",
"light": "Hell",
"loading": "Laden...",
@@ -368,7 +286,6 @@
"market_now": "Jetzt",
"market_pct_shielded": "%.0f%% Abgeschirmt",
"market_portfolio": "PORTFOLIO",
"market_price_loading": "Preisdaten werden geladen...",
"market_price_unavailable": "Preisdaten nicht verfügbar",
"market_refresh_price": "Preisdaten aktualisieren",
"market_trade_on": "Handeln auf %s",
@@ -394,13 +311,6 @@
"mining_address_copied": "Mining-Adresse kopiert",
"mining_all_time": "Gesamt",
"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_chart_1m_ago": "vor 1m",
"mining_chart_5m_ago": "vor 5m",
@@ -420,15 +330,8 @@
"mining_filter_tip_all": "Alle Einnahmen anzeigen",
"mining_filter_tip_pool": "Nur Pool-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_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_mine": "Minen",
"mining_mining_addr": "Mining-Adr.",
@@ -485,7 +388,6 @@
"no_addresses_available": "Keine Adressen verfügbar",
"no_addresses_match": "Keine Adressen passen zum Filter",
"no_addresses_with_balance": "Keine Adressen mit Guthaben",
"no_addresses_yet": "Noch keine Adressen",
"no_matching": "Keine passenden Transaktionen",
"no_recent_receives": "Keine kürzlichen Empfänge",
"no_recent_sends": "Keine kürzlichen Sendungen",
@@ -551,7 +453,6 @@
"peers_upper": "PEERS",
"peers_version": "Version",
"pending": "Ausstehend",
"pin_not_set": "PIN nicht gesetzt. Verwenden Sie das Passwort zum Entsperren.",
"ping": "Ping",
"price_chart": "Preisdiagramm",
"qr_code": "QR-Code",
@@ -572,9 +473,7 @@
"recent_received": "KÜRZLICH EMPFANGEN",
"recent_sends": "KÜRZLICH GESENDET",
"recipient": "EMPFÄNGER",
"recipient_balance": "Empfänger: %.8f → %.8f DRGX",
"recv_type": "Empf.",
"reduce_motion": "Bewegung reduzieren",
"refresh": "Aktualisieren",
"refresh_now": "Jetzt aktualisieren",
"remove_favorite": "Favorit entfernen",
@@ -594,10 +493,7 @@
"request_uri_copied": "Zahlungs-URI in Zwischenablage kopiert",
"rescan": "Neu scannen",
"reset_to_defaults": "Standardwerte zurücksetzen",
"restarting_after_encryption": "Daemon wird nach Verschlüsselung neu gestartet...",
"restore_address": "Adresse wiederherstellen",
"result_preview": "Ergebnisvorschau",
"retry": "Wiederholen",
"review_send": "Senden prüfen",
"rpc_host": "RPC-Host",
"rpc_pass": "Passwort",
@@ -606,55 +502,8 @@
"save": "Speichern",
"save_settings": "Einstellungen 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...",
"security": "SICHERHEIT",
"seed_backup_button": "Wiederherstellungsphrase",
"seed_backup_close": "Schließen",
"seed_backup_copy": "Kopieren",
"seed_backup_intro": "Diese Wörter sind die Master-Sicherung Ihrer Wallet. Wer sie besitzt, kontrolliert Ihre Gelder.",
"seed_backup_load_failed": "Die Wiederherstellungsphrase konnte nicht geladen werden.",
"seed_backup_loading": "Ihre Wiederherstellungsphrase wird geladen…",
"seed_backup_locked": "Entsperren Sie Ihre Wallet, um ihre Wiederherstellungsphrase anzuzeigen.",
"seed_backup_none": "Diese Wallet ist älter als Wiederherstellungsphrasen und hat daher keine 24-Wort-Phrase. Sichern Sie sie stattdessen mit „Sicherung...“ oder durch den Export Ihrer privaten Schlüssel.",
"seed_backup_reminder": "Ihre Wallet hat eine 24-Wort-Wiederherstellungsphrase. Sichern Sie sie jetzt unter Einstellungen → Node & Sicherheit.",
"seed_backup_save": "In Datei speichern…",
"seed_backup_save_failed": "Konnte nicht schreiben ",
"seed_backup_saved": "Gespeichert unter ",
"seed_backup_title": "Wiederherstellungsphrase sichern",
"seed_backup_warning": "Schreiben Sie sie der Reihe nach auf, bewahren Sie sie offline auf und teilen oder fotografieren Sie sie niemals. Wenn Sie sie verlieren, können Ihre Gelder nicht wiederhergestellt werden.",
"select_address": "Adresse auswählen...",
"select_receiving_address": "Empfangsadresse auswählen...",
"select_source_address": "Quelladresse auswählen...",
@@ -704,15 +553,12 @@
"send_valid_transparent": "Gültige transparente Adresse",
"send_wallet_empty": "Ihre Wallet ist leer",
"send_yes_clear": "Ja, leeren",
"sender_balance": "Absender: %.8f → %.8f DRGX",
"sending": "Transaktion wird gesendet",
"sending_from": "SENDEN VON",
"sends_full_balance_warning": "Dies sendet das gesamte Guthaben. Die Sendeadresse wird ein Nullguthaben haben.",
"sent": "gesendet",
"sent_filter": "Gesendet",
"sent_type": "Gesendet",
"sent_upper": "GESENDET",
"set_label": "Label setzen...",
"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_acrylic_level": "Acrylstufe:",
@@ -801,17 +647,13 @@
"shield_utxo_limit": "UTXO-Limit:",
"shield_wildcard_hint": "Verwenden Sie '*' um von allen transparenten Adressen abzuschirmen",
"shielded": "Abgeschirmt",
"shielded_address": "Geschirmte Adresse",
"shielded_to": "ABGESCHIRMT AN",
"shielded_type": "Abgeschirmt",
"shielding_notice": "Hinweis: Dies wird Gelder von einer transparenten (T) Adresse auf eine private (Z) Adresse schirmen.",
"show": "Anzeigen",
"show_hidden": "Ausgeblendete anzeigen (%d)",
"show_qr_code": "QR-Code anzeigen",
"showing_transactions": "Zeige %d%d von %d Transaktionen (gesamt: %zu)",
"showing_x_of_y": "%d von %d Adressen angezeigt",
"showing_transactions": "Zeige %d–%d von %d Transaktionen (gesamt: %zu)",
"simple_background": "Einfacher Hintergrund",
"slider_off": "Aus",
"start_mining": "Mining starten",
"status": "Status",
"stop_external": "Externen Daemon stoppen",
@@ -820,26 +662,17 @@
"success": "Erfolg",
"summary": "Zusammenfassung",
"syncing": "Synchronisiere...",
"t_address": "T-Adresse",
"t_addresses": "T-Adressen",
"test_connection": "Testen",
"theme": "Design",
"theme_effects": "Design-Effekte",
"theme_language": "THEMA & SPRACHE",
"time_days_ago": "vor %d Tagen",
"time_hours_ago": "vor %d Stunden",
"time_minutes_ago": "vor %d Minuten",
"time_seconds_ago": "vor %d Sekunden",
"timeout_15min": "15 Min",
"timeout_1hour": "1 Stunde",
"timeout_1min": "1 Min",
"timeout_30min": "30 Min",
"timeout_5min": "5 Min",
"timeout_off": "Aus",
"to": "An",
"to_upper": "AN",
"tools": "WERKZEUGE",
"tools_actions": "Werkzeuge & Aktionen...",
"total": "Gesamt",
"transaction_id": "TRANSAKTIONS-ID",
"transaction_sent": "Transaktion erfolgreich gesendet",
@@ -847,13 +680,7 @@
"transaction_url": "Transaktions-URL",
"transactions": "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_address": "Transparente Adresse",
"tt_addr_url": "Basis-URL zum Anzeigen von Adressen in einem Block-Explorer",
"tt_address_book": "Gespeicherte Adressen für schnelles Senden verwalten",
"tt_auto_lock": "Wallet nach dieser Inaktivitätszeit sperren",
@@ -868,8 +695,6 @@
"tt_custom_theme": "Benutzerdefiniertes Theme aktiv",
"tt_debug_collapse": "Debug-Protokollierungsoptionen einklappen",
"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_export_all": "Alle privaten Schlüssel in eine Datei exportieren",
"tt_export_csv": "Transaktionsverlauf als CSV-Tabelle exportieren",
@@ -887,7 +712,6 @@
"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_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_pin": "PIN entfernen und Passphrase zum Entsperren erfordern",
"tt_report_bug": "Ein Problem im Projekt-Tracker melden",
@@ -903,7 +727,6 @@
"tt_save_ztx": "Z-Adresse-Transaktionsverlauf lokal für schnelleres Laden speichern",
"tt_scan_themes": "Nach neuen Themes suchen.\\nTheme-Ordner ablegen in:\\n%s",
"tt_scanline": "CRT-Scanlinieneffekt in der Konsole",
"tt_seed_backup": "Die 24-Wort-Wiederherstellungsphrase Ihrer Wallet anzeigen und sichern",
"tt_set_pin": "Eine 4-8-stellige PIN für schnelles Entsperren festlegen",
"tt_shield_mining": "Transparente Mining-Belohnungen an eine geschirmte Adresse verschieben",
"tt_simple_bg": "Einen einfachen Verlauf für den Hintergrund verwenden\\nHotkey: Ctrl+Up",
@@ -966,9 +789,7 @@
"warning_upper": "WARNUNG!",
"website": "Webseite",
"window_opacity": "Fenster-Transparenz",
"wizard_daemon_start_failed": "Daemon-Start fehlgeschlagen — wird automatisch wiederholt",
"yes_clear": "Ja, leeren",
"your_addresses": "Ihre Adressen",
"z_address": "Z-Adresse",
"z_addresses": "Z-Adressen"
}

View File

@@ -43,8 +43,6 @@
"address_url": "URL de Dirección",
"addresses_appear_here": "Tus direcciones de recepción aparecerán aquí una vez conectado.",
"advanced": "AVANZADO",
"advanced_effects": "Efectos Avanzados...",
"ago": "atrás",
"all_filter": "Todos",
"allow_custom_fees": "Permitir comisiones personalizadas",
"amount": "Cantidad",
@@ -92,52 +90,12 @@
"block_timestamp": "Fecha y Hora:",
"block_transactions": "Transacciones:",
"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",
"characters": "caracteres",
"chat": "Chat",
"chat_cancel": "Cancelar",
"chat_contact_request": "solicitud de contacto",
"chat_empty_hint": "Aún no hay conversaciones. Los mensajes que recibas aparecerán aquí.",
"chat_locked_hint": "Desbloquea tu monedero para cargar tus chats.",
"chat_new_button": "Nueva conversación",
"chat_new_message": "Mensaje",
"chat_new_send": "Enviar solicitud",
"chat_new_title": "Nueva conversación",
"chat_new_zaddr": "Dirección z del destinatario",
"chat_select_hint": "Selecciona una conversación para verla.",
"chat_send": "Enviar",
"chat_send_failed": "no enviado",
"chat_toast_compose_failed": "No se pudo componer el mensaje (¿demasiado largo?).",
"chat_toast_lite_busy": "Ya hay un envío en curso, o no hay ningún monedero abierto.",
"chat_toast_no_zaddr": "No hay ninguna dirección z disponible desde la que enviar el chat.",
"chat_toast_not_connected": "Sin conexión: mensaje de chat no enviado.",
"chat_toast_request_compose_failed": "No se pudo componer la solicitud de contacto (¿dirección o texto no válidos?).",
"chat_toast_request_queued": "Solicitud de contacto en cola.",
"chat_toast_waiting_reply": "Espera a que este contacto responda antes de poder escribirle.",
"chat_waiting_reply": "Esperando a que este contacto responda: podrás escribirle una vez lo haga.",
"chat_you": "Tú",
"choose_icon": "Elegir Icono",
"clear": "Limpiar",
"clear_all_bans": "Limpiar Todos los Bloqueos",
"clear_anyway": "Limpiar de todos modos",
"clear_form_confirm": "¿Limpiar todos los campos del formulario?",
"clear_icon": "Borrar Icono",
"clear_request": "Limpiar Solicitud",
"click_copy_address": "Clic para copiar dirección",
"click_copy_uri": "Clic para copiar URI",
@@ -148,12 +106,8 @@
"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_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_transaction": "Confirmar Transacción",
"confirm_transfer": "Confirmar Transferencia",
"confirmations": "Confirmaciones",
"confirmations_display": "%d confirmaciones | %s",
"confirmed": "Confirmada",
@@ -218,7 +172,6 @@
"console_welcome": "Bienvenido a la Consola de ObsidianDragon",
"console_zoom_in": "Acercar",
"console_zoom_out": "Alejar",
"copied": "¡Copiado!",
"copy": "Copiar",
"copy_address": "Copiar Dirección Completa",
"copy_error": "Copiar Error",
@@ -227,45 +180,22 @@
"copy_uri": "Copiar URI",
"current_price": "Precio Actual",
"custom_fees": "Comisiones personalizadas",
"daemon_version": "Daemon",
"dark": "Oscuro",
"date": "Fecha",
"date_label": "Fecha:",
"debug_logging": "REGISTRO DE DEPURACIÓN",
"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",
"disconnected": "Desconectado",
"dismiss": "Descartar",
"display": "Pantalla",
"download": "Descargar",
"download_bootstrap": "Descargar Bootstrap",
"dragonx_green": "DragonX (Verde)",
"edit": "Editar",
"error": "Error",
"error_format": "Error: %s",
"est_time_to_block": "Tiempo Est. al Bloque",
"exit": "Salir",
"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",
"explorer": "EXPLORADOR",
"export": "Exportar",
"export_csv": "Exportar CSV",
"export_keys_btn": "Exportar Claves",
@@ -294,22 +224,14 @@
"fetch_prices": "Obtener precios",
"file": "Archivo",
"file_save_location": "El archivo se guardará en: ~/.config/ObsidianDragon/",
"filter": "Filtrar...",
"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_upper": "DESDE",
"full_details": "Detalles Completos",
"general": "General",
"generating": "Generando",
"go_to_receive": "Ir a Recibir",
"height": "Altura",
"help": "Ayuda",
"hidden_tag": " (oculto)",
"hide": "Ocultar",
"hide_address": "Ocultar dirección",
"hide_zero_balances": "Ocultar saldos 0",
@@ -331,9 +253,6 @@
"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_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",
"ip_address": "Dirección IP",
"keep": "Mantener",
@@ -347,7 +266,6 @@
"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.",
"label": "Etiqueta:",
"label_placeholder": "ej. Ahorros, Minería...",
"language": "Idioma",
"light": "Claro",
"loading": "Cargando...",
@@ -368,7 +286,6 @@
"market_now": "Ahora",
"market_pct_shielded": "%.0f%% Protegido",
"market_portfolio": "PORTAFOLIO",
"market_price_loading": "Cargando datos de precio...",
"market_price_unavailable": "Datos de precio no disponibles",
"market_refresh_price": "Actualizar datos de precio",
"market_trade_on": "Operar en %s",
@@ -394,13 +311,6 @@
"mining_address_copied": "Dirección de minería copiada",
"mining_all_time": "Todo el Tiempo",
"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_chart_1m_ago": "hace 1m",
"mining_chart_5m_ago": "hace 5m",
@@ -420,15 +330,8 @@
"mining_filter_tip_all": "Mostrar todas las ganancias",
"mining_filter_tip_pool": "Mostrar solo ganancias del pool",
"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_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_mine": "Minar",
"mining_mining_addr": "Dir. Minería",
@@ -485,7 +388,6 @@
"no_addresses_available": "No hay direcciones disponibles",
"no_addresses_match": "No hay direcciones que coincidan con el filtro",
"no_addresses_with_balance": "No hay direcciones con saldo",
"no_addresses_yet": "Aún no hay direcciones",
"no_matching": "No hay transacciones coincidentes",
"no_recent_receives": "No hay recepciones recientes",
"no_recent_sends": "No hay envíos recientes",
@@ -551,7 +453,6 @@
"peers_upper": "NODOS",
"peers_version": "Versión",
"pending": "Pendiente",
"pin_not_set": "PIN no configurado. Use la contraseña para desbloquear.",
"ping": "Ping",
"price_chart": "Gráfico de Precios",
"qr_code": "Código QR",
@@ -572,9 +473,7 @@
"recent_received": "RECIBIDOS RECIENTES",
"recent_sends": "ENVÍOS RECIENTES",
"recipient": "DESTINATARIO",
"recipient_balance": "Destinatario: %.8f → %.8f DRGX",
"recv_type": "Recibido",
"reduce_motion": "Reducir Movimiento",
"refresh": "Actualizar",
"refresh_now": "Actualizar Ahora",
"remove_favorite": "Quitar favorito",
@@ -594,10 +493,7 @@
"request_uri_copied": "URI de pago copiada al portapapeles",
"rescan": "Re-escanear",
"reset_to_defaults": "Restablecer Valores",
"restarting_after_encryption": "Reiniciando daemon después del cifrado...",
"restore_address": "Restaurar dirección",
"result_preview": "Vista previa del resultado",
"retry": "Reintentar",
"review_send": "Revisar Envío",
"rpc_host": "Host RPC",
"rpc_pass": "Contraseña",
@@ -606,55 +502,8 @@
"save": "Guardar",
"save_settings": "Guardar Configuración",
"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...",
"security": "SEGURIDAD",
"seed_backup_button": "Frase de recuperación",
"seed_backup_close": "Cerrar",
"seed_backup_copy": "Copiar",
"seed_backup_intro": "Estas palabras son el respaldo maestro de tu cartera. Cualquiera que las tenga controla tus fondos.",
"seed_backup_load_failed": "No se pudo cargar la frase de recuperación.",
"seed_backup_loading": "Cargando tu frase de recuperación…",
"seed_backup_locked": "Desbloquea tu cartera para revelar su frase de recuperación.",
"seed_backup_none": "Esta cartera es anterior a las frases de recuperación, por lo que no tiene una frase de 24 palabras. Respáldala con «Respaldo...» o exportando tus claves privadas en su lugar.",
"seed_backup_reminder": "Tu cartera tiene una frase de recuperación de 24 palabras. Respáldala ahora en Configuración → Nodo y Seguridad.",
"seed_backup_save": "Guardar en archivo…",
"seed_backup_save_failed": "No se pudo escribir ",
"seed_backup_saved": "Guardado en ",
"seed_backup_title": "Respaldar frase de recuperación",
"seed_backup_warning": "Anótalas en orden, guárdalas sin conexión y nunca las compartas ni las fotografíes. Si las pierdes, tus fondos no se podrán recuperar.",
"select_address": "Seleccionar dirección...",
"select_receiving_address": "Seleccionar dirección de recepción...",
"select_source_address": "Seleccionar dirección de origen...",
@@ -704,16 +553,13 @@
"send_valid_transparent": "Dirección transparente válida",
"send_wallet_empty": "Tu cartera está vacía",
"send_yes_clear": "Sí, Limpiar",
"sender_balance": "Remitente: %.8f → %.8f DRGX",
"sending": "Enviando transacción",
"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_filter": "Enviado",
"sent_type": "Enviado",
"sent_upper": "ENVIADO",
"set_label": "Establecer Etiqueta...",
"settings": "Ajustes",
"settings": "Configuración",
"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_address_book": "Libreta de direcciones...",
@@ -801,17 +647,13 @@
"shield_utxo_limit": "Límite UTXO:",
"shield_wildcard_hint": "Usa '*' para proteger desde todas las direcciones transparentes",
"shielded": "Blindada",
"shielded_address": "Dirección Protegida",
"shielded_to": "PROTEGIDA PARA",
"shielded_type": "Protegido",
"shielding_notice": "Nota: Esto blindará fondos de una dirección transparente (T) a una dirección privada (Z).",
"show": "Mostrar",
"show_hidden": "Mostrar ocultos (%d)",
"show_qr_code": "Mostrar Código QR",
"showing_transactions": "Mostrando %d%d de %d transacciones (total: %zu)",
"showing_x_of_y": "Mostrando %d de %d direcciones",
"simple_background": "Fondo simple",
"slider_off": "Apagado",
"start_mining": "Iniciar Minería",
"status": "Estado",
"stop_external": "Detener daemon externo",
@@ -820,26 +662,17 @@
"success": "Éxito",
"summary": "Resumen",
"syncing": "Sincronizando...",
"t_address": "Dirección T",
"t_addresses": "Direcciones T",
"test_connection": "Probar",
"theme": "Tema",
"theme_effects": "Efectos de tema",
"theme_language": "TEMA E IDIOMA",
"time_days_ago": "hace %d días",
"time_hours_ago": "hace %d horas",
"time_minutes_ago": "hace %d minutos",
"time_seconds_ago": "hace %d segundos",
"timeout_15min": "15 min",
"timeout_1hour": "1 hora",
"timeout_1min": "1 min",
"timeout_30min": "30 min",
"timeout_5min": "5 min",
"timeout_off": "Apagado",
"to": "Para",
"to_upper": "PARA",
"tools": "HERRAMIENTAS",
"tools_actions": "Herramientas y Acciones...",
"total": "Total",
"transaction_id": "ID DE TRANSACCIÓN",
"transaction_sent": "Transacción enviada exitosamente",
@@ -847,13 +680,7 @@
"transaction_url": "URL de Transacción",
"transactions": "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_address": "Dirección Transparente",
"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_auto_lock": "Bloquear billetera después de este tiempo de inactividad",
@@ -868,8 +695,6 @@
"tt_custom_theme": "Tema personalizado activo",
"tt_debug_collapse": "Colapsar 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_export_all": "Exportar todas las claves privadas a un archivo",
"tt_export_csv": "Exportar historial de transacciones como hoja de cálculo CSV",
@@ -887,7 +712,6 @@
"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_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_pin": "Quitar PIN y requerir contraseña para desbloquear",
"tt_report_bug": "Reportar un problema en el rastreador del proyecto",
@@ -903,7 +727,6 @@
"tt_save_ztx": "Almacenar historial de transacciones de z-address localmente para carga más rápida",
"tt_scan_themes": "Buscar nuevos temas.\\nColoque carpetas de temas en:\\n%s",
"tt_scanline": "Efecto de líneas de escaneo CRT en la consola",
"tt_seed_backup": "Muestra y respalda la frase de recuperación de 24 palabras de tu cartera",
"tt_set_pin": "Establecer un PIN de 4-8 dígitos para desbloqueo rápido",
"tt_shield_mining": "Mover recompensas de minería transparentes a una dirección blindada",
"tt_simple_bg": "Usar un gradiente simple para el fondo\\nAtajo: Ctrl+Up",
@@ -966,9 +789,7 @@
"warning_upper": "¡ADVERTENCIA!",
"website": "Sitio Web",
"window_opacity": "Opacidad de ventana",
"wizard_daemon_start_failed": "Error al iniciar el daemon — se reintentará automáticamente",
"yes_clear": "Sí, Limpiar",
"your_addresses": "Sus Direcciones",
"z_address": "Dirección Z",
"z_addresses": "Direcciones Z"
}

View File

@@ -43,8 +43,6 @@
"address_url": "URL de l'adresse",
"addresses_appear_here": "Vos adresses de réception apparaîtront ici une fois connecté.",
"advanced": "AVANCÉ",
"advanced_effects": "Effets avancés...",
"ago": "passé",
"all_filter": "Tout",
"allow_custom_fees": "Autoriser les frais personnalisés",
"amount": "Montant",
@@ -92,52 +90,12 @@
"block_timestamp": "Horodatage :",
"block_transactions": "Transactions :",
"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",
"characters": "caractères",
"chat": "Discussion",
"chat_cancel": "Annuler",
"chat_contact_request": "demande de contact",
"chat_empty_hint": "Aucune conversation pour l'instant. Les messages que vous recevez apparaîtront ici.",
"chat_locked_hint": "Déverrouillez votre portefeuille pour charger vos discussions.",
"chat_new_button": "Nouvelle conversation",
"chat_new_message": "Message",
"chat_new_send": "Envoyer la demande",
"chat_new_title": "Nouvelle conversation",
"chat_new_zaddr": "Adresse Z du destinataire",
"chat_select_hint": "Sélectionnez une conversation pour l'afficher.",
"chat_send": "Envoyer",
"chat_send_failed": "non envoyé",
"chat_toast_compose_failed": "Impossible de composer le message (trop long ?).",
"chat_toast_lite_busy": "Un envoi est déjà en cours, ou aucun portefeuille n'est ouvert.",
"chat_toast_no_zaddr": "Aucune adresse Z disponible pour envoyer le message.",
"chat_toast_not_connected": "Non connecté — message non envoyé.",
"chat_toast_request_compose_failed": "Impossible de composer la demande de contact (adresse / texte invalide ?).",
"chat_toast_request_queued": "Demande de contact mise en file d'attente.",
"chat_toast_waiting_reply": "En attente de la réponse de ce contact avant de pouvoir lui écrire.",
"chat_waiting_reply": "En attente de la réponse de ce contact — vous pourrez lui écrire dès qu'il aura répondu.",
"chat_you": "Vous",
"choose_icon": "Choisir une icône",
"clear": "Effacer",
"clear_all_bans": "Lever tous les bannissements",
"clear_anyway": "Effacer quand même",
"clear_form_confirm": "Effacer tous les champs du formulaire ?",
"clear_icon": "Effacer l'icône",
"clear_request": "Effacer la demande",
"click_copy_address": "Cliquez pour copier l'adresse",
"click_copy_uri": "Cliquez pour copier l'URI",
@@ -148,12 +106,8 @@
"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_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_transaction": "Confirmer la transaction",
"confirm_transfer": "Confirmer le transfert",
"confirmations": "Confirmations",
"confirmations_display": "%d confirmations | %s",
"confirmed": "Confirmé",
@@ -218,7 +172,6 @@
"console_welcome": "Bienvenue dans la console ObsidianDragon",
"console_zoom_in": "Agrandir",
"console_zoom_out": "Réduire",
"copied": "Copié !",
"copy": "Copier",
"copy_address": "Copier l'adresse complète",
"copy_error": "Copier l'erreur",
@@ -227,45 +180,22 @@
"copy_uri": "Copier l'URI",
"current_price": "Prix actuel",
"custom_fees": "Frais personnalisés",
"daemon_version": "Daemon",
"dark": "Sombre",
"date": "Date",
"date_label": "Date :",
"debug_logging": "JOURNALISATION DE DÉBOGAGE",
"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é",
"disconnected": "Déconnecté",
"dismiss": "Ignorer",
"display": "Affichage",
"download": "Télécharger",
"download_bootstrap": "Télécharger Bootstrap",
"dragonx_green": "DragonX (Vert)",
"edit": "Modifier",
"error": "Erreur",
"error_format": "Erreur : %s",
"est_time_to_block": "Temps est. par bloc",
"exit": "Quitter",
"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",
"explorer": "EXPLORATEUR",
"export": "Exporter",
"export_csv": "Exporter en CSV",
"export_keys_btn": "Exporter les clés",
@@ -294,22 +224,14 @@
"fetch_prices": "Récupérer les prix",
"file": "Fichier",
"file_save_location": "Le fichier sera enregistré dans : ~/.config/ObsidianDragon/",
"filter": "Filtrer...",
"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_upper": "DE",
"full_details": "Tous les détails",
"general": "Général",
"generating": "Génération",
"go_to_receive": "Aller à Recevoir",
"height": "Hauteur",
"help": "Aide",
"hidden_tag": " (masqué)",
"hide": "Masquer",
"hide_address": "Masquer l'adresse",
"hide_zero_balances": "Masquer les soldes à 0",
@@ -331,9 +253,6 @@
"import_key_warning": "Attention : Ne partagez jamais vos clés privées ! L'importation de clés provenant de sources non fiables peut compromettre votre portefeuille.",
"import_key_z_format": "Clés de dépenses z-adresse (secret-extended-key-...)",
"import_private_key": "Importer une clé privée...",
"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",
"ip_address": "Adresse IP",
"keep": "Conserver",
@@ -347,7 +266,6 @@
"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.",
"label": "Libellé :",
"label_placeholder": "ex. Épargne, Minage...",
"language": "Langue",
"light": "Clair",
"loading": "Chargement...",
@@ -368,7 +286,6 @@
"market_now": "Maintenant",
"market_pct_shielded": "%.0f%% Blindé",
"market_portfolio": "PORTEFEUILLE",
"market_price_loading": "Chargement des données de prix...",
"market_price_unavailable": "Données de prix indisponibles",
"market_refresh_price": "Actualiser les données de prix",
"market_trade_on": "Échanger sur %s",
@@ -394,13 +311,6 @@
"mining_address_copied": "Adresse de minage copiée",
"mining_all_time": "Tout le temps",
"mining_already_saved": "URL du pool déjà enregistrée",
"mining_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_chart_1m_ago": "il y a 1m",
"mining_chart_5m_ago": "il y a 5m",
@@ -420,15 +330,8 @@
"mining_filter_tip_all": "Afficher tous les gains",
"mining_filter_tip_pool": "Afficher uniquement les gains du pool",
"mining_filter_tip_solo": "Afficher uniquement les gains solo",
"mining_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_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_mine": "Miner",
"mining_mining_addr": "Adr. minage",
@@ -485,7 +388,6 @@
"no_addresses_available": "Aucune adresse disponible",
"no_addresses_match": "Aucune adresse ne correspond au filtre",
"no_addresses_with_balance": "Aucune adresse avec solde",
"no_addresses_yet": "Pas encore d'adresses",
"no_matching": "Aucune transaction correspondante",
"no_recent_receives": "Aucune réception récente",
"no_recent_sends": "Aucun envoi récent",
@@ -551,7 +453,6 @@
"peers_upper": "PAIRS",
"peers_version": "Version",
"pending": "En attente",
"pin_not_set": "PIN non défini. Utilisez le mot de passe pour déverrouiller.",
"ping": "Ping",
"price_chart": "Graphique des prix",
"qr_code": "Code QR",
@@ -572,9 +473,7 @@
"recent_received": "REÇUS RÉCENTS",
"recent_sends": "ENVOIS RÉCENTS",
"recipient": "DESTINATAIRE",
"recipient_balance": "Destinataire : %.8f → %.8f DRGX",
"recv_type": "Reçu",
"reduce_motion": "Réduire les animations",
"refresh": "Actualiser",
"refresh_now": "Actualiser maintenant",
"remove_favorite": "Retirer des favoris",
@@ -594,10 +493,7 @@
"request_uri_copied": "URI de paiement copiée dans le presse-papiers",
"rescan": "Re-scanner",
"reset_to_defaults": "Réinitialiser les paramètres",
"restarting_after_encryption": "Redémarrage du daemon après chiffrement...",
"restore_address": "Restaurer l'adresse",
"result_preview": "Aperçu du résultat",
"retry": "Réessayer",
"review_send": "Vérifier l'envoi",
"rpc_host": "Hôte RPC",
"rpc_pass": "Mot de passe",
@@ -606,55 +502,8 @@
"save": "Enregistrer",
"save_settings": "Enregistrer les paramètres",
"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...",
"security": "SÉCURITÉ",
"seed_backup_button": "Phrase de récupération",
"seed_backup_close": "Fermer",
"seed_backup_copy": "Copier",
"seed_backup_intro": "Ces mots sont la sauvegarde principale de votre portefeuille. Quiconque les possède contrôle vos fonds.",
"seed_backup_load_failed": "Impossible de charger la phrase de récupération.",
"seed_backup_loading": "Chargement de votre phrase de récupération…",
"seed_backup_locked": "Déverrouillez votre portefeuille pour révéler sa phrase de récupération.",
"seed_backup_none": "Ce portefeuille est antérieur aux phrases de récupération, il n'a donc pas de phrase de 24 mots. Sauvegardez-le plutôt avec « Sauvegarde » ou en exportant vos clés privées.",
"seed_backup_reminder": "Votre portefeuille possède une phrase de récupération de 24 mots. Sauvegardez-la maintenant dans Paramètres → Nœud et sécurité.",
"seed_backup_save": "Enregistrer dans un fichier…",
"seed_backup_save_failed": "Impossible d'écrire ",
"seed_backup_saved": "Enregistré dans ",
"seed_backup_title": "Sauvegarder la phrase de récupération",
"seed_backup_warning": "Notez-les dans l'ordre, conservez-les hors ligne, et ne les partagez ni ne les photographiez jamais. Si vous les perdez, vos fonds ne pourront pas être récupérés.",
"select_address": "Sélectionner une adresse...",
"select_receiving_address": "Sélectionner une adresse de réception...",
"select_source_address": "Sélectionner une adresse source...",
@@ -704,15 +553,12 @@
"send_valid_transparent": "Adresse transparente valide",
"send_wallet_empty": "Votre portefeuille est vide",
"send_yes_clear": "Oui, effacer",
"sender_balance": "Expéditeur : %.8f → %.8f DRGX",
"sending": "Envoi de la transaction",
"sending_from": "ENVOI DEPUIS",
"sends_full_balance_warning": "Cela envoie le solde complet. L'adresse d'envoi aura un solde nul.",
"sent": "envoyé",
"sent_filter": "Envoyé",
"sent_type": "Envoyé",
"sent_upper": "ENVOYÉ",
"set_label": "Définir le libellé...",
"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_acrylic_level": "Niveau acrylique :",
@@ -801,17 +647,13 @@
"shield_utxo_limit": "Limite UTXO :",
"shield_wildcard_hint": "Utilisez '*' pour blinder depuis toutes les adresses transparentes",
"shielded": "Blindé",
"shielded_address": "Adresse protégée",
"shielded_to": "BLINDÉ VERS",
"shielded_type": "Blindé",
"shielding_notice": "Note : Cela blindera des fonds d'une adresse transparente (T) vers une adresse privée (Z).",
"show": "Afficher",
"show_hidden": "Afficher masqués (%d)",
"show_qr_code": "Afficher le code QR",
"showing_transactions": "Affichage %d%d sur %d transactions (total : %zu)",
"showing_x_of_y": "Affichage de %d sur %d adresses",
"showing_transactions": "Affichage %d–%d sur %d transactions (total : %zu)",
"simple_background": "Arrière-plan simple",
"slider_off": "Désactivé",
"start_mining": "Démarrer le minage",
"status": "Statut",
"stop_external": "Arrêter le daemon externe",
@@ -820,26 +662,17 @@
"success": "Succès",
"summary": "Résumé",
"syncing": "Synchronisation...",
"t_address": "Adresse T",
"t_addresses": "Adresses T",
"test_connection": "Tester",
"theme": "Thème",
"theme_effects": "Effets de thème",
"theme_language": "THÈME & LANGUE",
"time_days_ago": "il y a %d jours",
"time_hours_ago": "il y a %d heures",
"time_minutes_ago": "il y a %d minutes",
"time_seconds_ago": "il y a %d secondes",
"timeout_15min": "15 min",
"timeout_1hour": "1 heure",
"timeout_1min": "1 min",
"timeout_30min": "30 min",
"timeout_5min": "5 min",
"timeout_off": "Désactivé",
"to": "À",
"to_upper": "À",
"tools": "OUTILS",
"tools_actions": "Outils & Actions...",
"total": "Total",
"transaction_id": "ID DE TRANSACTION",
"transaction_sent": "Transaction envoyée avec succès",
@@ -847,13 +680,7 @@
"transaction_url": "URL de transaction",
"transactions": "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_address": "Adresse transparente",
"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_auto_lock": "Verrouiller le portefeuille après cette durée d'inactivité",
@@ -868,8 +695,6 @@
"tt_custom_theme": "Thème personnalisé actif",
"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_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_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",
@@ -887,7 +712,6 @@
"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_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_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",
@@ -903,7 +727,6 @@
"tt_save_ztx": "Stocker l'historique des transactions z-address localement pour un chargement plus rapide",
"tt_scan_themes": "Rechercher de nouveaux thèmes.\\nPlacez les dossiers de thèmes dans :\\n%s",
"tt_scanline": "Effet de lignes de balayage CRT dans la console",
"tt_seed_backup": "Afficher et sauvegarder la phrase de récupération de 24 mots de votre portefeuille",
"tt_set_pin": "Définir un PIN de 4-8 chiffres pour un déverrouillage rapide",
"tt_shield_mining": "Déplacer les récompenses de minage transparentes vers une adresse blindée",
"tt_simple_bg": "Utiliser un dégradé simple pour l'arrière-plan\\nRaccourci : Ctrl+Up",
@@ -966,9 +789,7 @@
"warning_upper": "ATTENTION !",
"website": "Site web",
"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",
"your_addresses": "Vos adresses",
"z_address": "Adresse Z",
"z_addresses": "Adresses Z"
}

View File

@@ -43,8 +43,6 @@
"address_url": "アドレスURL",
"addresses_appear_here": "接続後、受信アドレスがここに表示されます。",
"advanced": "詳細設定",
"advanced_effects": "高度なエフェクト...",
"ago": "前",
"all_filter": "すべて",
"allow_custom_fees": "カスタム手数料を許可",
"amount": "金額",
@@ -92,52 +90,12 @@
"block_timestamp": "タイムスタンプ:",
"block_transactions": "トランザクション:",
"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": "キャンセル",
"characters": "文字",
"chat": "チャット",
"chat_cancel": "キャンセル",
"chat_contact_request": "連絡リクエスト",
"chat_empty_hint": "まだ会話はありません。受信したメッセージはここに表示されます。",
"chat_locked_hint": "チャットを読み込むにはウォレットのロックを解除してください。",
"chat_new_button": "新しい会話",
"chat_new_message": "メッセージ",
"chat_new_send": "リクエストを送信",
"chat_new_title": "新しい会話",
"chat_new_zaddr": "宛先Zアドレス",
"chat_select_hint": "表示する会話を選択してください。",
"chat_send": "送信",
"chat_send_failed": "未送信",
"chat_toast_compose_failed": "メッセージを作成できませんでした(長すぎませんか?)。",
"chat_toast_lite_busy": "すでに送信処理が進行中か、ウォレットが開かれていません。",
"chat_toast_no_zaddr": "送信元に使えるZアドレスがありません。",
"chat_toast_not_connected": "未接続 — チャットメッセージは送信されませんでした。",
"chat_toast_request_compose_failed": "連絡リクエストを作成できませんでした(アドレスまたはテキストが無効?)。",
"chat_toast_request_queued": "連絡リクエストを送信待ちに追加しました。",
"chat_toast_waiting_reply": "メッセージを送るには、この相手からの返信を待つ必要があります。",
"chat_waiting_reply": "この相手からの返信を待っています — 返信があればメッセージを送れます。",
"chat_you": "自分",
"choose_icon": "アイコンを選択",
"clear": "クリア",
"clear_all_bans": "すべてのブロックを解除",
"clear_anyway": "それでもクリア",
"clear_form_confirm": "すべてのフォームフィールドをクリアしますか?",
"clear_icon": "アイコンをクリア",
"clear_request": "リクエストをクリア",
"click_copy_address": "クリックしてアドレスをコピー",
"click_copy_uri": "クリックしてURIをコピー",
@@ -148,12 +106,8 @@
"confirm_clear_ztx_title": "Z-Tx 履歴クリアの確認",
"confirm_clear_ztx_warning1": "z-トランザクション履歴をクリアすると、ウォレットの再スキャンが実行されるまでシールド残高が0と表示される場合があります。",
"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_transaction": "取引を確認",
"confirm_transfer": "送金を確認",
"confirmations": "確認数",
"confirmations_display": "%d 確認 | %s",
"confirmed": "確認済み",
@@ -218,7 +172,6 @@
"console_welcome": "ObsidianDragonコンソールへようこそ",
"console_zoom_in": "拡大",
"console_zoom_out": "縮小",
"copied": "コピーしました!",
"copy": "コピー",
"copy_address": "完全なアドレスをコピー",
"copy_error": "エラーをコピー",
@@ -227,21 +180,15 @@
"copy_uri": "URIをコピー",
"current_price": "現在の価格",
"custom_fees": "カスタム手数料",
"daemon_version": "デーモン",
"dark": "ダーク",
"date": "日付",
"date_label": "日付:",
"debug_logging": "デバッグログ",
"delete": "削除",
"delete_blockchain": "ブロックチェーンを削除",
"delete_blockchain_confirm": "削除して再同期",
"deshielding_warning": "警告:プライベート (Z) アドレスからトランスペアレント (T) アドレスへ資金をデシールドします。",
"difficulty": "難易度",
"disconnected": "切断済み",
"dismiss": "閉じる",
"display": "表示",
"download": "ダウンロード",
"download_bootstrap": "ブートストラップをダウンロード",
"dragonx_green": "DragonXグリーン",
"edit": "編集",
"error": "エラー",
@@ -249,23 +196,6 @@
"est_time_to_block": "予測ブロック時間",
"exit": "終了",
"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_csv": "CSVエクスポート",
"export_keys_btn": "鍵をエクスポート",
@@ -294,22 +224,14 @@
"fetch_prices": "価格を取得",
"file": "ファイル",
"file_save_location": "ファイルの保存先:~/.config/ObsidianDragon/",
"filter": "フィルター...",
"font_scale": "フォントサイズ",
"force_quit": "強制終了",
"force_quit_confirm_msg": "クリーンシャットダウンなしでデーモンを即座に終了します。\nブロックチェーンインデックスが破損し、再同期が必要になる可能性があります。",
"force_quit_confirm_title": "強制終了しますか?",
"force_quit_warning": "クリーンシャットダウンなしでデーモンを即座に終了します。ブロックチェーンの再同期が必要になる場合があります。",
"force_quit_yes": "強制終了",
"from": "送信元",
"from_upper": "送信元",
"full_details": "詳細情報",
"general": "一般",
"generating": "生成中",
"go_to_receive": "受信へ移動",
"height": "高さ",
"help": "ヘルプ",
"hidden_tag": " (非表示)",
"hide": "非表示",
"hide_address": "アドレスを非表示",
"hide_zero_balances": "残高0を非表示",
@@ -331,9 +253,6 @@
"import_key_warning": "警告:秘密鍵を決して共有しないでください!信頼できないソースからの鍵のインポートはウォレットを危険にさらす可能性があります。",
"import_key_z_format": "Zアドレス支出鍵 (secret-extended-key-...)",
"import_private_key": "秘密鍵をインポート...",
"incorrect_passphrase": "パスフレーズが正しくありません",
"incorrect_pin": "PINが正しくありません",
"insufficient_funds": "この金額と手数料に対して残高が不足しています。",
"invalid_address": "無効なアドレス形式",
"ip_address": "IPアドレス",
"keep": "保持",
@@ -347,7 +266,6 @@
"key_export_viewing_keys_zonly": "ビューイングキーはシールド (z) アドレスでのみ利用可能です",
"key_export_viewing_warning": "この閲覧鍵を使うと、他者があなたの受信取引と残高を見ることができますが、資金を使うことはできません。信頼できる相手とのみ共有してください。",
"label": "ラベル:",
"label_placeholder": "例: 貯金、マイニング...",
"language": "言語",
"light": "ライト",
"loading": "読み込み中...",
@@ -368,7 +286,6 @@
"market_now": "現在",
"market_pct_shielded": "%.0f%% シールド済み",
"market_portfolio": "ポートフォリオ",
"market_price_loading": "価格データを読み込み中...",
"market_price_unavailable": "価格データが利用できません",
"market_refresh_price": "価格データを更新",
"market_trade_on": "%s で取引",
@@ -394,13 +311,6 @@
"mining_address_copied": "マイニングアドレスをコピーしました",
"mining_all_time": "全期間",
"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_chart_1m_ago": "1分前",
"mining_chart_5m_ago": "5分前",
@@ -420,15 +330,8 @@
"mining_filter_tip_all": "すべての収益を表示",
"mining_filter_tip_pool": "プール収益のみ表示",
"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_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_mine": "マイニング",
"mining_mining_addr": "マイニングアドレス",
@@ -485,7 +388,6 @@
"no_addresses_available": "利用可能なアドレスがありません",
"no_addresses_match": "フィルタに一致するアドレスがありません",
"no_addresses_with_balance": "残高のあるアドレスがありません",
"no_addresses_yet": "アドレスがまだありません",
"no_matching": "一致する取引がありません",
"no_recent_receives": "最近の受信がありません",
"no_recent_sends": "最近の送信がありません",
@@ -551,7 +453,6 @@
"peers_upper": "ピア",
"peers_version": "バージョン",
"pending": "保留中",
"pin_not_set": "PINが設定されていません。パスフレーズで解除してください。",
"ping": "Ping",
"price_chart": "価格チャート",
"qr_code": "QRコード",
@@ -572,9 +473,7 @@
"recent_received": "最近の受信",
"recent_sends": "最近の送信",
"recipient": "受取人",
"recipient_balance": "受取人: %.8f → %.8f DRGX",
"recv_type": "受信",
"reduce_motion": "モーションを減らす",
"refresh": "更新",
"refresh_now": "今すぐ更新",
"remove_favorite": "お気に入りを削除",
@@ -594,10 +493,7 @@
"request_uri_copied": "支払いURIをクリップボードにコピーしました",
"rescan": "再スキャン",
"reset_to_defaults": "デフォルトにリセット",
"restarting_after_encryption": "暗号化後にデーモンを再起動中...",
"restore_address": "アドレスを復元",
"result_preview": "結果プレビュー",
"retry": "再試行",
"review_send": "送金を確認",
"rpc_host": "RPCホスト",
"rpc_pass": "パスワード",
@@ -606,55 +502,8 @@
"save": "保存",
"save_settings": "設定を保存",
"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": "検索...",
"security": "セキュリティ",
"seed_backup_button": "シードフレーズ",
"seed_backup_close": "閉じる",
"seed_backup_copy": "コピー",
"seed_backup_intro": "これらの単語はウォレットのマスターバックアップです。これらを持つ人は誰でもあなたの資金を管理できます。",
"seed_backup_load_failed": "シードフレーズを読み込めませんでした。",
"seed_backup_loading": "シードフレーズを読み込み中...",
"seed_backup_locked": "シードフレーズを表示するにはウォレットのロックを解除してください。",
"seed_backup_none": "このウォレットはシードフレーズより前のものなので、24単語のフレーズはありません。代わりに「バックアップ...」で、または秘密鍵をエクスポートしてバックアップしてください。",
"seed_backup_reminder": "ウォレットには24単語の復元シードフレーズがあります。今すぐ 設定 → ノードとセキュリティ でバックアップしてください。",
"seed_backup_save": "ファイルに保存...",
"seed_backup_save_failed": "書き込めませんでした: ",
"seed_backup_saved": "保存先: ",
"seed_backup_title": "シードフレーズをバックアップ",
"seed_backup_warning": "順番どおりに書き写し、オフラインで保管してください。決して共有や撮影をしないでください。紛失すると資金を復元できません。",
"select_address": "アドレスを選択...",
"select_receiving_address": "受信アドレスを選択...",
"select_source_address": "送信元アドレスを選択...",
@@ -704,15 +553,12 @@
"send_valid_transparent": "有効な透明アドレス",
"send_wallet_empty": "ウォレットは空です",
"send_yes_clear": "はい、クリア",
"sender_balance": "送信者: %.8f → %.8f DRGX",
"sending": "取引を送信中",
"sending_from": "送信元",
"sends_full_balance_warning": "全残高を送信します。送信アドレスの残高はゼロになります。",
"sent": "送信済み",
"sent_filter": "送信済み",
"sent_type": "送信済み",
"sent_upper": "送信済み",
"set_label": "ラベルを設定...",
"settings": "設定",
"settings_about_text": "DragonX (DRGX) 用のシールド暗号通貨ウォレット。Dear ImGui で構築された軽量でポータブルな体験。",
"settings_acrylic_level": "アクリルレベル:",
@@ -801,17 +647,13 @@
"shield_utxo_limit": "UTXO制限",
"shield_wildcard_hint": "'*' を使用してすべての透明アドレスからシールド",
"shielded": "シールド",
"shielded_address": "シールドアドレス",
"shielded_to": "シールド先",
"shielded_type": "シールド",
"shielding_notice": "注意:トランスペアレント (T) アドレスからプライベート (Z) アドレスへ資金をシールドします。",
"show": "表示",
"show_hidden": "非表示を表示 (%d)",
"show_qr_code": "QRコードを表示",
"showing_transactions": "%d%d / %d 件の取引を表示中(合計:%zu",
"showing_x_of_y": "%d / %d アドレスを表示",
"showing_transactions": "%d–%d / %d 件の取引を表示中(合計:%zu",
"simple_background": "シンプル背景",
"slider_off": "オフ",
"start_mining": "マイニング開始",
"status": "ステータス",
"stop_external": "外部デーモンを停止",
@@ -820,26 +662,17 @@
"success": "成功",
"summary": "概要",
"syncing": "同期中...",
"t_address": "Tアドレス",
"t_addresses": "Tアドレス",
"test_connection": "テスト",
"theme": "テーマ",
"theme_effects": "テーマ効果",
"theme_language": "テーマと言語",
"time_days_ago": "%d日前",
"time_hours_ago": "%d時間前",
"time_minutes_ago": "%d分前",
"time_seconds_ago": "%d秒前",
"timeout_15min": "15分",
"timeout_1hour": "1時間",
"timeout_1min": "1分",
"timeout_30min": "30分",
"timeout_5min": "5分",
"timeout_off": "オフ",
"to": "宛先",
"to_upper": "宛先",
"tools": "ツール",
"tools_actions": "ツールとアクション...",
"total": "合計",
"transaction_id": "取引ID",
"transaction_sent": "取引の送信に成功しました",
@@ -847,13 +680,7 @@
"transaction_url": "取引URL",
"transactions": "取引",
"transactions_upper": "取引",
"transfer_failed": "送金失敗",
"transfer_funds": "資金を送金",
"transfer_sent": "送金完了",
"transfer_sent_desc": "送金がネットワークに送信されました。",
"transfer_to": "送金先:",
"transparent": "透明",
"transparent_address": "トランスペアレントアドレス",
"tt_addr_url": "ブロックエクスプローラーでアドレスを表示するためのベース URL",
"tt_address_book": "クイック送信用の保存済みアドレスを管理",
"tt_auto_lock": "この無操作時間後にウォレットをロック",
@@ -868,8 +695,6 @@
"tt_custom_theme": "カスタムテーマがアクティブ",
"tt_debug_collapse": "デバッグログオプションを折りたたむ",
"tt_debug_expand": "デバッグログオプションを展開",
"tt_delete_blockchain": "すべてのブロックチェーンデータを削除して新規同期を開始します。wallet.dat と設定は保持されます。",
"tt_download_bootstrap": "ブロックチェーンブートストラップをダウンロードして同期を高速化\n既存のブロックデータは置き換えられます",
"tt_encrypt": "パスフレーズで wallet.dat を暗号化",
"tt_export_all": "すべての秘密鍵をファイルにエクスポート",
"tt_export_csv": "トランザクション履歴を CSV スプレッドシートとしてエクスポート",
@@ -887,7 +712,6 @@
"tt_mine_idle": "システムがアイドル状態(キーボード/マウス入力なし)\\nのとき自動的にマイニングを開始",
"tt_noise": "グレインテクスチャ強度0%% = オフ、100%% = 最大)",
"tt_open_dir": "クリックしてファイルエクスプローラーで開く",
"tt_reduce_motion": "アクセシビリティのためにアニメーション遷移と残高補間を無効にする",
"tt_remove_encrypt": "暗号化を解除してウォレットを保護なしで保存",
"tt_remove_pin": "PIN を削除しアンロックにパスフレーズを要求",
"tt_report_bug": "プロジェクトトラッカーで問題を報告",
@@ -903,7 +727,6 @@
"tt_save_ztx": "z-address トランザクション履歴をローカルに保存して高速読み込み",
"tt_scan_themes": "新しいテーマをスキャン。\\nテーマフォルダーをここに配置\\n%s",
"tt_scanline": "コンソールでの CRT スキャンライン効果",
"tt_seed_backup": "ウォレットの24単語の復元シードフレーズを表示してバックアップします",
"tt_set_pin": "クイックアンロック用の 4-8 桁 PIN を設定",
"tt_shield_mining": "透明マイニング報酬をシールドアドレスに移動",
"tt_simple_bg": "背景にシンプルなグラデーションを使用\\nホットキーCtrl+Up",
@@ -966,9 +789,7 @@
"warning_upper": "警告!",
"website": "ウェブサイト",
"window_opacity": "ウィンドウ透明度",
"wizard_daemon_start_failed": "デーモンの起動に失敗しました — 自動的に再試行されます",
"yes_clear": "はい、クリア",
"your_addresses": "あなたのアドレス",
"z_address": "Zアドレス",
"z_addresses": "Zアドレス"
}

View File

@@ -43,8 +43,6 @@
"address_url": "주소 URL",
"addresses_appear_here": "연결 후 수신 주소가 여기에 표시됩니다.",
"advanced": "고급 설정",
"advanced_effects": "고급 효과...",
"ago": "전",
"all_filter": "전체",
"allow_custom_fees": "사용자 정의 수수료 허용",
"amount": "금액",
@@ -92,52 +90,12 @@
"block_timestamp": "타임스탬프:",
"block_transactions": "트랜잭션:",
"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": "취소",
"characters": "문자",
"chat": "채팅",
"chat_cancel": "취소",
"chat_contact_request": "연락 요청",
"chat_empty_hint": "아직 대화가 없습니다. 받은 메시지가 여기에 표시됩니다.",
"chat_locked_hint": "채팅을 불러오려면 지갑 잠금을 해제하세요.",
"chat_new_button": "새 대화",
"chat_new_message": "메시지",
"chat_new_send": "요청 보내기",
"chat_new_title": "새 대화",
"chat_new_zaddr": "받는 사람 z-주소",
"chat_select_hint": "볼 대화를 선택하세요.",
"chat_send": "전송",
"chat_send_failed": "전송 안 됨",
"chat_toast_compose_failed": "메시지를 작성할 수 없습니다 (너무 긴가요?).",
"chat_toast_lite_busy": "이미 전송이 진행 중이거나 열린 지갑이 없습니다.",
"chat_toast_no_zaddr": "채팅을 보낼 z-주소가 없습니다.",
"chat_toast_not_connected": "연결되지 않음 — 채팅 메시지가 전송되지 않았습니다.",
"chat_toast_request_compose_failed": "연락 요청을 작성할 수 없습니다 (잘못된 주소 / 텍스트?).",
"chat_toast_request_queued": "연락 요청이 대기열에 추가되었습니다.",
"chat_toast_waiting_reply": "상대방이 답장해야 메시지를 보낼 수 있습니다.",
"chat_waiting_reply": "상대방의 답장을 기다리는 중입니다 — 답장하면 메시지를 보낼 수 있습니다.",
"chat_you": "나",
"choose_icon": "아이콘 선택",
"clear": "지우기",
"clear_all_bans": "모든 차단 해제",
"clear_anyway": "그래도 삭제",
"clear_form_confirm": "모든 양식 필드를 지우시겠습니까?",
"clear_icon": "아이콘 지우기",
"clear_request": "요청 지우기",
"click_copy_address": "클릭하여 주소 복사",
"click_copy_uri": "클릭하여 URI 복사",
@@ -148,12 +106,8 @@
"confirm_clear_ztx_title": "Z-Tx 기록 삭제 확인",
"confirm_clear_ztx_warning1": "z-트랜잭션 기록을 삭제하면 지갑 재스캔이 수행될 때까지 차폐 잔액이 0으로 표시될 수 있습니다.",
"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_transaction": "거래 확인",
"confirm_transfer": "이체 확인",
"confirmations": "확인 수",
"confirmations_display": "%d 확인 | %s",
"confirmed": "확인됨",
@@ -218,7 +172,6 @@
"console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다",
"console_zoom_in": "확대",
"console_zoom_out": "축소",
"copied": "복사됨!",
"copy": "복사",
"copy_address": "전체 주소 복사",
"copy_error": "오류 복사",
@@ -227,21 +180,15 @@
"copy_uri": "URI 복사",
"current_price": "현재 가격",
"custom_fees": "사용자 정의 수수료",
"daemon_version": "데몬",
"dark": "다크",
"date": "날짜",
"date_label": "날짜:",
"debug_logging": "디버그 로깅",
"delete": "삭제",
"delete_blockchain": "블록체인 삭제",
"delete_blockchain_confirm": "삭제 후 재동기화",
"deshielding_warning": "경고: 프라이빗 (Z) 주소에서 투명 (T) 주소로 자금을 디실딩합니다.",
"difficulty": "난이도",
"disconnected": "연결 끊김",
"dismiss": "닫기",
"display": "디스플레이",
"download": "다운로드",
"download_bootstrap": "부트스트랩 다운로드",
"dragonx_green": "DragonX(그린)",
"edit": "편집",
"error": "오류",
@@ -249,23 +196,6 @@
"est_time_to_block": "예상 블록 시간",
"exit": "종료",
"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_csv": "CSV 내보내기",
"export_keys_btn": "키 내보내기",
@@ -294,22 +224,14 @@
"fetch_prices": "가격 조회",
"file": "파일",
"file_save_location": "파일 저장 위치: ~/.config/ObsidianDragon/",
"filter": "필터...",
"font_scale": "글꼴 크기",
"force_quit": "강제 종료",
"force_quit_confirm_msg": "정상 종료 없이 데몬을 즉시 종료합니다.\n블록체인 인덱스가 손상되어 재동기화가 필요할 수 있습니다.",
"force_quit_confirm_title": "강제 종료하시겠습니까?",
"force_quit_warning": "정상 종료 없이 데몬을 즉시 종료합니다. 블록체인 재동기화가 필요할 수 있습니다.",
"force_quit_yes": "강제 종료",
"from": "보낸 곳",
"from_upper": "보낸 곳",
"full_details": "전체 세부 정보",
"general": "일반",
"generating": "생성 중",
"go_to_receive": "수신으로 이동",
"height": "높이",
"help": "도움말",
"hidden_tag": " (숨김)",
"hide": "숨기기",
"hide_address": "주소 숨기기",
"hide_zero_balances": "잔액 0 숨기기",
@@ -331,9 +253,6 @@
"import_key_warning": "경고: 개인 키를 절대 공유하지 마세요! 신뢰할 수 없는 소스의 키를 가져오면 지갑이 위험해질 수 있습니다.",
"import_key_z_format": "Z 주소 지출 키 (secret-extended-key-...)",
"import_private_key": "개인 키 가져오기...",
"incorrect_passphrase": "잘못된 암호",
"incorrect_pin": "잘못된 PIN",
"insufficient_funds": "이 금액과 수수료를 위한 잔액이 부족합니다.",
"invalid_address": "잘못된 주소 형식",
"ip_address": "IP 주소",
"keep": "유지",
@@ -347,7 +266,6 @@
"key_export_viewing_keys_zonly": "보기 키는 차폐 (z) 주소에만 사용할 수 있습니다",
"key_export_viewing_warning": "이 조회 키를 사용하면 다른 사람이 수신 거래와 잔액을 볼 수 있지만 자금을 사용할 수는 없습니다. 신뢰할 수 있는 사람에게만 공유하세요.",
"label": "라벨:",
"label_placeholder": "예: 저축, 채굴...",
"language": "언어",
"light": "라이트",
"loading": "로딩 중...",
@@ -368,7 +286,6 @@
"market_now": "현재",
"market_pct_shielded": "%.0f%% 차폐됨",
"market_portfolio": "포트폴리오",
"market_price_loading": "가격 데이터를 불러오는 중...",
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
"market_refresh_price": "가격 데이터 새로고침",
"market_trade_on": "%s에서 거래",
@@ -394,13 +311,6 @@
"mining_address_copied": "채굴 주소가 복사되었습니다",
"mining_all_time": "전체 기간",
"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_chart_1m_ago": "1분 전",
"mining_chart_5m_ago": "5분 전",
@@ -420,15 +330,8 @@
"mining_filter_tip_all": "모든 수익 표시",
"mining_filter_tip_pool": "풀 수익만 표시",
"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_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_mine": "채굴",
"mining_mining_addr": "채굴 주소",
@@ -485,7 +388,6 @@
"no_addresses_available": "사용 가능한 주소 없음",
"no_addresses_match": "필터와 일치하는 주소가 없습니다",
"no_addresses_with_balance": "잔액이 있는 주소가 없습니다",
"no_addresses_yet": "아직 주소가 없습니다",
"no_matching": "일치하는 거래가 없습니다",
"no_recent_receives": "최근 수신 내역 없음",
"no_recent_sends": "최근 전송 내역 없음",
@@ -551,7 +453,6 @@
"peers_upper": "피어",
"peers_version": "버전",
"pending": "대기 중",
"pin_not_set": "PIN이 설정되지 않았습니다. 암호를 사용하여 잠금 해제하세요.",
"ping": "Ping",
"price_chart": "가격 차트",
"qr_code": "QR 코드",
@@ -572,9 +473,7 @@
"recent_received": "최근 수신",
"recent_sends": "최근 전송",
"recipient": "수신자",
"recipient_balance": "수신자: %.8f → %.8f DRGX",
"recv_type": "수신",
"reduce_motion": "모션 줄이기",
"refresh": "새로고침",
"refresh_now": "지금 새로고침",
"remove_favorite": "즐겨찾기 제거",
@@ -594,10 +493,7 @@
"request_uri_copied": "결제 URI가 클립보드에 복사되었습니다",
"rescan": "재스캔",
"reset_to_defaults": "기본값으로 재설정",
"restarting_after_encryption": "암호화 후 데몬 재시작 중...",
"restore_address": "주소 복원",
"result_preview": "결과 미리보기",
"retry": "재시도",
"review_send": "전송 검토",
"rpc_host": "RPC 호스트",
"rpc_pass": "비밀번호",
@@ -606,55 +502,8 @@
"save": "저장",
"save_settings": "설정 저장",
"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": "검색...",
"security": "보안",
"seed_backup_button": "시드 문구",
"seed_backup_close": "닫기",
"seed_backup_copy": "복사",
"seed_backup_intro": "이 단어들은 지갑의 마스터 백업입니다. 이 단어들을 가진 사람은 누구나 당신의 자금을 통제할 수 있습니다.",
"seed_backup_load_failed": "시드 문구를 불러올 수 없습니다.",
"seed_backup_loading": "시드 문구를 불러오는 중…",
"seed_backup_locked": "시드 문구를 표시하려면 지갑의 잠금을 해제하세요.",
"seed_backup_none": "이 지갑은 시드 문구 도입 이전에 만들어져 24단어 문구가 없습니다. 대신 \"백업...\"을 사용하거나 개인 키를 내보내 백업하세요.",
"seed_backup_reminder": "지갑에 24단어 복구 시드 문구가 있습니다. 지금 설정 → 노드 및 보안에서 백업하세요.",
"seed_backup_save": "파일로 저장…",
"seed_backup_save_failed": "쓸 수 없습니다: ",
"seed_backup_saved": "저장됨: ",
"seed_backup_title": "시드 문구 백업",
"seed_backup_warning": "순서대로 종이에 적어 오프라인으로 보관하고, 절대 공유하거나 사진으로 찍지 마세요. 이 단어들을 잃어버리면 자금을 복구할 수 없습니다.",
"select_address": "주소 선택...",
"select_receiving_address": "수신 주소 선택...",
"select_source_address": "보낼 주소 선택...",
@@ -704,15 +553,12 @@
"send_valid_transparent": "유효한 투명 주소",
"send_wallet_empty": "지갑이 비어 있습니다",
"send_yes_clear": "예, 지우기",
"sender_balance": "발신자: %.8f → %.8f DRGX",
"sending": "거래 전송 중",
"sending_from": "보내는 곳",
"sends_full_balance_warning": "전체 잔액을 전송합니다. 보내는 주소의 잔액이 0이 됩니다.",
"sent": "전송됨",
"sent_filter": "전송됨",
"sent_type": "전송됨",
"sent_upper": "전송됨",
"set_label": "라벨 설정...",
"settings": "설정",
"settings_about_text": "DragonX (DRGX)용 차폐 암호화폐 지갑으로, Dear ImGui로 제작되어 가볍고 휴대 가능합니다.",
"settings_acrylic_level": "아크릴 레벨:",
@@ -801,17 +647,13 @@
"shield_utxo_limit": "UTXO 제한:",
"shield_wildcard_hint": "'*'를 사용하여 모든 투명 주소에서 차폐",
"shielded": "차폐",
"shielded_address": "보호 주소",
"shielded_to": "차폐 대상",
"shielded_type": "차폐",
"shielding_notice": "참고: 투명 (T) 주소에서 프라이빗 (Z) 주소로 자금을 실딩합니다.",
"show": "표시",
"show_hidden": "숨겨진 항목 표시 (%d)",
"show_qr_code": "QR 코드 표시",
"showing_transactions": "%d%d / %d건의 거래 표시 중 (총: %zu)",
"showing_x_of_y": "%d / %d 주소 표시",
"showing_transactions": "%d–%d / %d건의 거래 표시 중 (총: %zu)",
"simple_background": "단순 배경",
"slider_off": "끔",
"start_mining": "채굴 시작",
"status": "상태",
"stop_external": "외부 데몬 중지",
@@ -820,26 +662,17 @@
"success": "성공",
"summary": "요약",
"syncing": "동기화 중...",
"t_address": "T 주소",
"t_addresses": "T 주소",
"test_connection": "테스트",
"theme": "테마",
"theme_effects": "테마 효과",
"theme_language": "테마 및 언어",
"time_days_ago": "%d일 전",
"time_hours_ago": "%d시간 전",
"time_minutes_ago": "%d분 전",
"time_seconds_ago": "%d초 전",
"timeout_15min": "15분",
"timeout_1hour": "1시간",
"timeout_1min": "1분",
"timeout_30min": "30분",
"timeout_5min": "5분",
"timeout_off": "끔",
"to": "받는 곳",
"to_upper": "받는 곳",
"tools": "도구",
"tools_actions": "도구 및 작업...",
"total": "합계",
"transaction_id": "거래 ID",
"transaction_sent": "거래 전송 성공",
@@ -847,13 +680,7 @@
"transaction_url": "거래 URL",
"transactions": "거래",
"transactions_upper": "거래",
"transfer_failed": "이체 실패",
"transfer_funds": "자금 이체",
"transfer_sent": "이체 전송됨",
"transfer_sent_desc": "이체가 네트워크에 제출되었습니다.",
"transfer_to": "이체 대상:",
"transparent": "투명",
"transparent_address": "투명 주소",
"tt_addr_url": "블록 탐색기에서 주소를 보기 위한 기본 URL",
"tt_address_book": "빠른 전송을 위해 저장된 주소 관리",
"tt_auto_lock": "이 비활성 시간 후 지갑 잠금",
@@ -868,8 +695,6 @@
"tt_custom_theme": "사용자 지정 테마 활성화됨",
"tt_debug_collapse": "디버그 로깅 옵션 접기",
"tt_debug_expand": "디버그 로깅 옵션 펼치기",
"tt_delete_blockchain": "모든 블록체인 데이터를 삭제하고 새로 동기화합니다. wallet.dat 및 설정은 보존됩니다.",
"tt_download_bootstrap": "블록체인 부트스트랩을 다운로드하여 동기화 가속\n기존 블록 데이터가 교체됩니다",
"tt_encrypt": "비밀번호로 wallet.dat 암호화",
"tt_export_all": "모든 개인키를 파일로 내보내기",
"tt_export_csv": "거래 내역을 CSV 스프레드시트로 내보내기",
@@ -887,7 +712,6 @@
"tt_mine_idle": "시스템이 유휴 상태(키보드/마우스 입력 없음)일 때\\n자동으로 채굴 시작",
"tt_noise": "그레인 텍스처 강도 (0%% = 끔, 100%% = 최대)",
"tt_open_dir": "파일 탐색기에서 열려면 클릭",
"tt_reduce_motion": "접근성을 위해 애니메이션 전환 및 잔액 보간 비활성화",
"tt_remove_encrypt": "암호화를 제거하고 지갑을 보호 없이 저장",
"tt_remove_pin": "PIN을 제거하고 잠금 해제 시 비밀번호 요구",
"tt_report_bug": "프로젝트 트래커에서 문제 보고",
@@ -903,7 +727,6 @@
"tt_save_ztx": "z-address 거래 기록을 로컬에 저장하여 빠른 로딩",
"tt_scan_themes": "새 테마 검색.\\n테마 폴더를 여기에 배치:\\n%s",
"tt_scanline": "콘솔에서 CRT 스캔라인 효과",
"tt_seed_backup": "지갑의 24단어 복구 시드 문구를 표시하고 백업합니다",
"tt_set_pin": "빠른 잠금 해제를 위한 4-8자리 PIN 설정",
"tt_shield_mining": "투명 채굴 보상을 차폐 주소로 이동",
"tt_simple_bg": "배경에 단순 그라데이션 사용\\n단축키: Ctrl+Up",
@@ -966,9 +789,7 @@
"warning_upper": "경고!",
"website": "웹사이트",
"window_opacity": "창 투명도",
"wizard_daemon_start_failed": "데몬 시작 실패 — 자동으로 재시도됩니다",
"yes_clear": "예, 지우기",
"your_addresses": "내 주소",
"z_address": "Z 주소",
"z_addresses": "Z 주소"
}

View File

@@ -43,8 +43,6 @@
"address_url": "URL do Endereço",
"addresses_appear_here": "Seus endereços de recebimento aparecerão aqui após a conexão.",
"advanced": "AVANÇADO",
"advanced_effects": "Efeitos Avançados...",
"ago": "atrás",
"all_filter": "Todos",
"allow_custom_fees": "Permitir taxas personalizadas",
"amount": "Valor",
@@ -92,52 +90,12 @@
"block_timestamp": "Carimbo de Data:",
"block_transactions": "Transações:",
"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",
"characters": "caracteres",
"chat": "Chat",
"chat_cancel": "Cancelar",
"chat_contact_request": "solicitação de contato",
"chat_empty_hint": "Nenhuma conversa ainda. As mensagens que você receber aparecerão aqui.",
"chat_locked_hint": "Desbloqueie sua carteira para carregar suas conversas.",
"chat_new_button": "Nova conversa",
"chat_new_message": "Mensagem",
"chat_new_send": "Enviar solicitação",
"chat_new_title": "Nova conversa",
"chat_new_zaddr": "Endereço-z do destinatário",
"chat_select_hint": "Selecione uma conversa para visualizá-la.",
"chat_send": "Enviar",
"chat_send_failed": "não enviada",
"chat_toast_compose_failed": "Não foi possível compor a mensagem (muito longa?).",
"chat_toast_lite_busy": "Já há um envio em andamento, ou nenhuma carteira está aberta.",
"chat_toast_no_zaddr": "Nenhum endereço-z disponível para enviar o chat.",
"chat_toast_not_connected": "Não conectado — mensagem de chat não enviada.",
"chat_toast_request_compose_failed": "Não foi possível compor a solicitação de contato (endereço / texto inválido?).",
"chat_toast_request_queued": "Solicitação de contato na fila.",
"chat_toast_waiting_reply": "Aguardando a resposta do contato antes que você possa enviar mensagens a ele.",
"chat_waiting_reply": "Aguardando a resposta deste contato — você poderá enviar mensagens assim que ele responder.",
"chat_you": "Você",
"choose_icon": "Escolher Ícone",
"clear": "Limpar",
"clear_all_bans": "Remover Todos os Banimentos",
"clear_anyway": "Limpar mesmo assim",
"clear_form_confirm": "Limpar todos os campos do formulário?",
"clear_icon": "Limpar Ícone",
"clear_request": "Limpar Solicitação",
"click_copy_address": "Clique para copiar o endereço",
"click_copy_uri": "Clique para copiar a URI",
@@ -148,12 +106,8 @@
"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_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_transaction": "Confirmar Transação",
"confirm_transfer": "Confirmar Transferência",
"confirmations": "Confirmações",
"confirmations_display": "%d confirmações | %s",
"confirmed": "Confirmado",
@@ -218,7 +172,6 @@
"console_welcome": "Bem-vindo ao Console ObsidianDragon",
"console_zoom_in": "Aumentar zoom",
"console_zoom_out": "Diminuir zoom",
"copied": "Copiado!",
"copy": "Copiar",
"copy_address": "Copiar Endereço Completo",
"copy_error": "Copiar Erro",
@@ -227,45 +180,22 @@
"copy_uri": "Copiar URI",
"current_price": "Preço Atual",
"custom_fees": "Taxas personalizadas",
"daemon_version": "Daemon",
"dark": "Escuro",
"date": "Data",
"date_label": "Data:",
"debug_logging": "REGISTRO DE DEPURAÇÃO",
"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",
"disconnected": "Desconectado",
"dismiss": "Dispensar",
"display": "Exibição",
"download": "Baixar",
"download_bootstrap": "Baixar Bootstrap",
"dragonx_green": "DragonX (Verde)",
"edit": "Editar",
"error": "Erro",
"error_format": "Erro: %s",
"est_time_to_block": "Tempo Est. por Bloco",
"exit": "Sair",
"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",
"explorer": "EXPLORADOR",
"export": "Exportar",
"export_csv": "Exportar CSV",
"export_keys_btn": "Exportar Chaves",
@@ -294,22 +224,14 @@
"fetch_prices": "Buscar preços",
"file": "Arquivo",
"file_save_location": "O arquivo será salvo em: ~/.config/ObsidianDragon/",
"filter": "Filtrar...",
"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_upper": "DE",
"full_details": "Detalhes Completos",
"general": "Geral",
"generating": "Gerando",
"go_to_receive": "Ir para Receber",
"height": "Altura",
"help": "Ajuda",
"hidden_tag": " (oculto)",
"hide": "Ocultar",
"hide_address": "Ocultar endereço",
"hide_zero_balances": "Ocultar saldos zero",
@@ -331,9 +253,6 @@
"import_key_warning": "Aviso: Nunca compartilhe suas chaves privadas! Importar chaves de fontes não confiáveis pode comprometer sua carteira.",
"import_key_z_format": "Chaves de gasto de z-endereço (secret-extended-key-...)",
"import_private_key": "Importar Chave Privada...",
"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",
"ip_address": "Endereço IP",
"keep": "Manter",
@@ -347,7 +266,6 @@
"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.",
"label": "Rótulo:",
"label_placeholder": "ex. Poupança, Mineração...",
"language": "Idioma",
"light": "Claro",
"loading": "Carregando...",
@@ -368,7 +286,6 @@
"market_now": "Agora",
"market_pct_shielded": "%.0f%% Blindado",
"market_portfolio": "PORTFÓLIO",
"market_price_loading": "Carregando dados de preço...",
"market_price_unavailable": "Dados de preço indisponíveis",
"market_refresh_price": "Atualizar dados de preço",
"market_trade_on": "Negociar no %s",
@@ -394,13 +311,6 @@
"mining_address_copied": "Endereço de mineração copiado",
"mining_all_time": "Todo o Tempo",
"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_chart_1m_ago": "1m atrás",
"mining_chart_5m_ago": "5m atrás",
@@ -420,15 +330,8 @@
"mining_filter_tip_all": "Mostrar todos os ganhos",
"mining_filter_tip_pool": "Mostrar apenas ganhos do pool",
"mining_filter_tip_solo": "Mostrar apenas ganhos solo",
"mining_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_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_mine": "Minerar",
"mining_mining_addr": "End. Mineração",
@@ -485,7 +388,6 @@
"no_addresses_available": "Nenhum endereço disponível",
"no_addresses_match": "Nenhum endereço corresponde ao filtro",
"no_addresses_with_balance": "Nenhum endereço com saldo",
"no_addresses_yet": "Nenhum endereço ainda",
"no_matching": "Nenhuma transação correspondente",
"no_recent_receives": "Nenhum recebimento recente",
"no_recent_sends": "Nenhum envio recente",
@@ -498,7 +400,7 @@
"notes": "Notas",
"notes_optional": "Notas (opcional):",
"output_filename": "Nome do arquivo de saída:",
"overview": "Resumo",
"overview": "Visão Geral",
"paste": "Colar",
"paste_from_clipboard": "Colar da Área de Transferência",
"pay_from": "Pagar de",
@@ -551,7 +453,6 @@
"peers_upper": "PARES",
"peers_version": "Versão",
"pending": "Pendente",
"pin_not_set": "PIN não definido. Use a senha para desbloquear.",
"ping": "Ping",
"price_chart": "Gráfico de Preços",
"qr_code": "Código QR",
@@ -572,9 +473,7 @@
"recent_received": "RECEBIDOS RECENTES",
"recent_sends": "ENVIOS RECENTES",
"recipient": "DESTINATÁRIO",
"recipient_balance": "Destinatário: %.8f → %.8f DRGX",
"recv_type": "Receb.",
"reduce_motion": "Reduzir Movimento",
"refresh": "Atualizar",
"refresh_now": "Atualizar Agora",
"remove_favorite": "Remover favorito",
@@ -594,10 +493,7 @@
"request_uri_copied": "URI de pagamento copiada para a área de transferência",
"rescan": "Reescanear",
"reset_to_defaults": "Redefinir Padrões",
"restarting_after_encryption": "Reiniciando daemon após criptografia...",
"restore_address": "Restaurar endereço",
"result_preview": "Pré-visualização do resultado",
"retry": "Tentar novamente",
"review_send": "Revisar Envio",
"rpc_host": "Host RPC",
"rpc_pass": "Senha",
@@ -606,55 +502,8 @@
"save": "Salvar",
"save_settings": "Salvar Configurações",
"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...",
"security": "SEGURANÇA",
"seed_backup_button": "Frase de recuperação",
"seed_backup_close": "Fechar",
"seed_backup_copy": "Copiar",
"seed_backup_intro": "Estas palavras são o backup mestre da sua carteira. Qualquer pessoa que as tenha controla seus fundos.",
"seed_backup_load_failed": "Não foi possível carregar a frase de recuperação.",
"seed_backup_loading": "Carregando sua frase de recuperação…",
"seed_backup_locked": "Desbloqueie sua carteira para revelar sua frase de recuperação.",
"seed_backup_none": "Esta carteira é anterior às frases de recuperação, portanto não possui uma frase de 24 palavras. Faça o backup dela com \"Backup...\" ou exportando suas chaves privadas.",
"seed_backup_reminder": "Sua carteira tem uma frase de recuperação de 24 palavras. Faça o backup dela agora em Configurações → Nó e Segurança.",
"seed_backup_save": "Salvar em arquivo…",
"seed_backup_save_failed": "Não foi possível gravar ",
"seed_backup_saved": "Salvo em ",
"seed_backup_title": "Backup da frase de recuperação",
"seed_backup_warning": "Anote-as em ordem, guarde-as offline e nunca as compartilhe ou fotografe. Se você as perder, seus fundos não poderão ser recuperados.",
"select_address": "Selecionar endereço...",
"select_receiving_address": "Selecionar endereço de recebimento...",
"select_source_address": "Selecionar endereço de origem...",
@@ -704,16 +553,13 @@
"send_valid_transparent": "Endereço transparente válido",
"send_wallet_empty": "Sua carteira está vazia",
"send_yes_clear": "Sim, Limpar",
"sender_balance": "Remetente: %.8f → %.8f DRGX",
"sending": "Enviando transação",
"sending_from": "ENVIANDO DE",
"sends_full_balance_warning": "Isso envia o saldo total. O endereço de envio terá saldo zero.",
"sent": "enviado",
"sent_filter": "Enviado",
"sent_type": "Enviado",
"sent_upper": "ENVIADO",
"set_label": "Definir Rótulo...",
"settings": "Ajustes",
"settings": "Configurações",
"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_address_book": "Livro de endereços...",
@@ -801,17 +647,13 @@
"shield_utxo_limit": "Limite UTXO:",
"shield_wildcard_hint": "Use '*' para blindar de todos os endereços transparentes",
"shielded": "Blindado",
"shielded_address": "Endereço Blindado",
"shielded_to": "BLINDADO PARA",
"shielded_type": "Blindado",
"shielding_notice": "Nota: Isso blindará fundos de um endereço transparente (T) para um endereço privado (Z).",
"show": "Mostrar",
"show_hidden": "Mostrar ocultos (%d)",
"show_qr_code": "Mostrar Código QR",
"showing_transactions": "Mostrando %d%d de %d transações (total: %zu)",
"showing_x_of_y": "Mostrando %d de %d endereços",
"showing_transactions": "Mostrando %d–%d de %d transações (total: %zu)",
"simple_background": "Fundo simples",
"slider_off": "Desligado",
"start_mining": "Iniciar Mineração",
"status": "Status",
"stop_external": "Parar daemon externo",
@@ -820,26 +662,17 @@
"success": "Sucesso",
"summary": "Resumo",
"syncing": "Sincronizando...",
"t_address": "Endereço T",
"t_addresses": "Endereços T",
"test_connection": "Testar",
"theme": "Tema",
"theme_effects": "Efeitos de tema",
"theme_language": "TEMA E IDIOMA",
"time_days_ago": "há %d dias",
"time_hours_ago": "há %d horas",
"time_minutes_ago": "há %d minutos",
"time_seconds_ago": "há %d segundos",
"timeout_15min": "15 min",
"timeout_1hour": "1 hora",
"timeout_1min": "1 min",
"timeout_30min": "30 min",
"timeout_5min": "5 min",
"timeout_off": "Desligado",
"to": "Para",
"to_upper": "PARA",
"tools": "FERRAMENTAS",
"tools_actions": "Ferramentas e Ações...",
"total": "Total",
"transaction_id": "ID DA TRANSAÇÃO",
"transaction_sent": "Transação enviada com sucesso",
@@ -847,13 +680,7 @@
"transaction_url": "URL da Transação",
"transactions": "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_address": "Endereço Transparente",
"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_auto_lock": "Bloquear carteira após este tempo de inatividade",
@@ -868,8 +695,6 @@
"tt_custom_theme": "Tema personalizado ativo",
"tt_debug_collapse": "Recolher 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_export_all": "Exportar todas as chaves privadas para um arquivo",
"tt_export_csv": "Exportar histórico de transações como planilha CSV",
@@ -887,7 +712,6 @@
"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_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_pin": "Remover PIN e exigir frase secreta para desbloquear",
"tt_report_bug": "Reportar um problema no rastreador do projeto",
@@ -903,7 +727,6 @@
"tt_save_ztx": "Armazenar histórico de transações z-address localmente para carregamento mais rápido",
"tt_scan_themes": "Procurar novos temas.\\nColoque pastas de temas em:\\n%s",
"tt_scanline": "Efeito de linhas de varredura CRT no console",
"tt_seed_backup": "Mostrar e fazer backup da frase de recuperação de 24 palavras da sua carteira",
"tt_set_pin": "Definir um PIN de 4-8 dígitos para desbloqueio rápido",
"tt_shield_mining": "Mover recompensas de mineração transparentes para um endereço blindado",
"tt_simple_bg": "Usar um gradiente simples para o fundo\\nAtalho: Ctrl+Up",
@@ -966,9 +789,7 @@
"warning_upper": "AVISO!",
"website": "Website",
"window_opacity": "Opacidade da Janela",
"wizard_daemon_start_failed": "Falha ao iniciar o daemon — será tentado novamente automaticamente",
"yes_clear": "Sim, Limpar",
"your_addresses": "Seus Endereços",
"z_address": "Endereço Z",
"z_addresses": "Endereços Z"
}

View File

@@ -42,9 +42,7 @@
"address_upper": "АДРЕС",
"address_url": "URL адреса",
"addresses_appear_here": "Ваши адреса для получения появятся здесь после подключения.",
"advanced": "ПРОЧЕЕ",
"advanced_effects": "Расширенные эффекты...",
"ago": "назад",
"advanced": "РАСШИРЕННЫЕ",
"all_filter": "Все",
"allow_custom_fees": "Разрешить пользовательские комиссии",
"amount": "Сумма",
@@ -92,52 +90,12 @@
"block_timestamp": "Временная метка:",
"block_transactions": "Транзакции:",
"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": "Отмена",
"characters": "символов",
"chat": "Чат",
"chat_cancel": "Отмена",
"chat_contact_request": "запрос контакта",
"chat_empty_hint": "Пока нет переписок. Полученные сообщения появятся здесь.",
"chat_locked_hint": "Разблокируйте кошелёк, чтобы загрузить переписку.",
"chat_new_button": "Новая переписка",
"chat_new_message": "Сообщение",
"chat_new_send": "Отправить запрос",
"chat_new_title": "Новая переписка",
"chat_new_zaddr": "Z-адрес получателя",
"chat_select_hint": "Выберите переписку для просмотра.",
"chat_send": "Отправить",
"chat_send_failed": "не отправлено",
"chat_toast_compose_failed": "Не удалось составить сообщение (слишком длинное?).",
"chat_toast_lite_busy": "Отправка уже выполняется, или кошелёк не открыт.",
"chat_toast_no_zaddr": "Нет доступного Z-адреса для отправки сообщений.",
"chat_toast_not_connected": "Нет подключения — сообщение не отправлено.",
"chat_toast_request_compose_failed": "Не удалось составить запрос контакта (неверный адрес / текст?).",
"chat_toast_request_queued": "Запрос контакта поставлен в очередь.",
"chat_toast_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему только после этого.",
"chat_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему, как только он ответит.",
"chat_you": "Вы",
"choose_icon": "Выбрать иконку",
"clear": "Очистить",
"clear_all_bans": "Снять все блокировки",
"clear_anyway": "Всё равно очистить",
"clear_form_confirm": "Очистить все поля формы?",
"clear_icon": "Удалить иконку",
"clear_request": "Очистить запрос",
"click_copy_address": "Нажмите, чтобы скопировать адрес",
"click_copy_uri": "Нажмите, чтобы скопировать URI",
@@ -148,12 +106,8 @@
"confirm_clear_ztx_title": "Подтвердить очистку истории Z-Tx",
"confirm_clear_ztx_warning1": "Очистка истории z-транзакций может привести к отображению защищённого баланса как 0, пока не будет выполнено пересканирование кошелька.",
"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_transaction": "Подтвердить транзакцию",
"confirm_transfer": "Подтвердить перевод",
"confirmations": "Подтверждения",
"confirmations_display": "%d подтверждений | %s",
"confirmed": "Подтверждено",
@@ -218,7 +172,6 @@
"console_welcome": "Добро пожаловать в консоль ObsidianDragon",
"console_zoom_in": "Увеличить",
"console_zoom_out": "Уменьшить",
"copied": "Скопировано!",
"copy": "Копировать",
"copy_address": "Копировать полный адрес",
"copy_error": "Копировать ошибку",
@@ -227,45 +180,22 @@
"copy_uri": "Копировать URI",
"current_price": "Текущая цена",
"custom_fees": "Пользовательские комиссии",
"daemon_version": "Демон",
"dark": "Тёмная",
"date": "Дата",
"date_label": "Дата:",
"debug_logging": "ЖУРНАЛ ОТЛАДКИ",
"delete": "Удалить",
"delete_blockchain": "Удалить блокчейн",
"delete_blockchain_confirm": "Удалить и пересинхронизировать",
"deshielding_warning": "Внимание: это переведёт средства из приватного (Z) адреса на прозрачный (T) адрес.",
"difficulty": "Сложность",
"disconnected": "Отключено",
"dismiss": "Отклонить",
"display": "Отображение",
"download": "Скачать",
"download_bootstrap": "Скачать бутстрап",
"dragonx_green": "DragonX (Зелёная)",
"edit": "Редактировать",
"error": "Ошибка",
"error_format": "Ошибка: %s",
"est_time_to_block": "Расч. время до блока",
"exit": "Выход",
"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": "Размер",
"explorer": "ОБОЗРЕВАТЕЛЬ",
"export": "Экспорт",
"export_csv": "Экспорт в CSV",
"export_keys_btn": "Экспорт ключей",
@@ -294,22 +224,14 @@
"fetch_prices": "Получить цены",
"file": "Файл",
"file_save_location": "Файл будет сохранён в: ~/.config/ObsidianDragon/",
"filter": "Фильтр...",
"font_scale": "Масштаб шрифта",
"force_quit": "Принудительный выход",
"force_quit_confirm_msg": "Это немедленно завершит демон без корректного завершения.\nЭто может повредить индекс блокчейна и потребовать повторной синхронизации.",
"force_quit_confirm_title": "Принудительный выход?",
"force_quit_warning": "Это немедленно завершит демон без корректного завершения. Может потребоваться повторная синхронизация блокчейна.",
"force_quit_yes": "Принудительный выход",
"from": "От",
"from_upper": "ОТ",
"full_details": "Полные детали",
"general": "Общие",
"generating": "Генерация",
"go_to_receive": "Перейти к получению",
"height": "Высота",
"help": "Справка",
"hidden_tag": " (скрыт)",
"hide": "Скрыть",
"hide_address": "Скрыть адрес",
"hide_zero_balances": "Скрыть нулевые балансы",
@@ -331,9 +253,6 @@
"import_key_warning": "Предупреждение: Никогда не делитесь своими приватными ключами! Импорт ключей из ненадёжных источников может скомпрометировать ваш кошелёк.",
"import_key_z_format": "Ключи расходования z-адресов (secret-extended-key-...)",
"import_private_key": "Импорт приватного ключа...",
"incorrect_passphrase": "Неверный пароль",
"incorrect_pin": "Неверный PIN",
"insufficient_funds": "Недостаточно средств для этой суммы плюс комиссия.",
"invalid_address": "Неверный формат адреса",
"ip_address": "IP-адрес",
"keep": "Сохранить",
@@ -347,7 +266,6 @@
"key_export_viewing_keys_zonly": "Ключи просмотра доступны только для экранированных (z) адресов",
"key_export_viewing_warning": "Этот ключ просмотра позволяет другим видеть входящие транзакции и баланс, но НЕ тратить ваши средства. Делитесь только с доверенными лицами.",
"label": "Метка:",
"label_placeholder": "напр. Накопления, Майнинг...",
"language": "Язык",
"light": "Светлая",
"loading": "Загрузка...",
@@ -368,7 +286,6 @@
"market_now": "Сейчас",
"market_pct_shielded": "%.0f%% Экранировано",
"market_portfolio": "ПОРТФЕЛЬ",
"market_price_loading": "Загрузка данных о ценах...",
"market_price_unavailable": "Данные о ценах недоступны",
"market_refresh_price": "Обновить данные о ценах",
"market_trade_on": "Торговать на %s",
@@ -394,13 +311,6 @@
"mining_address_copied": "Адрес майнинга скопирован",
"mining_all_time": "За всё время",
"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_chart_1m_ago": "1м назад",
"mining_chart_5m_ago": "5м назад",
@@ -420,15 +330,8 @@
"mining_filter_tip_all": "Показать все доходы",
"mining_filter_tip_pool": "Показать только доходы пула",
"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_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_mine": "Майнить",
"mining_mining_addr": "Адрес майн.",
@@ -485,7 +388,6 @@
"no_addresses_available": "Нет доступных адресов",
"no_addresses_match": "Нет адресов, соответствующих фильтру",
"no_addresses_with_balance": "Нет адресов с балансом",
"no_addresses_yet": "Пока нет адресов",
"no_matching": "Нет подходящих транзакций",
"no_recent_receives": "Нет недавних получений",
"no_recent_sends": "Нет недавних отправлений",
@@ -551,7 +453,6 @@
"peers_upper": "УЗЛЫ",
"peers_version": "Версия",
"pending": "Ожидание",
"pin_not_set": "PIN не установлен. Используйте пароль для разблокировки.",
"ping": "Пинг",
"price_chart": "График цен",
"qr_code": "QR-код",
@@ -572,9 +473,7 @@
"recent_received": "НЕДАВНО ПОЛУЧЕНО",
"recent_sends": "НЕДАВНО ОТПРАВЛЕНО",
"recipient": "ПОЛУЧАТЕЛЬ",
"recipient_balance": "Получатель: %.8f → %.8f DRGX",
"recv_type": "Получ.",
"reduce_motion": "Уменьшить анимацию",
"refresh": "Обновить",
"refresh_now": "Обновить сейчас",
"remove_favorite": "Удалить из избранного",
@@ -594,10 +493,7 @@
"request_uri_copied": "URI платежа скопирован в буфер обмена",
"rescan": "Пересканировать",
"reset_to_defaults": "Сбросить настройки",
"restarting_after_encryption": "Перезапуск демона после шифрования...",
"restore_address": "Восстановить адрес",
"result_preview": "Предпросмотр результата",
"retry": "Повторить",
"review_send": "Проверить отправку",
"rpc_host": "RPC-хост",
"rpc_pass": "Пароль",
@@ -606,55 +502,8 @@
"save": "Сохранить",
"save_settings": "Сохранить настройки",
"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": "Поиск...",
"security": "БЕЗОПАСНОСТЬ",
"seed_backup_button": "Сид-фраза",
"seed_backup_close": "Закрыть",
"seed_backup_copy": "Копировать",
"seed_backup_intro": "Эти слова — главная резервная копия вашего кошелька. Любой, у кого они есть, управляет вашими средствами.",
"seed_backup_load_failed": "Не удалось загрузить сид-фразу.",
"seed_backup_loading": "Загрузка вашей сид-фразы…",
"seed_backup_locked": "Разблокируйте кошелёк, чтобы показать его сид-фразу.",
"seed_backup_none": "Этот кошелёк создан до появления сид-фраз, поэтому у него нет фразы из 24 слов. Создайте резервную копию с помощью «Резервная копия...» или вместо этого экспортируйте свои приватные ключи.",
"seed_backup_reminder": "У вашего кошелька есть сид-фраза восстановления из 24 слов. Создайте её резервную копию прямо сейчас в Настройки → Узел и безопасность.",
"seed_backup_save": "Сохранить в файл…",
"seed_backup_save_failed": "Не удалось записать ",
"seed_backup_saved": "Сохранено в ",
"seed_backup_title": "Резервная копия сид-фразы",
"seed_backup_warning": "Запишите их по порядку, храните офлайн и никогда не делитесь ими и не фотографируйте их. Если вы их потеряете, ваши средства не удастся восстановить.",
"select_address": "Выбрать адрес...",
"select_receiving_address": "Выбрать адрес получения...",
"select_source_address": "Выбрать адрес-источник...",
@@ -704,15 +553,12 @@
"send_valid_transparent": "Действительный прозрачный адрес",
"send_wallet_empty": "Ваш кошелёк пуст",
"send_yes_clear": "Да, очистить",
"sender_balance": "Отправитель: %.8f → %.8f DRGX",
"sending": "Отправка транзакции",
"sending_from": "ОТПРАВКА С",
"sends_full_balance_warning": "Это отправит весь баланс. Адрес отправителя останется с нулевым балансом.",
"sent": "отправлено",
"sent_filter": "Отправлено",
"sent_type": "Отправлено",
"sent_upper": "ОТПРАВЛЕНО",
"set_label": "Установить метку...",
"settings": "Настройки",
"settings_about_text": "Защищённый криптовалютный кошелёк для DragonX (DRGX), созданный на Dear ImGui для лёгкого и портативного использования.",
"settings_acrylic_level": "Уровень акрила:",
@@ -801,17 +647,13 @@
"shield_utxo_limit": "Лимит UTXO:",
"shield_wildcard_hint": "Используйте '*' для экранирования со всех прозрачных адресов",
"shielded": "Экранированный",
"shielded_address": "Экранированный адрес",
"shielded_to": "ЭКРАНИРОВАНО НА",
"shielded_type": "Экранированный",
"shielding_notice": "Примечание: это переведёт средства с прозрачного (T) адреса на приватный (Z) адрес.",
"show": "Показать",
"show_hidden": "Показать скрытые (%d)",
"show_qr_code": "Показать QR-код",
"showing_transactions": "Показано %d%d из %d транзакций (всего: %zu)",
"showing_x_of_y": "Показано %d из %d адресов",
"showing_transactions": "Показано %d–%d из %d транзакций (всего: %zu)",
"simple_background": "Простой фон",
"slider_off": "Выкл",
"start_mining": "Начать майнинг",
"status": "Статус",
"stop_external": "Остановить внешний daemon",
@@ -820,26 +662,17 @@
"success": "Успешно",
"summary": "Итоги",
"syncing": "Синхронизация...",
"t_address": "T-адрес",
"t_addresses": "T-адреса",
"test_connection": "Тест",
"theme": "Тема",
"theme_effects": "Эффекты темы",
"theme_language": "ТЕМА И ЯЗЫК",
"time_days_ago": "%d дней назад",
"time_hours_ago": "%d часов назад",
"time_minutes_ago": "%d минут назад",
"time_seconds_ago": "%d секунд назад",
"timeout_15min": "15 мин",
"timeout_1hour": "1 час",
"timeout_1min": "1 мин",
"timeout_30min": "30 мин",
"timeout_5min": "5 мин",
"timeout_off": "Выкл",
"to": "Кому",
"to_upper": "КОМУ",
"tools": "УТИЛИТЫ",
"tools_actions": "Инструменты и действия...",
"tools": "ИНСТРУМЕНТЫ",
"total": "Итого",
"transaction_id": "ID ТРАНЗАКЦИИ",
"transaction_sent": "Транзакция успешно отправлена",
@@ -847,13 +680,7 @@
"transaction_url": "URL транзакции",
"transactions": "Транзакции",
"transactions_upper": "ТРАНЗАКЦИИ",
"transfer_failed": "Ошибка перевода",
"transfer_funds": "Перевести средства",
"transfer_sent": "Перевод отправлен",
"transfer_sent_desc": "Ваш перевод отправлен в сеть.",
"transfer_to": "Перевести на:",
"transparent": "Прозрачный",
"transparent_address": "Прозрачный адрес",
"tt_addr_url": "Базовый URL для просмотра адресов в обозревателе блоков",
"tt_address_book": "Управление сохранёнными адресами для быстрой отправки",
"tt_auto_lock": "Заблокировать кошелёк после этого времени бездействия",
@@ -868,8 +695,6 @@
"tt_custom_theme": "Пользовательская тема активна",
"tt_debug_collapse": "Свернуть параметры журнала отладки",
"tt_debug_expand": "Развернуть параметры журнала отладки",
"tt_delete_blockchain": "Удалить все данные блокчейна и начать синхронизацию заново. wallet.dat и конфигурация сохраняются.",
"tt_download_bootstrap": "Скачать бутстрап блокчейна для ускорения синхронизации\nСуществующие данные блоков будут заменены",
"tt_encrypt": "Зашифровать wallet.dat паролем",
"tt_export_all": "Экспортировать все приватные ключи в файл",
"tt_export_csv": "Экспортировать историю транзакций в виде таблицы CSV",
@@ -887,7 +712,6 @@
"tt_mine_idle": "Автоматически начать майнинг при\\nпростое системы (нет ввода с клавиатуры/мыши)",
"tt_noise": "Интенсивность зернистой текстуры (0%% = выкл., 100%% = максимум)",
"tt_open_dir": "Нажмите, чтобы открыть в проводнике",
"tt_reduce_motion": "Отключить анимированные переходы и плавное изменение баланса для доступности",
"tt_remove_encrypt": "Удалить шифрование и хранить кошелёк без защиты",
"tt_remove_pin": "Удалить PIN и требовать пароль для разблокировки",
"tt_report_bug": "Сообщить о проблеме в трекере проекта",
@@ -903,7 +727,6 @@
"tt_save_ztx": "Хранить историю транзакций z-адреса локально для более быстрой загрузки",
"tt_scan_themes": "Поиск новых тем.\\nРазместите папки тем в:\\n%s",
"tt_scanline": "Эффект развёртки ЭЛТ в консоли",
"tt_seed_backup": "Показать и создать резервную копию сид-фразы восстановления вашего кошелька из 24 слов",
"tt_set_pin": "Установить 4-8-значный PIN для быстрой разблокировки",
"tt_shield_mining": "Перевести прозрачные вознаграждения за майнинг на экранированный адрес",
"tt_simple_bg": "Использовать простой градиент для фона\\nГорячая клавиша: Ctrl+Up",
@@ -966,9 +789,7 @@
"warning_upper": "ПРЕДУПРЕЖДЕНИЕ!",
"website": "Веб-сайт",
"window_opacity": "Прозрачность окна",
"wizard_daemon_start_failed": "Не удалось запустить демон — будет повторено автоматически",
"yes_clear": "Да, очистить",
"your_addresses": "Ваши адреса",
"z_address": "Z-адрес",
"z_addresses": "Z-адреса"
}

View File

@@ -43,8 +43,6 @@
"address_url": "地址 URL",
"addresses_appear_here": "连接后,您的接收地址将显示在此处。",
"advanced": "高级",
"advanced_effects": "高级特效...",
"ago": "前",
"all_filter": "全部",
"allow_custom_fees": "允许自定义手续费",
"amount": "金额",
@@ -92,52 +90,12 @@
"block_timestamp": "时间戳:",
"block_transactions": "交易:",
"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": "取消",
"characters": "字符",
"chat": "聊天",
"chat_cancel": "取消",
"chat_contact_request": "联系人请求",
"chat_empty_hint": "暂无对话。您收到的消息将显示在此处。",
"chat_locked_hint": "解锁钱包以加载您的聊天记录。",
"chat_new_button": "新建对话",
"chat_new_message": "消息",
"chat_new_send": "发送请求",
"chat_new_title": "新建对话",
"chat_new_zaddr": "收款方 z 地址",
"chat_select_hint": "选择一个对话以查看。",
"chat_send": "发送",
"chat_send_failed": "未发送",
"chat_toast_compose_failed": "无法编写该消息(内容过长?)。",
"chat_toast_lite_busy": "已有发送正在进行中,或未打开任何钱包。",
"chat_toast_no_zaddr": "没有可用于发送聊天的 z 地址。",
"chat_toast_not_connected": "未连接——聊天消息未发送。",
"chat_toast_request_compose_failed": "无法编写联系人请求(地址或文本无效?)。",
"chat_toast_request_queued": "联系人请求已排队。",
"chat_toast_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
"chat_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
"chat_you": "我",
"choose_icon": "选择图标",
"clear": "清除",
"clear_all_bans": "解除所有封禁",
"clear_anyway": "仍然清除",
"clear_form_confirm": "清除所有表单字段?",
"clear_icon": "清除图标",
"clear_request": "清除请求",
"click_copy_address": "点击复制地址",
"click_copy_uri": "点击复制 URI",
@@ -148,12 +106,8 @@
"confirm_clear_ztx_title": "确认清除 Z-Tx 历史",
"confirm_clear_ztx_warning1": "清除 z-交易历史可能导致您的屏蔽余额显示为 0直到执行钱包重新扫描。",
"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_transaction": "确认交易",
"confirm_transfer": "确认转账",
"confirmations": "确认数",
"confirmations_display": "%d 次确认 | %s",
"confirmed": "已确认",
@@ -218,7 +172,6 @@
"console_welcome": "欢迎使用 ObsidianDragon 控制台",
"console_zoom_in": "放大",
"console_zoom_out": "缩小",
"copied": "已复制!",
"copy": "复制",
"copy_address": "复制完整地址",
"copy_error": "复制错误",
@@ -227,21 +180,15 @@
"copy_uri": "复制 URI",
"current_price": "当前价格",
"custom_fees": "自定义手续费",
"daemon_version": "守护进程",
"dark": "深色",
"date": "日期",
"date_label": "日期:",
"debug_logging": "调试日志",
"delete": "删除",
"delete_blockchain": "删除区块链",
"delete_blockchain_confirm": "删除并重新同步",
"deshielding_warning": "警告:这将把资金从隐私 (Z) 地址转移到透明 (T) 地址。",
"difficulty": "难度",
"disconnected": "已断开",
"dismiss": "关闭",
"display": "显示",
"download": "下载",
"download_bootstrap": "下载引导程序",
"dragonx_green": "DragonX绿色",
"edit": "编辑",
"error": "错误",
@@ -249,23 +196,6 @@
"est_time_to_block": "预计出块时间",
"exit": "退出",
"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_csv": "导出 CSV",
"export_keys_btn": "导出密钥",
@@ -294,22 +224,14 @@
"fetch_prices": "获取价格",
"file": "文件",
"file_save_location": "文件将保存至:~/.config/ObsidianDragon/",
"filter": "筛选...",
"font_scale": "字体大小",
"force_quit": "强制退出",
"force_quit_confirm_msg": "这将立即终止守护进程而不进行正常关闭。\n这可能会损坏区块链索引并需要重新同步。",
"force_quit_confirm_title": "强制退出?",
"force_quit_warning": "这将立即终止守护进程而不进行正常关闭。可能需要重新同步区块链。",
"force_quit_yes": "强制退出",
"from": "从",
"from_upper": "从",
"full_details": "完整详情",
"general": "常规",
"generating": "正在生成",
"go_to_receive": "前往接收",
"height": "高度",
"help": "帮助",
"hidden_tag": " (已隐藏)",
"hide": "隐藏",
"hide_address": "隐藏地址",
"hide_zero_balances": "隐藏零余额",
@@ -331,9 +253,6 @@
"import_key_warning": "警告:切勿分享您的私钥!从不可信来源导入密钥可能会危及您的钱包安全。",
"import_key_z_format": "Z 地址花费密钥 (secret-extended-key-...)",
"import_private_key": "导入私钥...",
"incorrect_passphrase": "密码错误",
"incorrect_pin": "PIN 错误",
"insufficient_funds": "余额不足以支付此金额加手续费。",
"invalid_address": "无效的地址格式",
"ip_address": "IP 地址",
"keep": "保留",
@@ -347,7 +266,6 @@
"key_export_viewing_keys_zonly": "查看密钥仅适用于屏蔽 (z) 地址",
"key_export_viewing_warning": "此查看密钥允许他人查看您的入账交易和余额,但不能花费您的资金。仅与信任的人分享。",
"label": "标签:",
"label_placeholder": "例如 储蓄、挖矿...",
"language": "语言",
"light": "浅色",
"loading": "加载中...",
@@ -368,7 +286,6 @@
"market_now": "现在",
"market_pct_shielded": "%.0f%% 屏蔽",
"market_portfolio": "投资组合",
"market_price_loading": "正在加载价格数据...",
"market_price_unavailable": "价格数据不可用",
"market_refresh_price": "刷新价格数据",
"market_trade_on": "在 %s 交易",
@@ -394,13 +311,6 @@
"mining_address_copied": "挖矿地址已复制",
"mining_all_time": "所有时间",
"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_chart_1m_ago": "1分钟前",
"mining_chart_5m_ago": "5分钟前",
@@ -420,15 +330,8 @@
"mining_filter_tip_all": "显示所有收益",
"mining_filter_tip_pool": "仅显示矿池收益",
"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_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_mine": "挖矿",
"mining_mining_addr": "挖矿地址",
@@ -485,7 +388,6 @@
"no_addresses_available": "无可用地址",
"no_addresses_match": "没有匹配过滤器的地址",
"no_addresses_with_balance": "没有有余额的地址",
"no_addresses_yet": "暂无地址",
"no_matching": "没有匹配的交易",
"no_recent_receives": "没有最近的接收",
"no_recent_sends": "没有最近的发送",
@@ -551,7 +453,6 @@
"peers_upper": "节点",
"peers_version": "版本",
"pending": "待处理",
"pin_not_set": "未设置 PIN。使用密码解锁。",
"ping": "延迟",
"price_chart": "价格图表",
"qr_code": "二维码",
@@ -572,9 +473,7 @@
"recent_received": "最近接收",
"recent_sends": "最近发送",
"recipient": "收款方",
"recipient_balance": "接收方: %.8f → %.8f DRGX",
"recv_type": "接收",
"reduce_motion": "减少动画",
"refresh": "刷新",
"refresh_now": "立即刷新",
"remove_favorite": "移除收藏",
@@ -594,10 +493,7 @@
"request_uri_copied": "付款 URI 已复制到剪贴板",
"rescan": "重新扫描",
"reset_to_defaults": "重置为默认值",
"restarting_after_encryption": "加密后重启守护进程...",
"restore_address": "恢复地址",
"result_preview": "结果预览",
"retry": "重试",
"review_send": "审核发送",
"rpc_host": "RPC 主机",
"rpc_pass": "密码",
@@ -606,55 +502,8 @@
"save": "保存",
"save_settings": "保存设置",
"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": "搜索...",
"security": "安全",
"seed_backup_button": "助记词",
"seed_backup_close": "关闭",
"seed_backup_copy": "复制",
"seed_backup_intro": "这些词是您钱包的主备份。任何拥有它们的人都能控制您的资金。",
"seed_backup_load_failed": "无法加载助记词。",
"seed_backup_loading": "正在加载您的助记词…",
"seed_backup_locked": "解锁您的钱包以显示其助记词。",
"seed_backup_none": "此钱包早于助记词功能,因此没有 24 词助记词。请改用“备份钱包”或导出私钥来备份它。",
"seed_backup_reminder": "您的钱包有一个 24 词恢复助记词。请立即在“设置 → 节点与安全”中备份它。",
"seed_backup_save": "保存到文件…",
"seed_backup_save_failed": "无法写入 ",
"seed_backup_saved": "已保存到 ",
"seed_backup_title": "备份助记词",
"seed_backup_warning": "请按顺序将它们抄写下来,离线保存,切勿分享或拍照。一旦丢失,您的资金将无法恢复。",
"select_address": "选择地址...",
"select_receiving_address": "选择接收地址...",
"select_source_address": "选择来源地址...",
@@ -704,15 +553,12 @@
"send_valid_transparent": "有效的透明地址",
"send_wallet_empty": "您的钱包是空的",
"send_yes_clear": "是,清除",
"sender_balance": "发送方: %.8f → %.8f DRGX",
"sending": "正在发送交易",
"sending_from": "发送来源",
"sends_full_balance_warning": "这将发送全部余额。发送地址将变为零余额。",
"sent": "已发送",
"sent_filter": "已发送",
"sent_type": "已发送",
"sent_upper": "已发送",
"set_label": "设置标签...",
"settings": "设置",
"settings_about_text": "DragonX (DRGX) 屏蔽加密货币钱包,使用 Dear ImGui 构建,提供轻量、便携的体验。",
"settings_acrylic_level": "亚克力级别:",
@@ -801,17 +647,13 @@
"shield_utxo_limit": "UTXO 限制:",
"shield_wildcard_hint": "使用 '*' 从所有透明地址屏蔽",
"shielded": "屏蔽",
"shielded_address": "隐蔽地址",
"shielded_to": "屏蔽至",
"shielded_type": "屏蔽",
"shielding_notice": "注意:这将把资金从透明 (T) 地址转移到隐私 (Z) 地址。",
"show": "显示",
"show_hidden": "显示已隐藏 (%d)",
"show_qr_code": "显示二维码",
"showing_transactions": "显示第 %d%d 笔,共 %d 笔交易(总计:%zu",
"showing_x_of_y": "显示 %d / %d 个地址",
"showing_transactions": "显示第 %d–%d 笔,共 %d 笔交易(总计:%zu",
"simple_background": "简单背景",
"slider_off": "关闭",
"start_mining": "开始挖矿",
"status": "状态",
"stop_external": "停止外部守护进程",
@@ -820,26 +662,17 @@
"success": "成功",
"summary": "摘要",
"syncing": "同步中...",
"t_address": "T 地址",
"t_addresses": "T 地址",
"test_connection": "测试",
"theme": "主题",
"theme_effects": "主题效果",
"theme_language": "主题与语言",
"time_days_ago": "%d 天前",
"time_hours_ago": "%d 小时前",
"time_minutes_ago": "%d 分钟前",
"time_seconds_ago": "%d 秒前",
"timeout_15min": "15分钟",
"timeout_1hour": "1小时",
"timeout_1min": "1分钟",
"timeout_30min": "30分钟",
"timeout_5min": "5分钟",
"timeout_off": "关闭",
"to": "至",
"to_upper": "至",
"tools": "工具",
"tools_actions": "工具与操作...",
"total": "合计",
"transaction_id": "交易 ID",
"transaction_sent": "交易发送成功",
@@ -847,13 +680,7 @@
"transaction_url": "交易 URL",
"transactions": "交易",
"transactions_upper": "交易",
"transfer_failed": "转账失败",
"transfer_funds": "转账",
"transfer_sent": "转账已发送",
"transfer_sent_desc": "您的转账已提交到网络。",
"transfer_to": "转账至:",
"transparent": "透明",
"transparent_address": "透明地址",
"tt_addr_url": "在区块浏览器中查看地址的基础 URL",
"tt_address_book": "管理已保存的地址以快速发送",
"tt_auto_lock": "在此不活动时间后锁定钱包",
@@ -868,8 +695,6 @@
"tt_custom_theme": "自定义主题已激活",
"tt_debug_collapse": "折叠调试日志选项",
"tt_debug_expand": "展开调试日志选项",
"tt_delete_blockchain": "删除所有区块链数据并重新同步。wallet.dat 和配置将被保留。",
"tt_download_bootstrap": "下载区块链引导程序以加速同步\n现有区块数据将被替换",
"tt_encrypt": "使用密码加密 wallet.dat",
"tt_export_all": "将所有私钥导出到文件",
"tt_export_csv": "将交易历史导出为 CSV 电子表格",
@@ -887,7 +712,6 @@
"tt_mine_idle": "系统空闲时自动开始挖矿\\n无键盘/鼠标输入)",
"tt_noise": "颗粒纹理强度0%% = 关闭100%% = 最大)",
"tt_open_dir": "点击在文件管理器中打开",
"tt_reduce_motion": "禁用动画过渡和余额渐变以提高无障碍性",
"tt_remove_encrypt": "移除加密并以未受保护状态存储钱包",
"tt_remove_pin": "移除 PIN 并要求密码解锁",
"tt_report_bug": "在项目跟踪器中报告问题",
@@ -903,7 +727,6 @@
"tt_save_ztx": "将 z-address 交易历史存储在本地以加快加载速度",
"tt_scan_themes": "扫描新主题。\\n将主题文件夹放在\\n%s",
"tt_scanline": "控制台中的 CRT 扫描线效果",
"tt_seed_backup": "显示并备份您钱包的 24 词恢复助记词",
"tt_set_pin": "设置 4-8 位 PIN 以快速解锁",
"tt_shield_mining": "将透明挖矿奖励转移到屏蔽地址",
"tt_simple_bg": "使用简单渐变作为背景\\n快捷键Ctrl+Up",
@@ -966,9 +789,7 @@
"warning_upper": "警告!",
"website": "网站",
"window_opacity": "窗口透明度",
"wizard_daemon_start_failed": "启动守护进程失败 — 将自动重试",
"yes_clear": "是,清除",
"your_addresses": "您的地址",
"z_address": "Z 地址",
"z_addresses": "Z 地址"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,8 +37,8 @@ images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/l
--on-secondary = "#000000"
--on-background = "#F0E0D8"
--on-surface = "#F0E0D8"
--on-surface-medium = "rgba(240,224,216,0.85)"
--on-surface-disabled = "rgba(240,224,216,0.58)"
--on-surface-medium = "rgba(240,224,216,0.7)"
--on-surface-disabled = "rgba(240,224,216,0.44)"
--error = "#FF5252"
--on-error = "#000000"
--success = "#81C784"
@@ -485,12 +485,12 @@ suggestion-trunc-len = { size = 50 }
fee-rounding = { size = 10.0 }
amount-bar-max-btn-width = { size = 80.0 }
amount-bar-height = { size = 22.0 }
confirm-popup-max-width = { size = 560.0 }
confirm-popup-max-width = { size = 420.0 }
confirm-addr-card-height = { size = 28.0 }
confirm-amount-card-height = { size = 96.0 }
confirm-row-step = { size = 24.0 }
confirm-val-col-x = { size = 150.0 }
confirm-usd-col-x = { size = 116.0 }
confirm-amount-card-height = { size = 70.0 }
confirm-row-step = { size = 16.0 }
confirm-val-col-x = { size = 90.0 }
confirm-usd-col-x = { size = 80.0 }
progress-card-height = { size = 36.0 }
progress-card-height-txid = { size = 52.0 }
progress-card-pad-x = { size = 12.0 }
@@ -516,10 +516,10 @@ error-icon-inset = { size = 20.0 }
error-btn-rounding = { size = 4.0 }
progress-glass-rounding-ratio = { size = 0.75 }
confirm-addr-card-min-height = { size = 24.0 }
confirm-val-col-min-x = { size = 112.0 }
confirm-usd-col-min-x = { size = 92.0 }
confirm-amount-card-min-height = { size = 82.0 }
confirm-row-step-min = { size = 20.0 }
confirm-val-col-min-x = { size = 70.0 }
confirm-usd-col-min-x = { size = 60.0 }
confirm-amount-card-min-height = { size = 54.0 }
confirm-row-step-min = { size = 12.0 }
action-btn-min-height = { size = 26.0 }
recent-icon-min-size = { size = 3.5 }
recent-icon-size = { size = 5.0 }
@@ -566,7 +566,7 @@ txid-trunc-len = { size = 14 }
txid-label-x-offset = { size = 20.0 }
txid-copy-btn-right-offset = { size = 50.0 }
txid-copy-btn-y-offset = { size = 2.0 }
confirm-popup-width-ratio = { size = 0.92 }
confirm-popup-width-ratio = { size = 0.85 }
confirm-glass-rounding-ratio = { size = 0.75 }
confirm-addr-trunc-len = { size = 48 }
confirm-divider-thickness = { size = 1.0 }
@@ -874,7 +874,7 @@ accent-stripe-inset-ratio = { size = 0.0 }
accent-stripe-left-offset = { size = 0.0 }
accent-stripe-width = { size = 4.0 }
accent-stripe-rounding = { size = 1.5 }
chart-y-axis-min-padding = { size = 54.0 }
chart-y-axis-min-padding = { size = 40.0 }
chart-y-axis-padding = { size = 70.0 }
chart-dot-min-radius = { size = 1.5 }
chart-dot-radius = { size = 2.0 }
@@ -908,7 +908,6 @@ scroll-fade-zone = { size = 24.0 }
[tabs.console]
input-area-padding = 8.0
bg-darken-alpha = { size = 110.0 } # black overlay alpha (0-255) for the terminal-dark output + input
output-line-spacing = 2.0
output = { line-spacing = 2 }
scroll-multiplier = { size = 3.0 }
@@ -931,7 +930,7 @@ scanline-speed = { size = 40.0 }
scanline-height = { size = 36.0 }
scanline-alpha = { size = 8.0 }
scanline-gap = { size = 2.0 }
scanline-line-alpha = { size = 2.0 }
scanline-line-alpha = { size = 4.0 }
scanline-glow-spread = { size = 4.0 }
scanline-glow-intensity = { size = 0.6 }
scanline-glow-color = { size = 255.0 }
@@ -1219,9 +1218,6 @@ notification-progress = { color = "var(--primary)", height = 4, position = 18 }
fill-alpha = { size = 12.0 }
noise-alpha = { size = 14.0 }
[components.overlay-dialog]
confirm-btn-height = { size = 40.0 }
[components.qr-code]
module-scale = { size = 4 }
border-modules = { size = 2 }
@@ -1234,9 +1230,8 @@ width = { size = 140.0 }
collapsed-width = { size = 64.0 }
collapse-anim-speed = { size = 10.0 }
auto-collapse-threshold = { size = 800.0 }
section-gap = { size = 8.0 }
section-gap = { size = 4.0 }
section-label-pad-left = { size = 16.0 }
section-label-pad-bottom = { size = 4.0 }
item-height = { size = 36.0 }
item-pad-x = { size = 8.0 }
min-height = { size = 360.0 }
@@ -1253,8 +1248,8 @@ icon-half-size = { size = 7.0 }
icon-label-gap = { size = 8.0 }
badge-radius-dot = { size = 4.0 }
badge-radius-number = { size = 8.0 }
button-spacing = { size = 6.0 }
bottom-padding = { size = 4.0 }
button-spacing = { size = 4.0 }
bottom-padding = { size = 0.0 }
exit-icon-gap = { size = 4.0 }
cutout-shadow-alpha = { size = 55 }
cutout-highlight-alpha = { size = 8 }
@@ -1283,7 +1278,7 @@ page-fade-speed = { size = 8.0 }
collapse-hysteresis = { size = 60.0 }
header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" }
coin-icon = { icon = "logos/logo_dragonx_128.png" }
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 }
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 }
[components.main-window.window]
padding = [12, 36]
@@ -1328,13 +1323,10 @@ wallet-btn-padding = { size = 24.0 }
rpc-label-min-width = { size = 70.0 }
rpc-label-width = { size = 85.0 }
security-combo-width = { size = 120.0 }
node-grid-breakpoint = { size = 900.0 }
port-input-min-width = { size = 60.0 }
port-input-width-ratio = { size = 0.4 }
idle-combo-width = { size = 64.0 }
# Reserved height basis for the About-card logo; the logo is drawn scaled to the
# card's actual height (aspect-preserved) and capped to this * aspect in width.
about-logo-size = { size = 150.0 }
about-logo-size = { size = 64.0 }
[components.main-layout]
app-bar-height = { size = 64.0 }
@@ -1385,17 +1377,6 @@ 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 }
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]
min-width = 180.0
width = 140.0
@@ -1528,6 +1509,7 @@ title = { font = "h5" }
input = { width = 320.0, height = 40.0 }
unlock-button = { width = 320.0, height = 44.0, font = "subtitle1" }
error-text = { font = "caption" }
backdrop-alpha = { opacity = 0.0 }
mode-toggle = { font = "caption" }
# ---------------------------------------------------------------------------

View File

@@ -1275,831 +1275,6 @@ TRANSLATIONS = {
"pt": "Clique para copiar", "ru": "Нажмите для копирования", "zh": "点击复制",
"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():

View File

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

View File

@@ -1,131 +0,0 @@
#!/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

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

View File

@@ -1,64 +0,0 @@
#!/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

@@ -1,47 +0,0 @@
#!/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

@@ -1,214 +0,0 @@
#!/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)"
BUILD_DIR="${SCRIPT_DIR}/build/linux"
APPDIR="${BUILD_DIR}/AppDir"
VERSION="1.2.0"
VERSION="1.1.1"
# Colors
GREEN='\033[0;32m'

View File

@@ -1,36 +0,0 @@
#!/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

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

645
scripts/gen_de.py Normal file
View File

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

665
scripts/gen_es.py Normal file
View File

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

646
scripts/gen_fr.py Normal file
View File

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

646
scripts/gen_ja.py Normal file
View File

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

646
scripts/gen_ko.py Normal file
View File

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

646
scripts/gen_pt.py Normal file
View File

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

646
scripts/gen_ru.py Normal file
View File

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

646
scripts/gen_zh.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -120,20 +120,20 @@ pkgs_core_debian="build-essential cmake git pkg-config
libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev
libxinerama-dev libxi-dev libxkbcommon-dev libwayland-dev
libsodium-dev libcurl4-openssl-dev
autoconf automake libtool wget python3 xxd"
autoconf automake libtool wget"
pkgs_core_fedora="gcc gcc-c++ cmake git pkg-config
mesa-libGL-devel libX11-devel libXcursor-devel libXrandr-devel
libXinerama-devel libXi-devel libxkbcommon-devel wayland-devel
libsodium-devel libcurl-devel
autoconf automake libtool wget python3 vim-common"
autoconf automake libtool wget"
pkgs_core_arch="base-devel cmake git pkg-config
mesa libx11 libxcursor libxrandr libxinerama libxi
libxkbcommon wayland libsodium curl
autoconf automake libtool wget python xxd"
autoconf automake libtool wget"
pkgs_core_macos="cmake python xxd"
pkgs_core_macos="cmake"
# Windows cross-compile (from Linux)
pkgs_win_debian="mingw-w64 zip"
@@ -245,7 +245,7 @@ if [[ -z "$core_pkgs" ]]; then
else
# Check if key tools are already present
NEED_CORE=false
has_cmd cmake && has_cmd g++ && has_cmd pkg-config && has_cmd python3 && has_cmd xxd || NEED_CORE=true
has_cmd cmake && has_cmd g++ && has_cmd pkg-config || NEED_CORE=true
if $NEED_CORE; then
install_pkgs "$core_pkgs" "core build"
@@ -258,8 +258,6 @@ check_tool cmake "cmake"
check_tool g++ "g++ (C++ compiler)"
check_tool git "git"
check_tool make "make"
check_tool python3 "python3 (theme expansion)"
check_tool xxd "xxd (embedded language headers)"
# ── 2. libsodium ────────────────────────────────────────────────────────────
header "libsodium"
@@ -442,53 +440,6 @@ copy_daemon_data() {
done
}
# ── Stale-daemon guard ───────────────────────────────────────────────────────
# A prebuilt daemon binary is only rebuilt on its platform's flag (--win/--mac),
# and build.sh merely BUNDLES whatever binary already exists — so a daemon left
# over from an old source revision silently ships in the wallet (e.g. the Network
# tab once reported v1.0.1 while the source was v1.0.2). These helpers compare the
# version baked into a prebuilt binary against the dragonx source and flag drift.
STALE_DAEMON=0
# MAJOR.MINOR.REVISION from the checked-out dragonx source (empty if unavailable).
dragonx_source_version() {
local hdr="$DRAGONX_SRC/src/clientversion.h"
[[ -f "$hdr" ]] || return 1
local maj min rev
maj=$(awk '/#define[ \t]+CLIENT_VERSION_MAJOR/{print $3; exit}' "$hdr")
min=$(awk '/#define[ \t]+CLIENT_VERSION_MINOR/{print $3; exit}' "$hdr")
rev=$(awk '/#define[ \t]+CLIENT_VERSION_REVISION/{print $3; exit}' "$hdr")
[[ -n "$maj" && -n "$min" && -n "$rev" ]] || return 1
printf '%s.%s.%s' "$maj" "$min" "$rev"
}
# vX.Y.Z baked into a built daemon binary (the daemon embeds "vX.Y.Z-<githash>").
# Uses grep -a so no `strings`/binutils dependency is required.
dragonx_binary_version() {
local bin="$1"
[[ -f "$bin" ]] || return 1
LC_ALL=C grep -aoE 'v[0-9]+\.[0-9]+\.[0-9]+-[0-9a-f]{6,}' "$bin" 2>/dev/null \
| head -1 | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/'
}
# Compare a prebuilt daemon against the source; warn (and set STALE_DAEMON) on drift.
# $1 = label, $2 = binary path, $3 = rebuild flag(s) (e.g. "--win", "" for Linux)
daemon_version_guard() {
local label="$1" bin="$2" rebuild_hint="$3"
[[ -f "$bin" ]] || return 0
local src bv
src=$(dragonx_source_version) || return 0 # no source checked out → can't compare
bv=$(dragonx_binary_version "$bin")
[[ -n "$bv" ]] || return 0 # couldn't read the binary's version
if [[ "$bv" == "$src" ]]; then
ok " $label daemon is v$bv (matches dragonx source)"
else
warn " $label daemon is v$bv but dragonx source is v$src — STALE"
warn " rebuild so the wallet ships the current daemon: ./setup.sh${rebuild_hint:+ $rebuild_hint}"
STALE_DAEMON=1
fi
}
# ── Linux daemon ─────────────────────────────────────────────────────────────
# Skip Linux daemon build if only cross-compile targets were requested
@@ -508,13 +459,11 @@ fi
if $CHECK_ONLY; then
if [[ -f "$DRAGONXD_LINUX/dragonxd" ]] || [[ -f "$DRAGONXD_LINUX/hushd" ]]; then
ok "dragonxd daemon (Linux) present"
daemon_version_guard "Linux" "$DRAGONXD_LINUX/dragonxd" ""
else
miss "dragonxd daemon (Linux) not built"
fi
elif $SKIP_LINUX_DAEMON; then
skip "dragonxd (Linux) — skipped, binaries already present (cross-compile only)"
daemon_version_guard "Linux" "$DRAGONXD_LINUX/dragonxd" ""
else
clone_dragonx_if_needed
@@ -549,11 +498,9 @@ fi
if ! $SETUP_WIN; then
skip "dragonxd (Windows) — use --win to cross-compile"
daemon_version_guard "Windows" "$DRAGONXD_WIN/dragonxd.exe" "--win"
elif $CHECK_ONLY; then
if [[ -f "$DRAGONXD_WIN/dragonxd.exe" ]] || [[ -f "$DRAGONXD_WIN/hushd.exe" ]]; then
ok "dragonxd daemon (Windows) present"
daemon_version_guard "Windows" "$DRAGONXD_WIN/dragonxd.exe" "--win"
else
miss "dragonxd daemon (Windows) not built"
fi
@@ -609,11 +556,9 @@ fi
if ! $SETUP_MAC; then
skip "dragonxd (macOS) — use --mac to cross-compile"
daemon_version_guard "macOS" "$DRAGONXD_MAC/dragonxd" "--mac"
elif $CHECK_ONLY; then
if [[ -f "$DRAGONXD_MAC/dragonxd" ]] || [[ -f "$DRAGONXD_MAC/hushd" ]]; then
ok "dragonxd daemon (macOS) present"
daemon_version_guard "macOS" "$DRAGONXD_MAC/dragonxd" "--mac"
else
miss "dragonxd daemon (macOS) not built"
fi
@@ -676,14 +621,6 @@ else
fi
fi
# Prominent reminder if any prebuilt daemon drifted from the source — these are bundled verbatim
# by build.sh, so a stale binary ships in the wallet (and shows an old version in the Network tab).
if [[ "$STALE_DAEMON" -eq 1 ]]; then
warn "One or more prebuilt daemons are OLDER than the dragonx source (see above)."
warn "build.sh bundles them as-is, so rebuild the stale platform(s) before releasing:"
warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac"
fi
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
header "xmrig-hac Mining Binary"

File diff suppressed because it is too large Load Diff

552
src/app.h
View File

@@ -9,23 +9,11 @@
#include <functional>
#include <thread>
#include <atomic>
#include <mutex>
#include <chrono>
#include <unordered_map>
#include <unordered_set>
#include <nlohmann/json_fwd.hpp>
#include "data/transaction_history_cache.h"
#include "data/address_book.h"
#include "data/wallet_state.h"
#include "rpc/connection.h"
#include "services/network_refresh_service.h"
#include "services/wallet_security_controller.h"
#include "services/wallet_security_workflow.h"
#include "util/async_task_manager.h"
#include "util/pool_stats_service.h"
#include "wallet/wallet_capabilities.h"
#include "chat/chat_service.h"
#include "chat/chat_database.h"
#include "ui/sidebar.h"
#include "ui/windows/console_tab.h"
#include "imgui.h"
@@ -37,9 +25,8 @@ namespace dragonx {
class RPCWorker;
}
namespace config { class Settings; }
namespace daemon { class DaemonController; class EmbeddedDaemon; class XmrigManager; }
namespace daemon { class EmbeddedDaemon; class XmrigManager; }
namespace util { class Bootstrap; class SecureVault; }
namespace wallet { class LiteWalletController; struct LiteWalletAppRefreshModel; }
}
namespace dragonx {
@@ -138,13 +125,6 @@ public:
* @brief Whether we are in the shutdown phase
*/
bool isShuttingDown() const { return shutting_down_; }
wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); }
bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); }
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
bool supportsSoloMining() const { return wallet::supportsSoloMining(walletCapabilities()); }
bool supportsPoolMining() const { return wallet::supportsPoolMining(walletCapabilities()); }
bool supportsLiteBackend() const { return wallet::supportsLiteBackend(walletCapabilities()); }
/**
* @brief Render the shutdown overlay (called instead of normal UI during shutdown)
@@ -160,42 +140,10 @@ public:
// Accessors for subsystems
rpc::RPCClient* rpc() { return rpc_.get(); }
rpc::RPCWorker* worker() { return worker_.get(); }
// Console backend accessors (fast-lane-preferring, defined in app.cpp where the
// subsystem types are complete) used by the shared console executor.
rpc::RPCClient* consoleRpc();
rpc::RPCWorker* consoleWorker();
daemon::EmbeddedDaemon* consoleDaemon();
daemon::XmrigManager* consoleXmrig();
config::Settings* settings() { return settings_.get(); }
// Lite wallet controller (non-null only in lite builds with a linked backend).
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
// HushChat service (identity + in-memory message store); the Chat tab reads its store.
chat::ChatService& chatService() { return chat_service_; }
// HushChat composing: construct, broadcast (broadcastChatMemos), and locally echo an outgoing
// message (to a conversation whose peer key we know) / a new-conversation contact request.
void sendChatMessage(const std::string& conversationId, const std::string& text);
void startChatConversation(const std::string& peerZaddr, const std::string& text);
// Debug/sweep convenience: give the Chat tab a demo identity + a few sample conversations
// (in-memory only, not persisted) so the screenshot sweep captures the populated UI. No-op when
// the chat feature is off.
void seedChatDemoData();
// Reason the lite wallet failed to auto-open this session (empty if none / opened OK).
const std::string& liteOpenError() const { return lite_open_error_; }
// Show the lite send-time unlock modal (called when a spend is attempted on a locked wallet).
void requestLiteUnlock() { lite_unlock_prompt_ = true; }
// Lock the lite wallet AND immediately tear down the chat session (the lite backend `lock`
// doesn't update state_.locked until the next poll, so chat secrets would otherwise linger).
bool lockLiteWallet();
// (Re)build the lite controller from current settings so a changed lite-server selection
// takes effect. No-op on non-lite/unlinked builds; preserves a live wallet (see app.cpp).
void rebuildLiteWallet(bool force = false);
WalletState& state() { return state_; }
const WalletState& state() const { return state_; }
const WalletState& getWalletState() const { return state_; }
// Shared contact store (Contacts tab / Send picker / future Chat roster). App-owned so
// every surface reads one source of truth instead of a per-dialog singleton.
data::AddressBook& addressBook() { return address_book_; }
const data::AddressBook& addressBook() const { return address_book_; }
// Connection state (convenience wrappers)
bool isConnected() const { return state_.connected; }
@@ -228,21 +176,6 @@ public:
int getXmrigRequestedThreads() const {
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
}
// True while the pool miner process is live — used to refuse replacing the binary under it.
bool isPoolMinerRunning() const {
return xmrig_manager_ && xmrig_manager_->isRunning();
}
// Auto-balance: latest per-pool hashrate snapshot (for the mining tab pool list),
// and a request to refresh it now (Refresh button / switching into Auto mode).
util::PoolStatsService::Snapshot poolStatsSnapshot() const {
return pool_stats_service_.snapshot();
}
void requestPoolBalanceRefresh() { balance_refresh_pending_ = true; }
// Installed miner version (detected from `xmrig --version`, cached; kicks the one-shot
// detection on first call) so the mining tab can show it before mining starts.
std::string poolMiningInstalledVersion();
// Mine-when-idle state query
bool isIdleMiningActive() const { return idle_mining_active_; }
@@ -250,9 +183,7 @@ public:
// Peers
const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; }
bool isPeerRefreshInProgress() const {
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Peers);
}
bool isPeerRefreshInProgress() const { return peer_refresh_in_progress_.load(std::memory_order_relaxed); }
void banPeer(const std::string& ip, int duration_seconds = 86400);
void unbanPeer(const std::string& ip);
void clearBans();
@@ -271,89 +202,38 @@ public:
void unfavoriteAddress(const std::string& addr);
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);
// Assign dense sort orders (0..N-1) to the given addresses in the given order and
// persist once. Used by drag-reorder so a drop always takes effect (even from the
// default un-ordered state, where a pairwise swap would be a no-op).
void reorderAddresses(const std::vector<std::string>& orderedAddrs);
bool isMiningAddress(const std::string& addr) const;
void setMiningAddress(const std::string& addr, bool mining);
void invalidateAddressValidationCache();
// Key export/import
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
// callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export.
void exportAllKeys(std::function<void(const std::string&, int, int)> callback);
void exportAllKeys(std::function<void(const std::string&)> callback);
void importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback);
// Wallet backup
void backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback);
// Export the wallet's BIP39 seed phrase (z_exportmnemonic). The callback receives
// (ok, noMnemonic, phrase, error): ok+phrase on success; noMnemonic=true when the
// wallet's seed is not mnemonic-derived (legacy wallet). Full-node only; the phrase
// is a secret and is wiped after the callback returns.
void exportSeedPhrase(std::function<void(bool ok, bool noMnemonic,
const std::string& phrase,
const std::string& error)> callback);
// Transaction operations
void sendTransaction(const std::string& from, const std::string& to,
// Transaction operations
void sendTransaction(const std::string& from, const std::string& to,
double amount, double fee, const std::string& memo,
std::function<void(bool success, const std::string& result)> callback);
// Register a daemon async operation id (z_shieldcoinbase / z_mergetoaddress /
// auto-shield) with the shared opid poller so its eventual success/failure is
// surfaced and balances/transactions refresh on completion. z_sendmany uses the
// richer pending-send path internally; this is for operations with no optimistic
// transaction row of their own.
void trackOperation(const std::string& opid);
// Force refresh
void refreshNow();
void refreshMiningInfo();
void refreshPeerInfo();
void refreshMarketData();
// Fetch the live exchange/pair list from CoinGecko once per session (venues are
// near-static); populates state.market.exchanges. Safe to call every frame.
void refreshExchanges();
// Fetch historical USD price series (CoinGecko market_chart) that back the portfolio
// sparkline intervals; self-throttled to ~30 min. Safe to call every frame.
void refreshMarketChart();
/// @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
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_; }
// Debug: screenshot sweep — cycles every skin x every enabled tab, one PNG each, into a
// timestamped folder under the config dir. Driven from App::render(); main.cpp polls
// wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls
// onScreenshotCaptured() to advance. Transient — restores the original skin/page when done.
void startScreenshotSweep();
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
void onScreenshotCaptured();
std::string screenshotDir() const; // <config>/screenshots (fixed; sweeps overwrite in place)
// Dialog triggers (used by settings page to open modal dialogs)
void showImportKeyDialog() { show_import_key_ = true; }
void showExportKeyDialog() { show_export_key_ = true; }
void showBackupDialog() { show_backup_ = true; }
void showSeedBackupDialog() { show_seed_backup_ = true; }
void showSeedMigrationDialog(); // opens the migration modal (resumes a pending one at Sweep)
void showAboutDialog() { show_about_ = true; }
// Legacy tab compat — maps int to NavPage
@@ -373,27 +253,10 @@ public:
bool startEmbeddedDaemon();
void stopEmbeddedDaemon();
bool isEmbeddedDaemonRunning() const;
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
void rescanBlockchain(); // restart daemon with -rescan flag (full-history nodes)
// Runtime rescanblockchain RPC starting at a snapshot-available height. Unlike the
// -rescan restart, this works on bootstrapped/pruned nodes (which lack pre-snapshot
// block data), reconciling the wallet's stale spent-state without a daemon restart.
void runtimeRescan(int startHeight);
// Async binary-search probe for the lowest block height the node still has on disk.
// cb(ok, lowestHeight, fullHistory): fullHistory==true when genesis is present (a normal,
// non-bootstrapped node). Runs on the UI thread via the RPC worker callbacks.
void detectLowestAvailableBlockHeight(std::function<void(bool ok, int lowestHeight, bool fullHistory)> cb);
// Flag that a bootstrap just finished so the wallet auto-reconciles spent-state once the
// daemon is back up (consumed in update()).
void markPostBootstrapRescanPending() { post_bootstrap_rescan_pending_ = true; }
bool runtimeRescanActive() const { return runtime_rescan_active_; }
void repairWallet(); // restart daemon with -zapwallettxes=2 (wipe & rebuild wallet tx records)
void reinstallBundledDaemon(); // stop daemon, overwrite installed binary with the bundled one, restart
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
void rescanBlockchain(); // restart daemon with -rescan flag
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
bool isBootstrapDownloading() const { return bootstrap_downloading_; }
void setBootstrapDownloading(bool v) { bootstrap_downloading_ = v; }
// Get daemon memory usage in MB (uses embedded daemon handle if available,
// falls back to platform-level process scan for external daemons)
@@ -439,12 +302,6 @@ public:
// Wallet encryption helpers
void encryptWalletWithPassphrase(const std::string& passphrase);
// Post-encrypt daemon restart: the daemon shuts itself down after
// encryptwallet, so restart it off the main thread. Shared by the
// immediate and deferred encryption continuations. When
// announceRestartStatus is true, connection_status_ is updated first so
// the loading overlay explains the restart.
void restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus);
void unlockWallet(const std::string& passphrase, int timeout);
void lockWallet();
void changePassphrase(const std::string& oldPass, const std::string& newPass);
@@ -464,7 +321,10 @@ public:
void showChangePassphraseDialog() { show_change_passphrase_ = true; }
void showDecryptDialog() {
show_decrypt_dialog_ = true;
wallet_security_workflow_.reset();
decrypt_phase_ = 0; // passphrase entry
decrypt_step_ = 0;
decrypt_status_.clear();
decrypt_in_progress_ = false;
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
}
@@ -476,100 +336,8 @@ public:
/// @brief Check if RPC worker has queued results waiting to be processed
bool hasPendingRPCResults() const;
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
std::string transactionSendProgressText() const;
std::string transactionRefreshProgressText() const;
// Copy a SECRET (seed phrase, private key) to the clipboard and arm an auto-clear: after a
// short delay the clipboard is wiped IF it still holds this secret (so we don't clobber
// something the user copied afterwards). Only a hash of the secret is retained, never the
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
void copySecretToClipboard(const std::string& secret);
void pumpSecretClipboardClear();
bool isTransactionRefreshInProgress() const {
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
}
private:
friend class AppDaemonLifecycleRuntime;
friend class AppDaemonLifecycleTaskContext;
// Global keyboard-shortcut handling, dispatched once per frame from update().
void handleGlobalShortcuts();
bool sendStopCommandSafely(rpc::RPCClient& client, const char* context);
void maybeFinishTransactionSendProgress();
// Shared body of createNewZAddress/createNewTAddress (which are thin public forwarders).
// `shielded` selects the z_getnewaddress/getnewaddress RPC, the "shielded"/"transparent" type
// string, and the z_addresses/t_addresses target (the new AddressInfo is pushed into both that
// list and state_.addresses). Lite builds derive locally via the controller and early-return.
void createNewAddress(bool shielded, std::function<void(const std::string&)> callback);
void upsertPendingSendTransaction(const std::string& opid,
const std::string& from,
const std::string& to,
double amount,
const std::string& memo,
double fee = 0.0);
// Work around a dragonxd note-selection bug: its z_sendmany picks notes to cover the recipient
// total but not the miner fee, so a shielded send whose largest notes sum exactly to the amount
// fails with "Insufficient shielded funds, have H, need H+fee" despite ample balance. When a
// failed opid matches that (H >= the requested amount), re-issue the send once with a tiny
// self-output that lifts the daemon's selection target past the boundary so it grabs another
// note; the recipient still receives the exact amount. Returns true if a retry was issued.
bool maybeRetrySendForFeeGap(const std::string& opid, const std::string& rawMsg);
void resendWithFeeGapWorkaround(const std::string& from, const std::string& to,
double amount, double fee, const std::string& memo,
std::function<void(bool, const std::string&)> callback);
// Shared z_sendmany submit path for sendTransaction (single recipient, markFeeGapRetry=false)
// and resendWithFeeGapWorkaround (recipient + self-output, markFeeGapRetry=true). Owns the
// in-flight increment, the worker post + call, and the result closure (dirty flags, opid
// tracking, pending-send bookkeeping, callback delivery). Callers build `recipients` (and pass
// the recipient `to`/`amount`/`memo`/`fee` used to record the optimistic pending-send row).
// When markFeeGapRetry is set, the returned opid is recorded in send_feegap_retried_opids_ so a
// retry of a retry is reported as a real error.
void submitZSendMany(const std::string& from, const std::string& to, double amount, double fee,
const std::string& memo, const nlohmann::json& recipients,
const char* traceLabel, bool markFeeGapRetry,
std::function<void(bool, const std::string&)> callback);
void markPendingSendTransactionSucceeded(const std::string& opid,
const std::string& txid);
void removePendingSendTransactions(const std::vector<std::string>& opids,
bool restoreBalances);
// Apply a signed per-address balance delta for a pending send: walk z_addresses then
// t_addresses for `fromAddress` and clamp its balance at >=0. A positive `signedAmount`
// restores a debit; a negative one applies it. When `includeAggregates` is set, adjust the
// private/transparent bucket (chosen by the address's leading 'z') and totalBalance with the
// same clamp. Shared by the three pending-send delta sites so they can't drift.
void applyPendingSendDelta(const std::string& fromAddress, double signedAmount,
bool includeAggregates);
// Deliver a deferred z_sendmany result to its waiting UI callback once the opid
// reaches a terminal status. Returns true if a callback was registered (and fired).
bool invokeSendResultCallback(const std::string& opid, bool ok,
const std::string& result);
void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
std::string transactionHistoryCacheWalletIdentity() const;
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
// Shared main-thread continuations for a wallet unlock attempt, so the passphrase
// and PIN paths cannot drift. applyUnlockFailure applies the escalating lockout
// curve on every failed path (a PIN RPC failure used to skip it).
void applyUnlockSuccess(const std::string& passphrase, int timeout);
void applyUnlockFailure(const std::string& errorMessage);
// Clear all rescan + witness-rebuild progress/accumulator state. Shared by the four
// rescan-completion sites so a new witness field can't be forgotten in one copy.
void resetWitnessRescanProgress();
void loadTransactionHistoryCacheIfAvailable();
void storeTransactionHistoryCacheIfAvailable();
void wipePendingTransactionHistoryCachePassphrase();
void resetTransactionHistoryCacheSession();
void pruneShieldedHistoryScanProgress();
void invalidateShieldedHistoryScanProgress(bool persistCache);
// Auto-balance pool selection: drive the periodic hashrate refresh and apply a
// freshly-completed snapshot (weighted-random pick + optional miner restart).
void updatePoolAutoBalance();
void applyPoolAutoBalance(const util::PoolStatsService::Snapshot& snap);
// Subsystems
std::unique_ptr<rpc::RPCClient> rpc_;
std::unique_ptr<rpc::RPCWorker> worker_;
@@ -584,67 +352,8 @@ private:
rpc::ConnectionConfig saved_config_;
std::unique_ptr<config::Settings> settings_;
std::unique_ptr<wallet::LiteWalletController> lite_wallet_; // lite builds w/ linked backend
// Pending send_tab callback for an in-flight lite send (delivered in update() once the
// controller's async broadcast result arrives). Only one lite send runs at a time.
std::function<void(bool, const std::string&)> lite_send_callback_;
// One-shot guard: auto-open an existing lite wallet on the first update() tick (kept off
// init() so a slow initialize_existing network call doesn't freeze startup before the window).
bool lite_autoopen_done_ = false;
double lite_open_last_attempt_ = 0.0; // ImGui time of the last async open attempt (retry timer)
// Reason an existing lite wallet failed to auto-open (e.g. server unreachable). Surfaced in
// the UI so a stuck "disconnected" state isn't silent; cleared once a wallet opens.
std::string lite_open_error_;
// HushChat (experimental; gated by DRAGONX_ENABLE_CHAT — inert when OFF). App owns the chat
// service so the transaction-refresh harvest can decrypt incoming memos into threaded messages.
// The identity is derived from the wallet's OWN SDXLite-compatible seed phrase (full-node
// z_exportmnemonic / lite exportSeed → the same KDF), so it is portable across both variants.
chat::ChatService chat_service_;
chat::ChatDatabase chat_db_; // persistent backing (seed-derived encryption at rest)
bool chat_identity_provisioned_ = false; // identity set on the service this session
bool chat_identity_fetch_in_flight_ = false; // a z_exportmnemonic worker job is pending
bool chat_identity_unavailable_ = false; // provisioning failed definitively (e.g. non-mnemonic wallet)
// Provision the chat identity once the wallet seed is reachable+unlocked (per-tick, both
// variants); derives via deriveChatIdentityFromSecret and wipes the secret. No-op when the
// feature is off, already provisioned, in flight, or unavailable.
void maybeProvisionChatIdentity();
// One-time nudge: on a full-node wallet that has a mnemonic, remind the user (once per
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
void maybeRemindSeedBackup();
// Seed-wallet migration (Phase 1: create a new mnemonic wallet in isolation, no funds moved).
void beginCreateSeedWallet(); // starts the isolated create on a background thread
void pumpSeedMigration(); // main thread: pick up background progress/result each frame
// Phase 2: sweep all legacy funds into the new wallet, then adopt it as the primary wallet.
void refreshSeedMigrationBalance(); // query the legacy total (shown on the Sweep step)
void beginSweepToSeedWallet(); // z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"] -> dest
void pollSweepStatus(); // Confirming step: poll sweep confirmations + legacy balance
void beginAdoptSeedWallet(); // stop daemon -> swap wallet.dat -> restart with -rescan
void provisionChatIdentityFromSecret(std::string secret);
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr for chat
std::string generateChatLocalId(const char* prefix, int numBytes) const; // unique echo id / cid
bool broadcastChatMemos(const chat::OutgoingChatMemos& memos); // returns true if submitted
bool broadcastChatMemosLite(const chat::OutgoingChatMemos& memos); // lite two-recipient send
void ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model); // lite chat receive harvest
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
bool lite_firstrun_dismissed_ = false;
// Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
bool lite_unlock_prompt_ = false;
// One-shot: prompt to unlock on startup once we learn the auto-opened wallet is encrypted+locked.
bool lite_startup_lock_checked_ = false;
std::unique_ptr<daemon::DaemonController> daemon_controller_;
std::unique_ptr<daemon::EmbeddedDaemon> embedded_daemon_;
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
// Auto-balance runtime state (pool mining, full-node only). The service fetches
// pool hashrates off-thread; the RNG drives the weighted-random pick.
util::PoolStatsService pool_stats_service_;
std::mt19937 balance_rng_;
long long last_balance_eval_ms_ = 0; // steady-clock ms of the last refresh kick
bool balance_refresh_pending_ = false; // UI asked for an immediate refresh
bool balance_snapshot_seen_ = false; // the current in-flight snapshot was applied
bool exchanges_fetch_started_ = false; // once-per-session CoinGecko tickers fetch
bool chart_fetch_in_flight_ = false; // a market_chart history fetch is on the worker
util::AsyncTaskManager async_tasks_;
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
// Wallet state
@@ -653,22 +362,16 @@ private:
// Shutdown state
std::atomic<bool> shutting_down_{false};
std::atomic<bool> shutdown_complete_{false};
std::atomic<bool> refresh_in_progress_{false};
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
std::string shutdown_status_;
std::thread shutdown_thread_;
float shutdown_timer_ = 0.0f;
bool force_quit_confirm_ = false;
std::chrono::steady_clock::time_point shutdown_start_time_;
// Daemon restart (e.g. after changing debug log categories)
std::atomic<bool> daemon_restarting_{false};
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
std::atomic<int> pending_delete_result_{-1};
// Encryption state check timeout
float encryption_check_timer_ = 0.0f;
std::thread daemon_restart_thread_;
// UI State
bool quit_requested_ = false;
@@ -678,46 +381,10 @@ private:
bool show_import_key_ = false;
bool show_export_key_ = false;
bool show_backup_ = false;
bool show_seed_backup_ = false;
// Seed-phrase backup dialog state. seed_backup_phrase_ holds a SECRET (the revealed
// mnemonic) and is wiped with sodium_memzero when the dialog closes.
std::string seed_backup_phrase_;
std::string seed_backup_status_;
bool seed_backup_fetch_started_ = false;
bool seed_backup_loading_ = false;
bool seed_backup_no_mnemonic_ = false;
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
// --- Seed-wallet migration (Phase 1: create; Phase 2: sweep + adopt) ---
enum class SeedMigrationStep { Intro, Working, ShowSeed, Sweep, Sweeping, Confirming, Adopting, Done, Error };
bool show_seed_migration_ = false;
SeedMigrationStep seed_migration_step_ = SeedMigrationStep::Intro;
bool seed_migration_in_flight_ = false; // main-thread guard while the bg create task runs
std::string seed_migration_seed_; // SECRET — the revealed phrase, wiped on close
std::string seed_migration_dest_; // new shielded z-address (Phase 2 sweep target)
std::string seed_migration_temp_dir_; // temp datadir root holding the new wallet (kept)
bool seed_migration_backed_up_ = false; // "I've written it down" confirmation
std::string seed_migration_status_; // main-thread display (progress / error text)
double seed_migration_balance_ = 0.0; // legacy total to sweep (shown on the Sweep step)
std::string seed_migration_sweep_txid_; // the z_mergetoaddress sweep transaction id
int seed_migration_sweep_confs_ = 0; // confirmations of the sweep tx (adopt gate: >= 1)
double seed_migration_legacy_remaining_ = -1.0; // legacy balance after sweep (-1 = unknown)
float seed_migration_poll_timer_ = 0.0f; // throttles the Confirming-step poll
bool seed_migration_confirm_in_flight_ = false; // guards the confirm poll
// Cross-thread handoff from the background create task (guarded by seed_migration_mutex_).
std::mutex seed_migration_mutex_;
std::string seed_migration_progress_; // latest progress line
bool seed_migration_done_ = false; // create result ready to consume
bool seed_migration_ok_ = false;
std::string seed_migration_r_seed_, seed_migration_r_dest_, seed_migration_r_tmp_, seed_migration_r_err_;
// Cross-thread handoff from the background adopt task (guarded by seed_migration_mutex_).
bool seed_migration_adopt_done_ = false;
bool seed_migration_adopt_ok_ = false;
std::string seed_migration_adopt_err_;
bool show_address_book_ = false;
// Embedded daemon state
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
bool use_embedded_daemon_ = true;
std::string daemon_status_;
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
@@ -734,16 +401,6 @@ private:
// Connection
std::string connection_status_ = "Disconnected";
bool connection_in_progress_ = false;
bool remote_rpc_plaintext_warning_shown_ = false;
// Startup daemon-launch diagnostics: bound the "RPC port busy, no config" wait before warning,
// and show the embedded-daemon start failure (binary/params/spawn) only once. Reset on connect.
int daemon_wait_attempts_ = 0;
bool daemon_start_error_shown_ = false;
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once
bool refresh_policy_syncing_ = false; // whether the sync-throttle refresh profile is active
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
std::uint64_t clipboard_secret_hash_ = 0;
double clipboard_clear_deadline_ = 0.0;
float loading_timer_ = 0.0f; // spinner animation for loading overlay
// Current page (sidebar navigation)
@@ -751,21 +408,6 @@ private:
ui::NavPage prev_page_ = ui::NavPage::Overview;
float page_alpha_ = 1.0f; // 0→1 fade on page switch
bool sidebar_collapsed_ = false; // true = icon-only mode
// Debug screenshot sweep state.
bool screenshot_sweep_active_ = false;
bool sweep_capture_this_frame_ = false;
int sweep_skin_idx_ = 0;
int sweep_page_idx_ = 0;
int sweep_settle_frames_ = 0; // frames to let a new skin/page settle before capturing
std::vector<std::string> sweep_skins_; // skin ids to cycle
std::vector<ui::NavPage> sweep_pages_; // enabled pages to cycle
std::string sweep_dir_; // output folder for this sweep
std::string sweep_current_path_; // PNG path for the frame about to be captured
std::string sweep_saved_skin_; // restore on completion
ui::NavPage sweep_saved_page_ = ui::NavPage::Overview;
void updateScreenshotSweep(); // called at the top of render() while active
void applySweepTarget(); // apply current (skin,page), compute path, arm settle
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
@@ -786,9 +428,8 @@ private:
int coin_logo_h_ = 0;
bool coin_logo_loaded_ = false;
// Console tab + its backend executor (full-node RPC or lite backend), created lazily.
// Console tab
ui::ConsoleTab console_tab_;
std::unique_ptr<ui::ConsoleCommandExecutor> console_exec_;
// Pending payment from URI
bool pending_payment_valid_ = false;
@@ -797,35 +438,47 @@ private:
std::string pending_memo_;
std::string pending_label_;
// Per-category refresh timers, policy, and worker queue guards.
services::NetworkRefreshService network_refresh_;
// Timers (in seconds since last update)
float refresh_timer_ = 0.0f;
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
// Mining toggle guard (prevents concurrent setgenerate calls)
std::atomic<bool> mining_toggle_in_progress_{false};
// True from a successful startPoolMining() until the miner is confirmed connected/hashing in the
// poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay).
std::atomic<bool> pool_starting_{false};
// Peer refresh guard (visual feedback for refresh button)
std::atomic<bool> peer_refresh_in_progress_{false};
// Auto-shield guard (prevents concurrent auto-shield operations)
std::atomic<bool> auto_shield_pending_{false};
// P4: Incremental transaction cache
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
std::size_t shielded_history_scan_cursor_ = 0;
bool shielded_history_scan_pending_ = false;
// False until the first full shielded-history scan finishes. Drives the History tab's
// "Loading older history…" progress so the user knows transactions are still streaming in
// after the first batch appears; goes quiet for the routine per-block re-scans afterward.
bool initial_history_scan_complete_ = false;
std::unordered_map<std::string, int> shielded_history_scan_heights_;
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for
// txids we've already enriched. Keyed by txid.
using ViewTxCacheEntry = services::NetworkRefreshService::TransactionViewCacheEntry;
services::NetworkRefreshService::TransactionViewCache viewtx_cache_;
struct ViewTxCacheEntry {
std::string from_address; // first spend address
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)
// are accumulated here and reused across refresh cycles. Only
@@ -836,58 +489,14 @@ private:
// Dirty flags for demand-driven refresh
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 encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
bool rescan_status_poll_in_progress_ = false;
// True once we've actually observed the rescan running (daemon restarted into -rescan warmup).
// Gates the "rescan complete" detection so a getrescaninfo poll that hits the still-running
// pre-restart daemon (which reports rescanning=false) can't fire a false "complete" instantly.
bool rescan_confirmed_active_ = false;
// A runtime rescanblockchain RPC is in flight (vs the -rescan daemon restart). While set,
// the per-second mining/rescan-status pollers are suppressed (the daemon holds cs_main for
// the whole scan and would block them); completion is signalled by the rescan RPC callback.
bool runtime_rescan_active_ = false;
// Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan
// that reconciles the preserved wallet.dat against the freshly-imported chain.
bool post_bootstrap_rescan_pending_ = false;
// Largest "blocks remaining" seen during the current witness-rebuild phase. The daemon's
// "Building Witnesses for block" fraction resets every call (it's re-invoked per connected
// block, each walking from its own start height to the tip), so we derive a stable, monotonic
// overall percentage from how far "remaining" has fallen below this peak. Reset per phase.
int witness_rebuild_total_blocks_ = 0;
// The daemon's primary witness signal is "Setting Initial Sapling Witness for tx <hash>, <i>
// of <N>", logged once per wallet tx as its initial witness is set. The <i> is the tx's slot in
// an UNORDERED map, so it bounces wildly (was the cause of the resetting progress). The honest
// monotonic metric is how many DISTINCT txs have been witnessed (the set only grows; it also
// dedups the daemon's occasional double-prints) over the reported total N.
std::unordered_set<std::string> witness_seen_txids_;
int witness_total_txs_ = 0;
bool opid_poll_in_progress_ = false;
// Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
// connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.
int consecutive_core_failures_ = 0;
// Pending z_sendmany operation tracking
bool send_progress_active_ = false;
int send_submissions_in_flight_ = 0;
std::vector<std::string> pending_opids_; // opids to poll for completion
struct PendingSendInfo {
std::string from;
std::string to;
std::string memo;
double amount = 0.0;
double fee = 0.0;
std::int64_t timestamp = 0;
};
std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
// Opids issued as a fee-gap auto-retry (see maybeRetrySendForFeeGap). Tracked so a retry that
// fails again is reported to the user instead of looping.
std::unordered_set<std::string> send_feegap_retried_opids_;
// z_sendmany UI callbacks held until the opid reaches a terminal status, so the
// user isn't told "sent successfully" before the tx is actually built/broadcast.
std::unordered_map<std::string, std::function<void(bool, const std::string&)>>
pending_send_callbacks_;
float opid_poll_timer_ = 0.0f;
static constexpr float OPID_POLL_INTERVAL = 2.0f;
// Txids from completed z_sendmany operations.
// Ensures shielded sends are discoverable by z_viewtransaction
// even when they don't appear in listtransactions or
@@ -897,24 +506,21 @@ private:
// First-run wizard state
WizardPhase wizard_phase_ = WizardPhase::None;
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_saved_passphrase_; // held until PinSetup completes/skipped
// Wallet security flow state shared by wizard/settings encryption paths.
services::WalletSecurityController wallet_security_;
services::WalletSecurityWorkflow wallet_security_workflow_;
// Deferred encryption (wizard background task)
std::string deferred_encrypt_passphrase_;
std::string deferred_encrypt_pin_;
bool deferred_encrypt_pending_ = false;
// Wizard: stopping an external daemon before bootstrap
bool wizard_stopping_external_ = false;
std::string wizard_stop_status_;
std::thread wizard_stop_thread_;
// PIN vault
std::unique_ptr<util::SecureVault> vault_;
data::TransactionHistoryCache transaction_history_cache_;
data::AddressBook address_book_; // shared contact store; loaded once in init(), self-saves on mutation
std::string pending_transaction_history_cache_passphrase_;
bool transaction_history_cache_loaded_ = false;
// Lock screen state
bool lock_screen_was_visible_ = false; // tracks lock→unlock transitions for auto-focus
@@ -956,7 +562,14 @@ private:
// Decrypt wallet dialog state
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] = {};
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
char wizard_pin_buf_[16] = {};
@@ -972,13 +585,10 @@ private:
// Private methods - rendering
void renderStatusBar();
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
void renderAboutDialog();
void renderImportKeyDialog();
void renderExportKeyDialog();
void renderBackupDialog();
void renderSeedBackupDialog(); // full-node "Back up seed phrase" modal (z_exportmnemonic)
void renderSeedMigrationDialog(); // "Migrate to a seed wallet" guided modal (Phase 1: create)
void renderFirstRunWizard();
void renderLockScreen();
void renderEncryptWalletDialog();
@@ -991,34 +601,14 @@ private:
void tryConnect();
void onConnected();
void onDisconnected(const std::string& reason);
// Set the "node is initializing" UI state (status line + overlay description) from the
// embedded/external daemon's launch state and its own console output (current phase + block
// height), so a connect probe that times out while the daemon loads shows WHAT it's doing.
// `reachableButBusy` is true when the probe connected but got no RPC reply (a timeout),
// false when the daemon is merely launching (not bound yet). Returns the status title.
std::string applyDaemonInitStatus(bool reachableButBusy);
// Tear down a connection that died mid-session (daemon crash / restart / dropped
// socket) so update()'s reconnect branch re-enters tryConnect(). Unlike onDisconnected
// alone, this also rpc_->disconnect()s so rpc_->isConnected() actually flips to false.
void handleLostConnection(const std::string& reason);
void applyDefaultBanlist();
// Private methods - data refresh
void refreshData(); // Orchestrator: dispatches per-category refreshes
void refreshCoreData(); // Balance + blockchain info (can use fast_worker_)
void refreshAddressData(); // Address lists + balances
void refreshTransactionData(); // Transaction list + z_viewtransaction enrichment
void 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 refreshData();
void refreshBalance();
void refreshAddresses();
void refreshPrice();
void refreshWalletEncryptionState();
void applyRefreshPolicy(ui::NavPage page);
bool currentPageNeedsWalletDataRefresh() const;
bool shouldRunWalletTransactionRefresh() const;
bool shouldRefreshTransactions() const;
bool shouldRefreshRecentTransactions() const;
void checkAutoLock();
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,7 +10,6 @@
#include "rpc/rpc_worker.h"
#include "rpc/connection.h"
#include "config/settings.h"
#include "daemon/daemon_controller.h"
#include "daemon/embedded_daemon.h"
#include "ui/notifications.h"
#include "ui/material/color_theme.h"
@@ -40,48 +39,13 @@ namespace dragonx {
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()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Lite wallet lifecycle requests are available from Settings as dry-run readiness checks");
return;
}
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
// Reset crash counter for fresh wizard attempt
if (daemon_controller_) {
daemon_controller_->resetCrashCount();
if (embedded_daemon_) {
embedded_daemon_->resetCrashCount();
}
// Disconnect RPC
@@ -92,11 +56,10 @@ void App::restartWizard()
// Stop the embedded daemon in a background thread to avoid
// blocking the UI for up to 32 seconds (RPC stop + process wait).
if (daemon_controller_ && isEmbeddedDaemonRunning()) {
async_tasks_.submit("wizard-restart-stop-daemon", [this](const util::AsyncTaskManager::Token& token) {
if (token.cancelled()) return;
if (embedded_daemon_ && isEmbeddedDaemonRunning()) {
std::thread([this]() {
stopEmbeddedDaemon();
});
}).detach();
}
// Enter wizard — the wizard completion handler already calls
@@ -110,7 +73,6 @@ void App::restartWizard()
// ===========================================================================
void App::renderFirstRunWizard() {
auto& wizardUi = s_wizardUi;
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
@@ -281,14 +243,15 @@ void App::renderFirstRunWizard() {
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
cy += 14.0f * dp;
float& wiz_blur_amount = wizardUi.blur_amount;
bool& wiz_theme_effects = wizardUi.theme_effects;
float& wiz_ui_opacity = wizardUi.ui_opacity;
bool& wiz_low_spec = wizardUi.low_spec;
bool& wiz_scanline = wizardUi.scanline;
std::string& wiz_balance_layout = wizardUi.balance_layout;
int& wiz_language_index = wizardUi.language_index;
bool& wiz_appearance_init = wizardUi.appearance_init;
// Statics for appearance settings
static float wiz_blur_amount = 1.5f;
static bool wiz_theme_effects = true;
static float wiz_ui_opacity = 1.0f;
static bool wiz_low_spec = false;
static bool wiz_scanline = true;
static std::string wiz_balance_layout = "classic";
static int wiz_language_index = 0;
static bool wiz_appearance_init = false;
if (!wiz_appearance_init) {
wiz_blur_amount = settings_->getBlurMultiplier();
wiz_theme_effects = settings_->getThemeEffectsEnabled();
@@ -435,7 +398,7 @@ void App::renderFirstRunWizard() {
// --- Low-spec mode checkbox ---
// Snapshot for restoring settings when low-spec is turned off
WizardLowSpecSnapshot& wiz_lsSnap = wizardUi.low_spec_snapshot;
static struct { bool valid; float blur; float uiOp; bool fx; bool scanline; } wiz_lsSnap = {};
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
@@ -611,7 +574,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Continue##app", ImVec2(btnW, btnH))) {
if (ImGui::Button("Continue##app", ImVec2(btnW, btnH))) {
// Save appearance choices, advance to Bootstrap
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
settings_->setAcrylicQuality(wiz_blur_amount > 0.001f
@@ -633,7 +596,7 @@ void App::renderFirstRunWizard() {
cy += cardPad;
// Lock card height to the tallest content ever seen
float& card0MaxH = wizardUi.card0_max_h;
static float card0MaxH = 0.0f;
card0MaxH = std::max(card0MaxH, cy - card0Top);
card0Bot = card0Top + card0MaxH;
@@ -763,7 +726,7 @@ void App::renderFirstRunWizard() {
float cancelBX = rightX + (colW - cancelW) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Cancel##bs", ImVec2(cancelW, cancelH))) {
if (ImGui::Button("Cancel##bs", ImVec2(cancelW, cancelH))) {
bootstrap_->cancel();
}
ImGui::PopStyleVar();
@@ -774,8 +737,6 @@ void App::renderFirstRunWizard() {
auto finalProg = bootstrap_->getProgress();
if (finalProg.state == util::Bootstrap::State::Completed) {
bootstrap_.reset();
// Reconcile the preserved wallet.dat against the new chain once the daemon is up.
markPostBootstrapRescanPending();
wizard_phase_ = WizardPhase::EncryptOffer;
} else {
wizard_phase_ = WizardPhase::BootstrapFailed;
@@ -811,22 +772,27 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton("Retry##bs", ImVec2(btnW2, btnH2))) {
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
stopDaemonForBootstrap();
if (isEmbeddedDaemonRunning()) {
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>();
std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Skip##bsfail", ImVec2(btnW2, btnH2))) {
if (ImGui::Button("Skip##bsfail", ImVec2(btnW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -842,23 +808,17 @@ void App::renderFirstRunWizard() {
if (isFocused) {
static std::atomic<bool> s_extCached{false};
static std::atomic<bool> s_checkInFlight{false};
double& s_extLastCheck = wizardUi.external_last_check;
static double s_extLastCheck = -10.0;
double now = ImGui::GetTime();
if (now - s_extLastCheck >= 2.0 && !s_checkInFlight.load()) {
s_extLastCheck = now;
bool embeddedRunning = isEmbeddedDaemonRunning();
s_checkInFlight.store(true);
async_tasks_.submit("wizard-external-daemon-check", [embeddedRunning](const util::AsyncTaskManager::Token& token) {
if (token.cancelled()) {
s_checkInFlight.store(false);
return;
}
std::thread([embeddedRunning]() {
bool inUse = daemon::EmbeddedDaemon::isRpcPortInUse();
if (!token.cancelled()) {
s_extCached.store(inUse && !embeddedRunning);
}
s_extCached.store(inUse && !embeddedRunning);
s_checkInFlight.store(false);
});
}).detach();
}
externalRunning = s_extCached.load();
}
@@ -896,22 +856,22 @@ void App::renderFirstRunWizard() {
IM_COL32(220, 60, 60, 255)));
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 255));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
if (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
wizard_stopping_external_ = true;
wizard_stop_status_ = "Sending stop command...";
async_tasks_.submit("wizard-stop-external-daemon", [this](const util::AsyncTaskManager::Token& token) {
if (wizard_stop_thread_.joinable()) wizard_stop_thread_.join();
wizard_stop_thread_ = std::thread([this]() {
auto config = rpc::Connection::autoDetectConfig();
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
auto tmp_rpc = std::make_unique<rpc::RPCClient>();
if (tmp_rpc->connect(config.host, config.port,
config.rpcuser, config.rpcpassword,
config.use_tls)) {
sendStopCommandSafely(*tmp_rpc, "Wizard external daemon stop");
config.rpcuser, config.rpcpassword)) {
try { tmp_rpc->call("stop"); } catch (...) {}
tmp_rpc->disconnect();
}
}
wizard_stop_status_ = "Waiting for daemon to shut down...";
for (int i = 0; i < 60 && !token.cancelled(); i++) {
for (int i = 0; i < 60; i++) {
std::this_thread::sleep_for(std::chrono::seconds(1));
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
wizard_stop_status_ = "Daemon stopped.";
@@ -919,7 +879,6 @@ void App::renderFirstRunWizard() {
return;
}
}
if (token.cancelled()) return;
wizard_stop_status_ = "Daemon did not stop — try manually.";
wizard_stopping_external_ = false;
});
@@ -929,7 +888,7 @@ void App::renderFirstRunWizard() {
ImGui::SetCursorScreenPos(ImVec2(bx + stopW + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Skip##extd", ImVec2(skipW2, btnH2))) {
if (ImGui::Button("Skip##extd", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -994,16 +953,21 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton("Download##bs", ImVec2(dlBtnW, btnH2))) {
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
stopDaemonForBootstrap();
if (isEmbeddedDaemonRunning()) {
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>();
std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
@@ -1013,18 +977,23 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
stopDaemonForBootstrap();
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
if (isEmbeddedDaemonRunning()) {
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>();
std::string dataDir = util::Platform::getDragonXDataDir();
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
bootstrap_->start(dataDir, mirrorUrl);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered()) {
ui::material::Tooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
@@ -1032,7 +1001,7 @@ void App::renderFirstRunWizard() {
// --- Skip button ---
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Skip##bs", ImVec2(skipW2, btnH2))) {
if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -1043,7 +1012,7 @@ void App::renderFirstRunWizard() {
cy += cardPad;
// Lock card height to the tallest content ever seen (but not when collapsed)
float& card1MaxH = wizardUi.card1_max_h;
static float card1MaxH = 0.0f;
if (isCollapsed) {
card1Bot = card1Top + (cy - card1Top);
} else {
@@ -1068,7 +1037,7 @@ void App::renderFirstRunWizard() {
// Pre-start daemon when encrypt card becomes focused so it's ready
// by the time the user finishes typing their passphrase
if (isFocused) {
bool& wiz_daemon_prestarted = wizardUi.daemon_prestarted;
static bool wiz_daemon_prestarted = false;
if (!wiz_daemon_prestarted) {
wiz_daemon_prestarted = true;
if (!state_.connected && isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
@@ -1127,7 +1096,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Continue##encok", ImVec2(btnW2, btnH2))) {
if (ImGui::Button("Continue##encok", ImVec2(btnW2, btnH2))) {
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
@@ -1285,24 +1254,10 @@ void App::renderFirstRunWizard() {
cy += captionFont->LegacySize + 6.0f * dp;
}
// Warn + block if the passphrase has leading/trailing whitespace. Silently trimming it would
// change the passphrase the user believes they set and lock them out on the next unlock.
bool passEdgeSpace = false;
if (size_t pl = strlen(encrypt_pass_buf_)) {
char a = encrypt_pass_buf_[0], b = encrypt_pass_buf_[pl - 1];
passEdgeSpace = (a == ' ' || a == '\t' || b == ' ' || b == '\t');
}
if (passEdgeSpace) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), "Passphrase has leading/trailing spaces — remove them");
cy += captionFont->LegacySize + 6.0f * dp;
}
// Buttons
{
bool passValid = strlen(encrypt_pass_buf_) >= 8 &&
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0 &&
!passEdgeSpace;
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0;
// PIN is optional: if entered, must be valid + confirmed
std::string pinStr(wizard_pin_buf_);
bool pinEntered = !pinStr.empty();
@@ -1324,11 +1279,12 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!canEncrypt);
if (ui::material::TactileButton("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
// Save passphrase + optional PIN for background processing
wallet_security_.beginDeferredEncryption(
std::string(encrypt_pass_buf_),
(pinEntered && pinOk) ? pinStr : std::string());
deferred_encrypt_passphrase_ = std::string(encrypt_pass_buf_);
if (pinEntered && pinOk)
deferred_encrypt_pin_ = pinStr;
deferred_encrypt_pending_ = true;
// Clear sensitive buffers
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
@@ -1338,10 +1294,7 @@ void App::renderFirstRunWizard() {
// Start daemon + finish wizard immediately
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
if (!startEmbeddedDaemon()) {
ui::Notifications::instance().warning(
TR("wizard_daemon_start_failed"));
}
startEmbeddedDaemon();
}
tryConnect();
wizard_phase_ = WizardPhase::Done;
@@ -1355,25 +1308,14 @@ void App::renderFirstRunWizard() {
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Skip##enc", ImVec2(skipW2, btnH2))) {
static bool s_skipEncConfirm = false;
if (!s_skipEncConfirm) {
// Skipping stores private keys UNENCRYPTED — require a confirming second click.
s_skipEncConfirm = true;
encrypt_status_ = "Continue WITHOUT encryption? Keys will be stored unencrypted — click Skip again to confirm.";
} else {
s_skipEncConfirm = false;
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
if (!startEmbeddedDaemon()) {
ui::Notifications::instance().warning(
TR("wizard_daemon_start_failed"));
}
}
tryConnect();
if (ImGui::Button("Skip##enc", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
startEmbeddedDaemon();
}
tryConnect();
}
ImGui::PopStyleVar();
cy += btnH2;

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,30 +0,0 @@
#pragma once
// DragonX Wallet - HushChat decrypted message model. Held in memory by ChatStore and persisted at
// rest (encrypted under a seed-derived key) by ChatDatabase.
#include <cstdint>
#include <string>
namespace dragonx::chat {
enum class ChatDirection { Incoming, Outgoing };
enum class ChatMessageKind { Message, ContactRequest };
// Outgoing delivery: Sent = the broadcast was submitted; Failed = it wasn't (not connected, no
// spendable address, a send already in progress). Always Sent for incoming.
enum class ChatDelivery { Sent, Failed };
struct ChatMessage {
ChatDirection direction = ChatDirection::Incoming;
ChatMessageKind kind = ChatMessageKind::Message;
std::string txid;
std::string conversation_id; // cid — the conversation thread key
std::string peer_zaddr; // header "z": peer's reply z-address
std::string peer_public_key_hex; // header "p": peer's crypto_kx public key
std::string body; // decrypted plaintext (Message) or request text (ContactRequest)
std::int64_t timestamp = 0; // tx time in seconds; set by the ingesting caller
std::size_t payload_position = 0; // together with txid, the dedup key
ChatDelivery delivery = ChatDelivery::Sent; // outgoing only
};
} // namespace dragonx::chat

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,108 +0,0 @@
// DragonX Wallet - HushChat service (implementation).
#include "chat_service.h"
#include "chat_database.h"
#include "chat_identity.h" // chatIdentityPublicKeyHex
#include <utility>
namespace dragonx::chat {
ChatService::~ChatService() {
clearIdentity();
}
void ChatService::setIdentity(const ChatKeyPair& keys) {
identity_ = keys;
has_identity_ = true;
}
void ChatService::clearIdentity() {
wipeChatKeyPair(identity_);
has_identity_ = false;
}
int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata,
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
std::int64_t fallbackTimestamp) {
if (!has_identity_) return 0;
int added = 0;
for (const auto& meta : metadata) {
ChatMessage message;
message.direction = ChatDirection::Incoming;
message.txid = meta.txid;
message.conversation_id = meta.conversation_id;
message.peer_zaddr = meta.reply_zaddr;
message.peer_public_key_hex = meta.sender_public_key_hex;
const auto timeIt = txTimestamps.find(meta.txid);
message.timestamp = timeIt != txTimestamps.end() ? timeIt->second : fallbackTimestamp;
message.payload_position = meta.payload_position;
if (meta.type == HushChatHeaderType::ContactRequest) {
message.kind = ChatMessageKind::ContactRequest;
message.body = meta.payload_memo; // plaintext request text
} else {
message.kind = ChatMessageKind::Message;
std::string plaintext;
const ChatCryptoStatus status = decryptIncoming(
identity_, meta.sender_public_key_hex, meta.secretstream_header_hex,
meta.payload_memo, plaintext);
if (status != ChatCryptoStatus::Ok) continue; // drop undecryptable silently
message.body = std::move(plaintext);
}
// In-memory store dedups (txid+position); only persist the genuinely new ones. On the next
// session loadFromDatabase() repopulates the store, so re-scanning the chain re-ingests but
// the store dedup prevents a duplicate write.
if (store_.append(message)) {
if (db_) db_->append(message);
++added;
}
}
return added;
}
void ChatService::loadFromDatabase() {
if (!db_) return;
for (const auto& message : db_->load()) {
store_.append(message);
}
}
std::string ChatService::identityPublicKeyHex() const {
if (!has_identity_) return {};
return chatIdentityPublicKeyHex(identity_);
}
ChatComposeStatus ChatService::composeMessage(const std::string& myReplyZaddr,
const std::string& peerPublicKeyHex,
const std::string& peerZaddr,
const std::string& conversationId,
const std::string& plaintext,
OutgoingChatMemos& out) const {
if (!has_identity_) return ChatComposeStatus::MissingField;
return buildOutgoingMessage(identity_, chatIdentityPublicKeyHex(identity_), myReplyZaddr,
peerPublicKeyHex, peerZaddr, conversationId, plaintext, out);
}
ChatComposeStatus ChatService::composeContactRequest(const std::string& myReplyZaddr,
const std::string& peerZaddr,
const std::string& conversationId,
const std::string& requestText,
OutgoingChatMemos& out) const {
if (!has_identity_) return ChatComposeStatus::MissingField;
return buildOutgoingContactRequest(chatIdentityPublicKeyHex(identity_), myReplyZaddr,
peerZaddr, conversationId, requestText, out);
}
bool ChatService::recordOutgoing(const ChatMessage& message) {
if (store_.append(message)) {
if (db_) db_->append(message);
return true;
}
return false;
}
} // namespace dragonx::chat

View File

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

View File

@@ -1,39 +0,0 @@
// DragonX Wallet - HushChat in-memory message store (implementation).
#include "chat_store.h"
namespace dragonx::chat {
std::string ChatStore::dedupKey(const ChatMessage& message) {
return message.txid + ":" + std::to_string(message.payload_position);
}
bool ChatStore::append(const ChatMessage& message) {
if (!seen_.insert(dedupKey(message)).second) return false;
messages_.push_back(message);
return true;
}
std::vector<ChatMessage> ChatStore::conversation(const std::string& conversationId) const {
std::vector<ChatMessage> out;
for (const auto& message : messages_) {
if (message.conversation_id == conversationId) out.push_back(message);
}
return out;
}
std::vector<std::string> ChatStore::conversationIds() const {
std::vector<std::string> ids;
std::unordered_set<std::string> seenIds;
for (const auto& message : messages_) {
if (seenIds.insert(message.conversation_id).second) ids.push_back(message.conversation_id);
}
return ids;
}
void ChatStore::clear() {
messages_.clear();
seen_.clear();
}
} // namespace dragonx::chat

View File

@@ -1,38 +0,0 @@
#pragma once
// DragonX Wallet - HushChat in-memory message store: the fast read model / dedup view. Durable
// persistence lives in ChatDatabase; ChatService rehydrates this store from it on unlock.
#include "chat_message.h"
#include <string>
#include <unordered_set>
#include <vector>
namespace dragonx::chat {
// Threads messages by conversation_id (cid) and deduplicates by (txid, payload_position) so
// re-scanning the chain never double-inserts. Not thread-safe — drive from the main thread.
class ChatStore {
public:
// Returns true if newly inserted, false if a duplicate was ignored.
bool append(const ChatMessage& message);
// Messages in a conversation, in insertion order.
std::vector<ChatMessage> conversation(const std::string& conversationId) const;
// Distinct conversation ids, in first-seen order.
std::vector<std::string> conversationIds() const;
std::size_t size() const { return messages_.size(); }
bool empty() const { return messages_.empty(); }
void clear();
private:
static std::string dedupKey(const ChatMessage& message);
std::vector<ChatMessage> messages_;
std::unordered_set<std::string> seen_;
};
} // namespace dragonx::chat

View File

@@ -1,9 +1,6 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// settings.cpp — JSON settings persistence. Loads/saves user preferences
// to ~/.config/ObsidianDragon/settings.json (Linux/macOS) or %APPDATA% (Windows).
#include "settings.h"
#include "version.h"
@@ -11,12 +8,8 @@
#include <nlohmann/json.hpp>
#include <fstream>
#include <filesystem>
#include <ctime>
#include <algorithm>
#include <type_traits>
#include "../util/logger.h"
#include "../util/platform.h"
#ifdef _WIN32
#include <shlobj.h>
@@ -34,87 +27,35 @@ namespace config {
Settings::Settings() = default;
Settings::~Settings() = default;
namespace {
Settings::LiteServerSelectionPreferenceMode parseLiteServerSelectionPreferenceMode(
const json& value)
{
if (!value.is_string()) return Settings::LiteServerSelectionPreferenceMode::Sticky;
const std::string mode = value.get<std::string>();
if (mode == "random" || mode == "Random") {
return Settings::LiteServerSelectionPreferenceMode::Random;
}
return Settings::LiteServerSelectionPreferenceMode::Sticky;
}
const char* liteServerSelectionPreferenceModeName(
Settings::LiteServerSelectionPreferenceMode mode)
{
switch (mode) {
case Settings::LiteServerSelectionPreferenceMode::Sticky:
return "sticky";
case Settings::LiteServerSelectionPreferenceMode::Random:
return "random";
}
return "sticky";
}
Settings::PoolSelectMode parsePoolSelectMode(const json& value)
{
if (!value.is_string()) return Settings::PoolSelectMode::Manual;
const std::string mode = value.get<std::string>();
if (mode == "auto_balance" || mode == "auto") {
return Settings::PoolSelectMode::AutoBalance;
}
return Settings::PoolSelectMode::Manual;
}
const char* poolSelectModeName(Settings::PoolSelectMode mode)
{
switch (mode) {
case Settings::PoolSelectMode::Manual: return "manual";
case Settings::PoolSelectMode::AutoBalance: return "auto_balance";
}
return "manual";
}
// True if j[key] exists and holds a JSON value convertible to T. Guards every scalar
// read so a malformed value (wrong type / missing key) leaves the field's default
// instead of throwing out of the whole load().
template <typename T>
bool jsonHasType(const json& v)
{
if constexpr (std::is_same_v<T, bool>) return v.is_boolean();
else if constexpr (std::is_same_v<T, std::string>) return v.is_string();
else if constexpr (std::is_floating_point_v<T>) return v.is_number();
else if constexpr (std::is_integral_v<T>) return v.is_number_integer();
else return false;
}
// Reads j[key] into field only when present AND of the matching type.
template <typename T>
void loadScalar(const json& j, const char* key, T& field)
{
if (j.contains(key) && jsonHasType<T>(j[key])) field = j[key].get<T>();
}
// Same as loadScalar but clamps the read value into [lo, hi].
template <typename T>
void loadClamped(const json& j, const char* key, T& field, T lo, T hi)
{
if (j.contains(key) && jsonHasType<T>(j[key]))
field = std::max(lo, std::min(hi, j[key].get<T>()));
}
} // namespace
std::string Settings::getDefaultPath()
{
// Single per-platform, per-variant config dir (util::Platform::getConfigDir handles the
// _WIN32 / __APPLE__ / XDG split and the DRAGONX_APP_NAME variant suffix in one place).
const std::string dir = util::Platform::getConfigDir();
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
std::string dir = std::string(path) + "\\ObsidianDragon";
fs::create_directories(dir);
return dir + "\\settings.json";
}
return "settings.json";
#elif defined(__APPLE__)
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/Library/Application Support/ObsidianDragon";
fs::create_directories(dir);
return (fs::path(dir) / "settings.json").string();
return dir + "/settings.json";
#else
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/.config/ObsidianDragon";
fs::create_directories(dir);
return dir + "/settings.json";
#endif
}
bool Settings::load()
@@ -135,30 +76,29 @@ bool Settings::load(const std::string& path)
json j;
file >> j;
loadScalar(j, "theme", theme_);
loadScalar(j, "save_ztxs", save_ztxs_);
loadScalar(j, "auto_shield", auto_shield_);
loadScalar(j, "use_tor", use_tor_);
loadScalar(j, "allow_custom_fees", allow_custom_fees_);
loadScalar(j, "default_fee", default_fee_);
loadScalar(j, "fetch_prices", fetch_prices_);
loadScalar(j, "tx_explorer_url", tx_explorer_url_);
loadScalar(j, "address_explorer_url", address_explorer_url_);
loadScalar(j, "language", language_);
loadScalar(j, "skin_id", skin_id_);
loadScalar(j, "chat_reply_zaddr", chat_reply_zaddr_);
loadScalar(j, "acrylic_enabled", acrylic_enabled_);
loadScalar(j, "acrylic_quality", acrylic_quality_);
loadScalar(j, "blur_multiplier", blur_multiplier_);
loadScalar(j, "noise_opacity", noise_opacity_);
loadScalar(j, "gradient_background", gradient_background_);
if (j.contains("theme")) theme_ = j["theme"].get<std::string>();
if (j.contains("save_ztxs")) save_ztxs_ = j["save_ztxs"].get<bool>();
if (j.contains("auto_shield")) auto_shield_ = j["auto_shield"].get<bool>();
if (j.contains("use_tor")) use_tor_ = j["use_tor"].get<bool>();
if (j.contains("allow_custom_fees")) allow_custom_fees_ = j["allow_custom_fees"].get<bool>();
if (j.contains("default_fee")) default_fee_ = j["default_fee"].get<double>();
if (j.contains("fetch_prices")) fetch_prices_ = j["fetch_prices"].get<bool>();
if (j.contains("tx_explorer_url")) tx_explorer_url_ = j["tx_explorer_url"].get<std::string>();
if (j.contains("address_explorer_url")) address_explorer_url_ = j["address_explorer_url"].get<std::string>();
if (j.contains("language")) language_ = j["language"].get<std::string>();
if (j.contains("skin_id")) skin_id_ = j["skin_id"].get<std::string>();
if (j.contains("acrylic_enabled")) acrylic_enabled_ = j["acrylic_enabled"].get<bool>();
if (j.contains("acrylic_quality")) acrylic_quality_ = j["acrylic_quality"].get<int>();
if (j.contains("blur_multiplier")) blur_multiplier_ = j["blur_multiplier"].get<float>();
if (j.contains("noise_opacity")) noise_opacity_ = j["noise_opacity"].get<float>();
if (j.contains("gradient_background")) gradient_background_ = j["gradient_background"].get<bool>();
// Migrate legacy reduced_transparency bool -> ui_opacity float
if (j.contains("ui_opacity")) {
ui_opacity_ = j["ui_opacity"].get<float>();
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) {
ui_opacity_ = 1.0f; // legacy: reduced = fully opaque
}
loadScalar(j, "window_opacity", window_opacity_);
if (j.contains("window_opacity")) window_opacity_ = j["window_opacity"].get<float>();
if (j.contains("balance_layout")) {
if (j["balance_layout"].is_string())
balance_layout_ = j["balance_layout"].get<std::string>();
@@ -172,7 +112,7 @@ bool Settings::load(const std::string& path)
if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
}
}
loadScalar(j, "scanline_enabled", scanline_enabled_);
if (j.contains("scanline_enabled")) scanline_enabled_ = j["scanline_enabled"].get<bool>();
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
hidden_addresses_.clear();
for (const auto& a : j["hidden_addresses"])
@@ -183,123 +123,37 @@ bool Settings::load(const std::string& path)
for (const auto& a : j["favorite_addresses"])
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;
}
}
loadScalar(j, "wizard_completed", wizard_completed_);
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
loadScalar(j, "seed_migration_pending", seed_migration_pending_);
loadScalar(j, "seed_migration_dest", seed_migration_dest_);
loadScalar(j, "seed_migration_temp_dir", seed_migration_temp_dir_);
loadScalar(j, "seed_migration_sweep_txid", seed_migration_sweep_txid_);
loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
loadScalar(j, "unlock_duration", unlock_duration_);
loadScalar(j, "pin_enabled", pin_enabled_);
loadScalar(j, "keep_daemon_running", keep_daemon_running_);
loadScalar(j, "stop_external_daemon", stop_external_daemon_);
loadScalar(j, "max_connections", max_connections_);
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
const auto& lite = j["lite_wallet"];
if (lite.contains("server_selection_mode")) {
lite_server_selection_mode_ = parseLiteServerSelectionPreferenceMode(
lite["server_selection_mode"]);
}
if (lite.contains("sticky_server_url") && lite["sticky_server_url"].is_string()) {
lite_sticky_server_url_ = lite["sticky_server_url"].get<std::string>();
}
if (lite.contains("chain_name") && lite["chain_name"].is_string()) {
lite_chain_name_ = lite["chain_name"].get<std::string>();
}
// Migration: the SDXL backend only accepts main/test/regtest and hard-panics
// (process abort) on any other chain name. Older builds persisted the "DRAGONX"
// ticker here, which crashed the lite backend on launch. Rewrite any invalid
// value to "main" and flag a re-save so the corrected setting persists.
if (lite_chain_name_ != "main" && lite_chain_name_ != "test" &&
lite_chain_name_ != "regtest") {
lite_chain_name_ = "main";
needs_upgrade_save_ = true;
}
if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_unsigned()) {
lite_random_selection_seed_ = lite["random_selection_seed"].get<std::size_t>();
} else if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_integer()) {
const auto seed = lite["random_selection_seed"].get<long long>();
lite_random_selection_seed_ = seed > 0 ? static_cast<std::size_t>(seed) : 0;
}
if (lite.contains("persist_selected_server") && lite["persist_selected_server"].is_boolean()) {
lite_persist_selected_server_ = lite["persist_selected_server"].get<bool>();
}
if (lite.contains("servers") && lite["servers"].is_array()) {
lite_servers_.clear();
for (const auto& server : lite["servers"]) {
if (!server.is_object()) continue;
LiteServerPreference preference;
if (server.contains("url") && server["url"].is_string()) {
preference.url = server["url"].get<std::string>();
}
if (server.contains("label") && server["label"].is_string()) {
preference.label = server["label"].get<std::string>();
}
if (server.contains("enabled") && server["enabled"].is_boolean()) {
preference.enabled = server["enabled"].get<bool>();
}
lite_servers_.push_back(preference);
}
}
if (lite.contains("rollout_override") && lite["rollout_override"].is_string()) {
const auto v = lite["rollout_override"].get<std::string>();
lite_rollout_override_ = (v == "force_on" || v == "force_off") ? v : "auto";
}
if (lite.contains("install_id") && lite["install_id"].is_string()) {
lite_install_id_ = lite["install_id"].get<std::string>();
}
if (lite.contains("hidden_servers") && lite["hidden_servers"].is_array()) {
lite_hidden_servers_.clear();
for (const auto& u : lite["hidden_servers"])
if (u.is_string()) lite_hidden_servers_.insert(u.get<std::string>());
}
}
loadScalar(j, "verbose_logging", verbose_logging_);
if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get<bool>();
if (j.contains("auto_lock_timeout")) auto_lock_timeout_ = j["auto_lock_timeout"].get<int>();
if (j.contains("unlock_duration")) unlock_duration_ = j["unlock_duration"].get<int>();
if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get<bool>();
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get<bool>();
if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get<bool>();
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
debug_categories_.clear();
for (const auto& c : j["debug_categories"])
if (c.is_string()) debug_categories_.insert(c.get<std::string>());
}
loadScalar(j, "theme_effects_enabled", theme_effects_enabled_);
loadScalar(j, "low_spec_mode", low_spec_mode_);
loadScalar(j, "reduce_motion", reduce_motion_);
loadScalar(j, "selected_exchange", selected_exchange_);
loadScalar(j, "selected_pair", selected_pair_);
loadScalar(j, "pool_url", pool_url_);
if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get<bool>();
if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get<bool>();
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>();
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
// Migrate old default pool URL that was missing the stratum port
if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
loadScalar(j, "pool_algo", pool_algo_);
loadScalar(j, "pool_worker", pool_worker_);
loadScalar(j, "pool_threads", pool_threads_);
loadScalar(j, "pool_tls", pool_tls_);
loadScalar(j, "pool_hugepages", pool_hugepages_);
loadScalar(j, "pool_mode", pool_mode_);
if (j.contains("pool_select_mode")) pool_select_mode_ = parsePoolSelectMode(j["pool_select_mode"]);
loadScalar(j, "mine_when_idle", mine_when_idle_);
loadScalar(j, "xmrig_version", xmrig_version_);
// Lower-bounded only (min 30s), matching setMineIdleDelay; now type-guarded too.
if (j.contains("mine_idle_delay") && j["mine_idle_delay"].is_number_integer())
mine_idle_delay_ = std::max(30, j["mine_idle_delay"].get<int>());
loadScalar(j, "idle_thread_scaling", idle_thread_scaling_);
loadScalar(j, "idle_threads_active", idle_threads_active_);
loadScalar(j, "idle_threads_idle", idle_threads_idle_);
loadScalar(j, "idle_gpu_aware", idle_gpu_aware_);
if (j.contains("pool_algo")) pool_algo_ = j["pool_algo"].get<std::string>();
if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get<std::string>();
if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get<int>();
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>();
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>());
if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get<bool>();
if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get<int>();
if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get<int>();
if (j.contains("idle_gpu_aware")) idle_gpu_aware_ = j["idle_gpu_aware"].get<bool>();
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
saved_pool_urls_.clear();
for (const auto& u : j["saved_pool_urls"])
@@ -310,55 +164,15 @@ bool Settings::load(const std::string& path)
for (const auto& w : j["saved_pool_workers"])
if (w.is_string()) saved_pool_workers_.push_back(w.get<std::string>());
}
if (j.contains("portfolio_entries") && j["portfolio_entries"].is_array()) {
portfolio_entries_.clear();
for (const auto& e : j["portfolio_entries"]) {
if (!e.is_object()) continue;
PortfolioEntry entry;
if (e.contains("label") && e["label"].is_string())
entry.label = e["label"].get<std::string>();
if (e.contains("addresses") && e["addresses"].is_array())
for (const auto& a : e["addresses"])
if (a.is_string()) entry.addresses.push_back(a.get<std::string>());
if (e.contains("icon") && e["icon"].is_string())
entry.icon = e["icon"].get<std::string>();
if (e.contains("color") && e["color"].is_number())
entry.color = e["color"].get<unsigned int>();
if (e.contains("outline_opacity") && e["outline_opacity"].is_number_integer())
entry.outlineOpacity = e["outline_opacity"].get<int>();
if (e.contains("price_basis") && e["price_basis"].is_number_integer())
entry.priceBasis = e["price_basis"].get<int>();
if (e.contains("manual_price") && e["manual_price"].is_number())
entry.manualPrice = e["manual_price"].get<double>();
if (e.contains("manual_currency") && e["manual_currency"].is_string())
entry.manualCurrency = e["manual_currency"].get<std::string>();
if (e.contains("show_drgx") && e["show_drgx"].is_boolean())
entry.showDrgx = e["show_drgx"].get<bool>();
if (e.contains("show_value") && e["show_value"].is_boolean())
entry.showValue = e["show_value"].get<bool>();
if (e.contains("show_24h") && e["show_24h"].is_boolean())
entry.show24h = e["show_24h"].get<bool>();
if (e.contains("show_sparkline") && e["show_sparkline"].is_boolean())
entry.showSparkline = e["show_sparkline"].get<bool>();
if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
entry.sparklineInterval = e["sparkline_interval"].get<int>();
if (e.contains("grid_col") && e["grid_col"].is_number_integer())
entry.gridCol = e["grid_col"].get<int>();
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
entry.gridRow = e["grid_row"].get<int>();
if (e.contains("grid_w") && e["grid_w"].is_number_integer())
entry.gridW = e["grid_w"].get<int>();
if (e.contains("grid_h") && e["grid_h"].is_number_integer())
entry.gridH = e["grid_h"].get<int>();
if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry));
}
}
loadClamped(j, "font_scale", font_scale_, 1.0f, 1.5f);
loadScalar(j, "window_width", window_width_);
loadScalar(j, "window_height", window_height_);
if (j.contains("font_scale") && j["font_scale"].is_number())
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>()));
if (j.contains("window_width") && j["window_width"].is_number_integer())
window_width_ = j["window_width"].get<int>();
if (j.contains("window_height") && j["window_height"].is_number_integer())
window_height_ = j["window_height"].get<int>();
// Version tracking — detect upgrades so we can re-save with new defaults
loadScalar(j, "settings_version", settings_version_);
if (j.contains("settings_version")) settings_version_ = j["settings_version"].get<std::string>();
if (settings_version_ != DRAGONX_VERSION) {
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
settings_version_.empty() ? "(none)" : settings_version_.c_str(),
@@ -369,17 +183,6 @@ bool Settings::load(const std::string& path)
return true;
} catch (const std::exception& e) {
DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
// The file exists but is unparseable (truncated/corrupt). Quarantine it so the
// next save() doesn't silently overwrite it with defaults — the user's data stays
// recoverable. Proceed with in-memory defaults.
file.close();
std::error_code ec;
const std::string quarantine =
path + ".corrupt-" + std::to_string(static_cast<long long>(std::time(nullptr)));
fs::rename(path, quarantine, ec);
if (!ec) {
DEBUG_LOGF("Quarantined corrupt settings to %s\n", quarantine.c_str());
}
return false;
}
}
@@ -407,7 +210,6 @@ bool Settings::save(const std::string& path)
j["address_explorer_url"] = address_explorer_url_;
j["language"] = language_;
j["skin_id"] = skin_id_;
j["chat_reply_zaddr"] = chat_reply_zaddr_;
j["acrylic_enabled"] = acrylic_enabled_;
j["acrylic_quality"] = acrylic_quality_;
j["blur_multiplier"] = blur_multiplier_;
@@ -423,59 +225,18 @@ bool Settings::save(const std::string& path)
j["favorite_addresses"] = json::array();
for (const auto& addr : favorite_addresses_)
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["seed_backup_reminded"] = seed_backup_reminded_;
j["seed_migration_pending"] = seed_migration_pending_;
j["seed_migration_dest"] = seed_migration_dest_;
j["seed_migration_temp_dir"] = seed_migration_temp_dir_;
j["seed_migration_sweep_txid"] = seed_migration_sweep_txid_;
j["auto_lock_timeout"] = auto_lock_timeout_;
j["unlock_duration"] = unlock_duration_;
j["pin_enabled"] = pin_enabled_;
j["keep_daemon_running"] = keep_daemon_running_;
j["stop_external_daemon"] = stop_external_daemon_;
j["max_connections"] = max_connections_;
{
json lite = json::object();
lite["server_selection_mode"] = liteServerSelectionPreferenceModeName(lite_server_selection_mode_);
lite["sticky_server_url"] = lite_sticky_server_url_;
lite["chain_name"] = lite_chain_name_;
lite["random_selection_seed"] = lite_random_selection_seed_;
lite["persist_selected_server"] = lite_persist_selected_server_;
lite["servers"] = json::array();
for (const auto& server : lite_servers_) {
json entry = json::object();
entry["url"] = server.url;
entry["label"] = server.label;
entry["enabled"] = server.enabled;
lite["servers"].push_back(entry);
}
lite["rollout_override"] = lite_rollout_override_;
lite["install_id"] = lite_install_id_;
lite["hidden_servers"] = json::array();
for (const auto& u : lite_hidden_servers_) lite["hidden_servers"].push_back(u);
j["lite_wallet"] = lite;
}
j["verbose_logging"] = verbose_logging_;
j["debug_categories"] = json::array();
for (const auto& cat : debug_categories_)
j["debug_categories"].push_back(cat);
j["theme_effects_enabled"] = theme_effects_enabled_;
j["low_spec_mode"] = low_spec_mode_;
j["reduce_motion"] = reduce_motion_;
j["selected_exchange"] = selected_exchange_;
j["selected_pair"] = selected_pair_;
j["pool_url"] = pool_url_;
@@ -485,9 +246,7 @@ bool Settings::save(const std::string& path)
j["pool_tls"] = pool_tls_;
j["pool_hugepages"] = pool_hugepages_;
j["pool_mode"] = pool_mode_;
j["pool_select_mode"] = poolSelectModeName(pool_select_mode_);
j["mine_when_idle"] = mine_when_idle_;
j["xmrig_version"] = xmrig_version_;
j["mine_idle_delay"]= mine_idle_delay_;
j["idle_thread_scaling"] = idle_thread_scaling_;
j["idle_threads_active"] = idle_threads_active_;
@@ -499,29 +258,6 @@ bool Settings::save(const std::string& path)
j["saved_pool_workers"] = json::array();
for (const auto& w : saved_pool_workers_)
j["saved_pool_workers"].push_back(w);
j["portfolio_entries"] = json::array();
for (const auto& e : portfolio_entries_) {
json entry;
entry["label"] = e.label;
entry["addresses"] = json::array();
for (const auto& a : e.addresses) entry["addresses"].push_back(a);
entry["icon"] = e.icon;
entry["color"] = e.color;
entry["outline_opacity"] = e.outlineOpacity;
entry["price_basis"] = e.priceBasis;
entry["manual_price"] = e.manualPrice;
entry["manual_currency"] = e.manualCurrency;
entry["show_drgx"] = e.showDrgx;
entry["show_value"] = e.showValue;
entry["show_24h"] = e.show24h;
entry["show_sparkline"] = e.showSparkline;
entry["sparkline_interval"] = e.sparklineInterval;
entry["grid_col"] = e.gridCol;
entry["grid_row"] = e.gridRow;
entry["grid_w"] = e.gridW;
entry["grid_h"] = e.gridH;
j["portfolio_entries"].push_back(std::move(entry));
}
j["font_scale"] = font_scale_;
j["settings_version"] = std::string(DRAGONX_VERSION);
if (window_width_ > 0 && window_height_ > 0) {
@@ -530,11 +266,17 @@ bool Settings::save(const std::string& path)
}
try {
// Atomic + durable: write to a temp file, fsync, then rename over the real file.
// A crash mid-write can no longer truncate settings.json (which would silently
// reset every preference on the next launch). Owner-only (0600) — it carries the
// lite-server list and address metadata.
return util::Platform::writeFileAtomically(path, j.dump(4), /*restrictPermissions=*/true);
// Ensure directory exists
fs::path p(path);
fs::create_directories(p.parent_path());
std::ofstream file(path);
if (!file.is_open()) {
return false;
}
file << j.dump(4);
return true;
} catch (const std::exception& e) {
DEBUG_LOGF("Failed to save settings: %s\n", e.what());
return false;

View File

@@ -5,8 +5,6 @@
#pragma once
#include <algorithm>
#include <cstddef>
#include <map>
#include <string>
#include <set>
#include <vector>
@@ -55,51 +53,6 @@ public:
*/
static std::string getDefaultPath();
enum class LiteServerSelectionPreferenceMode {
Sticky,
Random
};
// Pool selection mode for the mining tab: Manual (user picks the pool) or
// AutoBalance (the wallet spreads miners across the official pools by hashrate).
enum class PoolSelectMode {
Manual,
AutoBalance
};
struct LiteServerPreference {
std::string url;
std::string label;
bool enabled = true;
};
// A user-defined portfolio entry: a custom label tied to a group of wallet addresses.
// The Market tab's portfolio card sums these addresses' balances under the label.
struct PortfolioEntry {
std::string label;
std::vector<std::string> addresses;
std::string icon; // project_icons wallet-icon name; empty = no icon
unsigned int color = 0; // packed IM_COL32 accent; 0 = theme default
int outlineOpacity = 25; // accent-outline opacity, percent (0-100)
// Per-group price data. priceBasis: 0 = live market (USD), 1 = live market (BTC),
// 2 = DRGX only (no fiat value), 3 = manual price. Defaults preserve prior behavior
// (show DRGX + USD value).
int priceBasis = 0;
double manualPrice = 0.0; // price per DRGX for the Manual basis
std::string manualCurrency = "USD";
bool showDrgx = true; // show the DRGX amount on the card
bool showValue = true; // show the converted/fiat value on the card
bool show24h = false; // show the 24h % change (live-market bases only)
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history)
// Dashboard grid placement on the Market portfolio. col/row in fine ~32px square cells
// (-1 = auto-place), w/h in cells (span). Set when the user drags/resizes a card.
int gridCol = -1;
int gridRow = -1;
int gridW = 8; // default group width (cells); minimum is 8
int gridH = 3; // default group height (cells); minimum is 2, card design adapts
};
// Theme
std::string getTheme() const { return theme_; }
void setTheme(const std::string& theme) { theme_ = theme; }
@@ -108,12 +61,6 @@ public:
std::string getSkinId() const { return skin_id_; }
void setSkinId(const std::string& id) { skin_id_ = id; }
// Stable z-address chosen for HushChat: the reply-to address in outgoing headers, and (for
// legacy/non-mnemonic wallets) the source of the seed-derived chat identity. Persisted so the
// identity + reply address don't shift when new addresses are generated.
std::string getChatReplyZaddr() const { return chat_reply_zaddr_; }
void setChatReplyZaddr(const std::string& z) { chat_reply_zaddr_ = z; }
// Privacy
bool getSaveZtxs() const { return save_ztxs_; }
void setSaveZtxs(bool save) { save_ztxs_ = save; }
@@ -194,75 +141,10 @@ public:
void unfavoriteAddress(const std::string& addr) { favorite_addresses_.erase(addr); }
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
bool getWizardCompleted() const { return wizard_completed_; }
void setWizardCompleted(bool v) { wizard_completed_ = v; }
// Whether the one-time "back up your seed phrase" reminder has already been shown.
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
// Pending "migrate to a seed wallet" state (Phase 1 created the wallet; a later sweep/adopt
// step consumes it). dest = the new wallet's sweep-target z-address; tempDir = its datadir.
bool getSeedMigrationPending() const { return seed_migration_pending_; }
void setSeedMigrationPending(bool v) { seed_migration_pending_ = v; }
std::string getSeedMigrationDest() const { return seed_migration_dest_; }
void setSeedMigrationDest(const std::string& v) { seed_migration_dest_ = v; }
std::string getSeedMigrationTempDir() const { return seed_migration_temp_dir_; }
void setSeedMigrationTempDir(const std::string& v) { seed_migration_temp_dir_ = v; }
// The sweep transaction id, persisted once the sweep is submitted — non-empty means the
// migration is past the sweep, so a resume goes to the confirm/adopt stage (not sweep again).
std::string getSeedMigrationSweepTxid() const { return seed_migration_sweep_txid_; }
void setSeedMigrationSweepTxid(const std::string& v) { seed_migration_sweep_txid_ = v; }
// Security — auto-lock timeout (seconds; 0 = disabled)
int getAutoLockTimeout() const { return auto_lock_timeout_; }
void setAutoLockTimeout(int seconds) { auto_lock_timeout_ = seconds; }
@@ -283,43 +165,6 @@ public:
bool getStopExternalDaemon() const { return stop_external_daemon_; }
void setStopExternalDaemon(bool v) { stop_external_daemon_ = v; }
// Daemon — maximum peer connections (0 = daemon default)
int getMaxConnections() const { return max_connections_; }
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
// Lite wallet server selection
LiteServerSelectionPreferenceMode getLiteServerSelectionMode() const { return lite_server_selection_mode_; }
void setLiteServerSelectionMode(LiteServerSelectionPreferenceMode mode) { lite_server_selection_mode_ = mode; }
std::string getLiteStickyServerUrl() const { return lite_sticky_server_url_; }
void setLiteStickyServerUrl(const std::string& url) { lite_sticky_server_url_ = url; }
std::string getLiteChainName() const { return lite_chain_name_; }
void setLiteChainName(const std::string& chainName) { lite_chain_name_ = chainName; }
std::size_t getLiteRandomSelectionSeed() const { return lite_random_selection_seed_; }
void setLiteRandomSelectionSeed(std::size_t seed) { lite_random_selection_seed_ = seed; }
bool getLitePersistSelectedServer() const { return lite_persist_selected_server_; }
void setLitePersistSelectedServer(bool persist) { lite_persist_selected_server_ = persist; }
const std::vector<LiteServerPreference>& getLiteServers() const { return lite_servers_; }
void setLiteServers(const std::vector<LiteServerPreference>& servers) { lite_servers_ = servers; }
// User-defined portfolio entries (Market tab). "All funds" is implicit, not stored here.
const std::vector<PortfolioEntry>& getPortfolioEntries() const { return portfolio_entries_; }
void setPortfolioEntries(const std::vector<PortfolioEntry>& entries) { portfolio_entries_ = entries; }
// Lite servers the user has hidden from the Network tab (kept by URL, shown via a toggle).
const std::set<std::string>& getLiteHiddenServers() const { return lite_hidden_servers_; }
bool isLiteServerHidden(const std::string& url) const { return lite_hidden_servers_.count(url) > 0; }
void hideLiteServer(const std::string& url) { lite_hidden_servers_.insert(url); }
void unhideLiteServer(const std::string& url) { lite_hidden_servers_.erase(url); }
// Lite wallet rollout / kill-switch (see wallet/lite_rollout_policy.h).
// Override: "auto" (honor rollout manifest), "force_on", or "force_off".
std::string getLiteRolloutOverride() const { return lite_rollout_override_; }
void setLiteRolloutOverride(const std::string& v) { lite_rollout_override_ = v; }
// Stable, locally-generated install id used only to derive the staged-rollout bucket.
// Never transmitted; carries no PII. Generated on first use if empty.
std::string getLiteInstallId() const { return lite_install_id_; }
void setLiteInstallId(const std::string& v) { lite_install_id_ = v; }
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
bool getVerboseLogging() const { return verbose_logging_; }
void setVerboseLogging(bool v) { verbose_logging_ = v; }
@@ -341,10 +186,6 @@ public:
bool getLowSpecMode() const { return low_spec_mode_; }
void setLowSpecMode(bool v) { low_spec_mode_ = v; }
// Reduce motion — disables animated transitions for accessibility
bool getReduceMotion() const { return reduce_motion_; }
void setReduceMotion(bool v) { reduce_motion_ = v; }
// Market — last selected exchange + pair
std::string getSelectedExchange() const { return selected_exchange_; }
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
@@ -366,12 +207,6 @@ public:
void setPoolHugepages(bool v) { pool_hugepages_ = v; }
bool getPoolMode() const { return pool_mode_; }
void setPoolMode(bool v) { pool_mode_ = v; }
PoolSelectMode getPoolSelectMode() const { return pool_select_mode_; }
void setPoolSelectMode(PoolSelectMode v) { pool_select_mode_ = v; }
// Installed DRG-XMRig release tag (for in-app miner update detection); empty if unknown/bundled.
std::string getXmrigVersion() const { return xmrig_version_; }
void setXmrigVersion(const std::string& v) { xmrig_version_ = v; }
// Mine when idle (auto-start mining when system is idle)
bool getMineWhenIdle() const { return mine_when_idle_; }
@@ -433,7 +268,6 @@ private:
// Settings values
std::string theme_ = "dragonx";
std::string skin_id_ = "dragonx";
std::string chat_reply_zaddr_;
bool save_ztxs_ = true;
bool auto_shield_ = true;
bool use_tor_ = false;
@@ -459,48 +293,18 @@ private:
bool scanline_enabled_ = true;
std::set<std::string> hidden_addresses_;
std::set<std::string> favorite_addresses_;
std::map<std::string, AddressMeta> address_meta_;
bool wizard_completed_ = false;
bool seed_backup_reminded_ = false;
bool seed_migration_pending_ = false;
std::string seed_migration_dest_;
std::string seed_migration_temp_dir_;
std::string seed_migration_sweep_txid_;
int auto_lock_timeout_ = 900; // 15 minutes
int unlock_duration_ = 600; // 10 minutes
bool pin_enabled_ = false;
bool keep_daemon_running_ = false;
bool stop_external_daemon_ = false;
int max_connections_ = 0; // 0 = daemon default
// Lite wallet server preferences. These are user/server settings only;
// wallet secrets, wallet files, and lifecycle state are never stored here.
LiteServerSelectionPreferenceMode lite_server_selection_mode_ = LiteServerSelectionPreferenceMode::Sticky;
std::string lite_sticky_server_url_ = "https://lite.dragonx.is";
std::string lite_chain_name_ = "main"; // SDXL backend chain id; must be main/test/regtest
std::size_t lite_random_selection_seed_ = 0;
bool lite_persist_selected_server_ = true;
std::string lite_rollout_override_ = "auto"; // auto|force_on|force_off
std::string lite_install_id_; // random local-only id; rollout-bucket source
std::vector<LiteServerPreference> lite_servers_ = {
{"https://lite.dragonx.is", "DragonX Lite", true},
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
{"https://lite2.dragonx.is", "DragonX Lite 2", true},
{"https://lite3.dragonx.is", "DragonX Lite 3", true},
{"https://lite4.dragonx.is", "DragonX Lite 4", true},
{"https://lite5.dragonx.is", "DragonX Lite 5", true}
};
std::set<std::string> lite_hidden_servers_; // server URLs hidden from the Network tab
std::vector<PortfolioEntry> portfolio_entries_; // Market tab custom portfolio groups
bool verbose_logging_ = false;
std::set<std::string> debug_categories_;
bool theme_effects_enabled_ = true;
bool low_spec_mode_ = false;
bool reduce_motion_ = false;
std::string selected_exchange_ = "Nonkyc.io";
std::string selected_pair_ = "DRGX/USDT";
std::string selected_exchange_ = "TradeOgre";
std::string selected_pair_ = "DRGX/BTC";
// Pool mining
std::string pool_url_ = "pool.dragonx.is:3433";
@@ -510,8 +314,6 @@ private:
bool pool_tls_ = false;
bool pool_hugepages_ = true;
bool pool_mode_ = false; // false=solo, true=pool
PoolSelectMode pool_select_mode_ = PoolSelectMode::Manual; // manual vs auto-balance pool choice
std::string xmrig_version_; // installed DRG-XMRig release tag (update detection)
bool mine_when_idle_ = false; // auto-start mining when system idle
int mine_idle_delay_= 120; // seconds of idle before mining starts
bool idle_thread_scaling_ = false; // scale threads instead of start/stop

View File

@@ -1,3 +1,31 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "dragonx_generated_version.h"
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
#define DRAGONX_VERSION "1.1.1"
#define DRAGONX_VERSION_MAJOR 1
#define DRAGONX_VERSION_MINOR 1
#define DRAGONX_VERSION_PATCH 1
#define DRAGONX_APP_NAME "ObsidianDragon"
#define DRAGONX_ORG_NAME "Hush"
// Default RPC settings
#define DRAGONX_DEFAULT_RPC_HOST "127.0.0.1"
#define DRAGONX_DEFAULT_RPC_PORT "21769"
// Coin parameters
#define DRAGONX_TICKER "DRGX"
#define DRAGONX_COIN_NAME "DragonX"
#define DRAGONX_URI_SCHEME "drgx"
#define DRAGONX_ZATOSHI_PER_COIN 100000000
#define DRAGONX_DEFAULT_FEE 0.0001
// Config file names
#define DRAGONX_CONF_FILENAME "DRAGONX.conf"
#define DRAGONX_WALLET_FILENAME "wallet.dat"

View File

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

View File

@@ -1,128 +0,0 @@
#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::setZapOnNextStart(bool enabled)
{
daemon_->setZapOnNextStart(enabled);
}
bool DaemonController::zapOnNextStart() const
{
return daemon_->zapOnNextStart();
}
void DaemonController::prepareLifecycleOperation(const LifecycleDecision& decision,
const config::Settings* settings)
{
if (settings) syncSettings(settings);
if (decision.resetCrashCount) resetCrashCount();
if (decision.setRescanOnNextStart) setRescanOnNextStart(true);
if (decision.setZapOnNextStart) setZapOnNextStart(true);
}
DaemonController::ShutdownDecision DaemonController::shutdownDecision(
bool keepDaemonRunning, bool stopExternalDaemon) const
{
return evaluateShutdownPolicy(static_cast<bool>(daemon_),
daemon_ && daemon_->externalDaemonDetected(),
keepDaemonRunning,
stopExternalDaemon);
}
} // namespace daemon
} // namespace dragonx

View File

@@ -1,238 +0,0 @@
#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,
RepairWallet, // restart with -zapwallettxes=2 (wipe & rebuild wallet tx records)
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;
bool setZapOnNextStart = false;
};
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;
void setZapOnNextStart(bool enabled);
bool zapOnNextStart() 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::RepairWallet:
if (!usingEmbeddedDaemon || !hasDaemon) {
return {operation, false, daemonRunning, "", "",
"Wallet repair requires embedded daemon. Restart your daemon with -zapwallettxes=2 manually."};
}
return {operation, true, daemonRunning, "repair-wallet", "Repairing wallet...", "",
false, false, false, 3000, true};
case LifecycleOperation::DeleteBlockchainData:
if (!usingEmbeddedDaemon || !hasDaemon) {
return {operation, false, daemonRunning, "", "",
"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::RepairWallet:
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::RepairWallet ||
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,9 +1,6 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// embedded_daemon.cpp — Manages the dragonxd child process lifecycle:
// binary discovery, process spawn, stdout/stderr monitoring, crash recovery.
#include "embedded_daemon.h"
#include "../config/version.h"
@@ -205,18 +202,9 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
"-ac_reward=300000000",
"-ac_blocktime=36",
"-ac_private=1",
"-addnode=node.dragonx.is",
"-addnode=node1.dragonx.is",
"-addnode=node2.dragonx.is",
"-addnode=node3.dragonx.is",
"-addnode=node4.dragonx.is",
"-addnode=176.126.87.241",
"-experimentalfeatures",
"-developerencryptwallet",
// Create fresh wallets from a BIP39 mnemonic so their 24-word phrase can be
// exported (z_exportmnemonic) and is portable to SDXLite/ObsidianDragonLite.
// The daemon reads this ONLY inside GenerateNewSeed() when a wallet has no seed
// yet, so it is inert on existing wallets — safe to pass unconditionally.
"-usemnemonic=1",
dbcache_arg
};
}
@@ -448,10 +436,9 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
return true;
}
// Check if something is already listening on the RPC port. An isolated instance (migrate-to-
// seed) runs on its own non-default port alongside the main daemon, so it skips this bail.
// Check if something is already listening on the RPC port
int rpc_port = std::atoi(DRAGONX_DEFAULT_RPC_PORT);
if (!skip_port_check_ && isPortInUse(rpc_port)) {
if (isPortInUse(rpc_port)) {
std::string owner = getPortOwnerInfo(rpc_port);
VERBOSE_LOGF("[INFO] Port %d is already in use by %s — external daemon detected, will connect to it.\\n", rpc_port, owner.c_str());
external_daemon_detected_ = true;
@@ -483,36 +470,12 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
args.push_back("-debug=" + cat);
}
// Append max connections if configured (0 = daemon default)
if (max_connections_ > 0) {
args.push_back("-maxconnections=" + std::to_string(max_connections_));
}
// Add wallet-repair flag if requested (one-shot). -zapwallettxes=2 wipes all wallet tx/note
// records and rebuilds them from the chain; it implies -rescan, so don't also pass -rescan.
if (zap_on_next_start_.exchange(false)) {
DEBUG_LOGF("[INFO] Adding -zapwallettxes=2 flag for wallet repair (zap & rebuild)\n");
args.push_back("-zapwallettxes=2");
rescan_on_next_start_.store(false); // implied by zap; avoid redundant -rescan
} else if (rescan_on_next_start_.exchange(false)) {
// Add -rescan flag if requested (one-shot)
// Add -rescan flag if requested (one-shot)
if (rescan_on_next_start_.exchange(false)) {
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
args.push_back("-rescan");
}
// One-shot isolated-datadir override (migrate-to-seed flow): run this start against a
// throwaway datadir, plus any extra args (e.g. -connect=0). Consumed here so later starts
// revert to the normal datadir. The datadir's basename MUST be the assetchain name (DRAGONX)
// or the daemon mis-resolves its conf/port; it reads <datadir>/DRAGONX.conf automatically, so
// no -conf is passed (an explicit -conf confuses the Komodo/Hush path resolution).
if (!override_datadir_.empty()) {
DEBUG_LOGF("[INFO] Isolated start override: -datadir=%s\n", override_datadir_.c_str());
args.push_back("-datadir=" + override_datadir_);
}
for (const auto& a : override_extra_args_) args.push_back(a);
override_datadir_.clear();
override_extra_args_.clear();
if (!startProcess(daemon_path, args)) {
DEBUG_LOGF("[ERROR] Failed to start dragonxd process: %s\\n", last_error_.c_str());
setState(State::Error, "Failed to start dragonxd process");
@@ -1234,10 +1197,5 @@ bool EmbeddedDaemon::isRpcPortInUse()
return isPortInUse(port);
}
bool EmbeddedDaemon::tcpPortInUse(int port)
{
return isPortInUse(port);
}
} // namespace daemon
} // namespace dragonx

View File

@@ -116,26 +116,6 @@ public:
* @brief Get last N lines of daemon output (thread-safe snapshot)
*/
std::vector<std::string> getRecentLines(int maxLines = 8) const;
/**
* @brief Extract the latest block height from daemon output (thread-safe).
* Parses the last "height=N" from UpdateTip lines without copying
* the entire output buffer. Returns -1 if no UpdateTip found.
*/
int getLastBlockHeight() const {
std::lock_guard<std::mutex> lk(output_mutex_);
// Search backwards from the end for "height="
size_t pos = process_output_.rfind("height=");
if (pos == std::string::npos) return -1;
pos += 7; // skip "height="
int h = 0;
for (size_t i = pos; i < process_output_.size(); ++i) {
char c = process_output_[i];
if (c >= '0' && c <= '9') h = h * 10 + (c - '0');
else break;
}
return h > 0 ? h : -1;
}
/**
* @brief Whether start() detected an existing daemon on the RPC port.
@@ -172,48 +152,12 @@ public:
void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
const std::set<std::string>& getDebugCategories() const { return debug_categories_; }
/**
* @brief Set maximum peer connections (0 = use daemon default)
*/
void setMaxConnections(int v) { max_connections_ = v; }
/**
* @brief Request a blockchain rescan on the next daemon start
*/
void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; }
bool rescanOnNextStart() const { return rescan_on_next_start_.load(); }
/**
* @brief Request a wallet repair (-zapwallettxes=2) on the next daemon start. This deletes all
* wallet transaction/note records and rebuilds them from the chain (keys are kept); the
* daemon implicitly rescans afterwards. One-shot, like the rescan flag.
*/
void setZapOnNextStart(bool v) { zap_on_next_start_ = v; }
bool zapOnNextStart() const { return zap_on_next_start_.load(); }
/**
* @brief One-shot isolated-datadir override for the NEXT start(): run the daemon against a
* different datadir (with its own DRAGONX.conf) plus the given extra args. Used by the
* "migrate to a seed wallet" flow to mint a fresh mnemonic wallet in a throwaway datadir
* without touching the real one. Consumed on the next start(); later starts are normal.
* The caller must serialize this with start() (no concurrent starts).
*/
void setNextStartOverride(const std::string& datadir, std::vector<std::string> extraArgs) {
override_datadir_ = datadir;
override_extra_args_ = std::move(extraArgs);
}
void clearNextStartOverride() { override_datadir_.clear(); override_extra_args_.clear(); }
/**
* @brief Skip the "default RPC port already in use → external daemon" bail in start().
* Set true only for an isolated instance running on its OWN (non-default) port
* alongside the main daemon (migrate-to-seed flow).
*/
void setSkipPortCheck(bool v) { skip_port_check_ = v; }
/** @brief Is an arbitrary TCP port currently in use on localhost? (used to pick a free port) */
static bool tcpPortInUse(int port);
/** Get number of consecutive daemon crashes (resets on successful start or manual reset) */
int getCrashCount() const { return crash_count_.load(); }
/** Reset crash counter (call on successful connection or manual restart) */
@@ -250,13 +194,8 @@ private:
std::thread monitor_thread_;
std::atomic<bool> should_stop_{false};
std::set<std::string> debug_categories_;
int max_connections_ = 0; // 0 = daemon default
std::atomic<int> crash_count_{0}; // consecutive crash counter
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
std::atomic<bool> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start
std::string override_datadir_; // one-shot: -datadir for the next start
std::vector<std::string> override_extra_args_; // one-shot: extra args for the next start
bool skip_port_check_ = false; // isolated instance on a non-default port
};
} // namespace daemon

View File

@@ -1,93 +0,0 @@
#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

@@ -1,39 +0,0 @@
#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,138 +0,0 @@
#include "daemon/seed_wallet_creator.h"
#include <chrono>
#include <filesystem>
#include <thread>
#include <sodium.h>
#include "daemon/embedded_daemon.h"
#include "rpc/rpc_client.h"
#include "util/platform.h"
namespace fs = std::filesystem;
namespace dragonx {
namespace daemon {
namespace {
// Random alphanumeric token for the isolated node's throwaway RPC credentials (libsodium CSPRNG;
// sodium_init() has already run at app startup for the chat crypto).
std::string randomToken(int n)
{
static const char cs[] =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
std::string s;
s.reserve(n);
for (int i = 0; i < n; ++i)
s.push_back(cs[randombytes_uniform(sizeof(cs) - 1)]);
return s;
}
// A free localhost port for the isolated node — just above the default so it never collides with
// the main daemon (which keeps running on the default port throughout).
int pickFreePort()
{
for (int p = 21770; p < 21900; ++p)
if (!EmbeddedDaemon::tcpPortInUse(p))
return p;
return 0;
}
} // namespace
SeedWalletResult SeedWalletCreator::create(bool keepDatadir,
const std::function<void(const std::string&)>& progress)
{
auto report = [&](const std::string& m) { if (progress) progress(m); };
SeedWalletResult r;
std::error_code ec;
// 1. Isolated throwaway datadir. The Komodo/Hush daemon requires the datadir's basename to be
// the assetchain name (DRAGONX) — mirroring ~/.hush/DRAGONX — or it mis-resolves its conf and
// RPC port. So the wallet lives in <base>/DRAGONX; `base` is the migration root we clean up.
const std::string base = util::Platform::getConfigDir() + "/seed-migrate";
const std::string dataDir = base + "/DRAGONX";
fs::remove_all(base, ec);
fs::create_directories(dataDir, ec);
if (ec) { r.error = "Could not create the temporary wallet directory."; return r; }
// 2. Free port + fresh throwaway RPC credentials for the isolated node.
const int port = pickFreePort();
if (port <= 0) { r.error = "No free local port for the isolated node."; return r; }
const std::string user = randomToken(16);
const std::string pass = randomToken(32);
// 3. Minimal conf for the isolated node (own creds/port). The DRAGONX RPC is plaintext HTTP on
// localhost — `-tls=only` applies to P2P, not the RPC — so the client below connects without
// TLS, exactly as the main GUI does (its conf has no rpctls key either).
const std::string conf = "rpcuser=" + user + "\n"
"rpcpassword=" + pass + "\n"
"rpcport=" + std::to_string(port) + "\n"
"server=1\n";
if (!util::Platform::writeFileAtomically(dataDir + "/DRAGONX.conf", conf,
/*restrictPermissions=*/true)) {
r.error = "Could not write the isolated node config.";
fs::remove_all(base, ec);
return r;
}
// 4. Start the isolated daemon: fresh mnemonic wallet (-usemnemonic=1), no network/sync.
report("Starting an isolated node…");
EmbeddedDaemon temp;
temp.setSkipPortCheck(true); // runs on `port`, beside the main daemon on the default port
temp.setNextStartOverride(dataDir, {"-usemnemonic=1", "-connect=0", "-listen=0",
"-maxconnections=0"});
if (!temp.start("")) {
r.error = "Could not start the isolated node: " + temp.getLastError();
fs::remove_all(base, ec);
return r;
}
// 5. Connect to it, retrying until the RPC is responsive and past warmup.
report("Creating your new seed wallet…");
rpc::RPCClient cli;
bool ready = false;
for (int i = 0; i < 90 && !ready; ++i) {
if (cli.connect("127.0.0.1", std::to_string(port), user, pass, /*useTls=*/false)) {
try { cli.call("getinfo"); ready = true; } // succeeds only once past warmup (-28)
catch (...) { cli.disconnect(); }
}
if (!ready) std::this_thread::sleep_for(std::chrono::seconds(1));
}
if (!ready) {
r.error = "The isolated node did not become ready in time.";
temp.stop(20000);
fs::remove_all(base, ec);
return r;
}
// 6. Export the new seed phrase + a fresh shielded receive address (the future sweep target).
try {
auto m = cli.call("z_exportmnemonic");
if (m.contains("mnemonic") && m["mnemonic"].is_string())
r.seedPhrase = m["mnemonic"].get<std::string>();
r.destAddress = cli.call("z_getnewaddress").get<std::string>();
r.ok = !r.seedPhrase.empty() && !r.destAddress.empty();
if (!r.ok) r.error = "The isolated node returned an empty seed or address.";
} catch (const std::exception& e) {
r.error = std::string("Seed export failed: ") + e.what();
}
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly).
cli.disconnect();
temp.stop(20000);
// 8. Keep the temp wallet for a later sweep/adopt step, or scrub it. tempDatadir is the
// migration root `base`; the new wallet.dat lives in <base>/DRAGONX.
r.tempDatadir = base;
if (!keepDatadir || !r.ok) {
fs::remove_all(base, ec);
r.tempDatadir.clear();
}
return r;
}
} // namespace daemon
} // namespace dragonx

View File

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

View File

@@ -1,14 +1,10 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// xmrig_manager.cpp — Pool mining process management via xmrig-hac.
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
#include "xmrig_manager.h"
#include "../resources/embedded_resources.h"
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -23,7 +19,6 @@
#include <curl/curl.h>
#include "../util/logger.h"
#include "../util/pool_registry.h"
#ifdef _WIN32
#include <winsock2.h>
@@ -720,11 +715,6 @@ void XmrigManager::fetchStatsHttp() {
std::lock_guard<std::mutex> lk(stats_mutex_);
// Miner version (top-level in /2/summary) — lets the UI show the actually
// running miner's version even when no release tag was persisted (bundled miner).
if (resp.contains("version") && resp["version"].is_string())
stats_.version = resp["version"].get<std::string>();
if (resp.contains("hashrate") && resp["hashrate"].contains("total")) {
auto& total = resp["hashrate"]["total"];
if (total.is_array() && total.size() >= 3) {
@@ -775,14 +765,10 @@ void XmrigManager::fetchStatsHttp() {
void XmrigManager::fetchPoolApiStats() {
if (state_ != State::Running || pool_host_.empty()) return;
// Resolve the stats endpoint + JSON schema for this pool. Known pools carry their
// own API shape (pool.dragonx.is = custom /api/stats; pool.dragonx.cc = Miningcore
// /api/pools); unknown/custom hosts fall back to the .is convention.
const util::KnownPool* known = util::findKnownPoolByUrl(pool_host_);
const std::string url = known ? known->statsUrl
: ("https://" + pool_host_ + "/api/stats");
// Query the pool's public stats API
std::string url = "https://" + pool_host_ + "/api/stats";
std::string responseData;
CURL* curl = curl_easy_init();
if (!curl) return;
@@ -793,99 +779,31 @@ void XmrigManager::fetchPoolApiStats() {
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
// pool.dragonx.cc sits behind Cloudflare and 403s odd User-Agents.
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; ObsidianDragon)");
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res != CURLE_OK) return;
bool ok = false;
const double poolHR = util::parsePoolHashrate(
known ? known->schema : util::PoolStatsSchema::DragonXIs,
responseData, known ? known->miningcorePoolId : std::string{}, ok);
if (!ok) return;
try {
json resp = json::parse(responseData);
std::lock_guard<std::mutex> lk(stats_mutex_);
stats_.pool_hashrate = poolHR;
}
// ============================================================================
// Installed-miner version detection (`<binary> --version`, cached)
// ============================================================================
namespace {
std::mutex g_installed_ver_mutex;
std::string g_installed_ver;
std::atomic<bool> g_ver_detect_started{false};
// Extract the first "D.D[.D...]" version token from `--version` output (skips the
// build date, which uses '-' separators). Returns e.g. "6.21.0", or "" if none.
std::string parseMinerVersion(const std::string& out)
{
for (size_t i = 0; i < out.size(); ++i) {
if (std::isdigit(static_cast<unsigned char>(out[i]))) {
size_t j = i;
int dots = 0;
while (j < out.size() &&
(std::isdigit(static_cast<unsigned char>(out[j])) || out[j] == '.')) {
if (out[j] == '.') ++dots;
++j;
}
if (dots >= 1 && (j - i) >= 3) {
// Include a trailing build suffix like "-hac" / "-drg1" (e.g. "6.25.1-hac"),
// matching what the running miner's API reports.
size_t end = j;
if (end < out.size() && out[end] == '-') {
size_t k = end + 1;
while (k < out.size() && std::isalnum(static_cast<unsigned char>(out[k]))) ++k;
if (k > end + 1) end = k;
// Pool stats API format: { "pools": { "<name>": { "hashrate": ... } } }
double poolHR = 0;
if (resp.contains("pools") && resp["pools"].is_object()) {
for (auto& [key, pool] : resp["pools"].items()) {
if (pool.contains("hashrate") && pool["hashrate"].is_number()) {
poolHR = pool["hashrate"].get<double>();
break; // Use the first pool entry
}
return out.substr(i, end - i);
}
i = j;
}
std::lock_guard<std::mutex> lk(stats_mutex_);
stats_.pool_hashrate = poolHR;
} catch (...) {
// Malformed response — ignore
}
return {};
}
} // namespace
void XmrigManager::startVersionDetection()
{
if (g_ver_detect_started.exchange(true)) return; // one-shot
std::thread([]() {
const std::string bin = findXmrigBinary();
std::string ver;
if (!bin.empty()) {
const std::string cmd = "\"" + bin + "\" --version 2>&1";
#ifdef _WIN32
FILE* fp = _popen(cmd.c_str(), "r");
#else
FILE* fp = popen(cmd.c_str(), "r");
#endif
if (fp) {
std::string out;
char buf[256];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) out.append(buf, n);
#ifdef _WIN32
_pclose(fp);
#else
pclose(fp);
#endif
ver = parseMinerVersion(out);
}
}
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
g_installed_ver = ver;
}).detach();
}
std::string XmrigManager::installedVersion()
{
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
return g_installed_ver;
}
} // namespace daemon

View File

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

View File

@@ -9,7 +9,13 @@
#include <filesystem>
#include "../util/logger.h"
#include "../util/platform.h"
#ifdef _WIN32
#include <shlobj.h>
#else
#include <pwd.h>
#include <unistd.h>
#endif
namespace fs = std::filesystem;
using json = nlohmann::json;
@@ -22,11 +28,33 @@ AddressBook::~AddressBook() = default;
std::string AddressBook::getDefaultPath()
{
// Co-locate with settings.json in the per-variant config dir (Lite -> ObsidianDragonLite/).
// util::Platform::getConfigDir() owns the per-platform + per-variant path in one place.
const std::string dir = util::Platform::getConfigDir();
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
std::string dir = std::string(path) + "\\ObsidianDragon";
fs::create_directories(dir);
return dir + "\\addressbook.json";
}
return "addressbook.json";
#elif defined(__APPLE__)
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/Library/Application Support/ObsidianDragon";
fs::create_directories(dir);
return (fs::path(dir) / "addressbook.json").string();
return dir + "/addressbook.json";
#else
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
home = pw->pw_dir;
}
std::string dir = std::string(home) + "/.config/ObsidianDragon";
fs::create_directories(dir);
return dir + "/addressbook.json";
#endif
}
bool AddressBook::load()
@@ -85,16 +113,20 @@ bool AddressBook::save()
j["entries"].push_back(e);
}
// Atomic + durable: temp file + fsync + rename, so a crash mid-write can't
// truncate addressbook.json (which is fully rewritten on every entry change).
// Owner-only (0600) — it holds the user's saved contacts.
if (!util::Platform::writeFileAtomically(file_path_, j.dump(2), /*restrictPermissions=*/true)) {
DEBUG_LOGF("Could not write address book: %s\n", file_path_.c_str());
// Ensure directory exists
fs::path p(file_path_);
fs::create_directories(p.parent_path());
std::ofstream file(file_path_);
if (!file.is_open()) {
DEBUG_LOGF("Could not open address book for writing: %s\n", file_path_.c_str());
return false;
}
file << j.dump(2);
DEBUG_LOGF("Address book saved: %zu entries\n", entries_.size());
return true;
} catch (const std::exception& e) {
DEBUG_LOGF("Error saving address book: %s\n", e.what());
return false;

View File

@@ -4,19 +4,11 @@
#include "exchange_info.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <unordered_map>
namespace dragonx {
namespace data {
const std::vector<ExchangeInfo>& getExchangeRegistry()
{
// Offline seed / fallback. The live list from CoinGecko (parseCoinGeckoTickers)
// supersedes this when available; keep the known venues here so the tab is never
// empty without network.
static const std::vector<ExchangeInfo> registry = {
{
"Nonkyc.io",
@@ -25,68 +17,9 @@ const std::vector<ExchangeInfo>& getExchangeRegistry()
{"DRGX", "USDT", "DRGX/USDT", "https://nonkyc.io/market/DRGX_USDT"},
}
},
{
"OurBit",
"https://www.ourbit.com",
{
{"DRGX", "USDT", "DRGX/USDT", "https://www.ourbit.com/exchange/DRGX_USDT"},
}
},
};
return registry;
}
namespace {
// Extract the scheme://host origin from a URL (for ExchangeInfo.baseUrl).
std::string originOf(const std::string& url)
{
const auto scheme = url.find("://");
if (scheme == std::string::npos) return url;
const auto host = url.find('/', scheme + 3);
return (host == std::string::npos) ? url : url.substr(0, host);
}
} // namespace
std::vector<ExchangeInfo> parseCoinGeckoTickers(const std::string& body)
{
std::vector<ExchangeInfo> exchanges;
try {
const nlohmann::json j = nlohmann::json::parse(body);
if (!j.contains("tickers") || !j["tickers"].is_array()) return {};
std::unordered_map<std::string, size_t> byName; // exchange name -> index
for (const auto& t : j["tickers"]) {
if (!t.is_object()) continue;
const std::string base = t.value("base", std::string{});
const std::string target = t.value("target", std::string{});
std::string market;
if (t.contains("market") && t["market"].is_object())
market = t["market"].value("name", std::string{});
const std::string tradeUrl = t.value("trade_url", std::string{});
if (base.empty() || target.empty() || market.empty()) continue;
auto it = byName.find(market);
if (it == byName.end()) {
byName[market] = exchanges.size();
exchanges.push_back(ExchangeInfo{market, originOf(tradeUrl), {}});
it = byName.find(market);
}
ExchangePair pair{base, target, base + "/" + target, tradeUrl};
// Skip duplicate pairs on the same exchange.
bool dup = false;
for (const auto& p : exchanges[it->second].pairs)
if (p.displayName == pair.displayName) { dup = true; break; }
if (!dup) exchanges[it->second].pairs.push_back(std::move(pair));
}
} catch (...) {
return {};
}
// Drop exchanges that ended up with no pairs (defensive).
exchanges.erase(std::remove_if(exchanges.begin(), exchanges.end(),
[](const ExchangeInfo& e) { return e.pairs.empty(); }),
exchanges.end());
return exchanges;
}
} // namespace data
} // namespace dragonx

View File

@@ -30,19 +30,9 @@ struct ExchangeInfo {
};
/**
* @brief Returns the static registry of supported exchanges + pairs.
* Used as the offline fallback / seed when the live CoinGecko list is unavailable.
* @brief Returns the static registry of supported exchanges + pairs
*/
const std::vector<ExchangeInfo>& getExchangeRegistry();
/**
* @brief Parse a CoinGecko /coins/{id}/tickers JSON body into the exchange registry.
*
* Groups tickers by exchange (market.name), preserving first-seen order, into
* ExchangeInfo entries (each pair carries base/target/displayName/trade_url). Returns
* an empty vector on parse failure or when no usable tickers are present. Pure (no I/O).
*/
std::vector<ExchangeInfo> parseCoinGeckoTickers(const std::string& json);
} // namespace data
} // namespace dragonx

View File

@@ -1,99 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <vector>
#include <utility>
#include <ctime>
#include "wallet_state.h" // MarketInfo
// Pure (no-I/O, no-ImGui) price-series math backing the market chart and the portfolio-group
// sparklines. Kept header-only + inline so it can be unit-tested without the GUI (see
// tests/test_phase4.cpp), mirroring data/portfolio.h.
namespace dragonx {
namespace data {
// Resample a ~1-sample/minute price history to the given interval by averaging each block of K
// minute-samples into one point. interval: 0=min 1=hour 2=day 3=week 4=month.
inline std::vector<double> resampleHistory(const std::vector<double>& hist, int interval)
{
static const int kMins[5] = {1, 60, 1440, 10080, 43200};
int k = kMins[(interval >= 0 && interval < 5) ? interval : 0];
if (k <= 1) return hist;
std::vector<double> out;
for (size_t i = 0; i < hist.size(); i += (size_t)k) {
double sum = 0.0; size_t cnt = 0;
for (size_t j = i; j < hist.size() && j < i + (size_t)k; ++j) { sum += hist[j]; ++cnt; }
if (cnt) out.push_back(sum / (double)cnt);
}
return out;
}
// Bucket a timestamped (unix-seconds, price) series into fixed-width time windows, averaging the
// samples in each window into one point. Input is oldest->newest; output preserves that order.
inline std::vector<double> bucketBySeconds(
const std::vector<std::pair<std::time_t, double>>& series, long windowSec)
{
std::vector<double> out;
if (series.empty() || windowSec <= 0) return out;
long curBucket = 0; double sum = 0.0; int cnt = 0;
for (const auto& sample : series) {
long b = (long)(sample.first / windowSec);
if (cnt > 0 && b != curBucket) { out.push_back(sum / cnt); sum = 0.0; cnt = 0; }
curBucket = b;
sum += sample.second; ++cnt;
}
if (cnt > 0) out.push_back(sum / cnt);
return out;
}
// Resolve the price series backing a group's sparkline for the chosen interval:
// 0=minute -> the live in-session buffer; 1=hour -> intraday (5-min) data bucketed to hours;
// 2=day / 3=week / 4=month -> the ~1yr daily series bucketed accordingly.
// Falls back to the in-session buffer when the historical fetch hasn't populated yet.
inline std::vector<double> sparklineSeries(const MarketInfo& m, int interval)
{
const long kDay = 86400;
switch (interval) {
case 1: { auto v = bucketBySeconds(m.price_chart_intraday, 3600); if (v.size() >= 2) return v; break; }
case 2: { auto v = bucketBySeconds(m.price_chart_daily, kDay); if (v.size() >= 2) return v; break; }
case 3: { auto v = bucketBySeconds(m.price_chart_daily, 7 * kDay); if (v.size() >= 2) return v; break; }
case 4: { auto v = bucketBySeconds(m.price_chart_daily, 30 * kDay); if (v.size() >= 2) return v; break; }
default: break; // minute interval, or no historical data yet -> live buffer below
}
return resampleHistory(m.price_history, interval);
}
// Timestamped price series backing the main chart for the selected RANGE (Live/1H/1D/1W/1M),
// ending at `now`. The interval buttons select a time window ending at `now`: 1H = last hour,
// 1D = last 24h, 1W = last 7 days, 1M = last 30 days. 1H/1D come from the 5-minute intraday
// series; 1W/1M from the daily series. Live (or an un-fetched/empty range) uses the minute buffer.
inline std::vector<std::pair<std::time_t, double>> chartSeries(const MarketInfo& m, int interval,
std::time_t now)
{
const long kDay = 86400;
auto lastWindow = [now](const std::vector<std::pair<std::time_t, double>>& src, long rangeSec) {
std::vector<std::pair<std::time_t, double>> out;
std::time_t cutoff = now - (std::time_t)rangeSec;
for (const auto& s : src) if (s.first >= cutoff) out.push_back(s);
return out;
};
switch (interval) {
case 1: { auto v = lastWindow(m.price_chart_intraday, 3600); if (v.size() >= 2) return v; break; } // 1H
case 2: { auto v = lastWindow(m.price_chart_intraday, kDay); if (v.size() >= 2) return v; break; } // 1D
case 3: { auto v = lastWindow(m.price_chart_daily, 7 * kDay); if (v.size() >= 2) return v; break; } // 1W
case 4: { auto v = lastWindow(m.price_chart_daily, 30 * kDay); if (v.size() >= 2) return v; break; } // 1M
default: break;
}
std::vector<std::pair<std::time_t, double>> out;
const auto& h = m.price_history;
for (size_t i = 0; i < h.size(); i++)
out.push_back({ now - (std::time_t)((h.size() - 1 - i) * 60), h[i] });
return out;
}
} // namespace data
} // namespace dragonx

View File

@@ -1,61 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Portfolio helpers — pure balance math for the Market tab's configurable portfolio.
// A portfolio entry is a user label tied to a group of wallet addresses (persisted in
// Settings as PortfolioEntry); these helpers sum/query that group against the live
// per-address balance list. No I/O, no ImGui — unit-testable.
#pragma once
#include "wallet_state.h" // AddressInfo
#include <string>
#include <unordered_set>
#include <vector>
namespace dragonx {
namespace data {
// Sum the DRGX balance of `entryAddresses` by looking them up in the wallet's per-address
// list. Addresses not present in the wallet contribute 0 (e.g. removed/rescanned). Pure.
inline double SumPortfolioBalance(const std::vector<std::string>& entryAddresses,
const std::vector<AddressInfo>& walletAddresses)
{
if (entryAddresses.empty()) return 0.0;
std::unordered_set<std::string> want(entryAddresses.begin(), entryAddresses.end());
double sum = 0.0;
for (const auto& a : walletAddresses)
if (want.count(a.address)) sum += a.balance;
return sum;
}
// Whether `address` is part of the entry's address group.
inline bool PortfolioEntryContains(const std::vector<std::string>& entryAddresses,
const std::string& address)
{
for (const auto& a : entryAddresses)
if (a == address) return true;
return false;
}
// Add `address` to the group if absent (returns true if it was added).
inline bool PortfolioEntryAdd(std::vector<std::string>& entryAddresses, const std::string& address)
{
if (address.empty() || PortfolioEntryContains(entryAddresses, address)) return false;
entryAddresses.push_back(address);
return true;
}
// Remove `address` from the group if present (returns true if it was removed).
inline bool PortfolioEntryRemove(std::vector<std::string>& entryAddresses, const std::string& address)
{
for (auto it = entryAddresses.begin(); it != entryAddresses.end(); ++it) {
if (*it == address) { entryAddresses.erase(it); return true; }
}
return false;
}
} // namespace data
} // namespace dragonx

View File

@@ -1,538 +0,0 @@
#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>
#ifndef _WIN32
#include <sys/stat.h>
#endif
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;
}
#ifndef _WIN32
// Owner-only (0600): although the payload is encrypted, don't leave the cache (or its
// WAL/SHM sidecars) world-readable. Best-effort; sidecars may not exist until first write.
::chmod(database_path_.c_str(), 0600);
::chmod((database_path_ + "-wal").c_str(), 0600);
::chmod((database_path_ + "-shm").c_str(), 0600);
#endif
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;
if (sqlite3_step(statement.handle) != SQLITE_DONE) return false;
pruneOtherWallets(walletHash); // bound DB growth — drop stale-hash snapshots/salts
return true;
}
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::pruneOtherWallets(const std::string& keepWalletHash)
{
if (!ensureOpen() || keepWalletHash.empty()) return;
// Table names are hardcoded literals (no injection surface). Prune both the snapshot
// blobs and the now-orphaned salt rows so a stale salt can't outlive its ciphertext.
for (const char* table : {"transaction_history_snapshots", "transaction_history_keys"}) {
const std::string sql = std::string("DELETE FROM ") + table + " WHERE wallet_hash <> ?";
Statement statement(db_, sql.c_str());
if (!statement.handle) continue;
if (!bindText(statement.handle, 1, keepWalletHash)) continue;
sqlite3_step(statement.handle);
}
}
void TransactionHistoryCache::close()
{
if (!db_) return;
sqlite3_close(db_);
db_ = nullptr;
}
} // namespace data
} // namespace dragonx

View File

@@ -1,93 +0,0 @@
#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);
// Delete snapshot + salt rows for every wallet hash except the live one, bounding the
// DB so generating a new address (which changes the hash) doesn't orphan history forever.
void pruneOtherWallets(const std::string& keepWalletHash);
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,44 +3,12 @@
// Released under the GPLv3
#include "wallet_state.h"
#include <algorithm>
#include <ctime>
#include <sstream>
#include <iomanip>
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
{
if (timestamp == 0) return "Unknown";

View File

@@ -8,10 +8,6 @@
#include <vector>
#include <cstdint>
#include <chrono>
#include <ctime>
#include <utility>
#include "exchange_info.h"
namespace dragonx {
@@ -22,7 +18,6 @@ struct AddressInfo {
std::string address;
double balance = 0.0;
std::string type; // "shielded" or "transparent"
bool has_spending_key = true; // false for view-only (imported via z_importviewingkey)
// For display
std::string label;
@@ -30,13 +25,8 @@ struct AddressInfo {
// Derived
bool isZAddr() const { return !address.empty() && address[0] == 'z'; }
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
*/
@@ -130,20 +120,7 @@ struct SyncInfo {
bool rescanning = false;
float rescan_progress = 0.0f; // 0.0 - 1.0
std::string rescan_status; // e.g. "Rescanning... 25%"
// Sapling note witness rebuild — a distinct, often-long phase after a rescan/zap. The daemon
// reports it in TWO sub-phases with different signals, so we track which is active:
// 1 = initial pass ("Setting Initial Sapling Witness for tx <hash>, <i> of <N>") — progress
// is distinct-txs-witnessed / N (the <i> bounces, so it can't be used directly).
// 2 = witness-cache walk ("Building Witnesses for block <h> <frac> complete, <n> remaining")
// — progress derived from how far "remaining" has fallen from its per-phase peak.
// The two are sequential with different scales, so progress is NOT carried across the boundary
// (that would pin the bar at the initial pass's ~100% through the whole cache walk).
bool building_witnesses = false;
int witness_phase = 0; // 0 none, 1 initial-witness pass, 2 witness-cache walk
float witness_progress = 0.0f; // 0.0 - 1.0, within the current sub-phase
int witness_remaining = 0; // blocks left in the cache walk (0 if unknown / phase 1)
bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; }
};
@@ -157,27 +134,10 @@ struct MarketInfo {
double change_24h = 0.0;
double market_cap = 0.0;
std::string last_updated;
std::chrono::steady_clock::time_point last_fetch_time{};
bool price_loading = false;
std::string price_error;
// Live in-session price history: ~1 sample/minute, capped at MAX_HISTORY samples.
// Backs the main chart and the "minute" portfolio-sparkline interval.
// Price history for chart
std::vector<double> price_history;
static constexpr int MAX_HISTORY = 24; // 24 samples (~24 minutes at the 60s refresh)
// Historical USD price series fetched from CoinGecko market_chart, so the portfolio-group
// sparklines can show real hour/day/week/month trends instead of resampling the ~24-minute
// in-session buffer. Timestamped (unix seconds), oldest->newest; empty until the first fetch.
// Refreshed on a slow cadence (~30 min) since historical data moves slowly.
std::vector<std::pair<std::time_t, double>> price_chart_intraday; // ~24h @ 5-minute granularity
std::vector<std::pair<std::time_t, double>> price_chart_daily; // ~1yr @ daily granularity
std::chrono::steady_clock::time_point chart_last_fetch_time{};
bool chart_loaded = false;
// Exchanges/pairs fetched live from CoinGecko (empty until fetched; the Market tab
// falls back to data::getExchangeRegistry() while empty).
std::vector<data::ExchangeInfo> exchanges;
static constexpr int MAX_HISTORY = 24; // 24 hours
};
/**
@@ -188,7 +148,6 @@ struct PoolMiningState {
bool xmrig_running = false;
std::string pool_url;
std::string algo;
std::string version; // running miner's version (from its API)
double hashrate_10s = 0;
double hashrate_60s = 0;
@@ -220,15 +179,6 @@ struct PoolMiningState {
struct WalletState {
// Connection
bool connected = false;
bool warming_up = false; // daemon reachable but in RPC warmup (error -28)
// True when the daemon is up/launching but not yet answering RPC (e.g. the connect probe
// times out because the node is loading the block index). Distinct from warming_up, which
// needs a JSON-RPC -28 reply; here getinfo never returns, so we infer the state from the
// daemon's launch state + its own console output. Drives the same loading overlay so the
// user sees WHAT the node is doing instead of a bare "Connection failed".
bool daemon_initializing = false;
std::string warmup_status; // user-friendly title, e.g. "Processing blocks..."
std::string warmup_description; // subtitle explaining the stage
int daemon_version = 0;
std::string daemon_subversion;
int protocol_version = 0;
@@ -299,10 +249,6 @@ struct WalletState {
void clear() {
connected = false;
warming_up = false;
daemon_initializing = false;
warmup_status.clear();
warmup_description.clear();
daemon_version = 0;
daemon_subversion.clear();
protocol_version = 0;

View File

@@ -11,7 +11,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_medium, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Medium.ttf");
INCBIN(ubuntu_mono, "@CMAKE_SOURCE_DIR@/res/fonts/UbuntuMono-R.ttf");
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf");
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");
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.otf");

View File

@@ -24,15 +24,9 @@ extern "C" {
extern const unsigned char g_ubuntu_medium_data[];
extern const unsigned int g_ubuntu_medium_size;
extern const unsigned char g_ubuntu_mono_data[];
extern const unsigned int g_ubuntu_mono_size;
extern const unsigned char g_material_icons_data[];
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 int g_noto_cjk_subset_size;
}

3405
src/embedded/lang_de.h Normal file

File diff suppressed because it is too large Load Diff

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