Compare commits
449 Commits
v1.1.1
...
49b5ef6b20
| Author | SHA1 | Date | |
|---|---|---|---|
| 49b5ef6b20 | |||
| 6aaab9e01f | |||
| b7da6335d1 | |||
| b765d62e00 | |||
| 3dc4d2d860 | |||
| fe40d38f0d | |||
| 1d3a919ac6 | |||
| d549127e41 | |||
| 54a14485a5 | |||
| 8eefab99ec | |||
| 4fdf281230 | |||
| c4f69b5a85 | |||
| 171b705429 | |||
| 175950a2ec | |||
| 36efe99a0f | |||
| fadbd6a879 | |||
| 112c581247 | |||
| 57147fd06b | |||
| aa60188665 | |||
| cff958c21e | |||
| c50125523c | |||
| 063f1fa4be | |||
| ad7a150ad6 | |||
| 433c7ae46f | |||
| a5efbaf83d | |||
| 46fb0029b8 | |||
| aa587d1d84 | |||
| 3a9edf2200 | |||
| c25a4e4150 | |||
| 54945c94bf | |||
| 96a1c54556 | |||
| bedb482855 | |||
| c352364bdc | |||
| 6b7438b529 | |||
| c6624913de | |||
| e6bea8eb04 | |||
| 5e5f167fb0 | |||
| 1fd794ccc1 | |||
| ed4d61c364 | |||
| 3df516be70 | |||
| f8fbc32463 | |||
| 06c8b99bce | |||
| 5eff3adc3c | |||
| e010822a74 | |||
| b2882095bb | |||
| a80feb708e | |||
| 010465f835 | |||
| eb08b81706 | |||
| 8cbd0cab9d | |||
| a9f0d544cc | |||
| 13ff20d791 | |||
| d8cc1e9ee6 | |||
| e7a3f90102 | |||
| b70f1ae643 | |||
| 76a2ef51f3 | |||
| 8846a96e3d | |||
| 5db7941fbc | |||
| a09adb14fb | |||
| d23ee9b75f | |||
| e8a8ce68b2 | |||
| 72bf59149d | |||
| 82e61da62f | |||
| 48c79567d7 | |||
| 6c19fac6f5 | |||
| 0d9908019d | |||
| a86cb8f0f3 | |||
| 5228da707f | |||
| e66f2b6c8c | |||
| 7ee20bb497 | |||
| 718cb82c13 | |||
| 403e145b30 | |||
| 036199a011 | |||
| 715cfd622e | |||
| b75801f5c4 | |||
| 6ddca6aed0 | |||
| ca1ac22b17 | |||
| 6bfb56aeb9 | |||
| 6c08a08127 | |||
| f5bde67f64 | |||
| 516e5ec688 | |||
| 008cc82ee5 | |||
| 2e6188a36d | |||
| e191680782 | |||
| 4db609fb52 | |||
| 980a100edd | |||
| 46eec37013 | |||
| 1738468f8c | |||
| ba03de938e | |||
| d043538e2f | |||
| 2502487e44 | |||
| cec330ee47 | |||
| 0ff56bf5a4 | |||
| ddd53dc006 | |||
| ee59a9e2cd | |||
| 62998179ec | |||
| 061db16004 | |||
| ec48538881 | |||
| 658c0f355b | |||
| 41f5548593 | |||
| 1dbbd44759 | |||
| b0cc6bcef4 | |||
| dd9bc0bd2b | |||
| b2a8c4487e | |||
| 65bb98cd09 | |||
| 129a8e6449 | |||
| df14533ad3 | |||
| 24bb32eb61 | |||
| d3dcb5a015 | |||
| 7249d11899 | |||
| ee6903ae56 | |||
| 1c5bc0d4d6 | |||
| dfca9bbd92 | |||
| 840ead3645 | |||
| 1c8e97c0a4 | |||
| f1e66c08d1 | |||
| c83348ab63 | |||
| da56bb73a0 | |||
| ad59d62b82 | |||
| 07f6d2efc9 | |||
| e84cea27d9 | |||
| a457f0262e | |||
| 4c466d78d1 | |||
| 0d4eaa02e7 | |||
| dddf1b6ec7 | |||
| a523611779 | |||
| 7c90d72ded | |||
| bb2337b552 | |||
| 19e31388d9 | |||
| 30139a5698 | |||
| 94b57e91e3 | |||
| 10289b33b5 | |||
| 42df658275 | |||
| 9d2c4a8ad3 | |||
| 65c06d0e6b | |||
| c7f89e5cd3 | |||
| 34ded6b441 | |||
| 0ea423dcea | |||
| 61738ef24a | |||
| ccbb0783b5 | |||
| c6b32e86c0 | |||
| 5caffc84b0 | |||
| ebf01bd858 | |||
| d91be3b34e | |||
| 4b46d4a20c | |||
| 69cd8ba048 | |||
| 4543cdc495 | |||
| 71fb81670f | |||
| a092097141 | |||
| 50873d1112 | |||
| 7e8628f94c | |||
| 0ae52ee095 | |||
| 529a74f6a7 | |||
| 88f1415bcc | |||
| b8cfc1c79c | |||
| a0fd9aff82 | |||
| 14b08d1358 | |||
| d689b02de9 | |||
| 84498b1312 | |||
| 8c2b0521a6 | |||
| 4411cb45e2 | |||
| 7db6564a0e | |||
| faf8e4e430 | |||
| 1affea0273 | |||
| 32d8d5663a | |||
| 3568b12dbf | |||
| e74989ec2c | |||
| fe18628216 | |||
| 4dc6b2ef8f | |||
| 5afb339ba7 | |||
| 0e8816e220 | |||
| 2035f71c65 | |||
| d007f3de39 | |||
| 1fdb6656aa | |||
| 3305be6318 | |||
| 19482214f1 | |||
| 4aed72c73b | |||
| 3e6987d72f | |||
| 42c511b13a | |||
| ef9141c248 | |||
| 8c88afe16e | |||
| d48c6db21c | |||
| ced1773e70 | |||
| d76c109063 | |||
| 5aa3cc4d51 | |||
| 4e15a2b1ba | |||
| 021ba88522 | |||
| c1d5f79502 | |||
| db212a81c6 | |||
| 67f6156030 | |||
| 76ed09fad0 | |||
| 77219c5ce7 | |||
| b26aff392b | |||
| b0ded528b1 | |||
| 301b3df77e | |||
| ae4e6486c6 | |||
| d838243caa | |||
| 75f33e41eb | |||
| 1263058ba7 | |||
| 9bcdf51895 | |||
| 67feaea767 | |||
| 3e1cdd15f3 | |||
| af2db4366c | |||
| cb74fbbff0 | |||
| 121a79e768 | |||
| 262891229a | |||
| dcd09dc542 | |||
| 4635a56e89 | |||
| 9c533fa485 | |||
| de11850c74 | |||
| 280a71e973 | |||
| 7891c689fb | |||
| f8150434d4 | |||
| 530bf24abf | |||
| e3b53258b7 | |||
| f9e4db4abf | |||
| bdccddfeee | |||
| d68bc27aac | |||
| 51667b5a27 | |||
| 96e05b4ba6 | |||
| 36386b2b84 | |||
| 2fc577fb18 | |||
| 5180f74a68 | |||
| fc5ce019e2 | |||
| 7826be4b13 | |||
| 5d6e78c719 | |||
| a1e4acfe86 | |||
| 419f8bb9cb | |||
| c5ef9e42ed | |||
| f0243307f1 | |||
| 95ddfa05ee | |||
| ecc0043559 | |||
| 3a9ff5f021 | |||
| 1faaa54a07 | |||
| 61f8c54742 | |||
| 48fc9ba8bb | |||
| 2552234610 | |||
| ff20a8a44e | |||
| 245d9bf976 | |||
| 1c248d727a | |||
| 57291ef28c | |||
| 18c1de87db | |||
| 0cdbbd024e | |||
| 779c9682d4 | |||
| 33735b1d52 | |||
| 19e83eb12a | |||
| f12153cdd0 | |||
| 8abc0ec113 | |||
| 5a677f6202 | |||
| efec3a50a1 | |||
| 4ed69d4e9f | |||
| e0d6d6bc4d | |||
| e9121fe49e | |||
| b5001ad4db | |||
| 1266b6e68e | |||
| 57f52b7a6c | |||
| a6964621a8 | |||
| 14f5fdc559 | |||
| 1a00ec3359 | |||
| 5c490c8d53 | |||
| d0586e5e75 | |||
| 24e8fb4942 | |||
| 1393d9ae1a | |||
| 3ce62326f9 | |||
| b20e7efb16 | |||
| 4473e7e00a | |||
| 2e8e214689 | |||
| 69a6fb3e64 | |||
| ed6120eef9 | |||
| a5bd2dadd7 | |||
| 25ee1496b4 | |||
| e2bc3623b6 | |||
| b32fe07cb1 | |||
| a0532275dd | |||
| 7df00b0909 | |||
| 090e288c44 | |||
| 6544c10ac1 | |||
| b2e104358d | |||
| de70e68472 | |||
| 0fe12d65df | |||
| 37c8287a12 | |||
| 6ff80354df | |||
| f58d009703 | |||
| 0e2c786ebf | |||
| d54c7f9e11 | |||
| b3251e9244 | |||
| c40f4d5815 | |||
| 5547ab1cac | |||
| f0867084f3 | |||
| 5167b52cbd | |||
| e7d11f620a | |||
| 168cae9306 | |||
| 317d9028a3 | |||
| 09ab8d52c5 | |||
| 101c835c46 | |||
| c71c3c3378 | |||
| 2e115aef39 | |||
| 574307f6ac | |||
| 88851f5eea | |||
| 5796664b51 | |||
| b71f8ae0a8 | |||
| 555f541c84 | |||
| 2b70ee5cd8 | |||
| d8c055c125 | |||
| 2470675746 | |||
| 3a597482da | |||
| b9b2d469d4 | |||
| 25fef8ff4d | |||
| bc788d008e | |||
| 9ee8f9a43b | |||
| bf91c4eb6c | |||
| 4d78ca0d7d | |||
| 00ee61fe64 | |||
| 3a4998f57c | |||
| d27017daeb | |||
| 2182c060e6 | |||
| 4ee830c5dd | |||
| b6567ee196 | |||
| 1a8d6fd30f | |||
| 9389859ee9 | |||
| e63aba6959 | |||
| fa240e7b99 | |||
| e21f7bf8aa | |||
| 47f228fa47 | |||
| 9f82bba260 | |||
| 323cb341f1 | |||
| ee6cac41c4 | |||
| 094771af81 | |||
| e40962cdf2 | |||
| 63b3a04716 | |||
| cf77c6cbe0 | |||
| 560f2bcf91 | |||
| 255d9399fa | |||
| 1fb6dc44d9 | |||
| 2ba8a799ff | |||
| 41b380449e | |||
| 4a65dce947 | |||
| c8183241c3 | |||
| 0bf80d2757 | |||
| 6f9123f651 | |||
| 320c659689 | |||
| 6ff1fda870 | |||
| 8c51b092f8 | |||
| 788d71a549 | |||
| 3d4b013b0c | |||
| dc07491abb | |||
| 7e568e4bf1 | |||
| 85a1080b52 | |||
| dbeae3ac98 | |||
| 9ff5508989 | |||
| 79e5adcbd3 | |||
| 6531d0c4d2 | |||
| 142a6826af | |||
| 070a516f4e | |||
| 3cec333d84 | |||
| fc438ab962 | |||
| 8c2c1c2aaf | |||
| a605e35409 | |||
| 6ed80d2d79 | |||
| e978db85ca | |||
| e00772db6e | |||
| 8f22db5eea | |||
| 5c883d4b91 | |||
| 274f7ea1af | |||
| 3799330bb0 | |||
| 53a10e149d | |||
| 1bc7f5c8cd | |||
| 20b22410e9 | |||
| 7195c25376 | |||
| a6921bca60 | |||
| 8ba4233b9b | |||
| 732d892d4d | |||
| afd612be7e | |||
| 4d769c8719 | |||
| 0e1b19d0f2 | |||
| b24212fb8f | |||
| 64fe8fc6c9 | |||
| b9881278af | |||
| 85b53baeaf | |||
| eece57c025 | |||
| 8765fdf362 | |||
| 98e0cce8ec | |||
| 5c87bc6e87 | |||
| 946958b591 | |||
| f5561c0dac | |||
| b3c2282b53 | |||
| ca14aaddc7 | |||
| a5da5562cf | |||
| c676ec8287 | |||
| f474b0d633 | |||
| af252575cf | |||
| 74bd22958a | |||
| cd60bded9f | |||
| 59b8c4da81 | |||
| 950d7ace50 | |||
| d52d3d1b7f | |||
| 9569b0ba43 | |||
| 50b0419dfe | |||
| 4f7a4fb38e | |||
| f511c0d509 | |||
| 235504657d | |||
| 76c2ac5db8 | |||
| 89bd21018a | |||
| 9d7054b245 | |||
| 70608fcb7a | |||
| 0c819afdd4 | |||
| db4778e6a7 | |||
| eb6114ee19 | |||
| 5d317f6be3 | |||
| a677c09984 | |||
| 6611d57147 | |||
| 6a4e98b7ed | |||
| 4b9d6f7db5 | |||
| 043cdc7128 | |||
| 2daea67a1e | |||
| e6b91ca661 | |||
| c6e28fc4da | |||
| 268eba6321 | |||
| 3119440cd9 | |||
| 59c55e33f8 | |||
| a8b5d2f6a3 | |||
| 012341b1a4 | |||
| 5586f334a4 | |||
| 863d015628 | |||
| a78a13edf3 | |||
| e95ad50e41 | |||
| 975743f754 | |||
| 948ef419ac | |||
| 9edab31728 | |||
| ee8a08e569 | |||
| ddb810e2f3 | |||
| aa26ab5fbd | |||
| 40cec14ebf | |||
| 88d30c1612 | |||
| dbe6546f9f | |||
| 6e2db50675 | |||
| d2dccbac05 | |||
| 1860e9b277 | |||
| 648a6c29e0 | |||
| c013038ef7 | |||
| 20cbad687d | |||
| ddca8b2e43 | |||
| 50e9e7d75e | |||
| d755f6816b | |||
| 84d2b9c39d | |||
| 27e9a8df26 | |||
| 8d51f374cd | |||
| 7ab8f5d82c | |||
| e4b1b644b3 | |||
| 8ef8abeb37 |
16
.gitignore
vendored
16
.gitignore
vendored
@@ -37,6 +37,20 @@ asmap.dat
|
||||
/memory
|
||||
/todo.md
|
||||
/.github/
|
||||
/ObsidianDragon-agent/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.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
|
||||
|
||||
111
CLAUDE.md
Normal file
111
CLAUDE.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 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`).
|
||||
|
||||
## Seed phrase & migrate-to-seed (full node)
|
||||
|
||||
Full-node wallets are **BIP39-mnemonic-backed** and can **migrate a legacy (non-mnemonic) wallet into a seed wallet**. All of this **requires the `hd-transparent-keys`/`dev` daemon** (`z_exportmnemonic` + `-usemnemonic`); the older bundled binary lacks those RPCs, so these features degrade gracefully (chat falls back to a `z_exportkey` identity; the backup screen shows a "legacy wallet" note). The daemon source is vendored at `external/dragonx/` (build with its own `./build.sh`); deploy the built `dragonxd`/`dragonx-cli`/`dragonx-tx` into the wallet's daemon dir (`build/*/bin`) or via the daemon updater.
|
||||
|
||||
- **New wallets get a phrase.** `EmbeddedDaemon::getChainParams()` always passes `-usemnemonic=1`. The daemon reads it **only inside `GenerateNewSeed()` when a wallet has no seed yet**, so it is inert on existing wallets (safe to pass unconditionally) and makes every fresh wallet mnemonic-backed.
|
||||
- **Back up seed phrase.** Settings → Backup & Data → "Seed phrase" opens `renderSeedBackupDialog` (`App::exportSeedPhrase` → `z_exportmnemonic`, wiped via `sodium_memzero`). A one-time nudge (`maybeRemindSeedBackup`, settings flag `seed_backup_reminded`) reminds mnemonic-wallet users to back up.
|
||||
- **Migrate-to-seed** (`showSeedMigrationDialog` / `renderSeedMigrationDialog`, state machine `SeedMigrationStep`). **Phase 1 — create:** `daemon::SeedWalletCreator` runs a **second, isolated `dragonxd`** on its own port + throwaway datadir (`<config>/seed-migrate/DRAGONX`, basename MUST be the acname; `-usemnemonic=1 -connect=0`; RPC is plaintext http, not TLS), mints a mnemonic wallet, exports its seed + a sweep-target z-address, then stops. Uses the one-shot `EmbeddedDaemon::setNextStartOverride` + `setSkipPortCheck`. **Phase 2 — sweep + adopt:** `z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"]` sweeps all funds to the new address; a **Confirming gate** (`pollSweepStatus`) only allows adopt once the sweep tx is **mined (≥1 conf) AND the legacy balance is ~0** (offering a remainder re-sweep); adopt (`beginAdoptSeedWallet`, background) stops the daemon, moves `wallet.dat` aside to a **timestamped `.bak` (never deleted, restored on failure)**, installs the new wallet, and restarts with `-rescan`. Fund-moving code — **two rounds of adversarial review + a live mainnet run** gate it; the pending stage persists (`seed_migration_*` settings) so a restart resumes at sweep/confirm.
|
||||
|
||||
## Versioning
|
||||
|
||||
The version has a **single source of truth**: `project(... VERSION 1.2.0 ...)` plus `DRAGONX_VERSION_SUFFIX` in `CMakeLists.txt`. CMake generates `build/.../generated/dragonx_generated_version.h` from `src/config/version.h.in`. Do not hand-edit generated version output or hardcode version strings — bump the `project()` version in `CMakeLists.txt`.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **C++17.** Match the surrounding code's style per file.
|
||||
- **Icons:** use the Material Design icon font defines (`ICON_MD_*`); never raw Unicode glyphs.
|
||||
- **UI layout values** belong in `res/themes/ui.toml`, read via `schema::UI()` — do not hardcode pixel sizes/offsets in code.
|
||||
- **DPI / display scaling:** `schema::UI()` returns **logical (raw) px**; `Layout::dpiScale()` (= OS DPI × the in-app font-scale) is the single scale factor. The `Layout::k*()` helpers and `BeginOverlayDialog` already fold it in, and ImGui auto-layout scales via the DPI-rebuilt font atlas + `ScaleAllSizes` — so tabs/standard widgets scale for free. But **hand-drawn absolute geometry** (`dl->AddText` at manual `cy += 24.0f` offsets, `SetNextWindowSize`, explicit `ImVec2(width,0)` button sizes, `SameLine(x)` column strides) is immune to all of that and must be multiplied by `ui::Layout::dpiScale()` yourself, or it renders native-size (tiny) on a HiDPI display. Font metrics (`font->LegacySize`, `CalcTextSize`) are already scaled — don't double-scale those. Verify at scale with a full sweep run at `font_scale: 1.5` (same `dpiScale()==1.5` code path as OS 150%).
|
||||
- **i18n:** user-facing strings are translated via `src/util/i18n`; the English source of truth is the `strings_[...]` map in `src/util/i18n.cpp`, and the per-language translations live as the **source of truth** in `res/lang/` (`de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, `zh`). **Edit those JSONs directly and additively** — write with `json.dump(..., indent=4, sort_keys=True, ensure_ascii=False)`; never bulk-regenerate/overwrite a whole file (that path silently dropped ~285 keys/language before). `scripts/add_missing_translations.py` back-fills only the keys missing from a JSON (non-destructive), and `scripts/build_cjk_subset.py` rebuilds the CJK subset font (`res/fonts/NotoSansCJK-Subset.ttf`) after new CJK glyphs are added.
|
||||
- **Commits:** the history uses Conventional Commits (`feat(scope): …`, `fix(scope): …`). PRs target `master`.
|
||||
549
CMakeLists.txt
549
CMakeLists.txt
@@ -3,12 +3,32 @@
|
||||
# Released under the GPLv3
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(ObsidianDragon
|
||||
VERSION 1.1.1
|
||||
|
||||
# macOS: set deployment target and universal architectures BEFORE project()
|
||||
# so they propagate to all targets, including FetchContent dependencies (SDL3, etc.)
|
||||
if(APPLE)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum macOS version" FORCE)
|
||||
# Build universal binary (Apple Silicon + Intel) unless the user explicitly set architectures
|
||||
if(NOT DEFINED CMAKE_OSX_ARCHITECTURES OR CMAKE_OSX_ARCHITECTURES STREQUAL "")
|
||||
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures" FORCE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
project(ObsidianDragon
|
||||
VERSION 2.0.0
|
||||
LANGUAGES C CXX
|
||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||
)
|
||||
|
||||
# Pre-release suffix (e.g. "-rc1", "-beta2"). Leave empty for stable releases.
|
||||
set(DRAGONX_VERSION_SUFFIX "")
|
||||
|
||||
# ObsidianDragonLite is versioned INDEPENDENTLY of the full-node app above. The active variant's
|
||||
# version flows to the generated header, the Windows .rc/manifest, and build.sh's release names via
|
||||
# DRAGONX_APP_VERSION* (resolved in the lite/full block below).
|
||||
set(DRAGONX_LITE_VERSION "1.0.0")
|
||||
set(DRAGONX_LITE_VERSION_SUFFIX "")
|
||||
|
||||
# C++17 standard
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -22,6 +42,132 @@ 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)
|
||||
@@ -94,6 +240,32 @@ 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
|
||||
@@ -232,23 +404,72 @@ set(APP_SOURCES
|
||||
src/app.cpp
|
||||
src/app_network.cpp
|
||||
src/app_security.cpp
|
||||
src/app_sweep.cpp
|
||||
src/app_wizard.cpp
|
||||
src/services/network_refresh_service.cpp
|
||||
src/services/refresh_scheduler.cpp
|
||||
src/services/wallet_security_controller.cpp
|
||||
src/services/wallet_security_workflow.cpp
|
||||
src/services/wallet_security_workflow_executor.cpp
|
||||
src/chat/chat_protocol.cpp
|
||||
src/chat/chat_crypto.cpp
|
||||
src/chat/chat_identity.cpp
|
||||
src/chat/chat_store.cpp
|
||||
src/chat/chat_service.cpp
|
||||
src/chat/chat_database.cpp
|
||||
src/chat/chat_outgoing.cpp
|
||||
src/wallet/lite_owned_string.cpp
|
||||
src/wallet/lite_rollout_policy.cpp
|
||||
src/wallet/lite_client_bridge.cpp
|
||||
src/wallet/lite_connection_service.cpp
|
||||
src/wallet/lite_diagnostics.cpp
|
||||
src/wallet/lite_wallet_controller.cpp
|
||||
src/wallet/lite_result_parsers.cpp
|
||||
src/wallet/lite_sync_service.cpp
|
||||
src/wallet/lite_wallet_gateway.cpp
|
||||
src/wallet/lite_wallet_state_mapper.cpp
|
||||
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
|
||||
src/wallet/lite_wallet_server_selection_adapter.cpp
|
||||
src/wallet/lite_wallet_lifecycle_service.cpp
|
||||
src/data/wallet_state.cpp
|
||||
src/data/transaction_history_cache.cpp
|
||||
src/ui/theme.cpp
|
||||
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
|
||||
@@ -256,32 +477,47 @@ 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/address_book_dialog.cpp
|
||||
src/ui/windows/contacts_tab.cpp
|
||||
src/ui/windows/chat_tab.cpp
|
||||
src/ui/windows/shield_dialog.cpp
|
||||
src/ui/windows/request_payment_dialog.cpp
|
||||
src/ui/windows/block_info_dialog.cpp
|
||||
src/ui/windows/import_key_dialog.cpp
|
||||
src/ui/windows/export_all_keys_dialog.cpp
|
||||
src/ui/windows/export_transactions_dialog.cpp
|
||||
src/ui/windows/backup_wallet_dialog.cpp
|
||||
src/ui/widgets/qr_code.cpp
|
||||
src/rpc/rpc_client.cpp
|
||||
src/rpc/rpc_worker.cpp
|
||||
src/rpc/connection.cpp
|
||||
src/config/settings.cpp
|
||||
src/data/address_book.cpp
|
||||
src/data/wallet_index.cpp
|
||||
src/data/exchange_info.cpp
|
||||
src/util/logger.cpp
|
||||
src/util/async_task_manager.cpp
|
||||
src/util/amount_format.cpp
|
||||
src/util/address_validation.cpp
|
||||
src/util/base64.cpp
|
||||
src/util/single_instance.cpp
|
||||
src/util/i18n.cpp
|
||||
src/util/text_format.cpp
|
||||
src/util/platform.cpp
|
||||
src/util/payment_uri.cpp
|
||||
src/util/texture_loader.cpp
|
||||
src/util/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
|
||||
@@ -312,34 +548,77 @@ 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/address_book_dialog.h
|
||||
src/ui/windows/contacts_tab.h
|
||||
src/ui/windows/chat_tab.h
|
||||
src/ui/windows/contact_picker.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
|
||||
@@ -349,6 +628,8 @@ 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
|
||||
@@ -356,6 +637,9 @@ 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
|
||||
@@ -394,10 +678,12 @@ if(WIN32)
|
||||
set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc)
|
||||
endif()
|
||||
|
||||
# Generate version.h from the single project(VERSION ...) declaration
|
||||
# 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.
|
||||
configure_file(
|
||||
${CMAKE_SOURCE_DIR}/src/config/version.h.in
|
||||
${CMAKE_SOURCE_DIR}/src/config/version.h
|
||||
${CMAKE_BINARY_DIR}/generated/dragonx_generated_version.h
|
||||
@ONLY
|
||||
)
|
||||
|
||||
@@ -408,6 +694,22 @@ 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
|
||||
@@ -418,6 +720,8 @@ 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
|
||||
@@ -437,10 +741,63 @@ 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)
|
||||
@@ -469,6 +826,10 @@ 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)
|
||||
@@ -476,6 +837,26 @@ 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
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -499,6 +880,12 @@ 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(
|
||||
@@ -512,16 +899,18 @@ 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
|
||||
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})
|
||||
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()
|
||||
endforeach()
|
||||
add_custom_target(copy_langs ALL DEPENDS ${LANG_OUTPUTS})
|
||||
add_dependencies(ObsidianDragon copy_langs)
|
||||
@@ -568,6 +957,7 @@ 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(
|
||||
@@ -617,20 +1007,135 @@ install(TARGETS ObsidianDragon
|
||||
)
|
||||
|
||||
install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res
|
||||
DESTINATION share/ObsidianDragon
|
||||
DESTINATION share/${DRAGONX_BINARY_NAME}
|
||||
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/data/address_book.cpp
|
||||
src/data/wallet_index.cpp
|
||||
src/daemon/lifecycle_adapters.cpp
|
||||
src/rpc/connection.cpp
|
||||
src/config/settings.cpp
|
||||
src/resources/embedded_resources.cpp
|
||||
src/util/secure_vault.cpp
|
||||
src/util/platform.cpp
|
||||
src/util/logger.cpp
|
||||
src/util/lite_server_probe.cpp
|
||||
src/util/pool_registry_core.cpp
|
||||
src/util/http_download.cpp
|
||||
src/util/xmrig_updater.cpp
|
||||
src/util/xmrig_updater_core.cpp
|
||||
src/util/daemon_updater.cpp
|
||||
src/util/daemon_updater_core.cpp
|
||||
${MINIZ_SOURCES}
|
||||
)
|
||||
|
||||
target_include_directories(ObsidianDragonTests PRIVATE
|
||||
${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: ${PROJECT_VERSION}")
|
||||
message(STATUS " Version: ${DRAGONX_APP_VERSION}${DRAGONX_APP_VERSION_SUFFIX} (${DRAGONX_APP_NAME})")
|
||||
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 "")
|
||||
|
||||
49
README.md
49
README.md
@@ -1,6 +1,8 @@
|
||||
# DragonX Wallet - ImGui Edition
|
||||
# ObsidianDragon - DragonX Wallet
|
||||
|
||||
A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
|
||||
A lightweight, portable full-node cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
|
||||
|
||||
Current pre-release: **1.2.0-rc1**.
|
||||
|
||||

|
||||

|
||||
@@ -9,10 +11,13 @@ A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dea
|
||||
|
||||
- **Full Node Support**: Connects to dragonxd for complete blockchain verification
|
||||
- **Shielded Transactions**: Full z-address support with encrypted memos
|
||||
- **Integrated Mining**: CPU mining controls with hashrate monitoring
|
||||
- **Address Management**: Labels, icons, favorites, hidden addresses, and address-to-address transfers
|
||||
- **Integrated Mining**: Solo CPU mining plus pool mining through xmrig, with idle-mining controls
|
||||
- **Explorer Tools**: Block/transaction lookup and bootstrap snapshot download
|
||||
- **Market Data**: Real-time price charts from CoinGecko
|
||||
- **QR Codes**: Generate and display QR codes for receiving addresses
|
||||
- **Multi-language**: i18n support (English, Spanish, more coming)
|
||||
- **Multi-language**: i18n support for English, German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese
|
||||
- **CJK Fonts**: Bundled CJK subset font for translated interfaces
|
||||
- **Lightweight**: ~5-10MB binary vs ~50MB+ for Qt version
|
||||
- **Fast Builds**: Compiles in seconds, not minutes
|
||||
|
||||
@@ -116,7 +121,8 @@ 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:
|
||||
@@ -131,9 +137,10 @@ The wallet checks its **own directory first** when looking for DragonX node bina
|
||||
3. System-wide locations (`/usr/local/bin`, `~/dragonx/src`, etc.)
|
||||
|
||||
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
|
||||
@@ -148,44 +155,46 @@ ObsidianDragon/
|
||||
├── src/
|
||||
│ ├── main.cpp # Entry point, SDL/ImGui setup
|
||||
│ ├── app.cpp/h # Main application class
|
||||
│ ├── wallet_state.h # Wallet data structures
|
||||
│ ├── version.h # Version definitions
|
||||
│ ├── data/ # WalletState, address book, exchange info
|
||||
│ ├── config/ # Settings persistence and committed/generated version.h
|
||||
│ ├── ui/
|
||||
│ │ ├── theme.cpp/h # DragonX theme
|
||||
│ │ └── windows/ # UI tabs and dialogs
|
||||
│ │ ├── schema/ # TOML UI schema and skin manager
|
||||
│ │ ├── material/ # Material components, typography, layout
|
||||
│ │ ├── windows/ # Tabs and dialogs
|
||||
│ │ └── pages/ # Multi-page screens such as Settings
|
||||
│ ├── rpc/
|
||||
│ │ ├── rpc_client.cpp # JSON-RPC client
|
||||
│ │ └── connection.cpp # Daemon connection
|
||||
│ ├── config/
|
||||
│ │ └── settings.cpp # Settings persistence
|
||||
│ ├── resources/ # Embedded resource extraction
|
||||
│ ├── platform/ # Windows DX11/backdrop helpers
|
||||
│ ├── util/
|
||||
│ │ ├── i18n.cpp # Internationalization
|
||||
│ │ └── ...
|
||||
│ └── daemon/
|
||||
│ └── embedded_daemon.cpp
|
||||
├── res/
|
||||
│ ├── fonts/ # Ubuntu font
|
||||
│ ├── fonts/ # Ubuntu, icon, and CJK fonts
|
||||
│ └── lang/ # Translation files
|
||||
├── libs/
|
||||
│ └── qrcode/ # QR code generation
|
||||
├── CMakeLists.txt
|
||||
├── build-release.sh # Build script
|
||||
└── create-appimage.sh # AppImage packaging
|
||||
├── build.sh # Release/cross-platform build script
|
||||
└── scripts/create-appimage.sh # AppImage packaging
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
Fetched automatically by CMake (no manual install needed):
|
||||
Fetched or discovered by CMake:
|
||||
|
||||
- **[SDL3](https://github.com/libsdl-org/SDL)** — Cross-platform windowing/input
|
||||
- **[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/)** — HTTPS RPC transport (system on Linux, fetched on Windows)
|
||||
- **[libcurl](https://curl.se/libcurl/)** — HTTP/HTTPS transport for daemon RPC and network calls (system on Linux/macOS, fetched on Windows)
|
||||
|
||||
Bundled in `libs/`:
|
||||
|
||||
- **[Dear ImGui](https://github.com/ocornut/imgui)** — Immediate mode GUI
|
||||
- **[libsodium](https://libsodium.org)** — Cryptographic operations (fetched by `scripts/fetch-libsodium.sh`)
|
||||
- **[libsodium](https://libsodium.org)** — Cryptographic operations (system on Linux or fetched by `scripts/fetch-libsodium.sh`)
|
||||
- **[QR-Code-generator](https://github.com/nayuki/QR-Code-generator)** — QR code rendering
|
||||
- **[miniz](https://github.com/richgel999/miniz)** — ZIP compression
|
||||
- **[GLAD](https://glad.dav1d.de/)** — OpenGL loader (Linux/macOS)
|
||||
@@ -202,9 +211,11 @@ 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
|
||||
|
||||
|
||||
@@ -361,7 +361,22 @@ https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
---
|
||||
|
||||
## 13. IconFontCppHeaders
|
||||
## 13. Material Design Icons Pickaxe Subset Font
|
||||
|
||||
- **Location:** `res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf`
|
||||
- **Source:** https://github.com/Templarian/MaterialDesign-Webfont
|
||||
- **Derived from:** Pictogrammers Material Design Icons webfont (`materialdesignicons-webfont.ttf`)
|
||||
- **Copyright:** Pictogrammers contributors
|
||||
- **License:** Apache License 2.0
|
||||
|
||||
This bundled font is a local one-glyph subset containing only the MDI pickaxe
|
||||
icon, remapped onto a BMP private-use codepoint for Dear ImGui compatibility.
|
||||
The full text of the Apache License 2.0 is available at:
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
---
|
||||
|
||||
## 14. IconFontCppHeaders
|
||||
|
||||
- **Location:** `src/embedded/IconsMaterialDesign.h`
|
||||
- **Source:** https://github.com/juliettef/IconFontCppHeaders
|
||||
@@ -390,7 +405,7 @@ freely, subject to the following restrictions:
|
||||
|
||||
---
|
||||
|
||||
## 14. Ubuntu Font Family
|
||||
## 15. Ubuntu Font Family
|
||||
|
||||
- **Location:** `res/fonts/Ubuntu-Light.ttf`, `Ubuntu-Medium.ttf`, `Ubuntu-R.ttf`
|
||||
- **Source:** https://design.ubuntu.com/font
|
||||
|
||||
474
build.sh
474
build.sh
@@ -20,7 +20,9 @@
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
VERSION="1.1.1"
|
||||
# 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=""
|
||||
|
||||
# ── Colours ──────────────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'
|
||||
@@ -41,6 +43,8 @@ DO_DEV=false
|
||||
DO_LINUX=false
|
||||
DO_WIN=false
|
||||
DO_MAC=false
|
||||
DO_LITE=false
|
||||
DO_LITE_BACKEND=false
|
||||
CLEAN=false
|
||||
BUILD_TYPE="Release"
|
||||
|
||||
@@ -54,6 +58,10 @@ 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}/
|
||||
|
||||
@@ -74,6 +82,7 @@ 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
|
||||
@@ -85,6 +94,8 @@ 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 ;;
|
||||
@@ -98,6 +109,92 @@ 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=(
|
||||
@@ -197,7 +294,14 @@ bundle_linux_daemon() {
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
build_dev() {
|
||||
header "Dev Build ($(uname -s) / $BUILD_TYPE)"
|
||||
local bd="$SCRIPT_DIR/build/linux"
|
||||
|
||||
# Use platform-appropriate build directory
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
local bd="$SCRIPT_DIR/build/mac"
|
||||
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||
else
|
||||
local bd="$SCRIPT_DIR/build/linux"
|
||||
fi
|
||||
|
||||
if $CLEAN; then
|
||||
info "Cleaning $bd ..."; rm -rf "$bd"
|
||||
@@ -208,13 +312,14 @@ build_dev() {
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=ON
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=ON \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
|
||||
info "Building with $JOBS jobs ..."
|
||||
cmake --build . -j "$JOBS"
|
||||
|
||||
[[ -f "bin/ObsidianDragon" ]] || { err "Build failed"; exit 1; }
|
||||
info "Dev binary: $bd/bin/ObsidianDragon ($(du -h bin/ObsidianDragon | cut -f1))"
|
||||
[[ -f "bin/${APP_BASENAME}" ]] || { err "Build failed"; exit 1; }
|
||||
info "Dev binary: $bd/bin/${APP_BASENAME} ($(du -h "bin/${APP_BASENAME}" | cut -f1))"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -235,42 +340,51 @@ build_release_linux() {
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=ON
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=ON \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
|
||||
info "Building with $JOBS jobs ..."
|
||||
cmake --build . -j "$JOBS"
|
||||
|
||||
[[ -f "bin/ObsidianDragon" ]] || { err "Linux build failed"; exit 1; }
|
||||
[[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; }
|
||||
|
||||
info "Stripping ..."
|
||||
strip bin/ObsidianDragon
|
||||
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
|
||||
strip "bin/${APP_BASENAME}"
|
||||
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||
|
||||
# ── Bundle daemon ────────────────────────────────────────────────────────
|
||||
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
|
||||
if should_bundle_full_node_assets; then
|
||||
# ── 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"
|
||||
# ── Bundle Sapling params ────────────────────────────────────────────
|
||||
SAPLING_SPEND="" SAPLING_OUTPUT=""
|
||||
find_sapling_params && {
|
||||
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
|
||||
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
|
||||
info "Bundled Sapling params"
|
||||
} || warn "Sapling params not found — not bundled"
|
||||
else
|
||||
info "Lite mode: skipping daemon and Sapling/asmap bundling"
|
||||
fi
|
||||
|
||||
# ── Package: release/linux/ ──────────────────────────────────────────────
|
||||
rm -rf "$out"
|
||||
# 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/).
|
||||
mkdir -p "$out"
|
||||
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.AppImage"
|
||||
|
||||
local DIST="ObsidianDragon-${VERSION}-Linux-x64"
|
||||
local DIST="${APP_BASENAME}-${VERSION}-Linux-x64"
|
||||
local dist_dir="$out/$DIST"
|
||||
mkdir -p "$dist_dir"
|
||||
|
||||
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/"
|
||||
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
|
||||
# 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"
|
||||
@@ -292,27 +406,29 @@ build_release_linux() {
|
||||
"$APPDIR/usr/share/icons/hicolor/256x256/apps" \
|
||||
"$APPDIR/usr/share/ObsidianDragon/res"
|
||||
|
||||
cp bin/ObsidianDragon "$APPDIR/usr/bin/"
|
||||
cp "bin/${APP_BASENAME}" "$APPDIR/usr/bin/"
|
||||
cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true
|
||||
|
||||
[[ -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/"
|
||||
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
|
||||
# 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=ObsidianDragon
|
||||
Exec=${APP_BASENAME}
|
||||
Icon=ObsidianDragon
|
||||
Categories=Finance;Network;
|
||||
Terminal=false
|
||||
@@ -343,14 +459,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/ObsidianDragon" "$@"
|
||||
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}" "\$@"
|
||||
APPRUN
|
||||
chmod +x "$APPDIR/AppRun"
|
||||
|
||||
@@ -379,9 +495,9 @@ APPRUN
|
||||
local ARCH
|
||||
ARCH=$(uname -m)
|
||||
cd "$bd"
|
||||
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))"
|
||||
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))"
|
||||
} || warn "AppImage creation failed — binaries zip still in release/linux/"
|
||||
|
||||
info "Linux release artifacts: $out/"
|
||||
@@ -490,26 +606,48 @@ HDR
|
||||
|
||||
# ── Daemon binaries ──────────────────────────────────────────────
|
||||
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
||||
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
|
||||
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
|
||||
else
|
||||
warn "prebuilt-binaries/dragonxd-win/ not found — wallet-only build"
|
||||
info "Lite mode: skipping embedded daemon binaries"
|
||||
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))"
|
||||
@@ -592,34 +730,40 @@ HDR
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
|
||||
info "Building with $JOBS jobs ..."
|
||||
cmake --build . -j "$JOBS"
|
||||
|
||||
[[ -f "bin/ObsidianDragon.exe" ]] || { err "Windows build failed"; exit 1; }
|
||||
info "Binary: $(du -h bin/ObsidianDragon.exe | cut -f1)"
|
||||
[[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; }
|
||||
info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)"
|
||||
|
||||
# ── Package: release/windows/ ────────────────────────────────────────────
|
||||
rm -rf "$out"
|
||||
# Remove only THIS variant's prior artifacts so full-node and lite releases coexist here.
|
||||
mkdir -p "$out"
|
||||
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.exe"
|
||||
|
||||
local DIST="ObsidianDragon-${VERSION}-Windows-x64"
|
||||
local DIST="${APP_BASENAME}-${VERSION}-Windows-x64"
|
||||
local dist_dir="$out/$DIST"
|
||||
mkdir -p "$dist_dir"
|
||||
cp bin/ObsidianDragon.exe "$dist_dir/"
|
||||
cp "bin/${APP_BASENAME}.exe" "$dist_dir/"
|
||||
|
||||
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
||||
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
|
||||
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
||||
done
|
||||
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
|
||||
|
||||
# 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 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 xmrig for mining support
|
||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
|
||||
@@ -628,8 +772,8 @@ HDR
|
||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||
|
||||
# ── Single-file exe (all resources embedded) ────────────────────────────
|
||||
cp bin/ObsidianDragon.exe "$out/ObsidianDragon-${VERSION}.exe"
|
||||
info "Single-file exe: $out/ObsidianDragon-${VERSION}.exe ($(du -h "$out/ObsidianDragon-${VERSION}.exe" | cut -f1))"
|
||||
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))"
|
||||
|
||||
# ── Zip ──────────────────────────────────────────────────────────────────
|
||||
if command -v zip &>/dev/null; then
|
||||
@@ -732,7 +876,9 @@ build_release_mac() {
|
||||
fi
|
||||
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
|
||||
else
|
||||
MAC_ARCH=$(uname -m)
|
||||
# Native macOS: build universal binary (arm64 + x86_64)
|
||||
MAC_ARCH="universal"
|
||||
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||
fi
|
||||
|
||||
header "Release: macOS ($MAC_ARCH$(${IS_CROSS} && echo ' — cross-compile'))"
|
||||
@@ -809,41 +955,67 @@ 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"}
|
||||
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
else
|
||||
info "Configuring (native) ..."
|
||||
# Build libsodium as universal if needed
|
||||
local need_sodium=false
|
||||
if [[ ! -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]] && \
|
||||
[[ ! -f "$SCRIPT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
|
||||
need_sodium=true
|
||||
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
|
||||
# Rebuild if existing lib is not universal (single-arch won't link)
|
||||
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then
|
||||
info "Existing libsodium is not universal — rebuilding ..."
|
||||
rm -rf "$SCRIPT_DIR/libs/libsodium"
|
||||
need_sodium=true
|
||||
fi
|
||||
fi
|
||||
if $need_sodium; then
|
||||
info "Building libsodium (universal) ..."
|
||||
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
|
||||
fi
|
||||
|
||||
info "Configuring (native universal arm64+x86_64) ..."
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
fi
|
||||
|
||||
info "Building with $JOBS jobs ..."
|
||||
cmake --build . -j "$JOBS"
|
||||
|
||||
[[ -f "bin/ObsidianDragon" ]] || { err "macOS build failed"; exit 1; }
|
||||
[[ -f "bin/${APP_BASENAME}" ]] || { 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/ObsidianDragon
|
||||
"$STRIP_CMD" "bin/${APP_BASENAME}"
|
||||
else
|
||||
warn "osxcross strip not found at $STRIP_CMD — skipping"
|
||||
fi
|
||||
else
|
||||
info "Stripping ..."
|
||||
strip bin/ObsidianDragon
|
||||
strip "bin/${APP_BASENAME}"
|
||||
# Verify universal binary
|
||||
if command -v lipo &>/dev/null; then
|
||||
info "Architecture info:"
|
||||
lipo -info "bin/${APP_BASENAME}"
|
||||
fi
|
||||
fi
|
||||
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
|
||||
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||
|
||||
# ── Create .app bundle ───────────────────────────────────────────────────
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
|
||||
local APP="$out/ObsidianDragon.app"
|
||||
local APP="$out/${APP_BASENAME}.app"
|
||||
local CONTENTS="$APP/Contents"
|
||||
local MACOS="$CONTENTS/MacOS"
|
||||
local RESOURCES="$CONTENTS/Resources"
|
||||
@@ -852,39 +1024,43 @@ TOOLCHAIN
|
||||
mkdir -p "$MACOS" "$RESOURCES/res" "$FRAMEWORKS"
|
||||
|
||||
# Main binary
|
||||
cp bin/ObsidianDragon "$MACOS/"
|
||||
chmod +x "$MACOS/ObsidianDragon"
|
||||
cp "bin/${APP_BASENAME}" "$MACOS/"
|
||||
chmod +x "$MACOS/${APP_BASENAME}"
|
||||
|
||||
# Resources
|
||||
cp -r bin/res/* "$RESOURCES/res/" 2>/dev/null || true
|
||||
|
||||
# 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
|
||||
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
|
||||
else
|
||||
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
|
||||
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
|
||||
fi
|
||||
|
||||
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
|
||||
@@ -897,11 +1073,13 @@ TOOLCHAIN
|
||||
warn "xmrig not found — mining unavailable in .app"
|
||||
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"
|
||||
}
|
||||
if should_bundle_full_node_assets; then
|
||||
# asmap.dat — placed in MacOS/ so the daemon finds it next to its binary
|
||||
find_asmap 2>/dev/null && {
|
||||
cp "$ASMAP_DAT" "$MACOS/asmap.dat"
|
||||
info " Bundled asmap.dat"
|
||||
}
|
||||
fi
|
||||
|
||||
# Bundle SDL3 dylib
|
||||
local sdl_dylib=""
|
||||
@@ -921,26 +1099,34 @@ 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/ObsidianDragon" 2>/dev/null || true
|
||||
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
|
||||
else
|
||||
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/ObsidianDragon" 2>/dev/null || true
|
||||
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
|
||||
fi
|
||||
info " Bundled $sdl_name"
|
||||
fi
|
||||
|
||||
# Launcher script (ensures working dir + dylib path)
|
||||
mv "$MACOS/ObsidianDragon" "$MACOS/ObsidianDragon.bin"
|
||||
cat > "$MACOS/ObsidianDragon" <<'LAUNCH'
|
||||
# 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
|
||||
#!/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/ObsidianDragon.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/${APP_BASENAME}.bin" "\$@"
|
||||
LAUNCH
|
||||
chmod +x "$MACOS/ObsidianDragon"
|
||||
chmod +x "$MACOS/${APP_BASENAME}"
|
||||
|
||||
# Info.plist
|
||||
# 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
|
||||
cat > "$CONTENTS/Info.plist" <<PLIST
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
@@ -948,17 +1134,17 @@ LAUNCH
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>DragonX Wallet</string>
|
||||
<string>${APP_DISPLAY_NAME}</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>DragonX Wallet</string>
|
||||
<string>${APP_DISPLAY_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>is.hush.dragonx</string>
|
||||
<string>${APP_BUNDLE_ID}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${VERSION}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${VERSION}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>ObsidianDragon</string>
|
||||
<string>${APP_BASENAME}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>ObsidianDragon</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
@@ -1020,25 +1206,27 @@ PLIST
|
||||
info ".app bundle created: $APP"
|
||||
|
||||
# ── Zip the .app bundle ──────────────────────────────────────────────────
|
||||
local APP_ZIP="ObsidianDragon-${VERSION}-macOS-${MAC_ARCH}.app.zip"
|
||||
local APP_ZIP="${APP_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.app.zip"
|
||||
if command -v zip &>/dev/null; then
|
||||
(cd "$out" && zip -r "$APP_ZIP" "ObsidianDragon.app")
|
||||
(cd "$out" && zip -r "$APP_ZIP" "${APP_BASENAME}.app")
|
||||
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
|
||||
fi
|
||||
|
||||
# ── Create DMG ───────────────────────────────────────────────────────────
|
||||
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
||||
local DMG_BASENAME="DragonX_Wallet"
|
||||
$DO_LITE && DMG_BASENAME="DragonX_Wallet_Lite"
|
||||
local DMG_NAME="${DMG_BASENAME}-${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 "DragonX Wallet" \
|
||||
--volname "${APP_DISPLAY_NAME}" \
|
||||
--volicon "$RESOURCES/ObsidianDragon.icns" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 600 400 \
|
||||
--icon-size 100 \
|
||||
--icon "ObsidianDragon.app" 150 190 \
|
||||
--icon "${APP_BASENAME}.app" 150 190 \
|
||||
--app-drop-link 450 190 \
|
||||
--no-internet-enable \
|
||||
"$out/$DMG_NAME" \
|
||||
@@ -1053,7 +1241,7 @@ PLIST
|
||||
mkdir -p "$staging"
|
||||
cp -a "$APP" "$staging/"
|
||||
ln -s /Applications "$staging/Applications"
|
||||
hdiutil create -volname "DragonX Wallet" \
|
||||
hdiutil create -volname "${APP_DISPLAY_NAME}" \
|
||||
-srcfolder "$staging" \
|
||||
-ov -format UDZO \
|
||||
"$out/$DMG_NAME" 2>/dev/null && {
|
||||
@@ -1069,7 +1257,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 "DragonX Wallet" \
|
||||
genisoimage -V "${APP_DISPLAY_NAME}" \
|
||||
-D -R -apple -no-pad \
|
||||
-o "$out/$DMG_NAME" \
|
||||
"$staging" 2>/dev/null && {
|
||||
|
||||
270
overview_mockup.html
Normal file
270
overview_mockup.html
Normal file
File diff suppressed because one or more lines are too long
@@ -1,51 +0,0 @@
|
||||
<?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>
|
||||
@@ -5,7 +5,7 @@
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="DragonX.ObsidianDragon.Wallet"
|
||||
version="@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0"
|
||||
version="@DRAGONX_APP_VERSION_MAJOR@.@DRAGONX_APP_VERSION_MINOR@.@DRAGONX_APP_VERSION_PATCH@.0"
|
||||
processorArchitecture="amd64"
|
||||
/>
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
#include <winver.h>
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
|
||||
PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
|
||||
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
|
||||
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", "ObsidianDragon Wallet\0"
|
||||
VALUE "FileVersion", "@PROJECT_VERSION@\0"
|
||||
VALUE "InternalName", "ObsidianDragon\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 "LegalCopyright", "Copyright 2024-2026 DragonX Developers. GPLv3.\0"
|
||||
VALUE "OriginalFilename", "ObsidianDragon.exe\0"
|
||||
VALUE "ProductName", "ObsidianDragon\0"
|
||||
VALUE "ProductVersion", "@PROJECT_VERSION@\0"
|
||||
VALUE "OriginalFilename", "@DRAGONX_APP_NAME@.exe\0"
|
||||
VALUE "ProductName", "@DRAGONX_APP_NAME@\0"
|
||||
VALUE "ProductVersion", "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@\0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
BIN
res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf
Normal file
BIN
res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
res/fonts/NotoSansCJK-Subset.ttf
Normal file
BIN
res/fonts/NotoSansCJK-Subset.ttf
Normal file
Binary file not shown.
BIN
res/fonts/UbuntuMono-R.ttf
Normal file
BIN
res/fonts/UbuntuMono-R.ttf
Normal file
Binary file not shown.
611
res/lang/de.json
611
res/lang/de.json
File diff suppressed because it is too large
Load Diff
611
res/lang/es.json
611
res/lang/es.json
File diff suppressed because it is too large
Load Diff
611
res/lang/fr.json
611
res/lang/fr.json
File diff suppressed because it is too large
Load Diff
609
res/lang/ja.json
609
res/lang/ja.json
@@ -28,6 +28,7 @@
|
||||
"address_book_add": "アドレスを追加",
|
||||
"address_book_add_new": "新規追加",
|
||||
"address_book_added": "アドレスをアドレス帳に追加しました",
|
||||
"address_book_confirm_delete": "削除しますか?",
|
||||
"address_book_count": "%zu 件のアドレスを保存済み",
|
||||
"address_book_deleted": "エントリを削除しました",
|
||||
"address_book_edit": "アドレスを編集",
|
||||
@@ -39,10 +40,13 @@
|
||||
"address_copied": "アドレスをクリップボードにコピーしました",
|
||||
"address_details": "アドレス詳細",
|
||||
"address_label": "アドレス:",
|
||||
"address_reorder_hint": "行の端にドロップで並べ替え、中央にドロップで移動します",
|
||||
"address_upper": "アドレス",
|
||||
"address_url": "アドレスURL",
|
||||
"addresses_appear_here": "接続後、受信アドレスがここに表示されます。",
|
||||
"advanced": "詳細設定",
|
||||
"advanced_effects": "高度なエフェクト...",
|
||||
"ago": "前",
|
||||
"all_filter": "すべて",
|
||||
"allow_custom_fees": "カスタム手数料を許可",
|
||||
"amount": "金額",
|
||||
@@ -67,7 +71,10 @@
|
||||
"backup_wallet": "ウォレットをバックアップ...",
|
||||
"backup_wallet_not_found": "警告:予想される場所にwallet.datが見つかりません",
|
||||
"balance": "残高",
|
||||
"balance_history_collecting": "残高履歴 — データを収集中...",
|
||||
"balance_layout": "残高レイアウト",
|
||||
"balance_shielded_fmt": "シールド: %.8f",
|
||||
"balance_transparent_fmt": "透明: %.8f",
|
||||
"ban": "ブロック",
|
||||
"banned_peers": "ブロック済みピア",
|
||||
"block": "ブロック",
|
||||
@@ -90,12 +97,52 @@
|
||||
"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をコピー",
|
||||
@@ -106,8 +153,26 @@
|
||||
"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_lite_redownload_msg": "これにより、ウォレットにダウンロード済みのブロックデータがクリアされ、ライトサーバーからすべてのブロックを再取得して再スキャンします。しばらく時間がかかる場合があり、完了するまでウォレットに同期の進行状況が表示されます。",
|
||||
"confirm_lite_redownload_safe": "ウォレット、鍵、シードには影響しません — 再ダウンロードされるのはブロックデータのみです。",
|
||||
"confirm_lite_redownload_title": "ブロックを再ダウンロード",
|
||||
"confirm_reinstall_daemon_msg": "デーモンを停止し、インストール済みの dragonxd(および dragonx-cli/dragonx-tx)をこのウォレットビルドにバンドルされているバージョンで上書きしてから、ノードを再起動します。ノードのバイナリを復旧または更新する場合に使用してください。",
|
||||
"confirm_reinstall_daemon_safe": "ウォレット、鍵、ブロックチェーンデータには影響しません — 置き換えられるのはデーモンのプログラムファイルのみです。",
|
||||
"confirm_reinstall_daemon_title": "バンドル版デーモンをインストール",
|
||||
"confirm_repair_wallet_msg": "デーモンを -zapwallettxes=2 で再起動します:ウォレットのすべての取引およびノート記録を削除し、その後ブロックチェーンから再構築します。完全な再スキャン後でも取引の作成に失敗する(「Invalid sapling spend proof」/「shielded requirements not met」)場合に使用してください。長い時間がかかり、完了するまでウォレットはオフラインのままになります。",
|
||||
"confirm_repair_wallet_safe": "鍵、アドレス、残高は保持されます — 再構築されるのはキャッシュされたトランザクション記録のみです。",
|
||||
"confirm_repair_wallet_title": "ウォレットを修復",
|
||||
"confirm_rescan_msg": "デーモンを再起動し、ウォレットのトランザクションを探してブロックチェーン全体を再スキャンします。長時間かかる場合があり、完了するまでウォレットはオフラインのままになります。",
|
||||
"confirm_rescan_safe": "wallet.dat とブロックチェーンデータは削除されません — 再スキャンされるだけです。",
|
||||
"confirm_rescan_title": "ブロックチェーンを再スキャン",
|
||||
"confirm_restart_daemon_msg": "変更したオプションを適用するため、デーモンを停止して再起動します。ウォレットは一時的に切断され、再接続します。",
|
||||
"confirm_restart_daemon_title": "デーモンを再起動",
|
||||
"confirm_send": "送金を確認",
|
||||
"confirm_transaction": "取引を確認",
|
||||
"confirm_transfer": "送金を確認",
|
||||
"confirmations": "確認数",
|
||||
"confirmations_display": "%d 確認 | %s",
|
||||
"confirmed": "確認済み",
|
||||
@@ -115,6 +180,7 @@
|
||||
"connected_peers": "接続中のピア",
|
||||
"connecting": "接続中...",
|
||||
"console": "コンソール",
|
||||
"console_app": "アプリ",
|
||||
"console_auto_scroll": "自動スクロール",
|
||||
"console_available_commands": "利用可能なコマンド:",
|
||||
"console_capturing_output": "デーモン出力をキャプチャ中...",
|
||||
@@ -131,6 +197,7 @@
|
||||
"console_connected": "デーモンに接続済み",
|
||||
"console_copy_all": "すべてコピー",
|
||||
"console_copy_selected": "コピー",
|
||||
"console_copy_value": "コピー",
|
||||
"console_daemon": "デーモン",
|
||||
"console_daemon_error": "デーモンエラー!",
|
||||
"console_daemon_started": "デーモンが起動しました",
|
||||
@@ -149,16 +216,21 @@
|
||||
"console_help_setgenerate": " setgenerate - マイニングを制御",
|
||||
"console_help_stop": " stop - デーモンを停止",
|
||||
"console_line_count": "%zu 行",
|
||||
"console_matches": "件一致",
|
||||
"console_new_lines": "%d 新しい行",
|
||||
"console_no_daemon": "デーモンなし",
|
||||
"console_not_connected": "エラー:デーモンに接続されていません",
|
||||
"console_quit_note": "ここでは 'quit'/'exit' は不要です — ウィンドウを閉じるだけで構いません。",
|
||||
"console_rpc_reference": "RPCコマンドリファレンス",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "コンソールスキャンライン",
|
||||
"console_search_commands": "コマンドを検索...",
|
||||
"console_select_all": "すべて選択",
|
||||
"console_show_app_output": "[app] ウォレットのログ行を表示",
|
||||
"console_show_daemon_output": "デーモン出力を表示",
|
||||
"console_show_errors_only": "エラーのみ表示",
|
||||
"console_show_rpc_ref": "RPCコマンドリファレンスを表示",
|
||||
"console_show_rpc_trace": "アプリのRPC呼び出しを表示",
|
||||
"console_showing_lines": "%zu / %zu 行を表示中",
|
||||
"console_starting_node": "ノードを起動中...",
|
||||
"console_status_error": "エラー",
|
||||
@@ -172,6 +244,13 @@
|
||||
"console_welcome": "ObsidianDragonコンソールへようこそ",
|
||||
"console_zoom_in": "拡大",
|
||||
"console_zoom_out": "縮小",
|
||||
"contact_global": "すべてのウォレットで表示(グローバル連絡先)",
|
||||
"contact_global_badge_tt": "グローバル連絡先 — すべてのウォレットで表示",
|
||||
"contact_global_tt": "オン:この連絡先はどのウォレットを読み込んでも表示されます。オフ:現在のウォレットにのみ属します。",
|
||||
"contacts": "連絡先",
|
||||
"contacts_search_no_match": "一致する連絡先がありません",
|
||||
"contacts_search_placeholder": "連絡先を検索...",
|
||||
"copied": "コピーしました!",
|
||||
"copy": "コピー",
|
||||
"copy_address": "完全なアドレスをコピー",
|
||||
"copy_error": "エラーをコピー",
|
||||
@@ -180,15 +259,59 @@
|
||||
"copy_uri": "URIをコピー",
|
||||
"current_price": "現在の価格",
|
||||
"custom_fees": "カスタム手数料",
|
||||
"daemon_binary": "デーモンバイナリ",
|
||||
"daemon_bundled": "バンドル版",
|
||||
"daemon_install_bundled": "バンドル版をインストール",
|
||||
"daemon_installed": "インストール済み",
|
||||
"daemon_none_bundled": "このビルドにはなし",
|
||||
"daemon_not_installed": "未インストール",
|
||||
"daemon_status_differ": "インストール済みのバイナリはバンドル版と異なります。",
|
||||
"daemon_status_match": "インストール済みのバイナリはバンドル版と一致しています。",
|
||||
"daemon_status_missing": "デーモンがインストールされていません — バンドル版をインストールしてください。",
|
||||
"daemon_update_available": "新しいノードバージョンが利用可能です",
|
||||
"daemon_update_body": "このウォレットビルドには、現在インストールされているものよりも新しいDragonXノードがバンドルされています。更新すると、インストール済みのdragonxd(およびdragonx-cli/dragonx-tx)が置き換えられ、その後ノードを停止して再起動し、新しいバージョンが有効になります。新しいノードには、古いものにない機能(例:シードフレーズのサポート)が追加されている場合があるため、更新を推奨します。",
|
||||
"daemon_update_browse": "すべてのリリースを表示…",
|
||||
"daemon_update_check": "更新を確認…",
|
||||
"daemon_update_checking": "最新ノードを確認中…",
|
||||
"daemon_update_downgrade_note": "古いバージョンは現在のチェーンデータと互換性がない場合があります。別のバージョンをインストールすると、デーモンの再起動後に有効になります。",
|
||||
"daemon_update_download_install": "ダウンロードしてインストール",
|
||||
"daemon_update_downloading": "ダウンロード中…",
|
||||
"daemon_update_failed": "更新に失敗しました",
|
||||
"daemon_update_installed": "インストール済み:",
|
||||
"daemon_update_installed_ok": "ノードをインストールしました",
|
||||
"daemon_update_installing": "インストール中…",
|
||||
"daemon_update_keep": "現在のまま使用",
|
||||
"daemon_update_later": "後で",
|
||||
"daemon_update_latest": "最新:",
|
||||
"daemon_update_loading": "リリースを読み込み中…",
|
||||
"daemon_update_now": "今すぐ更新",
|
||||
"daemon_update_reinstall": "再インストール",
|
||||
"daemon_update_restart_note": "新しいバージョンを実行するにはデーモンを再起動してください。",
|
||||
"daemon_update_restart_now": "今すぐデーモンを再起動",
|
||||
"daemon_update_safe": "ウォレット、鍵、ブロックチェーンデータには影響しません — 置き換えられるのはデーモンのプログラムファイルのみです。意図的にカスタムノードを実行している場合は、「現在のまま使用」を選択してください。",
|
||||
"daemon_update_title": "ノードデーモンを更新しますか?",
|
||||
"daemon_update_unavailable_body": "このプラットフォーム向けのノードビルドはありません。",
|
||||
"daemon_update_unavailable_title": "ノードの更新は利用できません",
|
||||
"daemon_update_unknown_error": "不明なエラーです。",
|
||||
"daemon_update_up_to_date": "ノードは最新です",
|
||||
"daemon_update_verify_note": "ダウンロードは、インストール前にリリースで公開された SHA-256 と固定された ed25519 署名で検証されます。",
|
||||
"daemon_update_verifying": "検証中…",
|
||||
"daemon_update_version": "バージョン:",
|
||||
"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": "エラー",
|
||||
@@ -196,6 +319,27 @@
|
||||
"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_hash_not_found": "このハッシュに対応するブロックまたはトランザクションが見つかりません",
|
||||
"explorer_invalid_query": "ブロック高さまたは64文字のハッシュを入力してください",
|
||||
"explorer_invalid_response": "デーモンからの応答が無効です",
|
||||
"explorer_mempool": "メモリプール",
|
||||
"explorer_mempool_size": "サイズ",
|
||||
"explorer_mempool_txs": "トランザクション",
|
||||
"explorer_no_results": "一致するキャッシュ済みブロックがありません",
|
||||
"explorer_not_connected": "デーモンに未接続 — ブロックまたはトランザクションハッシュを検索できません",
|
||||
"explorer_recent_blocks": "最近のブロック",
|
||||
"explorer_search": "検索",
|
||||
"explorer_section": "エクスプローラー",
|
||||
"explorer_tx_outputs": "出力",
|
||||
"explorer_tx_size": "サイズ",
|
||||
"export": "エクスポート",
|
||||
"export_csv": "CSVエクスポート",
|
||||
"export_keys_btn": "鍵をエクスポート",
|
||||
@@ -224,16 +368,25 @@
|
||||
"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_qr": "QRを非表示",
|
||||
"hide_zero_balances": "残高0を非表示",
|
||||
"history": "履歴",
|
||||
"immature_type": "未成熟",
|
||||
@@ -253,6 +406,9 @@
|
||||
"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": "保持",
|
||||
@@ -266,29 +422,134 @@
|
||||
"key_export_viewing_keys_zonly": "ビューイングキーはシールド (z) アドレスでのみ利用可能です",
|
||||
"key_export_viewing_warning": "この閲覧鍵を使うと、他者があなたの受信取引と残高を見ることができますが、資金を使うことはできません。信頼できる相手とのみ共有してください。",
|
||||
"label": "ラベル:",
|
||||
"label_placeholder": "例: 貯金、マイニング...",
|
||||
"language": "言語",
|
||||
"light": "ライト",
|
||||
"lite_account_label": "アカウント",
|
||||
"lite_action": "アクション",
|
||||
"lite_backup_keys": "バックアップと鍵",
|
||||
"lite_birthday_backup": "誕生日:%llu (これもバックアップしてください)",
|
||||
"lite_birthday_hint": "スキャンを開始するブロック高。不明な場合は0のままにしてください(完全スキャンが遅くなります)。",
|
||||
"lite_birthday_label": "バースデー",
|
||||
"lite_console_help_passthrough": "その他の入力はライトウォレットのコンソールコマンドとして実行されます。",
|
||||
"lite_copy": "コピー",
|
||||
"lite_could_not_write": "書き込めませんでした ",
|
||||
"lite_encrypt_wallet": "ウォレットを暗号化",
|
||||
"lite_encryption_removed": "暗号化を解除しました",
|
||||
"lite_hide_wipe": "非表示にして消去",
|
||||
"lite_import": "インポート",
|
||||
"lite_import_key_label": "鍵をインポート",
|
||||
"lite_key_imported": "鍵をインポートしました — 履歴をスキャンするには同期を実行してください",
|
||||
"lite_lock_failed": "ロックに失敗しました",
|
||||
"lite_lock_now": "今すぐロック",
|
||||
"lite_maintenance": "メンテナンス",
|
||||
"lite_net_add": "追加",
|
||||
"lite_net_add_label_hint": "ラベル(任意)",
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "確認中…",
|
||||
"lite_net_connected": "接続済み",
|
||||
"lite_net_custom": "カスタム",
|
||||
"lite_net_disconnected": "未接続",
|
||||
"lite_net_hidden_section": "非表示のサーバー",
|
||||
"lite_net_hide": "非表示",
|
||||
"lite_net_in_use": "使用中",
|
||||
"lite_net_intro": "使用するサーバーを選ぶか、ウォレットにランダムで選ばせることができます。変更はすぐに反映されます。",
|
||||
"lite_net_invalid_url": "有効な http(s):// URL を入力してください。",
|
||||
"lite_net_no_wallet": "ウォレットが開かれていません",
|
||||
"lite_net_official": "公式",
|
||||
"lite_net_offline": "オフライン",
|
||||
"lite_net_random_active": "ランダムなサーバー選択が有効です。",
|
||||
"lite_net_random_server": "ランダムサーバー",
|
||||
"lite_net_refresh": "更新",
|
||||
"lite_net_show_hidden": "非表示のサーバーを表示",
|
||||
"lite_net_sync_label": "同期",
|
||||
"lite_net_synced": "同期済み",
|
||||
"lite_net_syncing": "同期中",
|
||||
"lite_net_title": "Liteサーバー",
|
||||
"lite_net_unhide": "再表示",
|
||||
"lite_net_use_random": "毎回ランダムなサーバーを使用",
|
||||
"lite_no_wallet": "開いているウォレットがありません — 設定で作成または開いてください",
|
||||
"lite_no_wallet_short": "ウォレットが開かれていません",
|
||||
"lite_op_create": "作成",
|
||||
"lite_op_open": "開く",
|
||||
"lite_op_restore": "復元",
|
||||
"lite_overwrite": "上書き",
|
||||
"lite_passphrase_label": "パスフレーズ",
|
||||
"lite_private_keys_warning": "秘密鍵 — これらを持つ者は誰でもあなたの資金を送金できます",
|
||||
"lite_redownload_blocks": "ブロックを再ダウンロード",
|
||||
"lite_redownload_desc": "ライトサーバーからすべてのブロックを再取得します(残高や履歴がおかしい場合に使用)。",
|
||||
"lite_redownload_running": "ブロックを再ダウンロード中…",
|
||||
"lite_remove_encryption": "暗号化を解除",
|
||||
"lite_restore_birthday_label": "バースデーブロック(任意 — 0で最初からスキャンし、遅くなります)",
|
||||
"lite_restore_btn": "ウォレットを復元",
|
||||
"lite_restore_intro": "復元用シードフレーズを入力してください。ウォレットが復元され、その後ライトサーバーから同期されます。",
|
||||
"lite_restore_ok": "ウォレットを復元しました — ライトサーバーから同期中…",
|
||||
"lite_restore_seed_label": "シードフレーズ(単語をスペースで区切る)",
|
||||
"lite_restore_title": "シードフレーズから復元",
|
||||
"lite_save_to_file": "ファイルに保存",
|
||||
"lite_saved_to": "保存しました(平文、所有者のみ):",
|
||||
"lite_security": "セキュリティ",
|
||||
"lite_seed_label": "シード",
|
||||
"lite_seed_warning": "シードフレーズ — ウォレットを復元する唯一の方法です。書き留めてオフラインで保管し、決して共有しないでください。",
|
||||
"lite_servers_network_tab": "ライトサーバーはネットワークタブで管理します。",
|
||||
"lite_show_private_keys": "秘密鍵を表示",
|
||||
"lite_show_seed": "シードを表示",
|
||||
"lite_unlock": "ロック解除",
|
||||
"lite_unlock_btn": "ロック解除",
|
||||
"lite_unlock_failed": "アンロックに失敗しました — パスフレーズが間違っていませんか?",
|
||||
"lite_unlock_msg": "送金のためにウォレットのロックを解除するには、パスフレーズを入力してください。",
|
||||
"lite_unlock_ok": "ウォレットのロックを解除しました — 送金できます",
|
||||
"lite_unlock_title": "ウォレットのロックを解除",
|
||||
"lite_validate": "検証",
|
||||
"lite_wallet_encrypted": "ウォレットを暗号化しました",
|
||||
"lite_wallet_label": "ウォレット",
|
||||
"lite_wallet_locked": "ウォレットはロックされています",
|
||||
"lite_wallet_ready": "ウォレット準備完了",
|
||||
"lite_wallet_request": "Liteウォレットのリクエスト",
|
||||
"lite_wallet_unlocked": "ウォレットのロックを解除しました",
|
||||
"lite_welcome_create": "新しいウォレットを作成",
|
||||
"lite_welcome_create_failed": "ウォレットを作成できませんでした",
|
||||
"lite_welcome_created": "ウォレットを作成しました — 今すぐ 設定 → バックアップと鍵 で復元フレーズをバックアップしてください",
|
||||
"lite_welcome_later": "後で",
|
||||
"lite_welcome_msg": "まだウォレットがありません。新規に作成するか、復元フレーズから復元してください。",
|
||||
"lite_welcome_restore": "シードから復元",
|
||||
"lite_welcome_restore_hint": "設定 → ライトウォレットリクエスト でウォレットを復元してください",
|
||||
"lite_welcome_title": "ObsidianDragon Lite へようこそ",
|
||||
"lite_word_count": "%d / 24 単語",
|
||||
"lite_working": "処理中…",
|
||||
"loading": "読み込み中...",
|
||||
"loading_addresses": "アドレスを読み込み中...",
|
||||
"loading_transactions": "トランザクションを読み込み中",
|
||||
"local_hashrate": "ローカルハッシュレート",
|
||||
"low_spec_mode": "省電力モード",
|
||||
"mark_mining_address": "マイニングアドレスとしてマーク",
|
||||
"market": "市場",
|
||||
"market_12h": "12時間",
|
||||
"market_18h": "18時間",
|
||||
"market_24h": "24時間",
|
||||
"market_24h_change": "24時間",
|
||||
"market_24h_volume": "24時間出来高",
|
||||
"market_6h": "6時間",
|
||||
"market_attribution": "価格データ:NonKYC提供",
|
||||
"market_btc_price": "BTC価格",
|
||||
"market_cap": "時価総額",
|
||||
"market_cap_short": "時価総額",
|
||||
"market_iv_1d": "1日",
|
||||
"market_iv_1h": "1時間",
|
||||
"market_iv_1m": "1ヶ月",
|
||||
"market_iv_1w": "1週",
|
||||
"market_iv_live": "ライブ",
|
||||
"market_no_history": "価格履歴がありません",
|
||||
"market_no_price": "価格データなし",
|
||||
"market_now": "現在",
|
||||
"market_pct_shielded": "%.0f%% シールド済み",
|
||||
"market_portfolio": "ポートフォリオ",
|
||||
"market_price_loading": "価格データを読み込み中...",
|
||||
"market_price_unavailable": "価格データが利用できません",
|
||||
"market_refresh_price": "価格データを更新",
|
||||
"market_trade_on": "%s で取引",
|
||||
"market_updated": "\\xc2\\xb7 更新: %s",
|
||||
"market_vol_short": "出来高",
|
||||
"mature": "成熟済み",
|
||||
"max": "最大",
|
||||
"memo": "メモ(任意、暗号化)",
|
||||
@@ -298,8 +559,52 @@
|
||||
"memo_z_only": "注:メモはシールド (z) アドレスへの送金時のみ利用可能です",
|
||||
"merge_description": "複数のUTXOを単一のシールドアドレスに統合します。ウォレットサイズの縮小とプライバシーの向上に役立ちます。",
|
||||
"merge_funds": "資金を統合",
|
||||
"merge_send_failed": "マージに失敗しました: ",
|
||||
"merge_started": "統合操作を開始しました",
|
||||
"merge_title": "アドレスに統合",
|
||||
"mig_adopt": "これを自分のウォレットにする",
|
||||
"mig_adopt_now": "シードウォレットを採用",
|
||||
"mig_adopting": "新しいウォレットをインストールして再スキャン中…",
|
||||
"mig_adopting_note": "ノードは新しいウォレットで再起動し、チェーンを再スキャンします — これには数分かかる場合があります。ウォレットは実行したままにしてください。自動的に再接続します。",
|
||||
"mig_already_mnemonic": "このウォレットにはすでに24単語のシードフレーズがあります — 移行するものはありません。バックアップして、単語を安全な場所に保管してください。",
|
||||
"mig_backed_up": "シードフレーズを書き写しました",
|
||||
"mig_backup_instead": "シードフレーズをバックアップ",
|
||||
"mig_balance": "現在のウォレット残高:%.8f DRGX(少額の手数料を除く)",
|
||||
"mig_check_failed": "ウォレットを確認できませんでした。ロックが解除され、接続されていることを確認してから、再確認してください。",
|
||||
"mig_checking_balance": "残高を確認中",
|
||||
"mig_confirming": "ウォレットを切り替える前に、スイープがオンチェーンで確認されるのを待っています — これにより資金が実際に移動したことが保証されます。",
|
||||
"mig_confs": "スイープの確認数:%d",
|
||||
"mig_continue_sweep": "スイープに進む",
|
||||
"mig_copy_seed": "シードをコピー",
|
||||
"mig_create": "シードウォレットを作成",
|
||||
"mig_daemon_too_old": "DragonXノードが古すぎるため、シードウォレットを作成できません — これに必要なニーモニックのサポートがありません。ノードを更新してから(設定 → ノードとセキュリティ → バンドル版をインストール、または更新を確認)、もう一度お試しください。",
|
||||
"mig_discard": "移行を破棄",
|
||||
"mig_done": "移行が完了しました。ウォレットはシードフレーズで保護されるようになりました。",
|
||||
"mig_done_btn": "完了",
|
||||
"mig_done_detail": "デーモンが資金を表示するために再スキャンしています — 数分かかる場合があります。以前のウォレットはデータフォルダに .bak として保存されました。",
|
||||
"mig_intro": "これは24単語のシードフレーズで保護された新規ウォレットを隔離されたノード内に作成し、資金をそこへ移動できるようにします。現在のウォレットには一切変更を加えず、このステップでは資金は移動しません — まず新しいシードをバックアップし、その後、別の確定ステップとして資金をスイープします。",
|
||||
"mig_later": "後で",
|
||||
"mig_no_funds": "このウォレットには移行する資金がありません。新しいシードウォレットを直接採用できます — 現在の(空の)ウォレットをシードフレーズで保護されたものに置き換えます。念のため、古いウォレットはタイムスタンプ付きのバックアップとして退避されます。",
|
||||
"mig_nofunds_confirm": "現在のウォレットが新しいシードウォレットに置き換えられることを理解しました",
|
||||
"mig_not_connected": "先にノードへ接続してから、これを再度開いて移行してください。",
|
||||
"mig_precheck_checking": "ウォレットを確認中",
|
||||
"mig_receive_addr": "新しいウォレットの受信アドレス:",
|
||||
"mig_recheck": "再確認",
|
||||
"mig_remainder": "一部の資金がまだ旧ウォレットに残っています(1回のトランザクションには入力が多すぎます) — 切り替える前に残りをスイープしてください。",
|
||||
"mig_remaining": "旧ウォレットの残り:%.8f DRGX",
|
||||
"mig_seed_warning": "これら24単語を順番どおりに書き写し、オフラインで保管してください。これは新しいウォレットの唯一のバックアップです — 紛失すると、移行した資金は永久に失われます。",
|
||||
"mig_step1_done": "2ステップ中1ステップが完了 — 資金はまだ移動していません。この新しいウォレットへ資金をスイープするのが次のステップです。",
|
||||
"mig_sweep_all": "すべての資金をスイープ",
|
||||
"mig_sweep_intro": "次に、現在のウォレットからすべての資金を新しいシードウォレットへスイープします。これは新しいウォレットのアドレスへ1件のトランザクションを送信します。これ以降は(バックアップ済みの)シードフレーズが資金を管理します。",
|
||||
"mig_sweep_remaining": "残りをスイープ",
|
||||
"mig_sweeping": "新しいウォレットへ資金をスイープ中…",
|
||||
"mig_title": "シードウォレットへ移行",
|
||||
"mig_to": "宛先: ",
|
||||
"mig_txid": "txid: ",
|
||||
"mig_unlock_first": "送金するには、まずウォレットのロックを解除してください。",
|
||||
"mig_unlock_to_migrate": "先にウォレットのロックを解除してから、これを再度開いて移行してください。",
|
||||
"mig_waiting_mine": "スイープトランザクションがマイニングされるのを待っています…",
|
||||
"mig_working": "隔離されたノード内に新しいシードウォレットを作成中です — 数分かかる場合があります。メインウォレットは動作を続けます。",
|
||||
"mine_when_idle": "アイドル時にマイニング",
|
||||
"mined": "採掘済み",
|
||||
"mined_filter": "採掘済み",
|
||||
@@ -311,6 +616,15 @@
|
||||
"mining_address_copied": "マイニングアドレスをコピーしました",
|
||||
"mining_all_time": "全期間",
|
||||
"mining_already_saved": "プールURLは既に保存済みです",
|
||||
"mining_auto_balance_desc": "ネットワークの分散化を促すため、ハッシュレートに応じて公式プール間にマイナーを分散します。各プールを定期的にチェックします。",
|
||||
"mining_auto_balanced_to": "マイニングを自動調整しました:",
|
||||
"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分前",
|
||||
@@ -330,11 +644,20 @@
|
||||
"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_manual_desc": "手動: 下のリストから選んだプールにマイニングします。",
|
||||
"mining_mine": "マイニング",
|
||||
"mining_mining_addr": "マイニングアドレス",
|
||||
"mining_mining_here": "ここでマイニング中",
|
||||
"mining_network": "ネットワーク",
|
||||
"mining_no_blocks_yet": "まだブロックが見つかっていません",
|
||||
"mining_no_payouts_yet": "まだプール支払いがありません",
|
||||
@@ -344,18 +667,24 @@
|
||||
"mining_on": "マイニングはオンです",
|
||||
"mining_open_in_explorer": "エクスプローラーで開く",
|
||||
"mining_payout_address": "支払いアドレス",
|
||||
"mining_payout_foreign": "⚠ この支払いアドレスは現在のウォレットに含まれていません — マイニング報酬が別のウォレットに送られます。ウォレットを切り替えた場合は更新してください。",
|
||||
"mining_payout_tooltip": "マイニング報酬の受取アドレス",
|
||||
"mining_pool": "プール",
|
||||
"mining_pool_fee": "手数料",
|
||||
"mining_pool_hashrate": "プールハッシュレート",
|
||||
"mining_pool_url": "プールURL",
|
||||
"mining_pools_header": "プール",
|
||||
"mining_recent_blocks": "最近のブロック",
|
||||
"mining_recent_payouts": "最近のプール支払い",
|
||||
"mining_refresh": "更新",
|
||||
"mining_remove": "削除",
|
||||
"mining_reset_defaults": "デフォルトにリセット",
|
||||
"mining_save_payout_address": "支払いアドレスを保存",
|
||||
"mining_save_pool_url": "プールURLを保存",
|
||||
"mining_saved_addresses": "保存済みアドレス:",
|
||||
"mining_saved_pools": "保存済みプール:",
|
||||
"mining_select_auto": "自動バランス",
|
||||
"mining_select_manual": "手動",
|
||||
"mining_shares": "シェア",
|
||||
"mining_show_chart": "チャート",
|
||||
"mining_show_log": "ログ",
|
||||
@@ -368,7 +697,9 @@
|
||||
"mining_stop_solo_for_pool_settings": "プール設定を変更するにはソロマイニングを停止してください",
|
||||
"mining_stopping": "停止中...",
|
||||
"mining_stopping_tooltip": "マイナーを停止中...",
|
||||
"mining_suggested_pools": "推奨プール",
|
||||
"mining_syncing_tooltip": "ブロックチェーン同期中...",
|
||||
"mining_tag": " · マイニング",
|
||||
"mining_threads": "マイニングスレッド",
|
||||
"mining_to_save": "保存する",
|
||||
"mining_today": "今日",
|
||||
@@ -388,10 +719,13 @@
|
||||
"no_addresses_available": "利用可能なアドレスがありません",
|
||||
"no_addresses_match": "フィルタに一致するアドレスがありません",
|
||||
"no_addresses_with_balance": "残高のあるアドレスがありません",
|
||||
"no_addresses_yet": "アドレスがまだありません",
|
||||
"no_icons_found": "検索に一致するアイコンがありません。",
|
||||
"no_matching": "一致する取引がありません",
|
||||
"no_recent_receives": "最近の受信がありません",
|
||||
"no_recent_sends": "最近の送信がありません",
|
||||
"no_transactions": "取引が見つかりません",
|
||||
"no_transactions_yet": "まだ取引がありません",
|
||||
"node": "ノード",
|
||||
"node_security": "ノードとセキュリティ",
|
||||
"noise": "ノイズ",
|
||||
@@ -453,8 +787,68 @@
|
||||
"peers_upper": "ピア",
|
||||
"peers_version": "バージョン",
|
||||
"pending": "保留中",
|
||||
"pin_not_set": "PINが設定されていません。パスフレーズで解除してください。",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "エントリを追加",
|
||||
"portfolio_add_to": "ポートフォリオに追加",
|
||||
"portfolio_addresses_hdr": "アドレス",
|
||||
"portfolio_addresses_sel": "%d 件選択中",
|
||||
"portfolio_all_funds": "すべての資金",
|
||||
"portfolio_all_shielded": "すべてシールド",
|
||||
"portfolio_all_transparent": "すべて透明",
|
||||
"portfolio_appearance": "外観",
|
||||
"portfolio_cancel": "キャンセル",
|
||||
"portfolio_clear_sel": "クリア",
|
||||
"portfolio_close": "閉じる",
|
||||
"portfolio_color": "色",
|
||||
"portfolio_currency": "通貨",
|
||||
"portfolio_custom_color": "カスタムカラー…",
|
||||
"portfolio_delete": "削除",
|
||||
"portfolio_detail_empty": "左側でグループを選択するか、追加して編集してください。",
|
||||
"portfolio_edit": "編集",
|
||||
"portfolio_funded": "資金あり",
|
||||
"portfolio_group_name": "グループ名",
|
||||
"portfolio_icon": "アイコン",
|
||||
"portfolio_label": "ラベル",
|
||||
"portfolio_manage": "管理…",
|
||||
"portfolio_manage_title": "ポートフォリオを管理",
|
||||
"portfolio_manual_price": "価格 / DRGX",
|
||||
"portfolio_new_entry": "新規エントリ",
|
||||
"portfolio_no_addr_match": "一致するアドレスがありません",
|
||||
"portfolio_no_entries": "カスタムエントリーはまだありません。アドレスのグループを追跡するには1つ追加してください。",
|
||||
"portfolio_no_icon": "なし",
|
||||
"portfolio_outline_opacity": "アウトラインの不透明度",
|
||||
"portfolio_price": "価格",
|
||||
"portfolio_price_btc": "マーケット · BTC",
|
||||
"portfolio_price_drgx": "DRGXのみ",
|
||||
"portfolio_price_manual": "手動",
|
||||
"portfolio_price_usd": "市場 · USD",
|
||||
"portfolio_remove_from": "ポートフォリオから削除",
|
||||
"portfolio_revert": "元に戻す",
|
||||
"portfolio_save": "保存",
|
||||
"portfolio_search": "アドレスを検索…",
|
||||
"portfolio_search_icons": "アイコンを検索…",
|
||||
"portfolio_select_all": "すべて",
|
||||
"portfolio_select_shown": "すべて選択",
|
||||
"portfolio_show": "表示:",
|
||||
"portfolio_show_24h": "24時間",
|
||||
"portfolio_show_sparkline": "スパークライン",
|
||||
"portfolio_show_value": "評価額",
|
||||
"portfolio_spark_day": "日",
|
||||
"portfolio_spark_hour": "時間",
|
||||
"portfolio_spark_min": "分",
|
||||
"portfolio_spark_month": "月",
|
||||
"portfolio_spark_week": "週",
|
||||
"portfolio_style_compact": "コンパクト行",
|
||||
"portfolio_style_detailed": "詳細行",
|
||||
"portfolio_style_featured": "注目行",
|
||||
"portfolio_style_label": "ポートフォリオスタイル",
|
||||
"portfolio_untitled": "無題",
|
||||
"price_chart": "価格チャート",
|
||||
"privacy_great": "優れたプライバシーです!",
|
||||
"privacy_low": "プライバシーが低い — 資金をシールドしてください",
|
||||
"privacy_medium": "さらにシールドすることを検討してください",
|
||||
"processing_transaction": "取引を処理中...",
|
||||
"qr_code": "QRコード",
|
||||
"qr_failed": "QRコードの生成に失敗しました",
|
||||
"qr_title": "QRコード",
|
||||
@@ -472,11 +866,15 @@
|
||||
"receiving_addresses": "あなたの受信アドレス",
|
||||
"recent_received": "最近の受信",
|
||||
"recent_sends": "最近の送信",
|
||||
"recent_transactions": "最近のトランザクション",
|
||||
"recipient": "受取人",
|
||||
"recipient_balance": "受取人: %.8f → %.8f DRGX",
|
||||
"recv_type": "受信",
|
||||
"reduce_motion": "モーションを減らす",
|
||||
"refresh": "更新",
|
||||
"refresh_now": "今すぐ更新",
|
||||
"remove_favorite": "お気に入りを削除",
|
||||
"repair_wallet": "ウォレットを修復",
|
||||
"report_bug": "バグを報告",
|
||||
"request_amount": "金額(任意):",
|
||||
"request_copy_uri": "URIをコピー",
|
||||
@@ -492,9 +890,16 @@
|
||||
"request_transparent_addrs": "-- 透明アドレス --",
|
||||
"request_uri_copied": "支払いURIをクリップボードにコピーしました",
|
||||
"rescan": "再スキャン",
|
||||
"rescan_bootstrapped_msg": "ノードはブートストラップされているため、スナップショットより下のブロックはディスク上になく、ジェネシスからの再スキャンは失敗します。ウォレットの使用済み残高を照合するには、スナップショットに含まれる高さから再スキャンしてください。wallet.dat とチェーンデータは削除されません。",
|
||||
"rescan_detecting": "ノードがディスク上に保有しているブロックを確認中…",
|
||||
"rescan_from_height": "ブロック高から再スキャン:",
|
||||
"reset_to_defaults": "デフォルトにリセット",
|
||||
"restarting_after_encryption": "暗号化後にデーモンを再起動中...",
|
||||
"restore_address": "アドレスを復元",
|
||||
"result_preview": "結果プレビュー",
|
||||
"retry": "再試行",
|
||||
"review_send": "送金を確認",
|
||||
"rpc_connection": "RPC接続...",
|
||||
"rpc_host": "RPCホスト",
|
||||
"rpc_pass": "パスワード",
|
||||
"rpc_port": "ポート",
|
||||
@@ -502,8 +907,65 @@
|
||||
"save": "保存",
|
||||
"save_settings": "設定を保存",
|
||||
"save_z_transactions": "Z取引を取引リストに保存",
|
||||
"sb_auth_failed": "認証失敗 — rpcuser/rpcpassword を確認してください",
|
||||
"sb_block": "ブロック: %d",
|
||||
"sb_building_witnesses": "証人を設定中",
|
||||
"sb_building_witnesses_pct": "ウィットネスを再構築中 %.0f%%",
|
||||
"sb_connecting_daemon": "dragonxd に接続中...",
|
||||
"sb_connecting_err": "デーモンに接続中 — %s",
|
||||
"sb_connecting_external": "外部デーモンに接続中...",
|
||||
"sb_connecting_generic": "デーモンに接続中...",
|
||||
"sb_daemon_crashed": "デーモンが %d 回クラッシュしました",
|
||||
"sb_daemon_not_found": "デーモンが見つかりません",
|
||||
"sb_daemon_start_failed": "dragonxd を起動できませんでした",
|
||||
"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": "ウォームアップ中...",
|
||||
"sb_witness_cache": "ウィットネスを再構築中",
|
||||
"screenshot_open_dir": "場所を開く",
|
||||
"screenshot_sweep": "スクリーンショットスイープを実行",
|
||||
"screenshot_sweep_desc": "すべてのテーマをすべてのタブで巡回し、それぞれのスクリーンショットを設定ディレクトリの screenshots フォルダ内のタブごとのサブフォルダに保存します(前回のスイープを上書きします)。数秒間実行されます。",
|
||||
"screenshot_sweep_full": "UI全体スイープ",
|
||||
"search_icons": "アイコンを検索...",
|
||||
"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": "順番どおりに書き写し、オフラインで保管してください。決して共有や撮影をしないでください。紛失すると資金を復元できません。",
|
||||
"seed_migrate_button": "シードウォレットへ移行…",
|
||||
"select_address": "アドレスを選択...",
|
||||
"select_receiving_address": "受信アドレスを選択...",
|
||||
"select_source_address": "送信元アドレスを選択...",
|
||||
@@ -512,8 +974,10 @@
|
||||
"send_amount_details": "金額の詳細",
|
||||
"send_amount_upper": "金額",
|
||||
"send_clear_fields": "すべてのフォームフィールドをクリアしますか?",
|
||||
"send_contacts_button": "連絡先から選択",
|
||||
"send_copy_error": "エラーをコピー",
|
||||
"send_dismiss": "閉じる",
|
||||
"send_err_needs_rescan": "ウォレットのシールドノートデータがブロックチェーンと同期していません(ブートストラップや再インデックスの後に発生します)。設定 → ブロックチェーンを再スキャン から完全な再スキャンを実行し、最後まで完了させてから、もう一度送金をお試しください。",
|
||||
"send_error_copied": "エラーをクリップボードにコピーしました",
|
||||
"send_error_prefix": "エラー:%s",
|
||||
"send_exceeds_available": "利用可能額を超過 (%.8f)",
|
||||
@@ -532,6 +996,7 @@
|
||||
"send_recipient": "受取人",
|
||||
"send_select_source": "送信元アドレスを選択...",
|
||||
"send_sending_from": "送信元",
|
||||
"send_status_unconfirmed": "取引のステータスを確認できませんでした",
|
||||
"send_submitting": "取引を送信中...",
|
||||
"send_switch_to_receive": "受信に切り替えてアドレスを取得し、資金の受け取りを開始してください。",
|
||||
"send_to": "送金先",
|
||||
@@ -553,12 +1018,15 @@
|
||||
"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": "アクリルレベル:",
|
||||
@@ -601,6 +1069,8 @@
|
||||
"settings_noise_opacity": "ノイズ不透明度:",
|
||||
"settings_not_encrypted": "暗号化されていません",
|
||||
"settings_not_found": "見つかりません",
|
||||
"settings_open_app_dir": "アプリフォルダを開く",
|
||||
"settings_open_data_dir": "データフォルダを開く",
|
||||
"settings_other": "その他",
|
||||
"settings_pin_active": "PIN",
|
||||
"settings_privacy": "プライバシー",
|
||||
@@ -634,26 +1104,43 @@
|
||||
"shield_check_status": "ステータスを確認",
|
||||
"shield_completed": "操作が正常に完了しました!",
|
||||
"shield_description": "透明アドレスのcoinbase出力をシールドアドレスに送信して、マイニング報酬をシールドします。マイニング収入を隠すことでプライバシーが向上します。",
|
||||
"shield_error_prefix": "エラー: ",
|
||||
"shield_from_address": "送信元アドレス:",
|
||||
"shield_funds": "資金をシールド",
|
||||
"shield_in_progress": "操作進行中...",
|
||||
"shield_max_utxos": "1回の操作あたりの最大UTXO数",
|
||||
"shield_merge_done": "シールド/統合が完了しました!",
|
||||
"shield_op_failed": "操作に失敗しました: ",
|
||||
"shield_op_submitted": "操作を送信しました: ",
|
||||
"shield_operation_id": "オペレーション ID:%s",
|
||||
"shield_select_z": "zアドレスを選択...",
|
||||
"shield_send_failed": "シールドに失敗しました: ",
|
||||
"shield_started": "シールド操作を開始しました",
|
||||
"shield_status_check_error": "状態の確認エラー: ",
|
||||
"shield_status_label": "ステータス: ",
|
||||
"shield_submitting": "操作を送信中...",
|
||||
"shield_title": "Coinbase報酬をシールド",
|
||||
"shield_to_address": "送信先アドレス(シールド):",
|
||||
"shield_unknown_error": "不明なエラー",
|
||||
"shield_utxo_limit": "UTXO制限:",
|
||||
"shield_wildcard_hint": "'*' を使用してすべての透明アドレスからシールド",
|
||||
"shielded": "シールド",
|
||||
"shielded_address": "シールドアドレス",
|
||||
"shielded_to": "シールド先",
|
||||
"shielded_type": "シールド",
|
||||
"shielding_notice": "注意:トランスペアレント (T) アドレスからプライベート (Z) アドレスへ資金をシールドします。",
|
||||
"show": "表示",
|
||||
"show_hidden": "非表示を表示 (%d)",
|
||||
"show_qr": "QRを表示",
|
||||
"show_qr_code": "QRコードを表示",
|
||||
"showing_transactions": "%dâ%d / %d 件の取引を表示中(合計:%zu)",
|
||||
"showing_transactions": "%d–%d / %d 件の取引を表示中(合計:%zu)",
|
||||
"showing_x_of_y": "%d / %d アドレスを表示",
|
||||
"simple_background": "シンプル背景",
|
||||
"slider_off": "オフ",
|
||||
"sort_amount_high": "金額が大きい順",
|
||||
"sort_amount_low": "金額が小さい順",
|
||||
"sort_date_newest": "新しい順",
|
||||
"sort_date_oldest": "古い順",
|
||||
"start_mining": "マイニング開始",
|
||||
"status": "ステータス",
|
||||
"stop_external": "外部デーモンを停止",
|
||||
@@ -662,17 +1149,26 @@
|
||||
"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": "取引の送信に成功しました",
|
||||
@@ -680,7 +1176,13 @@
|
||||
"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": "この無操作時間後にウォレットをロック",
|
||||
@@ -693,8 +1195,12 @@
|
||||
"tt_clear_ztx": "ローカルにキャッシュされた z-トランザクション履歴を削除",
|
||||
"tt_custom_fees": "トランザクション送信時に手動手数料入力を有効化",
|
||||
"tt_custom_theme": "カスタムテーマがアクティブ",
|
||||
"tt_daemon_install_bundled": "ノードを停止し、インストール済みの dragonxd をこのウォレットビルドにバンドルされたバージョンで上書きしてから再起動します",
|
||||
"tt_daemon_update_check": "プロジェクトの Gitea から最新の dragonxd フルノードをダウンロードして検証し、再起動して適用します",
|
||||
"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 スプレッドシートとしてエクスポート",
|
||||
@@ -706,14 +1212,19 @@
|
||||
"tt_keep_daemon": "セットアップウィザード実行時にデーモンは停止します",
|
||||
"tt_language": "ウォレット UI のインターフェース言語",
|
||||
"tt_layout_hotkey": "ホットキー:左右矢印キーでバランスレイアウトを切り替え",
|
||||
"tt_lite_redownload": "ライトサーバーからすべてのブロックを再ダウンロードして再スキャン",
|
||||
"tt_lock": "ウォレットを即座にロック",
|
||||
"tt_low_spec": "すべての重い視覚効果を無効化\\nホットキー:Ctrl+Shift+Down",
|
||||
"tt_merge": "複数の UTXO を一つのアドレスに統合",
|
||||
"tt_mine_idle": "システムがアイドル状態(キーボード/マウス入力なし)\\nのとき自動的にマイニングを開始",
|
||||
"tt_noise": "グレインテクスチャ強度(0%% = オフ、100%% = 最大)",
|
||||
"tt_open_app_dir": "ObsidianDragon フォルダ(設定、テーマ、ログ)をファイルマネージャーで開く",
|
||||
"tt_open_data_dir": "ファイルマネージャーでウォレットとブロックチェーンデータのフォルダを開きます",
|
||||
"tt_open_dir": "クリックしてファイルエクスプローラーで開く",
|
||||
"tt_reduce_motion": "アクセシビリティのためにアニメーション遷移と残高補間を無効にする",
|
||||
"tt_remove_encrypt": "暗号化を解除してウォレットを保護なしで保存",
|
||||
"tt_remove_pin": "PIN を削除しアンロックにパスフレーズを要求",
|
||||
"tt_repair_wallet": "ウォレットのトランザクション記録をブロックチェーンから消去して再構築します(再スキャン後に送信に失敗するノートを修復します)",
|
||||
"tt_report_bug": "プロジェクトトラッカーで問題を報告",
|
||||
"tt_request_payment": "QR コード付きの支払い請求を生成",
|
||||
"tt_rescan": "欠落したトランザクションのためにブロックチェーンを再スキャン",
|
||||
@@ -727,6 +1238,8 @@
|
||||
"tt_save_ztx": "z-address トランザクション履歴をローカルに保存して高速読み込み",
|
||||
"tt_scan_themes": "新しいテーマをスキャン。\\nテーマフォルダーをここに配置:\\n%s",
|
||||
"tt_scanline": "コンソールでの CRT スキャンライン効果",
|
||||
"tt_seed_backup": "ウォレットの24単語の復元シードフレーズを表示してバックアップします",
|
||||
"tt_seed_migrate": "新しいシードフレーズウォレットを作成し、資金をそこへ移動します",
|
||||
"tt_set_pin": "クイックアンロック用の 4-8 桁 PIN を設定",
|
||||
"tt_shield_mining": "透明マイニング報酬をシールドアドレスに移動",
|
||||
"tt_simple_bg": "背景にシンプルなグラデーションを使用\\nホットキー:Ctrl+Up",
|
||||
@@ -740,6 +1253,7 @@
|
||||
"tt_ui_opacity": "カードとサイドバーの不透明度(100%% = 完全不透明、低い = より透過)",
|
||||
"tt_validate": "DragonX アドレスが有効かどうかを確認",
|
||||
"tt_verbose": "詳細な接続診断、デーモン状態、\\nポート所有者情報をコンソールタブに記録",
|
||||
"tt_wallets_button": "ウォレットファイルを一覧表示して切り替えます",
|
||||
"tt_website": "DragonX ウェブサイトを開く",
|
||||
"tt_window_opacity": "背景の不透明度(低い = デスクトップがウィンドウ越しに見える)",
|
||||
"tt_wizard": "初期セットアップウィザードを再実行\\nデーモンは再起動されます",
|
||||
@@ -748,7 +1262,18 @@
|
||||
"tx_from_address": "送信元アドレス:",
|
||||
"tx_id_label": "取引ID:",
|
||||
"tx_immature": "未成熟",
|
||||
"tx_loading_enriching_sends": "送信取引の詳細を確認中 (%d)",
|
||||
"tx_loading_fetching_transparent": "透明履歴を取得中",
|
||||
"tx_loading_history_progress": "過去の履歴を読み込み中 (%d%%)",
|
||||
"tx_loading_queued": "取引の更新を待機中",
|
||||
"tx_loading_refreshing_cached": "ウォレット履歴を更新中(%d 件キャッシュ済み)",
|
||||
"tx_loading_scanning_shielded": "シールド履歴をスキャン中 (%d アドレス)",
|
||||
"tx_mined": "採掘済み",
|
||||
"tx_progress_balances": "残高を更新中",
|
||||
"tx_progress_finalizing": "トランザクションを確定中",
|
||||
"tx_progress_history": "履歴を更新中",
|
||||
"tx_progress_submitting": "トランザクションをデーモンに送信中",
|
||||
"tx_progress_waiting_ops": "操作を待機中 (%d)",
|
||||
"tx_received": "受信済み",
|
||||
"tx_sent": "送信済み",
|
||||
"tx_to_address": "送信先アドレス:",
|
||||
@@ -760,6 +1285,14 @@
|
||||
"unconfirmed": "未確認",
|
||||
"undo_clear": "クリアを元に戻す",
|
||||
"unknown": "不明",
|
||||
"unmark_mining_address": "マイニングアドレスのマークを解除",
|
||||
"upd_back": "戻る",
|
||||
"upd_install": "インストール",
|
||||
"upd_installed_badge": "インストール済み",
|
||||
"upd_no_build_platform": "このプラットフォーム向けのビルドがありません",
|
||||
"upd_prerelease": "プレリリース",
|
||||
"upd_reinstall": "再インストール",
|
||||
"upd_select_version": "インストールするバージョンを選択",
|
||||
"use_embedded_daemon": "内蔵dragonxdを使用",
|
||||
"use_tor": "Torを使用",
|
||||
"validate_btn": "検証",
|
||||
@@ -779,17 +1312,91 @@
|
||||
"verbose_logging": "詳細ログ",
|
||||
"version": "バージョン",
|
||||
"view": "表示",
|
||||
"view_all": "すべて表示",
|
||||
"view_details": "詳細を表示",
|
||||
"view_on_explorer": "エクスプローラーで表示",
|
||||
"waiting_for_daemon": "デーモン接続を待機中...",
|
||||
"wallet": "ウォレット",
|
||||
"wallet_empty": "ウォレットは空です",
|
||||
"wallet_empty_hint": "受信に切り替えてアドレスを取得し、資金の受け取りを開始してください。",
|
||||
"wallets_active": "アクティブ",
|
||||
"wallets_add": "フォルダを追加",
|
||||
"wallets_add_folder": "ウォレットファイルを検索する別のフォルダを追加:",
|
||||
"wallets_add_folder_toggle": "+ 別のフォルダーをウォレット検索…",
|
||||
"wallets_badge_encrypted": "暗号化済み(パスフレーズ保護)",
|
||||
"wallets_badge_encrypted_short": "暗号化",
|
||||
"wallets_badge_legacy": "レガシーウォレット(シードフレーズなし)",
|
||||
"wallets_badge_legacy_short": "レガシー",
|
||||
"wallets_badge_seed": "シードフレーズウォレット (HD)",
|
||||
"wallets_badge_seed_short": "シードフレーズ",
|
||||
"wallets_badge_unknown": "ウォレットの種類を完全に判定できません(大きなファイル — 開いて確認)",
|
||||
"wallets_badge_unknown_short": "不明",
|
||||
"wallets_button": "ウォレット…",
|
||||
"wallets_col_addresses": "アドレス",
|
||||
"wallets_col_balance": "残高",
|
||||
"wallets_col_keys": "個の鍵",
|
||||
"wallets_col_name": "ウォレット",
|
||||
"wallets_col_opened": "最終使用",
|
||||
"wallets_col_size": "サイズ",
|
||||
"wallets_col_txs": "件",
|
||||
"wallets_create": "ウォレットを作成",
|
||||
"wallets_created": "作成",
|
||||
"wallets_creating": "ウォレットを作成中 — ノードが再起動します…",
|
||||
"wallets_current": "現在",
|
||||
"wallets_exists": "その名前のウォレットは既に存在します。",
|
||||
"wallets_external_tt": "データディレクトリの外 —「開く」はその場でリンクします(コピーなし)。",
|
||||
"wallets_folder_hint": "wallet .dat ファイルのあるフォルダのパス",
|
||||
"wallets_folder_invalid": "そのフォルダは存在しません。",
|
||||
"wallets_intro": "データディレクトリ(および追加したフォルダ)内のウォレットファイルです。開いて切り替えると、それを読み込むためにノードが再起動します。各ウォレットは個別のデータを保持します。",
|
||||
"wallets_name_invalid": "有効なウォレット名を入力してください。",
|
||||
"wallets_never": "未使用",
|
||||
"wallets_new_hint": "名前(例:savings)",
|
||||
"wallets_new_label": "新しいウォレットを作成:",
|
||||
"wallets_open": "開く",
|
||||
"wallets_open_failed": "このウォレットをその場で開けませんでした。データディレクトリとは別のドライブにある可能性があります — 同じドライブに移動してください(Windows では開発者モードを有効にするとドライブ間のリンクも可能になります)。",
|
||||
"wallets_open_folder": "フォルダーの場所を開く",
|
||||
"wallets_open_inplace_tt": "このウォレットをその場で開きます — データディレクトリにリンク(コピーなし)",
|
||||
"wallets_reveal": "フォルダを開く",
|
||||
"wallets_sort_addresses": "アドレス",
|
||||
"wallets_sort_asc": "昇順(古い/少ない/小さい順)",
|
||||
"wallets_sort_by": "並べ替え:",
|
||||
"wallets_sort_created": "作成",
|
||||
"wallets_sort_desc": "降順(新しい/多い/大きい順)",
|
||||
"wallets_sort_size": "サイズ",
|
||||
"wallets_sort_txs": "取引",
|
||||
"wallets_title": "ウォレット",
|
||||
"warning": "警告",
|
||||
"warning_upper": "警告!",
|
||||
"website": "ウェブサイト",
|
||||
"window_opacity": "ウィンドウ透明度",
|
||||
"wizard_daemon_start_failed": "デーモンの起動に失敗しました — 自動的に再試行されます",
|
||||
"xmrig_browse_releases": "すべてのリリースを表示…",
|
||||
"xmrig_checking": "最新のマイナーを確認中…",
|
||||
"xmrig_current": "現在:",
|
||||
"xmrig_download_install": "ダウンロードしてインストール",
|
||||
"xmrig_downloading": "ダウンロード中…",
|
||||
"xmrig_installed": "インストール済み:",
|
||||
"xmrig_installed_ok": "マイナーをインストールしました",
|
||||
"xmrig_installing": "インストール中…",
|
||||
"xmrig_latest": "最新:",
|
||||
"xmrig_loading_releases": "リリースを読み込み中…",
|
||||
"xmrig_none": "なし",
|
||||
"xmrig_reinstall": "再インストール",
|
||||
"xmrig_stop_mining_first": "マイナーを更新する前にマイニングを停止してください。",
|
||||
"xmrig_unavailable_body": "このプラットフォーム向けのマイナービルドは利用できません。",
|
||||
"xmrig_unavailable_title": "マイナーの更新は利用できません",
|
||||
"xmrig_unknown_error": "不明なエラー。",
|
||||
"xmrig_up_to_date": "マイナーは最新です",
|
||||
"xmrig_update_available": "新しいマイナーが利用可能です",
|
||||
"xmrig_update_button": "マイナーを更新…",
|
||||
"xmrig_update_failed": "更新に失敗しました",
|
||||
"xmrig_update_short": "更新",
|
||||
"xmrig_update_title": "マイナーを更新",
|
||||
"xmrig_verify_note": "ダウンロードは、インストール前にリリースで公開されている SHA-256 チェックサムと照合されます。",
|
||||
"xmrig_verifying": "検証中…",
|
||||
"xmrig_version": "バージョン:",
|
||||
"yes_clear": "はい、クリア",
|
||||
"your_addresses": "あなたのアドレス",
|
||||
"z_address": "Zアドレス",
|
||||
"z_addresses": "Zアドレス"
|
||||
}
|
||||
|
||||
609
res/lang/ko.json
609
res/lang/ko.json
@@ -28,6 +28,7 @@
|
||||
"address_book_add": "주소 추가",
|
||||
"address_book_add_new": "새로 추가",
|
||||
"address_book_added": "주소록에 주소를 추가했습니다",
|
||||
"address_book_confirm_delete": "삭제하시겠습니까?",
|
||||
"address_book_count": "저장된 주소 %zu개",
|
||||
"address_book_deleted": "항목이 삭제되었습니다",
|
||||
"address_book_edit": "주소 편집",
|
||||
@@ -39,10 +40,13 @@
|
||||
"address_copied": "주소가 클립보드에 복사되었습니다",
|
||||
"address_details": "주소 상세",
|
||||
"address_label": "주소:",
|
||||
"address_reorder_hint": "행의 가장자리에 놓으면 순서를 바꾸고, 가운데에 놓으면 이동합니다",
|
||||
"address_upper": "주소",
|
||||
"address_url": "주소 URL",
|
||||
"addresses_appear_here": "연결 후 수신 주소가 여기에 표시됩니다.",
|
||||
"advanced": "고급 설정",
|
||||
"advanced_effects": "고급 효과...",
|
||||
"ago": "전",
|
||||
"all_filter": "전체",
|
||||
"allow_custom_fees": "사용자 정의 수수료 허용",
|
||||
"amount": "금액",
|
||||
@@ -67,7 +71,10 @@
|
||||
"backup_wallet": "지갑 백업...",
|
||||
"backup_wallet_not_found": "경고: 예상 위치에서 wallet.dat를 찾을 수 없습니다",
|
||||
"balance": "잔액",
|
||||
"balance_history_collecting": "잔액 내역 — 데이터 수집 중...",
|
||||
"balance_layout": "잔액 레이아웃",
|
||||
"balance_shielded_fmt": "차폐: %.8f",
|
||||
"balance_transparent_fmt": "투명: %.8f",
|
||||
"ban": "차단",
|
||||
"banned_peers": "차단된 피어",
|
||||
"block": "블록",
|
||||
@@ -90,12 +97,52 @@
|
||||
"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 복사",
|
||||
@@ -106,8 +153,26 @@
|
||||
"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_lite_redownload_msg": "지갑에 다운로드된 블록 데이터를 지우고, 라이트 서버에서 모든 블록을 다시 가져와 재스캔합니다. 시간이 걸릴 수 있으며, 완료될 때까지 지갑에 동기화 진행률이 표시됩니다.",
|
||||
"confirm_lite_redownload_safe": "지갑, 키, 시드는 영향을 받지 않으며 — 블록 데이터만 다시 다운로드됩니다.",
|
||||
"confirm_lite_redownload_title": "블록 다시 다운로드",
|
||||
"confirm_reinstall_daemon_msg": "데몬을 중지하고, 설치된 dragonxd(및 dragonx-cli/dragonx-tx)를 이 지갑 빌드에 번들된 버전으로 덮어쓴 다음, 노드를 재시작합니다. 노드 바이너리를 복구하거나 업데이트할 때 사용하세요.",
|
||||
"confirm_reinstall_daemon_safe": "지갑, 키, 블록체인 데이터는 그대로이며 — 데몬 프로그램 파일만 교체됩니다.",
|
||||
"confirm_reinstall_daemon_title": "번들 데몬 설치",
|
||||
"confirm_repair_wallet_msg": "데몬을 -zapwallettxes=2 옵션으로 재시작합니다. 지갑의 모든 거래 및 노트 기록을 삭제한 뒤 블록체인에서 다시 생성합니다. 전체 재스캔 후에도 거래 생성이 실패할 때(\"Invalid sapling spend proof\" / \"shielded requirements not met\") 사용하세요. 시간이 오래 걸리며 완료될 때까지 지갑은 오프라인 상태로 유지됩니다.",
|
||||
"confirm_repair_wallet_safe": "키, 주소, 잔액은 그대로 유지되며 — 캐시된 거래 기록만 다시 만들어집니다.",
|
||||
"confirm_repair_wallet_title": "지갑 복구",
|
||||
"confirm_rescan_msg": "이 작업은 데몬을 다시 시작하고 지갑의 거래를 찾기 위해 전체 블록체인을 다시 스캔합니다. 오래 걸릴 수 있으며 완료될 때까지 지갑은 오프라인 상태로 유지됩니다.",
|
||||
"confirm_rescan_safe": "wallet.dat와 블록체인 데이터는 삭제되지 않고 — 재스캔만 수행됩니다.",
|
||||
"confirm_rescan_title": "블록체인 재스캔",
|
||||
"confirm_restart_daemon_msg": "변경된 옵션을 적용하기 위해 데몬을 중지했다가 다시 시작합니다. 지갑이 잠시 연결이 끊겼다가 다시 연결됩니다.",
|
||||
"confirm_restart_daemon_title": "데몬 재시작",
|
||||
"confirm_send": "전송 확인",
|
||||
"confirm_transaction": "거래 확인",
|
||||
"confirm_transfer": "이체 확인",
|
||||
"confirmations": "확인 수",
|
||||
"confirmations_display": "%d 확인 | %s",
|
||||
"confirmed": "확인됨",
|
||||
@@ -115,6 +180,7 @@
|
||||
"connected_peers": "연결된 피어",
|
||||
"connecting": "연결 중...",
|
||||
"console": "콘솔",
|
||||
"console_app": "앱",
|
||||
"console_auto_scroll": "자동 스크롤",
|
||||
"console_available_commands": "사용 가능한 명령어:",
|
||||
"console_capturing_output": "데몬 출력 캡처 중...",
|
||||
@@ -131,6 +197,7 @@
|
||||
"console_connected": "데몬에 연결됨",
|
||||
"console_copy_all": "모두 복사",
|
||||
"console_copy_selected": "복사",
|
||||
"console_copy_value": "복사",
|
||||
"console_daemon": "데몬",
|
||||
"console_daemon_error": "데몬 오류!",
|
||||
"console_daemon_started": "데몬이 시작되었습니다",
|
||||
@@ -149,16 +216,21 @@
|
||||
"console_help_setgenerate": " setgenerate - 채굴 제어",
|
||||
"console_help_stop": " stop - 데몬 중지",
|
||||
"console_line_count": "%zu줄",
|
||||
"console_matches": "일치",
|
||||
"console_new_lines": "%d 새 줄",
|
||||
"console_no_daemon": "데몬 없음",
|
||||
"console_not_connected": "오류: 데몬에 연결되지 않았습니다",
|
||||
"console_quit_note": "여기서는 'quit'/'exit'가 필요 없습니다 — 그냥 창을 닫으세요.",
|
||||
"console_rpc_reference": "RPC 명령어 참조",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "콘솔 스캔라인",
|
||||
"console_search_commands": "명령어 검색...",
|
||||
"console_select_all": "모두 선택",
|
||||
"console_show_app_output": "[app] 지갑 로그 줄 표시",
|
||||
"console_show_daemon_output": "데몬 출력 표시",
|
||||
"console_show_errors_only": "오류만 표시",
|
||||
"console_show_rpc_ref": "RPC 명령어 참조 표시",
|
||||
"console_show_rpc_trace": "앱 RPC 호출 표시",
|
||||
"console_showing_lines": "%zu / %zu줄 표시 중",
|
||||
"console_starting_node": "노드 시작 중...",
|
||||
"console_status_error": "오류",
|
||||
@@ -172,6 +244,13 @@
|
||||
"console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다",
|
||||
"console_zoom_in": "확대",
|
||||
"console_zoom_out": "축소",
|
||||
"contact_global": "모든 지갑에 표시(전역 연락처)",
|
||||
"contact_global_badge_tt": "전역 연락처 — 모든 지갑에서 표시됨",
|
||||
"contact_global_tt": "켜짐: 어떤 지갑을 불러오든 이 연락처가 계속 표시됩니다. 꺼짐: 현재 지갑에만 속합니다.",
|
||||
"contacts": "연락처",
|
||||
"contacts_search_no_match": "일치하는 연락처 없음",
|
||||
"contacts_search_placeholder": "연락처 검색...",
|
||||
"copied": "복사됨!",
|
||||
"copy": "복사",
|
||||
"copy_address": "전체 주소 복사",
|
||||
"copy_error": "오류 복사",
|
||||
@@ -180,15 +259,59 @@
|
||||
"copy_uri": "URI 복사",
|
||||
"current_price": "현재 가격",
|
||||
"custom_fees": "사용자 정의 수수료",
|
||||
"daemon_binary": "데몬 바이너리",
|
||||
"daemon_bundled": "번들",
|
||||
"daemon_install_bundled": "번들 버전 설치",
|
||||
"daemon_installed": "설치됨",
|
||||
"daemon_none_bundled": "이 빌드에 없음",
|
||||
"daemon_not_installed": "설치되지 않음",
|
||||
"daemon_status_differ": "설치된 바이너리가 번들 버전과 다릅니다.",
|
||||
"daemon_status_match": "설치된 바이너리가 번들 버전과 일치합니다.",
|
||||
"daemon_status_missing": "설치된 데몬 없음 — 번들 버전을 설치하세요.",
|
||||
"daemon_update_available": "새 노드 버전이 있습니다",
|
||||
"daemon_update_body": "이 지갑 빌드에는 현재 설치된 것보다 새로운 DragonX 노드가 번들로 포함되어 있습니다. 업데이트하면 설치된 dragonxd(및 dragonx-cli/dragonx-tx)가 교체된 후, 새 버전이 적용되도록 노드를 중지했다가 다시 시작합니다. 새 노드는 기존 노드에 없는 기능(예: 시드 문구 지원)을 추가할 수 있으므로 권장합니다.",
|
||||
"daemon_update_browse": "모든 릴리스 보기…",
|
||||
"daemon_update_check": "업데이트 확인…",
|
||||
"daemon_update_checking": "최신 노드 확인 중…",
|
||||
"daemon_update_downgrade_note": "이전 버전은 현재 체인 데이터와 호환되지 않을 수 있습니다. 다른 버전을 설치하면 데몬을 재시작한 후 적용됩니다.",
|
||||
"daemon_update_download_install": "다운로드 및 설치",
|
||||
"daemon_update_downloading": "다운로드 중…",
|
||||
"daemon_update_failed": "업데이트 실패",
|
||||
"daemon_update_installed": "설치됨:",
|
||||
"daemon_update_installed_ok": "노드가 설치되었습니다",
|
||||
"daemon_update_installing": "설치 중…",
|
||||
"daemon_update_keep": "현재 버전 유지",
|
||||
"daemon_update_later": "나중에",
|
||||
"daemon_update_latest": "최신:",
|
||||
"daemon_update_loading": "릴리스 불러오는 중…",
|
||||
"daemon_update_now": "지금 업데이트",
|
||||
"daemon_update_reinstall": "다시 설치",
|
||||
"daemon_update_restart_note": "새 버전을 실행하려면 데몬을 재시작하세요.",
|
||||
"daemon_update_restart_now": "지금 데몬 재시작",
|
||||
"daemon_update_safe": "지갑, 키, 블록체인 데이터는 그대로이며 — 데몬 프로그램 파일만 교체됩니다. 커스텀 노드를 일부러 실행 중이라면 현재 버전 유지를 선택하세요.",
|
||||
"daemon_update_title": "노드 데몬을 업데이트할까요?",
|
||||
"daemon_update_unavailable_body": "이 플랫폼에 사용할 수 있는 노드 빌드가 없습니다.",
|
||||
"daemon_update_unavailable_title": "노드 업데이트를 사용할 수 없음",
|
||||
"daemon_update_unknown_error": "알 수 없는 오류입니다.",
|
||||
"daemon_update_up_to_date": "노드가 최신 상태입니다",
|
||||
"daemon_update_verify_note": "다운로드는 설치 전에 릴리스에 게시된 SHA-256과 고정된 ed25519 서명으로 검증됩니다.",
|
||||
"daemon_update_verifying": "확인 중…",
|
||||
"daemon_update_version": "버전:",
|
||||
"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": "오류",
|
||||
@@ -196,6 +319,27 @@
|
||||
"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_hash_not_found": "이 해시에 해당하는 블록이나 트랜잭션을 찾을 수 없습니다",
|
||||
"explorer_invalid_query": "블록 높이 또는 64자 해시를 입력하세요",
|
||||
"explorer_invalid_response": "데몬으로부터 잘못된 응답",
|
||||
"explorer_mempool": "멤풀",
|
||||
"explorer_mempool_size": "크기",
|
||||
"explorer_mempool_txs": "트랜잭션",
|
||||
"explorer_no_results": "일치하는 캐시된 블록이 없습니다",
|
||||
"explorer_not_connected": "데몬에 연결되지 않음 — 블록 또는 트랜잭션 해시를 조회할 수 없습니다",
|
||||
"explorer_recent_blocks": "최근 블록",
|
||||
"explorer_search": "검색",
|
||||
"explorer_section": "탐색기",
|
||||
"explorer_tx_outputs": "출력",
|
||||
"explorer_tx_size": "크기",
|
||||
"export": "내보내기",
|
||||
"export_csv": "CSV 내보내기",
|
||||
"export_keys_btn": "키 내보내기",
|
||||
@@ -224,16 +368,25 @@
|
||||
"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_qr": "QR 숨기기",
|
||||
"hide_zero_balances": "잔액 0 숨기기",
|
||||
"history": "내역",
|
||||
"immature_type": "미성숙",
|
||||
@@ -253,6 +406,9 @@
|
||||
"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": "유지",
|
||||
@@ -266,29 +422,134 @@
|
||||
"key_export_viewing_keys_zonly": "보기 키는 차폐 (z) 주소에만 사용할 수 있습니다",
|
||||
"key_export_viewing_warning": "이 조회 키를 사용하면 다른 사람이 수신 거래와 잔액을 볼 수 있지만 자금을 사용할 수는 없습니다. 신뢰할 수 있는 사람에게만 공유하세요.",
|
||||
"label": "라벨:",
|
||||
"label_placeholder": "예: 저축, 채굴...",
|
||||
"language": "언어",
|
||||
"light": "라이트",
|
||||
"lite_account_label": "계정",
|
||||
"lite_action": "작업",
|
||||
"lite_backup_keys": "백업 및 키",
|
||||
"lite_birthday_backup": "생성 블록: %llu (이 값도 백업하세요)",
|
||||
"lite_birthday_hint": "스캔을 시작할 블록 높이입니다. 모르면 0으로 두세요(전체 스캔이 느려짐).",
|
||||
"lite_birthday_label": "생일 블록",
|
||||
"lite_console_help_passthrough": "그 외 입력은 라이트 지갑 콘솔 명령으로 실행됩니다.",
|
||||
"lite_copy": "복사",
|
||||
"lite_could_not_write": "쓸 수 없습니다: ",
|
||||
"lite_encrypt_wallet": "지갑 암호화",
|
||||
"lite_encryption_removed": "암호화가 제거되었습니다",
|
||||
"lite_hide_wipe": "숨기고 삭제",
|
||||
"lite_import": "가져오기",
|
||||
"lite_import_key_label": "키 가져오기",
|
||||
"lite_key_imported": "키를 가져왔습니다 — 동기화를 실행하여 기록을 스캔하세요",
|
||||
"lite_lock_failed": "잠금 실패",
|
||||
"lite_lock_now": "지금 잠그기",
|
||||
"lite_maintenance": "유지 관리",
|
||||
"lite_net_add": "추가",
|
||||
"lite_net_add_label_hint": "레이블(선택 사항)",
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "확인 중…",
|
||||
"lite_net_connected": "연결됨",
|
||||
"lite_net_custom": "사용자 지정",
|
||||
"lite_net_disconnected": "연결되지 않음",
|
||||
"lite_net_hidden_section": "숨겨진 서버",
|
||||
"lite_net_hide": "숨기기",
|
||||
"lite_net_in_use": "사용 중",
|
||||
"lite_net_intro": "사용할 서버를 선택하거나, 지갑이 무작위로 선택하도록 하세요. 변경 사항은 즉시 적용됩니다.",
|
||||
"lite_net_invalid_url": "유효한 http(s):// URL을 입력하세요.",
|
||||
"lite_net_no_wallet": "열린 지갑 없음",
|
||||
"lite_net_official": "공식",
|
||||
"lite_net_offline": "오프라인",
|
||||
"lite_net_random_active": "무작위 서버 선택이 활성화되어 있습니다.",
|
||||
"lite_net_random_server": "무작위 서버",
|
||||
"lite_net_refresh": "새로고침",
|
||||
"lite_net_show_hidden": "숨긴 서버 표시",
|
||||
"lite_net_sync_label": "동기화",
|
||||
"lite_net_synced": "동기화됨",
|
||||
"lite_net_syncing": "동기화 중",
|
||||
"lite_net_title": "라이트 서버",
|
||||
"lite_net_unhide": "숨김 해제",
|
||||
"lite_net_use_random": "매번 무작위 서버 사용",
|
||||
"lite_no_wallet": "열린 지갑 없음 — 설정에서 지갑을 만들거나 여세요",
|
||||
"lite_no_wallet_short": "열린 지갑 없음",
|
||||
"lite_op_create": "생성",
|
||||
"lite_op_open": "열기",
|
||||
"lite_op_restore": "복원",
|
||||
"lite_overwrite": "덮어쓰기",
|
||||
"lite_passphrase_label": "암호",
|
||||
"lite_private_keys_warning": "개인 키 — 이 키를 가진 사람은 누구나 당신의 자금을 사용할 수 있습니다",
|
||||
"lite_redownload_blocks": "블록 다시 다운로드",
|
||||
"lite_redownload_desc": "라이트 서버에서 모든 블록을 다시 가져옵니다(잔액이나 내역이 잘못 보일 때 사용).",
|
||||
"lite_redownload_running": "블록 다시 다운로드 중…",
|
||||
"lite_remove_encryption": "암호화 제거",
|
||||
"lite_restore_birthday_label": "생일 블록 (선택 사항 — 0은 처음부터 스캔하며 느림)",
|
||||
"lite_restore_btn": "지갑 복원",
|
||||
"lite_restore_intro": "복구 시드 문구를 입력하세요. 지갑이 복원된 후 라이트 서버에서 동기화됩니다.",
|
||||
"lite_restore_ok": "지갑이 복원되었습니다 — 라이트 서버에서 동기화 중…",
|
||||
"lite_restore_seed_label": "시드 문구(단어를 공백으로 구분)",
|
||||
"lite_restore_title": "시드 문구로 복구",
|
||||
"lite_save_to_file": "파일로 저장",
|
||||
"lite_saved_to": "저장됨(평문, 소유자 전용): ",
|
||||
"lite_security": "보안",
|
||||
"lite_seed_label": "시드",
|
||||
"lite_seed_warning": "시드 문구 — 지갑을 복구할 수 있는 유일한 방법입니다. 종이에 적어 오프라인으로 보관하고 절대 공유하지 마세요.",
|
||||
"lite_servers_network_tab": "라이트 서버는 네트워크 탭에서 관리합니다.",
|
||||
"lite_show_private_keys": "개인 키 표시",
|
||||
"lite_show_seed": "시드 표시",
|
||||
"lite_unlock": "잠금 해제",
|
||||
"lite_unlock_btn": "잠금 해제",
|
||||
"lite_unlock_failed": "잠금 해제 실패 — 암호가 틀렸나요?",
|
||||
"lite_unlock_msg": "지출을 위해 지갑의 잠금을 해제하려면 암호를 입력하세요.",
|
||||
"lite_unlock_ok": "지갑 잠금이 해제되었습니다 — 이제 전송할 수 있습니다",
|
||||
"lite_unlock_title": "지갑 잠금 해제",
|
||||
"lite_validate": "확인",
|
||||
"lite_wallet_encrypted": "지갑이 암호화되었습니다",
|
||||
"lite_wallet_label": "지갑",
|
||||
"lite_wallet_locked": "지갑 잠김",
|
||||
"lite_wallet_ready": "지갑 준비됨",
|
||||
"lite_wallet_request": "라이트 지갑 요청",
|
||||
"lite_wallet_unlocked": "지갑 잠금이 해제되었습니다",
|
||||
"lite_welcome_create": "새 지갑 만들기",
|
||||
"lite_welcome_create_failed": "지갑을 만들 수 없습니다",
|
||||
"lite_welcome_created": "지갑이 생성되었습니다 — 지금 설정 → 백업 및 키에서 복구 문구를 백업하세요",
|
||||
"lite_welcome_later": "나중에",
|
||||
"lite_welcome_msg": "아직 지갑이 없습니다. 새로 만들거나 복구 문구로 복원하세요.",
|
||||
"lite_welcome_restore": "시드로 복원",
|
||||
"lite_welcome_restore_hint": "설정 → 라이트 지갑 요청에서 지갑을 복원하세요",
|
||||
"lite_welcome_title": "ObsidianDragon Lite에 오신 것을 환영합니다",
|
||||
"lite_word_count": "%d / 24 단어",
|
||||
"lite_working": "작업 중…",
|
||||
"loading": "로딩 중...",
|
||||
"loading_addresses": "주소 로딩 중...",
|
||||
"loading_transactions": "거래를 불러오는 중",
|
||||
"local_hashrate": "로컬 해시레이트",
|
||||
"low_spec_mode": "저사양 모드",
|
||||
"mark_mining_address": "채굴 주소로 표시",
|
||||
"market": "시장",
|
||||
"market_12h": "12시간",
|
||||
"market_18h": "18시간",
|
||||
"market_24h": "24시간",
|
||||
"market_24h_change": "24시간",
|
||||
"market_24h_volume": "24시간 거래량",
|
||||
"market_6h": "6시간",
|
||||
"market_attribution": "가격 데이터: NonKYC 제공",
|
||||
"market_btc_price": "BTC 가격",
|
||||
"market_cap": "시가총액",
|
||||
"market_cap_short": "시총",
|
||||
"market_iv_1d": "1일",
|
||||
"market_iv_1h": "1시간",
|
||||
"market_iv_1m": "1개월",
|
||||
"market_iv_1w": "1주",
|
||||
"market_iv_live": "실시간",
|
||||
"market_no_history": "가격 내역 없음",
|
||||
"market_no_price": "가격 데이터 없음",
|
||||
"market_now": "현재",
|
||||
"market_pct_shielded": "%.0f%% 차폐됨",
|
||||
"market_portfolio": "포트폴리오",
|
||||
"market_price_loading": "가격 데이터를 불러오는 중...",
|
||||
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
|
||||
"market_refresh_price": "가격 데이터 새로고침",
|
||||
"market_trade_on": "%s에서 거래",
|
||||
"market_updated": "\\xc2\\xb7 업데이트됨 %s",
|
||||
"market_vol_short": "거래량",
|
||||
"mature": "성숙됨",
|
||||
"max": "최대",
|
||||
"memo": "메모 (선택, 암호화)",
|
||||
@@ -298,8 +559,52 @@
|
||||
"memo_z_only": "참고: 메모는 차폐 (z) 주소로 전송할 때만 사용할 수 있습니다",
|
||||
"merge_description": "여러 UTXO를 단일 차폐 주소로 통합합니다. 지갑 크기를 줄이고 프라이버시를 향상시킵니다.",
|
||||
"merge_funds": "자금 통합",
|
||||
"merge_send_failed": "통합 실패: ",
|
||||
"merge_started": "통합 작업이 시작되었습니다",
|
||||
"merge_title": "주소로 통합",
|
||||
"mig_adopt": "이 지갑을 내 지갑으로 사용",
|
||||
"mig_adopt_now": "시드 지갑 사용",
|
||||
"mig_adopting": "새 지갑을 설치하고 재스캔하는 중…",
|
||||
"mig_adopting_note": "노드가 새 지갑으로 다시 시작되어 체인을 재스캔합니다 — 몇 분 정도 걸릴 수 있습니다. 지갑을 계속 실행해 두세요. 자동으로 다시 연결됩니다.",
|
||||
"mig_already_mnemonic": "지갑에 이미 24단어 시드 문구가 있어 이전할 것이 없습니다. 백업해서 단어들을 안전한 곳에 보관하기만 하면 됩니다.",
|
||||
"mig_backed_up": "시드 문구를 적어 두었습니다",
|
||||
"mig_backup_instead": "시드 문구 백업",
|
||||
"mig_balance": "현재 지갑 잔액: %.8f DRGX (소액 수수료 제외)",
|
||||
"mig_check_failed": "지갑을 확인할 수 없습니다. 잠금이 해제되고 연결되어 있는지 확인한 다음 다시 확인하세요.",
|
||||
"mig_checking_balance": "잔액을 확인하는 중",
|
||||
"mig_confirming": "지갑을 전환하기 전에 쓸어 담기가 온체인에서 확인되기를 기다리는 중입니다 — 이는 자금이 실제로 이동했음을 보장합니다.",
|
||||
"mig_confs": "쓸어 담기 확인 수: %d",
|
||||
"mig_continue_sweep": "쓸어 담기로 계속",
|
||||
"mig_copy_seed": "시드 복사",
|
||||
"mig_create": "시드 지갑 생성",
|
||||
"mig_daemon_too_old": "DragonX 노드가 너무 오래되어 시드 지갑을 만들 수 없습니다 — 여기에 필요한 니모닉 지원이 없습니다. 노드를 업데이트한 뒤(설정 → 노드 및 보안 → 번들 설치, 또는 업데이트 확인) 다시 시도하세요.",
|
||||
"mig_discard": "이전 취소",
|
||||
"mig_done": "이전이 완료되었습니다. 이제 지갑이 시드 문구로 백업됩니다.",
|
||||
"mig_done_btn": "완료",
|
||||
"mig_done_detail": "데몬이 자금을 표시하기 위해 재스캔하는 중입니다 — 몇 분 정도 걸릴 수 있습니다. 이전 지갑은 데이터 폴더에 .bak 파일로 저장되었습니다.",
|
||||
"mig_intro": "24단어 시드 문구로 백업되는 완전히 새로운 지갑을 격리된 노드에 생성하여 자금을 그곳으로 옮길 수 있습니다. 현재 지갑은 그대로 유지되며 이 단계에서는 어떤 자금도 이동하지 않습니다 — 먼저 새 시드를 백업한 다음, 별도의 확인된 단계로 자금을 그곳에 쓸어 담습니다.",
|
||||
"mig_later": "나중에",
|
||||
"mig_no_funds": "지갑에 이전할 자금이 없습니다. 새 시드 지갑을 바로 사용할 수 있습니다 — 현재의 (비어 있는) 지갑을 시드로 백업되는 지갑으로 교체합니다. 만일을 대비해 이전 지갑은 타임스탬프가 붙은 백업으로 별도 보관됩니다.",
|
||||
"mig_nofunds_confirm": "현재 지갑이 새 시드 지갑으로 교체된다는 것을 이해합니다",
|
||||
"mig_not_connected": "먼저 노드에 연결한 다음, 이 화면을 다시 열어 이전하세요.",
|
||||
"mig_precheck_checking": "지갑을 확인하는 중",
|
||||
"mig_receive_addr": "새 지갑 수신 주소:",
|
||||
"mig_recheck": "다시 확인",
|
||||
"mig_remainder": "일부 자금이 아직 이전 지갑에 남아 있습니다 (한 거래에 담기에는 입력이 너무 많음) — 전환하기 전에 나머지를 쓸어 담으세요.",
|
||||
"mig_remaining": "이전 지갑에 남은 잔액: %.8f DRGX",
|
||||
"mig_seed_warning": "이 24개의 단어를 순서대로 종이에 적어 오프라인으로 보관하세요. 이 단어들은 새 지갑의 유일한 백업입니다 — 잃어버리면 이전한 자금을 영원히 복구할 수 없습니다.",
|
||||
"mig_step1_done": "2단계 중 1단계 완료 — 아직 자금이 이동하지 않았습니다. 이 새 지갑으로 자금을 쓸어 담는 것이 다음 단계입니다.",
|
||||
"mig_sweep_all": "모든 자금 쓸어 담기",
|
||||
"mig_sweep_intro": "이제 현재 지갑의 모든 자금을 새 시드 지갑으로 쓸어 담습니다. 새 지갑 주소로 단일 거래를 전송하며, 이후로는 (이미 백업한) 시드 문구가 자금을 관리합니다.",
|
||||
"mig_sweep_remaining": "나머지 쓸어 담기",
|
||||
"mig_sweeping": "새 지갑으로 자금을 쓸어 담는 중…",
|
||||
"mig_title": "시드 지갑으로 이전",
|
||||
"mig_to": "받는 곳: ",
|
||||
"mig_txid": "txid: ",
|
||||
"mig_unlock_first": "지갑에서 자금을 전송하려면 먼저 잠금을 해제하세요.",
|
||||
"mig_unlock_to_migrate": "먼저 지갑의 잠금을 해제한 다음, 이 화면을 다시 열어 이전하세요.",
|
||||
"mig_waiting_mine": "쓸어 담기 거래가 채굴되기를 기다리는 중…",
|
||||
"mig_working": "격리된 노드에 새 시드 지갑을 생성하는 중입니다 — 1분 정도 걸릴 수 있습니다. 메인 지갑은 계속 실행됩니다.",
|
||||
"mine_when_idle": "유휴 시 채굴",
|
||||
"mined": "채굴됨",
|
||||
"mined_filter": "채굴됨",
|
||||
@@ -311,6 +616,15 @@
|
||||
"mining_address_copied": "채굴 주소가 복사되었습니다",
|
||||
"mining_all_time": "전체 기간",
|
||||
"mining_already_saved": "풀 URL이 이미 저장되어 있습니다",
|
||||
"mining_auto_balance_desc": "네트워크 분산화를 돕기 위해 해시레이트에 따라 채굴기를 공식 풀에 분산합니다. 각 풀을 주기적으로 확인합니다.",
|
||||
"mining_auto_balanced_to": "채굴을 자동 조정한 대상",
|
||||
"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분 전",
|
||||
@@ -330,11 +644,20 @@
|
||||
"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_manual_desc": "수동: 아래 목록에서 선택한 풀로 채굴합니다.",
|
||||
"mining_mine": "채굴",
|
||||
"mining_mining_addr": "채굴 주소",
|
||||
"mining_mining_here": "여기서 채굴 중",
|
||||
"mining_network": "네트워크",
|
||||
"mining_no_blocks_yet": "아직 블록을 찾지 못했습니다",
|
||||
"mining_no_payouts_yet": "아직 풀 지급이 없습니다",
|
||||
@@ -344,18 +667,24 @@
|
||||
"mining_on": "채굴이 켜져 있습니다",
|
||||
"mining_open_in_explorer": "탐색기에서 열기",
|
||||
"mining_payout_address": "지급 주소",
|
||||
"mining_payout_foreign": "⚠ 이 지급 주소는 현재 지갑에 없습니다 — 채굴한 보상이 다른 지갑으로 전송됩니다. 지갑을 전환했다면 주소를 업데이트하세요.",
|
||||
"mining_payout_tooltip": "채굴 보상 수신 주소",
|
||||
"mining_pool": "풀",
|
||||
"mining_pool_fee": "수수료",
|
||||
"mining_pool_hashrate": "풀 해시레이트",
|
||||
"mining_pool_url": "풀 URL",
|
||||
"mining_pools_header": "풀",
|
||||
"mining_recent_blocks": "최근 블록",
|
||||
"mining_recent_payouts": "최근 풀 지급",
|
||||
"mining_refresh": "새로고침",
|
||||
"mining_remove": "제거",
|
||||
"mining_reset_defaults": "기본값으로 재설정",
|
||||
"mining_save_payout_address": "지급 주소 저장",
|
||||
"mining_save_pool_url": "풀 URL 저장",
|
||||
"mining_saved_addresses": "저장된 주소:",
|
||||
"mining_saved_pools": "저장된 풀:",
|
||||
"mining_select_auto": "자동 분산",
|
||||
"mining_select_manual": "수동",
|
||||
"mining_shares": "셰어",
|
||||
"mining_show_chart": "차트",
|
||||
"mining_show_log": "로그",
|
||||
@@ -368,7 +697,9 @@
|
||||
"mining_stop_solo_for_pool_settings": "풀 설정을 변경하려면 솔로 채굴을 중지하세요",
|
||||
"mining_stopping": "중지 중...",
|
||||
"mining_stopping_tooltip": "채굴기 중지 중...",
|
||||
"mining_suggested_pools": "추천 풀",
|
||||
"mining_syncing_tooltip": "블록체인 동기화 중...",
|
||||
"mining_tag": " · 채굴",
|
||||
"mining_threads": "채굴 스레드",
|
||||
"mining_to_save": "저장하려면",
|
||||
"mining_today": "오늘",
|
||||
@@ -388,10 +719,13 @@
|
||||
"no_addresses_available": "사용 가능한 주소 없음",
|
||||
"no_addresses_match": "필터와 일치하는 주소가 없습니다",
|
||||
"no_addresses_with_balance": "잔액이 있는 주소가 없습니다",
|
||||
"no_addresses_yet": "아직 주소가 없습니다",
|
||||
"no_icons_found": "검색과 일치하는 아이콘이 없습니다.",
|
||||
"no_matching": "일치하는 거래가 없습니다",
|
||||
"no_recent_receives": "최근 수신 내역 없음",
|
||||
"no_recent_sends": "최근 전송 내역 없음",
|
||||
"no_transactions": "거래 내역이 없습니다",
|
||||
"no_transactions_yet": "아직 거래 내역이 없습니다",
|
||||
"node": "노드",
|
||||
"node_security": "노드 및 보안",
|
||||
"noise": "노이즈",
|
||||
@@ -453,8 +787,68 @@
|
||||
"peers_upper": "피어",
|
||||
"peers_version": "버전",
|
||||
"pending": "대기 중",
|
||||
"pin_not_set": "PIN이 설정되지 않았습니다. 암호를 사용하여 잠금 해제하세요.",
|
||||
"ping": "Ping",
|
||||
"portfolio_add_entry": "항목 추가",
|
||||
"portfolio_add_to": "포트폴리오에 추가",
|
||||
"portfolio_addresses_hdr": "주소",
|
||||
"portfolio_addresses_sel": "%d개 선택됨",
|
||||
"portfolio_all_funds": "모든 자금",
|
||||
"portfolio_all_shielded": "모두 차폐",
|
||||
"portfolio_all_transparent": "전체 투명",
|
||||
"portfolio_appearance": "외관",
|
||||
"portfolio_cancel": "취소",
|
||||
"portfolio_clear_sel": "지우기",
|
||||
"portfolio_close": "닫기",
|
||||
"portfolio_color": "색상",
|
||||
"portfolio_currency": "통화",
|
||||
"portfolio_custom_color": "사용자 지정 색상\\xE2\\x80\\xA6",
|
||||
"portfolio_delete": "삭제",
|
||||
"portfolio_detail_empty": "왼쪽에서 그룹을 선택하거나 새로 추가하여 편집하세요.",
|
||||
"portfolio_edit": "편집",
|
||||
"portfolio_funded": "자금 보유",
|
||||
"portfolio_group_name": "그룹 이름",
|
||||
"portfolio_icon": "아이콘",
|
||||
"portfolio_label": "라벨",
|
||||
"portfolio_manage": "관리…",
|
||||
"portfolio_manage_title": "포트폴리오 관리",
|
||||
"portfolio_manual_price": "가격 / DRGX",
|
||||
"portfolio_new_entry": "새 항목",
|
||||
"portfolio_no_addr_match": "일치하는 주소 없음",
|
||||
"portfolio_no_entries": "아직 사용자 지정 항목이 없습니다. 주소 그룹을 추적하려면 항목을 추가하세요.",
|
||||
"portfolio_no_icon": "없음",
|
||||
"portfolio_outline_opacity": "외곽선 불투명도",
|
||||
"portfolio_price": "가격",
|
||||
"portfolio_price_btc": "시장 · BTC",
|
||||
"portfolio_price_drgx": "DRGX만",
|
||||
"portfolio_price_manual": "수동",
|
||||
"portfolio_price_usd": "시장 \\xC2\\xB7 USD",
|
||||
"portfolio_remove_from": "포트폴리오에서 제거",
|
||||
"portfolio_revert": "되돌리기",
|
||||
"portfolio_save": "저장",
|
||||
"portfolio_search": "주소 검색…",
|
||||
"portfolio_search_icons": "아이콘 검색\\xE2\\x80\\xA6",
|
||||
"portfolio_select_all": "전체",
|
||||
"portfolio_select_shown": "모두 선택",
|
||||
"portfolio_show": "표시:",
|
||||
"portfolio_show_24h": "24시간",
|
||||
"portfolio_show_sparkline": "스파크라인",
|
||||
"portfolio_show_value": "가치",
|
||||
"portfolio_spark_day": "일",
|
||||
"portfolio_spark_hour": "시간",
|
||||
"portfolio_spark_min": "분",
|
||||
"portfolio_spark_month": "월",
|
||||
"portfolio_spark_week": "주",
|
||||
"portfolio_style_compact": "간결한 행",
|
||||
"portfolio_style_detailed": "상세 행",
|
||||
"portfolio_style_featured": "강조 행",
|
||||
"portfolio_style_label": "포트폴리오 스타일",
|
||||
"portfolio_untitled": "제목 없음",
|
||||
"price_chart": "가격 차트",
|
||||
"privacy_great": "프라이버시가 우수합니다!",
|
||||
"privacy_low": "낮은 프라이버시 — 자금을 차폐하세요",
|
||||
"privacy_medium": "더 많은 자금을 차폐하는 것을 고려하세요",
|
||||
"processing_transaction": "거래 처리 중...",
|
||||
"qr_code": "QR 코드",
|
||||
"qr_failed": "QR 코드 생성 실패",
|
||||
"qr_title": "QR 코드",
|
||||
@@ -472,11 +866,15 @@
|
||||
"receiving_addresses": "수신 주소",
|
||||
"recent_received": "최근 수신",
|
||||
"recent_sends": "최근 전송",
|
||||
"recent_transactions": "최근 거래",
|
||||
"recipient": "수신자",
|
||||
"recipient_balance": "수신자: %.8f → %.8f DRGX",
|
||||
"recv_type": "수신",
|
||||
"reduce_motion": "모션 줄이기",
|
||||
"refresh": "새로고침",
|
||||
"refresh_now": "지금 새로고침",
|
||||
"remove_favorite": "즐겨찾기 제거",
|
||||
"repair_wallet": "지갑 복구",
|
||||
"report_bug": "버그 신고",
|
||||
"request_amount": "금액 (선택):",
|
||||
"request_copy_uri": "URI 복사",
|
||||
@@ -492,9 +890,16 @@
|
||||
"request_transparent_addrs": "-- 투명 주소 --",
|
||||
"request_uri_copied": "결제 URI가 클립보드에 복사되었습니다",
|
||||
"rescan": "재스캔",
|
||||
"rescan_bootstrapped_msg": "노드가 부트스트랩되어 스냅샷 이전 블록은 디스크에 없으므로 제네시스부터의 재스캔은 실패합니다. 지갑의 사용된 잔액을 조정하려면 스냅샷에 포함된 높이부터 재스캔하세요. wallet.dat과 체인 데이터는 삭제되지 않습니다.",
|
||||
"rescan_detecting": "노드가 디스크에 어떤 블록을 가지고 있는지 확인하는 중…",
|
||||
"rescan_from_height": "재스캔 시작 블록 높이:",
|
||||
"reset_to_defaults": "기본값으로 재설정",
|
||||
"restarting_after_encryption": "암호화 후 데몬 재시작 중...",
|
||||
"restore_address": "주소 복원",
|
||||
"result_preview": "결과 미리보기",
|
||||
"retry": "재시도",
|
||||
"review_send": "전송 검토",
|
||||
"rpc_connection": "RPC 연결...",
|
||||
"rpc_host": "RPC 호스트",
|
||||
"rpc_pass": "비밀번호",
|
||||
"rpc_port": "포트",
|
||||
@@ -502,8 +907,65 @@
|
||||
"save": "저장",
|
||||
"save_settings": "설정 저장",
|
||||
"save_z_transactions": "Z 거래를 거래 목록에 저장",
|
||||
"sb_auth_failed": "인증 실패 — rpcuser/rpcpassword를 확인하세요",
|
||||
"sb_block": "블록: %d",
|
||||
"sb_building_witnesses": "증인 설정 중",
|
||||
"sb_building_witnesses_pct": "위트니스 재생성 중 %.0f%%",
|
||||
"sb_connecting_daemon": "dragonxd에 연결 중...",
|
||||
"sb_connecting_err": "데몬 연결 중 — %s",
|
||||
"sb_connecting_external": "외부 데몬에 연결 중...",
|
||||
"sb_connecting_generic": "데몬에 연결 중...",
|
||||
"sb_daemon_crashed": "데몬이 %d회 충돌함",
|
||||
"sb_daemon_not_found": "데몬을 찾을 수 없음",
|
||||
"sb_daemon_start_failed": "dragonxd를 시작할 수 없습니다",
|
||||
"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": "워밍업 중...",
|
||||
"sb_witness_cache": "증인 재구축 중",
|
||||
"screenshot_open_dir": "위치 열기",
|
||||
"screenshot_sweep": "스크린샷 스윕 실행",
|
||||
"screenshot_sweep_desc": "모든 탭에 대해 모든 테마를 순회하며 각각의 스크린샷을 설정 디렉터리의 screenshots 폴더 아래 탭별 하위 폴더에 저장합니다(이전 스윕을 덮어씀). 몇 초 동안 실행됩니다.",
|
||||
"screenshot_sweep_full": "전체 UI 스윕",
|
||||
"search_icons": "아이콘 검색...",
|
||||
"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": "순서대로 종이에 적어 오프라인으로 보관하고, 절대 공유하거나 사진으로 찍지 마세요. 이 단어들을 잃어버리면 자금을 복구할 수 없습니다.",
|
||||
"seed_migrate_button": "시드 지갑으로 이전…",
|
||||
"select_address": "주소 선택...",
|
||||
"select_receiving_address": "수신 주소 선택...",
|
||||
"select_source_address": "보낼 주소 선택...",
|
||||
@@ -512,8 +974,10 @@
|
||||
"send_amount_details": "금액 상세",
|
||||
"send_amount_upper": "금액",
|
||||
"send_clear_fields": "모든 양식 필드를 지우시겠습니까?",
|
||||
"send_contacts_button": "연락처에서 선택",
|
||||
"send_copy_error": "오류 복사",
|
||||
"send_dismiss": "닫기",
|
||||
"send_err_needs_rescan": "지갑의 차폐 노트 데이터가 블록체인과 일치하지 않습니다(부트스트랩 또는 재색인 후에 발생합니다). 설정 → 블록체인 재스캔에서 전체 재스캔을 실행하고 완전히 끝날 때까지 기다린 뒤 다시 전송해 보세요.",
|
||||
"send_error_copied": "오류가 클립보드에 복사되었습니다",
|
||||
"send_error_prefix": "오류: %s",
|
||||
"send_exceeds_available": "사용 가능 금액 초과 (%.8f)",
|
||||
@@ -532,6 +996,7 @@
|
||||
"send_recipient": "수신자",
|
||||
"send_select_source": "보낼 주소 선택...",
|
||||
"send_sending_from": "보내는 곳",
|
||||
"send_status_unconfirmed": "거래 상태를 확인할 수 없습니다",
|
||||
"send_submitting": "거래 제출 중...",
|
||||
"send_switch_to_receive": "수신으로 전환하여 주소를 받고 자금 수신을 시작하세요.",
|
||||
"send_to": "받는 곳",
|
||||
@@ -553,12 +1018,15 @@
|
||||
"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": "아크릴 레벨:",
|
||||
@@ -601,6 +1069,8 @@
|
||||
"settings_noise_opacity": "노이즈 불투명도:",
|
||||
"settings_not_encrypted": "암호화되지 않음",
|
||||
"settings_not_found": "찾을 수 없음",
|
||||
"settings_open_app_dir": "앱 폴더 열기",
|
||||
"settings_open_data_dir": "데이터 폴더 열기",
|
||||
"settings_other": "기타",
|
||||
"settings_pin_active": "PIN",
|
||||
"settings_privacy": "개인 정보",
|
||||
@@ -634,26 +1104,43 @@
|
||||
"shield_check_status": "상태 확인",
|
||||
"shield_completed": "작업이 성공적으로 완료되었습니다!",
|
||||
"shield_description": "투명 주소의 코인베이스 출력을 차폐 주소로 전송하여 채굴 보상을 차폐합니다. 채굴 수입을 숨겨 프라이버시가 향상됩니다.",
|
||||
"shield_error_prefix": "오류: ",
|
||||
"shield_from_address": "보내는 주소:",
|
||||
"shield_funds": "자금 차폐",
|
||||
"shield_in_progress": "작업 진행 중...",
|
||||
"shield_max_utxos": "작업당 최대 UTXO 수",
|
||||
"shield_merge_done": "차폐/통합이 완료되었습니다!",
|
||||
"shield_op_failed": "작업 실패: ",
|
||||
"shield_op_submitted": "작업이 제출되었습니다: ",
|
||||
"shield_operation_id": "작업 ID: %s",
|
||||
"shield_select_z": "z 주소 선택...",
|
||||
"shield_send_failed": "차폐 실패: ",
|
||||
"shield_started": "차폐 작업이 시작되었습니다",
|
||||
"shield_status_check_error": "상태 확인 오류: ",
|
||||
"shield_status_label": "상태: ",
|
||||
"shield_submitting": "작업 제출 중...",
|
||||
"shield_title": "코인베이스 보상 차폐",
|
||||
"shield_to_address": "받는 주소 (차폐):",
|
||||
"shield_unknown_error": "알 수 없는 오류",
|
||||
"shield_utxo_limit": "UTXO 제한:",
|
||||
"shield_wildcard_hint": "'*'를 사용하여 모든 투명 주소에서 차폐",
|
||||
"shielded": "차폐",
|
||||
"shielded_address": "보호 주소",
|
||||
"shielded_to": "차폐 대상",
|
||||
"shielded_type": "차폐",
|
||||
"shielding_notice": "참고: 투명 (T) 주소에서 프라이빗 (Z) 주소로 자금을 실딩합니다.",
|
||||
"show": "표시",
|
||||
"show_hidden": "숨겨진 항목 표시 (%d)",
|
||||
"show_qr": "QR 표시",
|
||||
"show_qr_code": "QR 코드 표시",
|
||||
"showing_transactions": "%dâ%d / %d건의 거래 표시 중 (총: %zu)",
|
||||
"showing_transactions": "%d–%d / %d건의 거래 표시 중 (총: %zu)",
|
||||
"showing_x_of_y": "%d / %d 주소 표시",
|
||||
"simple_background": "단순 배경",
|
||||
"slider_off": "끔",
|
||||
"sort_amount_high": "금액 큰 순",
|
||||
"sort_amount_low": "금액 적은 순",
|
||||
"sort_date_newest": "최신순",
|
||||
"sort_date_oldest": "오래된 순",
|
||||
"start_mining": "채굴 시작",
|
||||
"status": "상태",
|
||||
"stop_external": "외부 데몬 중지",
|
||||
@@ -662,17 +1149,26 @@
|
||||
"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": "거래 전송 성공",
|
||||
@@ -680,7 +1176,13 @@
|
||||
"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": "이 비활성 시간 후 지갑 잠금",
|
||||
@@ -693,8 +1195,12 @@
|
||||
"tt_clear_ztx": "로컬에 캐시된 z-트랜잭션 기록 삭제",
|
||||
"tt_custom_fees": "거래 전송 시 수동 수수료 입력 활성화",
|
||||
"tt_custom_theme": "사용자 지정 테마 활성화됨",
|
||||
"tt_daemon_install_bundled": "노드를 중지하고 설치된 dragonxd를 이 지갑 빌드에 번들된 버전으로 덮어쓴 다음 재시작합니다",
|
||||
"tt_daemon_update_check": "프로젝트 Gitea에서 최신 dragonxd 풀 노드를 다운로드하고 검증한 다음, 재시작하여 적용합니다",
|
||||
"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 스프레드시트로 내보내기",
|
||||
@@ -706,14 +1212,19 @@
|
||||
"tt_keep_daemon": "설정 마법사를 실행하면 데몬이 여전히 중지됩니다",
|
||||
"tt_language": "지갑 UI 인터페이스 언어",
|
||||
"tt_layout_hotkey": "단축키: 좌/우 화살표 키로 잔액 레이아웃 전환",
|
||||
"tt_lite_redownload": "라이트 서버에서 모든 블록을 다시 다운로드하고 다시 스캔합니다",
|
||||
"tt_lock": "지갑 즉시 잠금",
|
||||
"tt_low_spec": "모든 고부하 시각 효과 비활성화\\n단축키: Ctrl+Shift+Down",
|
||||
"tt_merge": "여러 UTXO를 하나의 주소로 통합",
|
||||
"tt_mine_idle": "시스템이 유휴 상태(키보드/마우스 입력 없음)일 때\\n자동으로 채굴 시작",
|
||||
"tt_noise": "그레인 텍스처 강도 (0%% = 끔, 100%% = 최대)",
|
||||
"tt_open_app_dir": "파일 관리자에서 ObsidianDragon 폴더(설정, 테마, 로그)를 엽니다",
|
||||
"tt_open_data_dir": "지갑 및 블록체인 데이터가 있는 폴더를 파일 탐색기에서 엽니다",
|
||||
"tt_open_dir": "파일 탐색기에서 열려면 클릭",
|
||||
"tt_reduce_motion": "접근성을 위해 애니메이션 전환 및 잔액 보간 비활성화",
|
||||
"tt_remove_encrypt": "암호화를 제거하고 지갑을 보호 없이 저장",
|
||||
"tt_remove_pin": "PIN을 제거하고 잠금 해제 시 비밀번호 요구",
|
||||
"tt_repair_wallet": "지갑의 거래 기록을 블록체인에서 지우고 다시 만듭니다 (재스캔 후 전송에 실패하는 노트를 수정)",
|
||||
"tt_report_bug": "프로젝트 트래커에서 문제 보고",
|
||||
"tt_request_payment": "QR 코드가 포함된 결제 요청 생성",
|
||||
"tt_rescan": "누락된 거래를 찾기 위해 블록체인 재스캔",
|
||||
@@ -727,6 +1238,8 @@
|
||||
"tt_save_ztx": "z-address 거래 기록을 로컬에 저장하여 빠른 로딩",
|
||||
"tt_scan_themes": "새 테마 검색.\\n테마 폴더를 여기에 배치:\\n%s",
|
||||
"tt_scanline": "콘솔에서 CRT 스캔라인 효과",
|
||||
"tt_seed_backup": "지갑의 24단어 복구 시드 문구를 표시하고 백업합니다",
|
||||
"tt_seed_migrate": "새 시드 문구 지갑을 만들고 자금을 그곳으로 옮깁니다",
|
||||
"tt_set_pin": "빠른 잠금 해제를 위한 4-8자리 PIN 설정",
|
||||
"tt_shield_mining": "투명 채굴 보상을 차폐 주소로 이동",
|
||||
"tt_simple_bg": "배경에 단순 그라데이션 사용\\n단축키: Ctrl+Up",
|
||||
@@ -740,6 +1253,7 @@
|
||||
"tt_ui_opacity": "카드 및 사이드바 불투명도 (100%% = 완전 불투명, 낮을수록 더 투명)",
|
||||
"tt_validate": "DragonX 주소가 유효한지 확인",
|
||||
"tt_verbose": "콘솔 탭에 상세 연결 진단,\\n데몬 상태 및 포트 소유자 정보 기록",
|
||||
"tt_wallets_button": "지갑 파일 목록을 보고 전환합니다",
|
||||
"tt_website": "DragonX 웹사이트 열기",
|
||||
"tt_window_opacity": "배경 불투명도 (낮을수록 = 창을 통해 바탕 화면이 보임)",
|
||||
"tt_wizard": "초기 설정 마법사 다시 실행\\n데몬이 재시작됩니다",
|
||||
@@ -748,7 +1262,18 @@
|
||||
"tx_from_address": "보낸 주소:",
|
||||
"tx_id_label": "거래 ID:",
|
||||
"tx_immature": "미성숙",
|
||||
"tx_loading_enriching_sends": "보낸 거래 세부 정보 확인 중 (%d)",
|
||||
"tx_loading_fetching_transparent": "투명 내역을 가져오는 중",
|
||||
"tx_loading_history_progress": "이전 기록 불러오는 중 (%d%%)",
|
||||
"tx_loading_queued": "거래 새로고침 대기 중",
|
||||
"tx_loading_refreshing_cached": "지갑 내역을 새로고침하는 중 (%d개 캐시됨)",
|
||||
"tx_loading_scanning_shielded": "차폐 내역 스캔 중 (%d개 주소)",
|
||||
"tx_mined": "채굴됨",
|
||||
"tx_progress_balances": "잔액을 새로 고치는 중",
|
||||
"tx_progress_finalizing": "트랜잭션 마무리 중",
|
||||
"tx_progress_history": "기록 새로고침 중",
|
||||
"tx_progress_submitting": "데몬에 트랜잭션을 제출하는 중",
|
||||
"tx_progress_waiting_ops": "작업 대기 중 (%d)",
|
||||
"tx_received": "수신됨",
|
||||
"tx_sent": "전송됨",
|
||||
"tx_to_address": "받는 주소:",
|
||||
@@ -760,6 +1285,14 @@
|
||||
"unconfirmed": "미확인",
|
||||
"undo_clear": "지우기 취소",
|
||||
"unknown": "알 수 없음",
|
||||
"unmark_mining_address": "채굴 주소 표시 해제",
|
||||
"upd_back": "뒤로",
|
||||
"upd_install": "설치",
|
||||
"upd_installed_badge": "설치됨",
|
||||
"upd_no_build_platform": "이 플랫폼용 빌드가 없습니다",
|
||||
"upd_prerelease": "사전 릴리스",
|
||||
"upd_reinstall": "재설치",
|
||||
"upd_select_version": "설치할 버전을 선택하세요",
|
||||
"use_embedded_daemon": "내장 dragonxd 사용",
|
||||
"use_tor": "Tor 사용",
|
||||
"validate_btn": "검증",
|
||||
@@ -779,17 +1312,91 @@
|
||||
"verbose_logging": "상세 로깅",
|
||||
"version": "버전",
|
||||
"view": "보기",
|
||||
"view_all": "모두 보기",
|
||||
"view_details": "상세 보기",
|
||||
"view_on_explorer": "탐색기에서 보기",
|
||||
"waiting_for_daemon": "데몬 연결 대기 중...",
|
||||
"wallet": "지갑",
|
||||
"wallet_empty": "지갑이 비어 있습니다",
|
||||
"wallet_empty_hint": "수신으로 전환하여 주소를 받고 자금 수신을 시작하세요.",
|
||||
"wallets_active": "활성",
|
||||
"wallets_add": "폴더 추가",
|
||||
"wallets_add_folder": "지갑 파일을 검색할 폴더를 추가로 지정:",
|
||||
"wallets_add_folder_toggle": "+ 다른 폴더에서 지갑 검색…",
|
||||
"wallets_badge_encrypted": "암호화됨 (암호로 보호됨)",
|
||||
"wallets_badge_encrypted_short": "암호화됨",
|
||||
"wallets_badge_legacy": "레거시 지갑 (시드 문구 없음)",
|
||||
"wallets_badge_legacy_short": "레거시",
|
||||
"wallets_badge_seed": "시드 문구 지갑 (HD)",
|
||||
"wallets_badge_seed_short": "시드 문구",
|
||||
"wallets_badge_unknown": "지갑 유형을 완전히 확인하지 못함 (큰 파일 — 열어서 확인)",
|
||||
"wallets_badge_unknown_short": "알 수 없음",
|
||||
"wallets_button": "지갑…",
|
||||
"wallets_col_addresses": "주소",
|
||||
"wallets_col_balance": "잔액",
|
||||
"wallets_col_keys": "개 키",
|
||||
"wallets_col_name": "지갑",
|
||||
"wallets_col_opened": "마지막 열람",
|
||||
"wallets_col_size": "크기",
|
||||
"wallets_col_txs": "건",
|
||||
"wallets_create": "지갑 만들기",
|
||||
"wallets_created": "생성",
|
||||
"wallets_creating": "지갑을 만드는 중 — 노드가 재시작됩니다…",
|
||||
"wallets_current": "현재",
|
||||
"wallets_exists": "같은 이름의 지갑이 이미 존재합니다.",
|
||||
"wallets_external_tt": "데이터 디렉터리 밖 — '열기'는 제자리에 링크합니다 (복사 없음).",
|
||||
"wallets_folder_hint": "wallet-*.dat 파일이 있는 폴더 경로",
|
||||
"wallets_folder_invalid": "해당 폴더가 존재하지 않습니다.",
|
||||
"wallets_intro": "데이터 디렉터리(및 추가한 폴더)에 있는 지갑 파일입니다. 하나를 열면 전환됩니다 — 노드가 재시작되어 해당 지갑을 불러오며, 각 지갑은 고유한 데이터를 유지합니다.",
|
||||
"wallets_name_invalid": "유효한 지갑 이름을 입력하세요.",
|
||||
"wallets_never": "미사용",
|
||||
"wallets_new_hint": "이름(예: savings)",
|
||||
"wallets_new_label": "새 지갑 만들기:",
|
||||
"wallets_open": "열기",
|
||||
"wallets_open_failed": "이 지갑을 제자리에서 열 수 없습니다. 데이터 디렉터리와 다른 드라이브에 있을 가능성이 높습니다 — 같은 드라이브로 옮기세요 (Windows에서는 개발자 모드를 켜면 드라이브 간 링크도 가능합니다).",
|
||||
"wallets_open_folder": "폴더 위치 열기",
|
||||
"wallets_open_inplace_tt": "이 지갑을 있는 자리에서 엽니다 — 데이터 디렉터리에 링크 (복사 없음)",
|
||||
"wallets_reveal": "폴더 열기",
|
||||
"wallets_sort_addresses": "주소",
|
||||
"wallets_sort_asc": "오름차순 (오래된/적은/작은 순)",
|
||||
"wallets_sort_by": "정렬:",
|
||||
"wallets_sort_created": "생성",
|
||||
"wallets_sort_desc": "내림차순 (최신/많은/큰 순)",
|
||||
"wallets_sort_size": "크기",
|
||||
"wallets_sort_txs": "거래",
|
||||
"wallets_title": "지갑",
|
||||
"warning": "경고",
|
||||
"warning_upper": "경고!",
|
||||
"website": "웹사이트",
|
||||
"window_opacity": "창 투명도",
|
||||
"wizard_daemon_start_failed": "데몬 시작 실패 — 자동으로 재시도됩니다",
|
||||
"xmrig_browse_releases": "모든 릴리스 보기…",
|
||||
"xmrig_checking": "최신 채굴기를 확인하는 중…",
|
||||
"xmrig_current": "현재:",
|
||||
"xmrig_download_install": "다운로드 및 설치",
|
||||
"xmrig_downloading": "다운로드 중…",
|
||||
"xmrig_installed": "설치됨:",
|
||||
"xmrig_installed_ok": "채굴기가 설치되었습니다",
|
||||
"xmrig_installing": "설치 중…",
|
||||
"xmrig_latest": "최신:",
|
||||
"xmrig_loading_releases": "릴리스를 불러오는 중…",
|
||||
"xmrig_none": "없음",
|
||||
"xmrig_reinstall": "재설치",
|
||||
"xmrig_stop_mining_first": "채굴기를 업데이트하기 전에 채굴을 중지하세요.",
|
||||
"xmrig_unavailable_body": "이 플랫폼에서 사용 가능한 채굴기 빌드가 없습니다.",
|
||||
"xmrig_unavailable_title": "채굴기 업데이트를 사용할 수 없습니다",
|
||||
"xmrig_unknown_error": "알 수 없는 오류.",
|
||||
"xmrig_up_to_date": "채굴기가 최신 상태입니다",
|
||||
"xmrig_update_available": "새 채굴기를 사용할 수 있습니다",
|
||||
"xmrig_update_button": "채굴기 업데이트…",
|
||||
"xmrig_update_failed": "업데이트 실패",
|
||||
"xmrig_update_short": "업데이트",
|
||||
"xmrig_update_title": "채굴기 업데이트",
|
||||
"xmrig_verify_note": "다운로드는 설치 전에 릴리스에 게시된 SHA-256 체크섬과 대조하여 검증됩니다.",
|
||||
"xmrig_verifying": "확인 중…",
|
||||
"xmrig_version": "버전:",
|
||||
"yes_clear": "예, 지우기",
|
||||
"your_addresses": "내 주소",
|
||||
"z_address": "Z 주소",
|
||||
"z_addresses": "Z 주소"
|
||||
}
|
||||
|
||||
615
res/lang/pt.json
615
res/lang/pt.json
File diff suppressed because it is too large
Load Diff
615
res/lang/ru.json
615
res/lang/ru.json
File diff suppressed because it is too large
Load Diff
609
res/lang/zh.json
609
res/lang/zh.json
@@ -28,6 +28,7 @@
|
||||
"address_book_add": "添加地址",
|
||||
"address_book_add_new": "添加新地址",
|
||||
"address_book_added": "地址已添加到通讯录",
|
||||
"address_book_confirm_delete": "确认删除?",
|
||||
"address_book_count": "已保存 %zu 个地址",
|
||||
"address_book_deleted": "条目已删除",
|
||||
"address_book_edit": "编辑地址",
|
||||
@@ -39,10 +40,13 @@
|
||||
"address_copied": "地址已复制到剪贴板",
|
||||
"address_details": "地址详情",
|
||||
"address_label": "地址:",
|
||||
"address_reorder_hint": "拖放到某行的边缘可重新排序,拖到中间可转移",
|
||||
"address_upper": "地址",
|
||||
"address_url": "地址 URL",
|
||||
"addresses_appear_here": "连接后,您的接收地址将显示在此处。",
|
||||
"advanced": "高级",
|
||||
"advanced_effects": "高级特效...",
|
||||
"ago": "前",
|
||||
"all_filter": "全部",
|
||||
"allow_custom_fees": "允许自定义手续费",
|
||||
"amount": "金额",
|
||||
@@ -67,7 +71,10 @@
|
||||
"backup_wallet": "备份钱包...",
|
||||
"backup_wallet_not_found": "警告:在预期位置未找到 wallet.dat",
|
||||
"balance": "余额",
|
||||
"balance_history_collecting": "余额历史——正在收集数据…",
|
||||
"balance_layout": "余额布局",
|
||||
"balance_shielded_fmt": "屏蔽:%.8f",
|
||||
"balance_transparent_fmt": "透明:%.8f",
|
||||
"ban": "封禁",
|
||||
"banned_peers": "已封禁节点",
|
||||
"block": "区块",
|
||||
@@ -90,12 +97,52 @@
|
||||
"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",
|
||||
@@ -106,8 +153,26 @@
|
||||
"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_lite_redownload_msg": "这将清除钱包已下载的区块数据,并从轻钱包服务器重新获取并重新扫描每个区块。可能需要一段时间;在完成之前,钱包会显示同步进度。",
|
||||
"confirm_lite_redownload_safe": "您的钱包、密钥和助记词不受影响——仅重新下载区块数据。",
|
||||
"confirm_lite_redownload_title": "重新下载区块",
|
||||
"confirm_reinstall_daemon_msg": "此操作会停止守护进程,用本钱包版本内置的版本覆盖已安装的 dragonxd(以及 dragonx-cli/dragonx-tx),然后重启节点。用于恢复或更新节点程序文件。",
|
||||
"confirm_reinstall_daemon_safe": "您的钱包、密钥和区块链数据不会被改动——仅替换守护进程的程序文件。",
|
||||
"confirm_reinstall_daemon_title": "安装内置守护进程",
|
||||
"confirm_repair_wallet_msg": "这将以 -zapwallettxes=2 重启守护进程:它会删除钱包的所有交易和备注记录,然后从区块链重建它们。当即使完整重新扫描后交易仍无法构建(“Invalid sapling spend proof”/“shielded requirements not met”)时,请使用此功能。它耗时较长,钱包在完成前将保持离线。",
|
||||
"confirm_repair_wallet_safe": "您的密钥、地址和余额将被保留——仅重建缓存的交易记录。",
|
||||
"confirm_repair_wallet_title": "修复钱包",
|
||||
"confirm_rescan_msg": "此操作会重启守护进程并重新扫描整个区块链以查找您钱包的交易。可能需要很长时间,且钱包在完成前将保持离线。",
|
||||
"confirm_rescan_safe": "您的 wallet.dat 和区块链数据不会被删除——仅重新扫描。",
|
||||
"confirm_rescan_title": "重新扫描区块链",
|
||||
"confirm_restart_daemon_msg": "这将停止并重启守护进程以应用更改的选项。钱包将短暂断开连接后重新连接。",
|
||||
"confirm_restart_daemon_title": "重启守护进程",
|
||||
"confirm_send": "确认发送",
|
||||
"confirm_transaction": "确认交易",
|
||||
"confirm_transfer": "确认转账",
|
||||
"confirmations": "确认数",
|
||||
"confirmations_display": "%d 次确认 | %s",
|
||||
"confirmed": "已确认",
|
||||
@@ -115,6 +180,7 @@
|
||||
"connected_peers": "已连接节点",
|
||||
"connecting": "连接中...",
|
||||
"console": "控制台",
|
||||
"console_app": "应用",
|
||||
"console_auto_scroll": "自动滚动",
|
||||
"console_available_commands": "可用命令:",
|
||||
"console_capturing_output": "正在捕获守护进程输出...",
|
||||
@@ -131,6 +197,7 @@
|
||||
"console_connected": "已连接到守护进程",
|
||||
"console_copy_all": "全部复制",
|
||||
"console_copy_selected": "复制",
|
||||
"console_copy_value": "复制",
|
||||
"console_daemon": "守护进程",
|
||||
"console_daemon_error": "守护进程错误!",
|
||||
"console_daemon_started": "守护进程已启动",
|
||||
@@ -149,16 +216,21 @@
|
||||
"console_help_setgenerate": " setgenerate - 控制挖矿",
|
||||
"console_help_stop": " stop - 停止守护进程",
|
||||
"console_line_count": "%zu 行",
|
||||
"console_matches": "个匹配",
|
||||
"console_new_lines": "%d 新行",
|
||||
"console_no_daemon": "无守护进程",
|
||||
"console_not_connected": "错误:未连接到守护进程",
|
||||
"console_quit_note": "这里不需要 'quit'/'exit'——直接关闭窗口即可。",
|
||||
"console_rpc_reference": "RPC 命令参考",
|
||||
"console_rpc_trace": "RPC",
|
||||
"console_scanline": "控制台扫描线",
|
||||
"console_search_commands": "搜索命令...",
|
||||
"console_select_all": "全选",
|
||||
"console_show_app_output": "显示[应用]钱包日志行",
|
||||
"console_show_daemon_output": "显示守护进程输出",
|
||||
"console_show_errors_only": "仅显示错误",
|
||||
"console_show_rpc_ref": "显示 RPC 命令参考",
|
||||
"console_show_rpc_trace": "显示应用 RPC 调用",
|
||||
"console_showing_lines": "显示 %zu / %zu 行",
|
||||
"console_starting_node": "正在启动节点...",
|
||||
"console_status_error": "错误",
|
||||
@@ -172,6 +244,13 @@
|
||||
"console_welcome": "欢迎使用 ObsidianDragon 控制台",
|
||||
"console_zoom_in": "放大",
|
||||
"console_zoom_out": "缩小",
|
||||
"contact_global": "在每个钱包中显示(全局联系人)",
|
||||
"contact_global_badge_tt": "全局联系人——在每个钱包中可见",
|
||||
"contact_global_tt": "开启:无论您加载哪个钱包,此联系人都保持可见。关闭:它仅属于当前钱包。",
|
||||
"contacts": "联系人",
|
||||
"contacts_search_no_match": "没有匹配的联系人",
|
||||
"contacts_search_placeholder": "搜索联系人...",
|
||||
"copied": "已复制!",
|
||||
"copy": "复制",
|
||||
"copy_address": "复制完整地址",
|
||||
"copy_error": "复制错误",
|
||||
@@ -180,15 +259,59 @@
|
||||
"copy_uri": "复制 URI",
|
||||
"current_price": "当前价格",
|
||||
"custom_fees": "自定义手续费",
|
||||
"daemon_binary": "守护进程二进制文件",
|
||||
"daemon_bundled": "内置",
|
||||
"daemon_install_bundled": "安装内置版本",
|
||||
"daemon_installed": "已安装",
|
||||
"daemon_none_bundled": "此版本未内置",
|
||||
"daemon_not_installed": "未安装",
|
||||
"daemon_status_differ": "已安装的程序文件与内置版本不同。",
|
||||
"daemon_status_match": "已安装的程序与内置版本一致。",
|
||||
"daemon_status_missing": "未安装守护进程——请安装内置版本。",
|
||||
"daemon_update_available": "有新的节点版本可用",
|
||||
"daemon_update_body": "此钱包版本内置的 DragonX 节点比当前已安装的更新。更新会替换已安装的 dragonxd(以及 dragonx-cli/dragonx-tx),然后停止并重启节点以使新版本生效。推荐更新——更新的节点可能增加旧版本所缺少的功能(例如助记词支持)。",
|
||||
"daemon_update_browse": "浏览所有版本…",
|
||||
"daemon_update_check": "检查更新…",
|
||||
"daemon_update_checking": "正在检查最新节点…",
|
||||
"daemon_update_downgrade_note": "较旧的版本可能与您当前的链数据不兼容。安装其他版本将在守护进程重启后生效。",
|
||||
"daemon_update_download_install": "下载并安装",
|
||||
"daemon_update_downloading": "正在下载…",
|
||||
"daemon_update_failed": "更新失败",
|
||||
"daemon_update_installed": "已安装:",
|
||||
"daemon_update_installed_ok": "节点已安装",
|
||||
"daemon_update_installing": "正在安装…",
|
||||
"daemon_update_keep": "保持当前版本",
|
||||
"daemon_update_later": "稍后",
|
||||
"daemon_update_latest": "最新:",
|
||||
"daemon_update_loading": "正在加载版本…",
|
||||
"daemon_update_now": "立即更新",
|
||||
"daemon_update_reinstall": "重新安装",
|
||||
"daemon_update_restart_note": "重启守护进程以开始运行新版本。",
|
||||
"daemon_update_restart_now": "立即重启守护进程",
|
||||
"daemon_update_safe": "您的钱包、密钥和区块链数据不会被改动——仅替换守护进程的程序文件。如果您是有意运行自定义节点,请选择“保持当前版本”。",
|
||||
"daemon_update_title": "更新节点守护进程?",
|
||||
"daemon_update_unavailable_body": "此平台没有可用的节点版本。",
|
||||
"daemon_update_unavailable_title": "节点更新不可用",
|
||||
"daemon_update_unknown_error": "未知错误。",
|
||||
"daemon_update_up_to_date": "节点已是最新版本",
|
||||
"daemon_update_verify_note": "在安装前,会根据该版本发布的 SHA-256 和固定的 ed25519 签名对下载内容进行校验。",
|
||||
"daemon_update_verifying": "正在验证…",
|
||||
"daemon_update_version": "版本:",
|
||||
"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": "错误",
|
||||
@@ -196,6 +319,27 @@
|
||||
"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_hash_not_found": "未找到此哈希对应的区块或交易",
|
||||
"explorer_invalid_query": "输入区块高度或64位哈希",
|
||||
"explorer_invalid_response": "守护进程返回的响应无效",
|
||||
"explorer_mempool": "内存池",
|
||||
"explorer_mempool_size": "大小",
|
||||
"explorer_mempool_txs": "交易",
|
||||
"explorer_no_results": "没有匹配的缓存区块",
|
||||
"explorer_not_connected": "未连接到守护进程——无法查询区块或交易哈希",
|
||||
"explorer_recent_blocks": "最近区块",
|
||||
"explorer_search": "搜索",
|
||||
"explorer_section": "浏览器",
|
||||
"explorer_tx_outputs": "输出",
|
||||
"explorer_tx_size": "大小",
|
||||
"export": "导出",
|
||||
"export_csv": "导出 CSV",
|
||||
"export_keys_btn": "导出密钥",
|
||||
@@ -224,16 +368,25 @@
|
||||
"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_qr": "隐藏二维码",
|
||||
"hide_zero_balances": "隐藏零余额",
|
||||
"history": "历史",
|
||||
"immature_type": "未成熟",
|
||||
@@ -253,6 +406,9 @@
|
||||
"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": "保留",
|
||||
@@ -266,29 +422,134 @@
|
||||
"key_export_viewing_keys_zonly": "查看密钥仅适用于屏蔽 (z) 地址",
|
||||
"key_export_viewing_warning": "此查看密钥允许他人查看您的入账交易和余额,但不能花费您的资金。仅与信任的人分享。",
|
||||
"label": "标签:",
|
||||
"label_placeholder": "例如 储蓄、挖矿...",
|
||||
"language": "语言",
|
||||
"light": "浅色",
|
||||
"lite_account_label": "账户",
|
||||
"lite_action": "操作",
|
||||
"lite_backup_keys": "备份与密钥",
|
||||
"lite_birthday_backup": "生日区块:%llu (也请一并备份)",
|
||||
"lite_birthday_hint": "开始扫描的区块高度。如未知请保留 0(完整扫描更慢)。",
|
||||
"lite_birthday_label": "诞生区块",
|
||||
"lite_console_help_passthrough": "其他任何输入都将作为轻钱包控制台命令运行。",
|
||||
"lite_copy": "复制",
|
||||
"lite_could_not_write": "无法写入 ",
|
||||
"lite_encrypt_wallet": "加密钱包",
|
||||
"lite_encryption_removed": "已移除加密",
|
||||
"lite_hide_wipe": "隐藏并清除",
|
||||
"lite_import": "导入",
|
||||
"lite_import_key_label": "导入密钥",
|
||||
"lite_key_imported": "密钥已导入——运行同步以扫描其历史记录",
|
||||
"lite_lock_failed": "锁定失败",
|
||||
"lite_lock_now": "立即锁定",
|
||||
"lite_maintenance": "维护",
|
||||
"lite_net_add": "添加",
|
||||
"lite_net_add_label_hint": "标签(可选)",
|
||||
"lite_net_add_url_hint": "https://your-lite-server",
|
||||
"lite_net_checking": "检查中…",
|
||||
"lite_net_connected": "已连接",
|
||||
"lite_net_custom": "自定义",
|
||||
"lite_net_disconnected": "未连接",
|
||||
"lite_net_hidden_section": "隐藏的服务器",
|
||||
"lite_net_hide": "隐藏",
|
||||
"lite_net_in_use": "使用中",
|
||||
"lite_net_intro": "选择一个要使用的服务器,或让钱包随机选择一个。更改会立即生效。",
|
||||
"lite_net_invalid_url": "请输入有效的 http(s):// URL。",
|
||||
"lite_net_no_wallet": "没有打开的钱包",
|
||||
"lite_net_official": "官方",
|
||||
"lite_net_offline": "离线",
|
||||
"lite_net_random_active": "已启用随机服务器选择。",
|
||||
"lite_net_random_server": "随机服务器",
|
||||
"lite_net_refresh": "刷新",
|
||||
"lite_net_show_hidden": "显示已隐藏的服务器",
|
||||
"lite_net_sync_label": "同步",
|
||||
"lite_net_synced": "已同步",
|
||||
"lite_net_syncing": "正在同步",
|
||||
"lite_net_title": "Lite 服务器",
|
||||
"lite_net_unhide": "取消隐藏",
|
||||
"lite_net_use_random": "每次使用随机服务器",
|
||||
"lite_no_wallet": "未打开钱包——请在设置中创建或打开一个",
|
||||
"lite_no_wallet_short": "没有打开的钱包",
|
||||
"lite_op_create": "创建",
|
||||
"lite_op_open": "打开",
|
||||
"lite_op_restore": "恢复",
|
||||
"lite_overwrite": "覆盖",
|
||||
"lite_passphrase_label": "密码短语",
|
||||
"lite_private_keys_warning": "私钥——任何拥有它们的人都能花费您的资金",
|
||||
"lite_redownload_blocks": "重新下载区块",
|
||||
"lite_redownload_desc": "从轻钱包服务器重新获取所有区块(如果您的余额或历史记录看起来有误,请使用此功能)。",
|
||||
"lite_redownload_running": "正在重新下载区块…",
|
||||
"lite_remove_encryption": "移除加密",
|
||||
"lite_restore_birthday_label": "诞生区块(可选——0 表示从头扫描,速度较慢)",
|
||||
"lite_restore_btn": "恢复钱包",
|
||||
"lite_restore_intro": "输入您的恢复助记词。钱包将被恢复,然后从轻钱包服务器同步。",
|
||||
"lite_restore_ok": "钱包已恢复——正在从轻钱包服务器同步…",
|
||||
"lite_restore_seed_label": "助记词(单词以空格分隔)",
|
||||
"lite_restore_title": "从助记词恢复",
|
||||
"lite_save_to_file": "保存到文件",
|
||||
"lite_saved_to": "已保存(明文,仅所有者可读)至 ",
|
||||
"lite_security": "安全",
|
||||
"lite_seed_label": "助记词",
|
||||
"lite_seed_warning": "助记词——恢复钱包的唯一方式。请抄写下来,离线保存,切勿分享。",
|
||||
"lite_servers_network_tab": "轻钱包服务器在网络标签页中管理。",
|
||||
"lite_show_private_keys": "显示私钥",
|
||||
"lite_show_seed": "显示助记词",
|
||||
"lite_unlock": "解锁",
|
||||
"lite_unlock_btn": "解锁",
|
||||
"lite_unlock_failed": "解锁失败——密码错误?",
|
||||
"lite_unlock_msg": "输入您的密码短语以解锁钱包进行支付。",
|
||||
"lite_unlock_ok": "钱包已解锁——您现在可以发送了",
|
||||
"lite_unlock_title": "解锁钱包",
|
||||
"lite_validate": "验证",
|
||||
"lite_wallet_encrypted": "钱包已加密",
|
||||
"lite_wallet_label": "钱包",
|
||||
"lite_wallet_locked": "钱包已锁定",
|
||||
"lite_wallet_ready": "钱包就绪",
|
||||
"lite_wallet_request": "Lite 钱包请求",
|
||||
"lite_wallet_unlocked": "钱包已解锁",
|
||||
"lite_welcome_create": "创建新钱包",
|
||||
"lite_welcome_create_failed": "无法创建钱包",
|
||||
"lite_welcome_created": "钱包已创建——请立即在“设置 → 备份与密钥”中备份您的恢复助记词",
|
||||
"lite_welcome_later": "稍后",
|
||||
"lite_welcome_msg": "您还没有钱包。创建一个新钱包或从恢复助记词恢复。",
|
||||
"lite_welcome_restore": "从助记词恢复",
|
||||
"lite_welcome_restore_hint": "在设置 → 轻钱包请求中恢复您的钱包",
|
||||
"lite_welcome_title": "欢迎使用 ObsidianDragon Lite",
|
||||
"lite_word_count": "%d / 24 词",
|
||||
"lite_working": "处理中…",
|
||||
"loading": "加载中...",
|
||||
"loading_addresses": "正在加载地址...",
|
||||
"loading_transactions": "正在加载交易",
|
||||
"local_hashrate": "本地算力",
|
||||
"low_spec_mode": "低配模式",
|
||||
"mark_mining_address": "标记为挖矿地址",
|
||||
"market": "市场",
|
||||
"market_12h": "12小时",
|
||||
"market_18h": "18小时",
|
||||
"market_24h": "24小时",
|
||||
"market_24h_change": "24小时",
|
||||
"market_24h_volume": "24小时交易量",
|
||||
"market_6h": "6小时",
|
||||
"market_attribution": "价格数据来自 NonKYC",
|
||||
"market_btc_price": "BTC 价格",
|
||||
"market_cap": "市值",
|
||||
"market_cap_short": "市值",
|
||||
"market_iv_1d": "1天",
|
||||
"market_iv_1h": "1时",
|
||||
"market_iv_1m": "1M",
|
||||
"market_iv_1w": "1周",
|
||||
"market_iv_live": "实时",
|
||||
"market_no_history": "无价格历史",
|
||||
"market_no_price": "无价格数据",
|
||||
"market_now": "现在",
|
||||
"market_pct_shielded": "%.0f%% 屏蔽",
|
||||
"market_portfolio": "投资组合",
|
||||
"market_price_loading": "正在加载价格数据...",
|
||||
"market_price_unavailable": "价格数据不可用",
|
||||
"market_refresh_price": "刷新价格数据",
|
||||
"market_trade_on": "在 %s 交易",
|
||||
"market_updated": "\\xc2\\xb7 已更新 %s",
|
||||
"market_vol_short": "成交量",
|
||||
"mature": "已成熟",
|
||||
"max": "最大",
|
||||
"memo": "备注(可选,加密)",
|
||||
@@ -298,8 +559,52 @@
|
||||
"memo_z_only": "注意:备注仅在发送到屏蔽 (z) 地址时可用",
|
||||
"merge_description": "将多个 UTXO 合并到一个屏蔽地址。这可以帮助减小钱包大小并提高隐私性。",
|
||||
"merge_funds": "合并资金",
|
||||
"merge_send_failed": "合并失败:",
|
||||
"merge_started": "合并操作已开始",
|
||||
"merge_title": "合并到地址",
|
||||
"mig_adopt": "将其设为我的钱包",
|
||||
"mig_adopt_now": "采用助记词钱包",
|
||||
"mig_adopting": "正在安装您的新钱包并重新扫描…",
|
||||
"mig_adopting_note": "节点正在您的新钱包上重启,并将重新扫描区块链——这可能需要几分钟。请让钱包保持运行;它会自动重新连接。",
|
||||
"mig_already_mnemonic": "您的钱包已经有 24 词助记词——无需迁移。只需备份它并将这些词妥善保存在安全的地方即可。",
|
||||
"mig_backed_up": "我已抄写下我的助记词",
|
||||
"mig_backup_instead": "备份助记词",
|
||||
"mig_balance": "当前钱包余额:%.8f DRGX(扣除少量手续费)",
|
||||
"mig_check_failed": "无法检查您的钱包。请确保它已解锁并已连接,然后重新检查。",
|
||||
"mig_checking_balance": "正在检查您的余额",
|
||||
"mig_confirming": "正在等待此次扫入在链上确认,然后再切换钱包——这可确保您的资金确实已经转移。",
|
||||
"mig_confs": "扫入确认数:%d",
|
||||
"mig_continue_sweep": "继续扫入",
|
||||
"mig_copy_seed": "复制助记词",
|
||||
"mig_create": "创建助记词钱包",
|
||||
"mig_daemon_too_old": "您的 DragonX 节点版本太旧,无法创建助记词钱包——它缺少此功能所需的助记词支持。请更新节点(设置 → 节点与安全 → 安装内置版本,或检查更新),然后重试。",
|
||||
"mig_discard": "放弃迁移",
|
||||
"mig_done": "迁移完成。您的钱包现在由您的助记词支持。",
|
||||
"mig_done_btn": "完成",
|
||||
"mig_done_detail": "守护进程正在重新扫描以显示您的资金——这可能需要几分钟。您之前的钱包已作为 .bak 保存在数据文件夹中。",
|
||||
"mig_intro": "这会在一个隔离的节点中创建一个由 24 词助记词支持的全新钱包,以便您将资金转入其中。您当前的钱包不会受到影响,本步骤也不会转移任何资金——您将先备份新助记词,然后再作为单独的、经确认的步骤把资金扫入其中。",
|
||||
"mig_later": "稍后",
|
||||
"mig_no_funds": "您的钱包没有可迁移的资金。您可以直接采用新的助记词钱包——它会用由助记词支持的钱包替换您当前的(空)钱包。以防万一,您的旧钱包仍会移到一个带时间戳的备份中。",
|
||||
"mig_nofunds_confirm": "我明白这会用新的助记词钱包替换我当前的钱包",
|
||||
"mig_not_connected": "请先连接到您的节点,然后重新打开此界面进行迁移。",
|
||||
"mig_precheck_checking": "正在检查您的钱包",
|
||||
"mig_receive_addr": "新钱包接收地址:",
|
||||
"mig_recheck": "重新检查",
|
||||
"mig_remainder": "您的旧钱包中仍有部分资金(一笔交易的输入过多)——请在切换前扫入剩余部分。",
|
||||
"mig_remaining": "旧钱包中剩余:%.8f DRGX",
|
||||
"mig_seed_warning": "请按顺序将这 24 个词抄写下来并离线保存。它们是您新钱包唯一的备份——一旦丢失,您迁移的资金将永久无法恢复。",
|
||||
"mig_step1_done": "2 步中的第 1 步已完成——您的资金尚未转移。将它们扫入这个新钱包是下一步。",
|
||||
"mig_sweep_all": "扫入全部资金",
|
||||
"mig_sweep_intro": "现在将您当前钱包中的所有资金扫入新的助记词钱包。这会向新钱包的地址发送一笔交易;从此以后由您的助记词(已备份)控制这些资金。",
|
||||
"mig_sweep_remaining": "扫入剩余部分",
|
||||
"mig_sweeping": "正在将您的资金扫入新钱包…",
|
||||
"mig_title": "迁移到助记词钱包",
|
||||
"mig_to": "至:",
|
||||
"mig_txid": "txid: ",
|
||||
"mig_unlock_first": "请先解锁您的钱包才能从中花费。",
|
||||
"mig_unlock_to_migrate": "请先解锁您的钱包,然后重新打开此界面进行迁移。",
|
||||
"mig_waiting_mine": "正在等待扫入交易被打包…",
|
||||
"mig_working": "正在隔离的节点中创建您的新助记词钱包——这可能需要一分钟。您的主钱包将继续运行。",
|
||||
"mine_when_idle": "空闲时挖矿",
|
||||
"mined": "已挖得",
|
||||
"mined_filter": "已挖得",
|
||||
@@ -311,6 +616,15 @@
|
||||
"mining_address_copied": "挖矿地址已复制",
|
||||
"mining_all_time": "所有时间",
|
||||
"mining_already_saved": "矿池 URL 已保存",
|
||||
"mining_auto_balance_desc": "按算力将矿工分布到各官方矿池,以帮助去中心化网络。定期检查每个矿池。",
|
||||
"mining_auto_balanced_to": "已自动平衡挖矿至",
|
||||
"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分钟前",
|
||||
@@ -330,11 +644,20 @@
|
||||
"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_manual_desc": "手动:挖矿到您从下方列表选择的矿池。",
|
||||
"mining_mine": "挖矿",
|
||||
"mining_mining_addr": "挖矿地址",
|
||||
"mining_mining_here": "在此挖矿",
|
||||
"mining_network": "网络",
|
||||
"mining_no_blocks_yet": "尚未找到区块",
|
||||
"mining_no_payouts_yet": "尚无矿池支付",
|
||||
@@ -344,18 +667,24 @@
|
||||
"mining_on": "挖矿已开启",
|
||||
"mining_open_in_explorer": "在浏览器中打开",
|
||||
"mining_payout_address": "支付地址",
|
||||
"mining_payout_foreign": "⚠ 此支付地址不在您当前的钱包中——挖矿奖励将进入另一个钱包。如果您切换过钱包,请更新它。",
|
||||
"mining_payout_tooltip": "接收挖矿奖励的地址",
|
||||
"mining_pool": "矿池",
|
||||
"mining_pool_fee": "费用",
|
||||
"mining_pool_hashrate": "矿池算力",
|
||||
"mining_pool_url": "矿池 URL",
|
||||
"mining_pools_header": "矿池",
|
||||
"mining_recent_blocks": "最近区块",
|
||||
"mining_recent_payouts": "最近矿池支付",
|
||||
"mining_refresh": "刷新",
|
||||
"mining_remove": "移除",
|
||||
"mining_reset_defaults": "重置默认值",
|
||||
"mining_save_payout_address": "保存支付地址",
|
||||
"mining_save_pool_url": "保存矿池 URL",
|
||||
"mining_saved_addresses": "已保存地址:",
|
||||
"mining_saved_pools": "已保存矿池:",
|
||||
"mining_select_auto": "自动平衡",
|
||||
"mining_select_manual": "手动",
|
||||
"mining_shares": "份额",
|
||||
"mining_show_chart": "图表",
|
||||
"mining_show_log": "日志",
|
||||
@@ -368,7 +697,9 @@
|
||||
"mining_stop_solo_for_pool_settings": "请停止单人挖矿以更改矿池设置",
|
||||
"mining_stopping": "停止中...",
|
||||
"mining_stopping_tooltip": "矿工正在停止...",
|
||||
"mining_suggested_pools": "推荐矿池",
|
||||
"mining_syncing_tooltip": "区块链同步中...",
|
||||
"mining_tag": " · 挖矿",
|
||||
"mining_threads": "挖矿线程",
|
||||
"mining_to_save": "保存",
|
||||
"mining_today": "今天",
|
||||
@@ -388,10 +719,13 @@
|
||||
"no_addresses_available": "无可用地址",
|
||||
"no_addresses_match": "没有匹配过滤器的地址",
|
||||
"no_addresses_with_balance": "没有有余额的地址",
|
||||
"no_addresses_yet": "暂无地址",
|
||||
"no_icons_found": "没有与您的搜索匹配的图标。",
|
||||
"no_matching": "没有匹配的交易",
|
||||
"no_recent_receives": "没有最近的接收",
|
||||
"no_recent_sends": "没有最近的发送",
|
||||
"no_transactions": "未找到交易",
|
||||
"no_transactions_yet": "尚无交易",
|
||||
"node": "节点",
|
||||
"node_security": "节点与安全",
|
||||
"noise": "噪点",
|
||||
@@ -453,8 +787,68 @@
|
||||
"peers_upper": "节点",
|
||||
"peers_version": "版本",
|
||||
"pending": "待处理",
|
||||
"pin_not_set": "未设置 PIN。使用密码解锁。",
|
||||
"ping": "延迟",
|
||||
"portfolio_add_entry": "添加条目",
|
||||
"portfolio_add_to": "添加到投资组合",
|
||||
"portfolio_addresses_hdr": "地址",
|
||||
"portfolio_addresses_sel": "已选 %d 个",
|
||||
"portfolio_all_funds": "全部资金",
|
||||
"portfolio_all_shielded": "全部屏蔽",
|
||||
"portfolio_all_transparent": "全部透明",
|
||||
"portfolio_appearance": "外观",
|
||||
"portfolio_cancel": "取消",
|
||||
"portfolio_clear_sel": "清除",
|
||||
"portfolio_close": "关闭",
|
||||
"portfolio_color": "颜色",
|
||||
"portfolio_currency": "货币",
|
||||
"portfolio_custom_color": "自定义颜色…",
|
||||
"portfolio_delete": "删除",
|
||||
"portfolio_detail_empty": "在左侧选择一个分组,或添加一个分组进行编辑。",
|
||||
"portfolio_edit": "编辑",
|
||||
"portfolio_funded": "已注资",
|
||||
"portfolio_group_name": "分组名称",
|
||||
"portfolio_icon": "图标",
|
||||
"portfolio_label": "标签",
|
||||
"portfolio_manage": "管理…",
|
||||
"portfolio_manage_title": "管理投资组合",
|
||||
"portfolio_manual_price": "价格 / DRGX",
|
||||
"portfolio_new_entry": "新条目",
|
||||
"portfolio_no_addr_match": "没有匹配的地址",
|
||||
"portfolio_no_entries": "尚无自定义条目。添加一个以跟踪一组地址。",
|
||||
"portfolio_no_icon": "无",
|
||||
"portfolio_outline_opacity": "描边不透明度",
|
||||
"portfolio_price": "价格",
|
||||
"portfolio_price_btc": "市场 · BTC",
|
||||
"portfolio_price_drgx": "仅 DRGX",
|
||||
"portfolio_price_manual": "手动",
|
||||
"portfolio_price_usd": "市场 · 美元",
|
||||
"portfolio_remove_from": "从投资组合中移除",
|
||||
"portfolio_revert": "还原",
|
||||
"portfolio_save": "保存",
|
||||
"portfolio_search": "搜索地址…",
|
||||
"portfolio_search_icons": "搜索图标…",
|
||||
"portfolio_select_all": "全部",
|
||||
"portfolio_select_shown": "全选",
|
||||
"portfolio_show": "显示:",
|
||||
"portfolio_show_24h": "24小时",
|
||||
"portfolio_show_sparkline": "迷你走势图",
|
||||
"portfolio_show_value": "价值",
|
||||
"portfolio_spark_day": "日",
|
||||
"portfolio_spark_hour": "小时",
|
||||
"portfolio_spark_min": "分钟",
|
||||
"portfolio_spark_month": "月",
|
||||
"portfolio_spark_week": "周",
|
||||
"portfolio_style_compact": "紧凑行",
|
||||
"portfolio_style_detailed": "详细行",
|
||||
"portfolio_style_featured": "特色行",
|
||||
"portfolio_style_label": "投资组合样式",
|
||||
"portfolio_untitled": "未命名",
|
||||
"price_chart": "价格图表",
|
||||
"privacy_great": "隐私性极佳!",
|
||||
"privacy_low": "隐私性低——请屏蔽资金",
|
||||
"privacy_medium": "考虑屏蔽更多资金",
|
||||
"processing_transaction": "正在处理交易...",
|
||||
"qr_code": "二维码",
|
||||
"qr_failed": "无法生成二维码",
|
||||
"qr_title": "二维码",
|
||||
@@ -472,11 +866,15 @@
|
||||
"receiving_addresses": "您的接收地址",
|
||||
"recent_received": "最近接收",
|
||||
"recent_sends": "最近发送",
|
||||
"recent_transactions": "最近交易",
|
||||
"recipient": "收款方",
|
||||
"recipient_balance": "接收方: %.8f → %.8f DRGX",
|
||||
"recv_type": "接收",
|
||||
"reduce_motion": "减少动画",
|
||||
"refresh": "刷新",
|
||||
"refresh_now": "立即刷新",
|
||||
"remove_favorite": "移除收藏",
|
||||
"repair_wallet": "修复钱包",
|
||||
"report_bug": "报告错误",
|
||||
"request_amount": "金额(可选):",
|
||||
"request_copy_uri": "复制 URI",
|
||||
@@ -492,9 +890,16 @@
|
||||
"request_transparent_addrs": "-- 透明地址 --",
|
||||
"request_uri_copied": "付款 URI 已复制到剪贴板",
|
||||
"rescan": "重新扫描",
|
||||
"rescan_bootstrapped_msg": "您的节点使用了引导程序,因此快照以下的区块不在磁盘上,从创世区块重新扫描将失败。请从快照所包含的高度开始重新扫描,以核对钱包的已花费余额。您的 wallet.dat 和区块链数据不会被删除。",
|
||||
"rescan_detecting": "正在检查您的节点在磁盘上已有哪些区块…",
|
||||
"rescan_from_height": "从区块高度重新扫描:",
|
||||
"reset_to_defaults": "重置为默认值",
|
||||
"restarting_after_encryption": "加密后重启守护进程...",
|
||||
"restore_address": "恢复地址",
|
||||
"result_preview": "结果预览",
|
||||
"retry": "重试",
|
||||
"review_send": "审核发送",
|
||||
"rpc_connection": "RPC 连接...",
|
||||
"rpc_host": "RPC 主机",
|
||||
"rpc_pass": "密码",
|
||||
"rpc_port": "端口",
|
||||
@@ -502,8 +907,65 @@
|
||||
"save": "保存",
|
||||
"save_settings": "保存设置",
|
||||
"save_z_transactions": "将 Z 交易保存到列表",
|
||||
"sb_auth_failed": "认证失败 — 请检查 rpcuser/rpcpassword",
|
||||
"sb_block": "区块: %d",
|
||||
"sb_building_witnesses": "正在设置见证",
|
||||
"sb_building_witnesses_pct": "正在重建见证 %.0f%%",
|
||||
"sb_connecting_daemon": "正在连接 dragonxd...",
|
||||
"sb_connecting_err": "连接守护进程 — %s",
|
||||
"sb_connecting_external": "正在连接外部守护进程...",
|
||||
"sb_connecting_generic": "正在连接守护进程...",
|
||||
"sb_daemon_crashed": "守护进程崩溃 %d 次",
|
||||
"sb_daemon_not_found": "未找到守护进程",
|
||||
"sb_daemon_start_failed": "无法启动 dragonxd",
|
||||
"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": "正在预热...",
|
||||
"sb_witness_cache": "正在重建见证",
|
||||
"screenshot_open_dir": "打开位置",
|
||||
"screenshot_sweep": "运行截图批处理",
|
||||
"screenshot_sweep_desc": "遍历每个标签页的每一种主题,并将每一个的截图保存到配置目录 screenshots 文件夹下的各标签页子文件夹中(覆盖上一次的遍历)。运行几秒钟。",
|
||||
"screenshot_sweep_full": "完整 UI 遍历",
|
||||
"search_icons": "搜索图标...",
|
||||
"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": "请按顺序将它们抄写下来,离线保存,切勿分享或拍照。一旦丢失,您的资金将无法恢复。",
|
||||
"seed_migrate_button": "迁移到助记词钱包…",
|
||||
"select_address": "选择地址...",
|
||||
"select_receiving_address": "选择接收地址...",
|
||||
"select_source_address": "选择来源地址...",
|
||||
@@ -512,8 +974,10 @@
|
||||
"send_amount_details": "金额详情",
|
||||
"send_amount_upper": "金额",
|
||||
"send_clear_fields": "清除所有表单字段?",
|
||||
"send_contacts_button": "从联系人中选择",
|
||||
"send_copy_error": "复制错误",
|
||||
"send_dismiss": "关闭",
|
||||
"send_err_needs_rescan": "您钱包的屏蔽备注数据与区块链不同步(这通常发生在引导或重建索引之后)。请通过 设置 → 重新扫描区块链 运行一次完整的重新扫描并让其彻底完成,然后再次尝试发送。",
|
||||
"send_error_copied": "错误已复制到剪贴板",
|
||||
"send_error_prefix": "错误:%s",
|
||||
"send_exceeds_available": "超过可用额 (%.8f)",
|
||||
@@ -532,6 +996,7 @@
|
||||
"send_recipient": "收款方",
|
||||
"send_select_source": "选择来源地址...",
|
||||
"send_sending_from": "发送来源",
|
||||
"send_status_unconfirmed": "无法确认交易状态",
|
||||
"send_submitting": "正在提交交易...",
|
||||
"send_switch_to_receive": "切换到接收页面获取您的地址并开始接收资金。",
|
||||
"send_to": "发送至",
|
||||
@@ -553,12 +1018,15 @@
|
||||
"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": "亚克力级别:",
|
||||
@@ -601,6 +1069,8 @@
|
||||
"settings_noise_opacity": "噪点不透明度:",
|
||||
"settings_not_encrypted": "未加密",
|
||||
"settings_not_found": "未找到",
|
||||
"settings_open_app_dir": "打开应用文件夹",
|
||||
"settings_open_data_dir": "打开数据文件夹",
|
||||
"settings_other": "其他",
|
||||
"settings_pin_active": "PIN",
|
||||
"settings_privacy": "隐私",
|
||||
@@ -634,26 +1104,43 @@
|
||||
"shield_check_status": "检查状态",
|
||||
"shield_completed": "操作成功完成!",
|
||||
"shield_description": "通过将透明地址的 coinbase 输出发送到屏蔽地址来屏蔽您的挖矿奖励。这可以隐藏您的挖矿收入,提高隐私性。",
|
||||
"shield_error_prefix": "错误:",
|
||||
"shield_from_address": "从地址:",
|
||||
"shield_funds": "屏蔽资金",
|
||||
"shield_in_progress": "操作进行中...",
|
||||
"shield_max_utxos": "每次操作最大 UTXO 数",
|
||||
"shield_merge_done": "屏蔽/合并完成!",
|
||||
"shield_op_failed": "操作失败:",
|
||||
"shield_op_submitted": "操作已提交:",
|
||||
"shield_operation_id": "操作 ID:%s",
|
||||
"shield_select_z": "选择 z 地址...",
|
||||
"shield_send_failed": "屏蔽失败:",
|
||||
"shield_started": "屏蔽操作已开始",
|
||||
"shield_status_check_error": "检查状态出错:",
|
||||
"shield_status_label": "状态:",
|
||||
"shield_submitting": "正在提交操作...",
|
||||
"shield_title": "屏蔽 Coinbase 奖励",
|
||||
"shield_to_address": "至地址(屏蔽):",
|
||||
"shield_unknown_error": "未知错误",
|
||||
"shield_utxo_limit": "UTXO 限制:",
|
||||
"shield_wildcard_hint": "使用 '*' 从所有透明地址屏蔽",
|
||||
"shielded": "屏蔽",
|
||||
"shielded_address": "隐蔽地址",
|
||||
"shielded_to": "屏蔽至",
|
||||
"shielded_type": "屏蔽",
|
||||
"shielding_notice": "注意:这将把资金从透明 (T) 地址转移到隐私 (Z) 地址。",
|
||||
"show": "显示",
|
||||
"show_hidden": "显示已隐藏 (%d)",
|
||||
"show_qr": "显示二维码",
|
||||
"show_qr_code": "显示二维码",
|
||||
"showing_transactions": "显示第 %dâ%d 笔,共 %d 笔交易(总计:%zu)",
|
||||
"showing_transactions": "显示第 %d–%d 笔,共 %d 笔交易(总计:%zu)",
|
||||
"showing_x_of_y": "显示 %d / %d 个地址",
|
||||
"simple_background": "简单背景",
|
||||
"slider_off": "关闭",
|
||||
"sort_amount_high": "金额最大",
|
||||
"sort_amount_low": "金额最小",
|
||||
"sort_date_newest": "最新优先",
|
||||
"sort_date_oldest": "最早优先",
|
||||
"start_mining": "开始挖矿",
|
||||
"status": "状态",
|
||||
"stop_external": "停止外部守护进程",
|
||||
@@ -662,17 +1149,26 @@
|
||||
"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": "交易发送成功",
|
||||
@@ -680,7 +1176,13 @@
|
||||
"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": "在此不活动时间后锁定钱包",
|
||||
@@ -693,8 +1195,12 @@
|
||||
"tt_clear_ztx": "删除本地缓存的 z-交易历史",
|
||||
"tt_custom_fees": "发送交易时启用手动费用输入",
|
||||
"tt_custom_theme": "自定义主题已激活",
|
||||
"tt_daemon_install_bundled": "停止节点,用此钱包版本内置的 dragonxd 覆盖已安装的版本,然后重启",
|
||||
"tt_daemon_update_check": "从项目 Gitea 下载并验证最新的 dragonxd 全节点,然后重启以应用",
|
||||
"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 电子表格",
|
||||
@@ -706,14 +1212,19 @@
|
||||
"tt_keep_daemon": "运行设置向导时守护进程仍会停止",
|
||||
"tt_language": "钱包界面语言",
|
||||
"tt_layout_hotkey": "快捷键:左/右箭头键切换余额布局",
|
||||
"tt_lite_redownload": "从轻钱包服务器重新下载并重新扫描所有区块",
|
||||
"tt_lock": "立即锁定钱包",
|
||||
"tt_low_spec": "禁用所有重度视觉效果\\n快捷键:Ctrl+Shift+Down",
|
||||
"tt_merge": "将多个 UTXO 合并到一个地址",
|
||||
"tt_mine_idle": "系统空闲时自动开始挖矿\\n(无键盘/鼠标输入)",
|
||||
"tt_noise": "颗粒纹理强度(0%% = 关闭,100%% = 最大)",
|
||||
"tt_open_app_dir": "在文件管理器中打开 ObsidianDragon 文件夹(设置、主题、日志)",
|
||||
"tt_open_data_dir": "在文件管理器中打开包含您钱包和区块链数据的文件夹",
|
||||
"tt_open_dir": "点击在文件管理器中打开",
|
||||
"tt_reduce_motion": "禁用动画过渡和余额渐变以提高无障碍性",
|
||||
"tt_remove_encrypt": "移除加密并以未受保护状态存储钱包",
|
||||
"tt_remove_pin": "移除 PIN 并要求密码解锁",
|
||||
"tt_repair_wallet": "从区块链清除并重建钱包的交易记录(修复重扫描后无法发送的 note)",
|
||||
"tt_report_bug": "在项目跟踪器中报告问题",
|
||||
"tt_request_payment": "生成带二维码的付款请求",
|
||||
"tt_rescan": "重新扫描区块链以查找丢失的交易",
|
||||
@@ -727,6 +1238,8 @@
|
||||
"tt_save_ztx": "将 z-address 交易历史存储在本地以加快加载速度",
|
||||
"tt_scan_themes": "扫描新主题。\\n将主题文件夹放在:\\n%s",
|
||||
"tt_scanline": "控制台中的 CRT 扫描线效果",
|
||||
"tt_seed_backup": "显示并备份您钱包的 24 词恢复助记词",
|
||||
"tt_seed_migrate": "创建一个新的助记词钱包并将您的资金转入其中",
|
||||
"tt_set_pin": "设置 4-8 位 PIN 以快速解锁",
|
||||
"tt_shield_mining": "将透明挖矿奖励转移到屏蔽地址",
|
||||
"tt_simple_bg": "使用简单渐变作为背景\\n快捷键:Ctrl+Up",
|
||||
@@ -740,6 +1253,7 @@
|
||||
"tt_ui_opacity": "卡片和侧边栏不透明度(100%% = 完全不透明,越低越透明)",
|
||||
"tt_validate": "检查 DragonX 地址是否有效",
|
||||
"tt_verbose": "将详细连接诊断、守护进程状态\\n和端口所有者信息记录到控制台选项卡",
|
||||
"tt_wallets_button": "列出您的钱包文件并在它们之间切换",
|
||||
"tt_website": "打开 DragonX 网站",
|
||||
"tt_window_opacity": "背景不透明度(越低 = 桌面透过窗口可见)",
|
||||
"tt_wizard": "重新运行初始设置向导\\n守护进程将被重启",
|
||||
@@ -748,7 +1262,18 @@
|
||||
"tx_from_address": "发送地址:",
|
||||
"tx_id_label": "交易 ID:",
|
||||
"tx_immature": "未成熟",
|
||||
"tx_loading_enriching_sends": "正在检查已发送交易详情(%d)",
|
||||
"tx_loading_fetching_transparent": "正在获取透明交易历史",
|
||||
"tx_loading_history_progress": "正在加载较早的历史记录(%d%%)",
|
||||
"tx_loading_queued": "已排队的交易刷新",
|
||||
"tx_loading_refreshing_cached": "正在刷新钱包历史(已缓存 %d 条)",
|
||||
"tx_loading_scanning_shielded": "正在扫描屏蔽历史(%d 个地址)",
|
||||
"tx_mined": "已挖得",
|
||||
"tx_progress_balances": "正在刷新余额",
|
||||
"tx_progress_finalizing": "正在完成交易",
|
||||
"tx_progress_history": "正在刷新历史记录",
|
||||
"tx_progress_submitting": "正在向守护进程提交交易",
|
||||
"tx_progress_waiting_ops": "正在等待操作(%d)",
|
||||
"tx_received": "已接收",
|
||||
"tx_sent": "已发送",
|
||||
"tx_to_address": "接收地址:",
|
||||
@@ -760,6 +1285,14 @@
|
||||
"unconfirmed": "未确认",
|
||||
"undo_clear": "撤销清除",
|
||||
"unknown": "未知",
|
||||
"unmark_mining_address": "取消标记挖矿地址",
|
||||
"upd_back": "返回",
|
||||
"upd_install": "安装",
|
||||
"upd_installed_badge": "已安装",
|
||||
"upd_no_build_platform": "没有适用于此平台的版本",
|
||||
"upd_prerelease": "预发布",
|
||||
"upd_reinstall": "重新安装",
|
||||
"upd_select_version": "选择要安装的版本",
|
||||
"use_embedded_daemon": "使用内置 dragonxd",
|
||||
"use_tor": "使用 Tor",
|
||||
"validate_btn": "验证",
|
||||
@@ -779,17 +1312,91 @@
|
||||
"verbose_logging": "详细日志",
|
||||
"version": "版本",
|
||||
"view": "查看",
|
||||
"view_all": "查看全部",
|
||||
"view_details": "查看详情",
|
||||
"view_on_explorer": "在浏览器中查看",
|
||||
"waiting_for_daemon": "等待守护进程连接...",
|
||||
"wallet": "钱包",
|
||||
"wallet_empty": "您的钱包是空的",
|
||||
"wallet_empty_hint": "切换到接收页面获取您的地址并开始接收资金。",
|
||||
"wallets_active": "活跃",
|
||||
"wallets_add": "添加文件夹",
|
||||
"wallets_add_folder": "同时扫描另一个文件夹中的钱包文件:",
|
||||
"wallets_add_folder_toggle": "+ 扫描其他文件夹中的钱包…",
|
||||
"wallets_badge_encrypted": "已加密(密码保护)",
|
||||
"wallets_badge_encrypted_short": "已加密",
|
||||
"wallets_badge_legacy": "旧版钱包(无助记词)",
|
||||
"wallets_badge_legacy_short": "旧版",
|
||||
"wallets_badge_seed": "助记词钱包 (HD)",
|
||||
"wallets_badge_seed_short": "助记词",
|
||||
"wallets_badge_unknown": "钱包类型未完全确定(文件较大——打开以确认)",
|
||||
"wallets_badge_unknown_short": "未知",
|
||||
"wallets_button": "钱包…",
|
||||
"wallets_col_addresses": "地址",
|
||||
"wallets_col_balance": "余额",
|
||||
"wallets_col_keys": "个密钥",
|
||||
"wallets_col_name": "钱包",
|
||||
"wallets_col_opened": "上次打开",
|
||||
"wallets_col_size": "大小",
|
||||
"wallets_col_txs": "笔交易",
|
||||
"wallets_create": "创建钱包",
|
||||
"wallets_created": "创建于",
|
||||
"wallets_creating": "正在创建钱包——节点将重启…",
|
||||
"wallets_current": "当前",
|
||||
"wallets_exists": "已存在同名钱包。",
|
||||
"wallets_external_tt": "在数据目录之外 —「打开」会就地链接(不复制)。",
|
||||
"wallets_folder_hint": "/含 .dat 钱包文件的文件夹路径",
|
||||
"wallets_folder_invalid": "该文件夹不存在。",
|
||||
"wallets_intro": "位于您数据目录(以及您添加的任何文件夹)中的钱包文件。打开一个即可切换——节点将重启以加载它,且每个钱包各自保留其数据。",
|
||||
"wallets_name_invalid": "请输入有效的钱包名称。",
|
||||
"wallets_never": "从未打开",
|
||||
"wallets_new_hint": "名称(例如 savings)",
|
||||
"wallets_new_label": "创建新钱包:",
|
||||
"wallets_open": "打开",
|
||||
"wallets_open_failed": "无法在原位置打开此钱包。它可能与数据目录位于不同的驱动器上 — 请将其移动到同一驱动器(在 Windows 上,启用开发者模式也可跨驱动器链接)。",
|
||||
"wallets_open_folder": "打开文件夹位置",
|
||||
"wallets_open_inplace_tt": "在原位置打开此钱包 — 链接到数据目录(不复制)",
|
||||
"wallets_reveal": "打开文件夹",
|
||||
"wallets_sort_addresses": "地址",
|
||||
"wallets_sort_asc": "升序(最早/最少/最小优先)",
|
||||
"wallets_sort_by": "排序:",
|
||||
"wallets_sort_created": "创建",
|
||||
"wallets_sort_desc": "降序(最新/最多/最大优先)",
|
||||
"wallets_sort_size": "大小",
|
||||
"wallets_sort_txs": "交易",
|
||||
"wallets_title": "钱包",
|
||||
"warning": "警告",
|
||||
"warning_upper": "警告!",
|
||||
"website": "网站",
|
||||
"window_opacity": "窗口透明度",
|
||||
"wizard_daemon_start_failed": "启动守护进程失败 — 将自动重试",
|
||||
"xmrig_browse_releases": "浏览所有版本…",
|
||||
"xmrig_checking": "正在检查最新的矿工程序…",
|
||||
"xmrig_current": "当前:",
|
||||
"xmrig_download_install": "下载并安装",
|
||||
"xmrig_downloading": "正在下载…",
|
||||
"xmrig_installed": "已安装:",
|
||||
"xmrig_installed_ok": "矿工程序已安装",
|
||||
"xmrig_installing": "正在安装…",
|
||||
"xmrig_latest": "最新:",
|
||||
"xmrig_loading_releases": "正在加载发行版…",
|
||||
"xmrig_none": "无",
|
||||
"xmrig_reinstall": "重新安装",
|
||||
"xmrig_stop_mining_first": "更新矿工程序前请先停止挖矿。",
|
||||
"xmrig_unavailable_body": "此平台没有可用的矿工构建版本。",
|
||||
"xmrig_unavailable_title": "矿工更新不可用",
|
||||
"xmrig_unknown_error": "未知错误。",
|
||||
"xmrig_up_to_date": "矿工程序已是最新版本",
|
||||
"xmrig_update_available": "有新的矿工程序可用",
|
||||
"xmrig_update_button": "更新矿工…",
|
||||
"xmrig_update_failed": "更新失败",
|
||||
"xmrig_update_short": "更新",
|
||||
"xmrig_update_title": "更新矿工程序",
|
||||
"xmrig_verify_note": "下载内容会在安装前与发布版本公布的 SHA-256 校验和进行核对。",
|
||||
"xmrig_verifying": "正在验证…",
|
||||
"xmrig_version": "版本:",
|
||||
"yes_clear": "是,清除",
|
||||
"your_addresses": "您的地址",
|
||||
"z_address": "Z 地址",
|
||||
"z_addresses": "Z 地址"
|
||||
}
|
||||
|
||||
@@ -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.72)"
|
||||
--on-surface-disabled = "rgba(232,230,240,0.40)"
|
||||
--on-surface-medium = "rgba(232,230,240,0.85)"
|
||||
--on-surface-disabled = "rgba(232,230,240,0.58)"
|
||||
--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.72)"
|
||||
--window-control = "rgba(232,230,240,0.85)"
|
||||
--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)"
|
||||
|
||||
@@ -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.72)"
|
||||
--on-surface-disabled = "rgba(30,30,42,0.38)"
|
||||
--on-surface-medium = "rgba(30,30,42,0.86)"
|
||||
--on-surface-disabled = "rgba(30,30,42,0.62)"
|
||||
--error = "#E0304A"
|
||||
--on-error = "#FFFFFF"
|
||||
--success = "#18A860"
|
||||
--on-success = "#FFFFFF"
|
||||
--warning = "#E09020"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(30,30,60,0.12)"
|
||||
--outline = "rgba(30,30,60,0.15)"
|
||||
--divider = "rgba(30,30,60,0.20)"
|
||||
--outline = "rgba(30,30,60,0.24)"
|
||||
--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.55)"
|
||||
--glass-fill = "rgba(255,255,255,0.20)"
|
||||
--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.04)"
|
||||
--hover-overlay = "rgba(96,64,224,0.10)"
|
||||
--active-overlay = "rgba(96,64,224,0.08)"
|
||||
--rim-light = "rgba(96,64,224,0.06)"
|
||||
--rim-light = "rgba(30,30,42,0.22)"
|
||||
--status-divider = "rgba(30,30,60,0.08)"
|
||||
--sidebar-hover = "rgba(96,64,224,0.07)"
|
||||
--sidebar-icon = "rgba(30,30,42,0.50)"
|
||||
|
||||
@@ -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.75)"
|
||||
--on-surface-disabled = "rgba(208,208,212,0.45)"
|
||||
--on-surface-medium = "rgba(208,208,212,0.85)"
|
||||
--on-surface-disabled = "rgba(208,208,212,0.58)"
|
||||
--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.75)"
|
||||
--window-control = "rgba(208,208,212,0.85)"
|
||||
--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)"
|
||||
|
||||
@@ -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.68)"
|
||||
--on-surface-disabled = "rgba(58,46,34,0.38)"
|
||||
--on-surface-medium = "rgba(58,46,34,0.86)"
|
||||
--on-surface-disabled = "rgba(58,46,34,0.62)"
|
||||
--error = "#A0524A"
|
||||
--on-error = "#FFFFFF"
|
||||
--success = "#6A8A5C"
|
||||
--success = "#4E8A42"
|
||||
--on-success = "#FFFFFF"
|
||||
--warning = "#C08840"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(140,110,70,0.14)"
|
||||
--outline = "rgba(140,110,70,0.16)"
|
||||
--divider = "rgba(140,110,70,0.20)"
|
||||
--outline = "rgba(140,110,70,0.24)"
|
||||
--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.58)"
|
||||
--glass-fill = "rgba(255,252,245,0.20)"
|
||||
--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.05)"
|
||||
--hover-overlay = "rgba(176,120,64,0.10)"
|
||||
--active-overlay = "rgba(176,120,64,0.10)"
|
||||
--rim-light = "rgba(212,160,108,0.10)"
|
||||
--rim-light = "rgba(58,46,34,0.22)"
|
||||
--status-divider = "rgba(176,120,64,0.10)"
|
||||
--sidebar-hover = "rgba(176,120,64,0.08)"
|
||||
--sidebar-icon = "rgba(58,46,34,0.50)"
|
||||
|
||||
@@ -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.72)"
|
||||
--on-surface-disabled = "rgba(28,21,37,0.40)"
|
||||
--on-surface-medium = "rgba(28,21,37,0.86)"
|
||||
--on-surface-disabled = "rgba(28,21,37,0.62)"
|
||||
--error = "#C62828"
|
||||
--on-error = "#FFFFFF"
|
||||
--success = "#2E7D32"
|
||||
--on-success = "#FFFFFF"
|
||||
--warning = "#E65100"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(120,80,160,0.12)"
|
||||
--outline = "rgba(120,80,160,0.14)"
|
||||
--divider = "rgba(120,80,160,0.20)"
|
||||
--outline = "rgba(120,80,160,0.24)"
|
||||
--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.55)"
|
||||
--glass-fill = "rgba(255,255,255,0.20)"
|
||||
--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.05)"
|
||||
--hover-overlay = "rgba(140,107,175,0.10)"
|
||||
--active-overlay = "rgba(140,107,175,0.10)"
|
||||
--rim-light = "rgba(180,140,255,0.10)"
|
||||
--rim-light = "rgba(28,21,37,0.22)"
|
||||
--status-divider = "rgba(140,107,175,0.10)"
|
||||
--sidebar-hover = "rgba(140,107,175,0.08)"
|
||||
--sidebar-icon = "rgba(28,21,37,0.50)"
|
||||
|
||||
@@ -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.68)"
|
||||
--on-surface-disabled = "rgba(42,44,48,0.38)"
|
||||
--on-surface-medium = "rgba(42,44,48,0.86)"
|
||||
--on-surface-disabled = "rgba(42,44,48,0.62)"
|
||||
--error = "#8C5A62"
|
||||
--on-error = "#FFFFFF"
|
||||
--success = "#5A7E5C"
|
||||
--success = "#3D7A42"
|
||||
--on-success = "#FFFFFF"
|
||||
--warning = "#8A7A52"
|
||||
--warning = "#9A7A2E"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(42,44,48,0.12)"
|
||||
--outline = "rgba(42,44,48,0.14)"
|
||||
--divider = "rgba(42,44,48,0.20)"
|
||||
--outline = "rgba(42,44,48,0.24)"
|
||||
--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.55)"
|
||||
--glass-fill = "rgba(255,255,255,0.20)"
|
||||
--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.04)"
|
||||
--hover-overlay = "rgba(42,44,48,0.10)"
|
||||
--active-overlay = "rgba(42,44,48,0.08)"
|
||||
--rim-light = "rgba(42,44,48,0.06)"
|
||||
--rim-light = "rgba(42,44,48,0.22)"
|
||||
--status-divider = "rgba(42,44,48,0.08)"
|
||||
--sidebar-hover = "rgba(42,44,48,0.05)"
|
||||
--sidebar-icon = "rgba(42,44,48,0.45)"
|
||||
|
||||
@@ -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.68)"
|
||||
--on-surface-disabled = "rgba(44,42,40,0.38)"
|
||||
--on-surface-medium = "rgba(44,42,40,0.86)"
|
||||
--on-surface-disabled = "rgba(44,42,40,0.62)"
|
||||
--error = "#8C5250"
|
||||
--on-error = "#FFFFFF"
|
||||
--success = "#5C7A62"
|
||||
--success = "#3F7A48"
|
||||
--on-success = "#FFFFFF"
|
||||
--warning = "#8A7A4C"
|
||||
--warning = "#9A7A2E"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(80,75,68,0.12)"
|
||||
--outline = "rgba(80,75,68,0.14)"
|
||||
--divider = "rgba(80,75,68,0.20)"
|
||||
--outline = "rgba(80,75,68,0.24)"
|
||||
--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.62)"
|
||||
--glass-fill = "rgba(255,255,254,0.20)"
|
||||
--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.04)"
|
||||
--hover-overlay = "rgba(110,117,128,0.10)"
|
||||
--active-overlay = "rgba(110,117,128,0.08)"
|
||||
--rim-light = "rgba(180,175,168,0.10)"
|
||||
--rim-light = "rgba(44,42,40,0.22)"
|
||||
--status-divider = "rgba(110,117,128,0.08)"
|
||||
--sidebar-hover = "rgba(110,117,128,0.06)"
|
||||
--sidebar-icon = "rgba(44,42,40,0.48)"
|
||||
|
||||
@@ -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.75)"
|
||||
--on-surface-disabled = "rgba(232,224,240,0.45)"
|
||||
--on-surface-medium = "rgba(232,224,240,0.85)"
|
||||
--on-surface-disabled = "rgba(232,224,240,0.58)"
|
||||
--error = "#CF6679"
|
||||
--on-error = "#000000"
|
||||
--success = "#81C784"
|
||||
|
||||
@@ -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.7)"
|
||||
--on-surface-disabled = "rgba(240,224,216,0.44)"
|
||||
--on-surface-medium = "rgba(240,224,216,0.85)"
|
||||
--on-surface-disabled = "rgba(240,224,216,0.58)"
|
||||
--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 = 420.0 }
|
||||
confirm-popup-max-width = { size = 560.0 }
|
||||
confirm-addr-card-height = { size = 28.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 }
|
||||
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 }
|
||||
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 = 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 }
|
||||
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 }
|
||||
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.85 }
|
||||
confirm-popup-width-ratio = { size = 0.92 }
|
||||
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 = 40.0 }
|
||||
chart-y-axis-min-padding = { size = 54.0 }
|
||||
chart-y-axis-padding = { size = 70.0 }
|
||||
chart-dot-min-radius = { size = 1.5 }
|
||||
chart-dot-radius = { size = 2.0 }
|
||||
@@ -908,6 +908,7 @@ 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 }
|
||||
@@ -930,7 +931,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 = 4.0 }
|
||||
scanline-line-alpha = { size = 2.0 }
|
||||
scanline-glow-spread = { size = 4.0 }
|
||||
scanline-glow-intensity = { size = 0.6 }
|
||||
scanline-glow-color = { size = 255.0 }
|
||||
@@ -1218,6 +1219,9 @@ 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 }
|
||||
@@ -1230,8 +1234,9 @@ width = { size = 140.0 }
|
||||
collapsed-width = { size = 64.0 }
|
||||
collapse-anim-speed = { size = 10.0 }
|
||||
auto-collapse-threshold = { size = 800.0 }
|
||||
section-gap = { size = 4.0 }
|
||||
section-gap = { size = 8.0 }
|
||||
section-label-pad-left = { size = 16.0 }
|
||||
section-label-pad-bottom = { size = 4.0 }
|
||||
item-height = { size = 36.0 }
|
||||
item-pad-x = { size = 8.0 }
|
||||
min-height = { size = 360.0 }
|
||||
@@ -1248,8 +1253,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 = 4.0 }
|
||||
bottom-padding = { size = 0.0 }
|
||||
button-spacing = { size = 6.0 }
|
||||
bottom-padding = { size = 4.0 }
|
||||
exit-icon-gap = { size = 4.0 }
|
||||
cutout-shadow-alpha = { size = 55 }
|
||||
cutout-highlight-alpha = { size = 8 }
|
||||
@@ -1278,7 +1283,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 = 14.0, pad-x = 22.0, pad-y = 6.0, logo-gap = 4.0, opacity = 0.7, offset-y = 4.0 }
|
||||
header-title = { font = "subtitle1", size = 12.0, pad-x = 8.0, pad-y = 10.0, logo-gap = 4.0, opacity = 0.7, offset-y = -2.0 }
|
||||
|
||||
[components.main-window.window]
|
||||
padding = [12, 36]
|
||||
@@ -1323,10 +1328,13 @@ 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 }
|
||||
about-logo-size = { 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 }
|
||||
|
||||
[components.main-layout]
|
||||
app-bar-height = { size = 64.0 }
|
||||
@@ -1377,6 +1385,17 @@ summary = { min-width = 280.0, max-width = 400.0, width-ratio = 0.32, min-height
|
||||
side-panel = { min-width = 280.0, max-width = 450.0, width-ratio = 0.4, min-height = 200.0, max-height = 350.0, height-ratio = 0.8 }
|
||||
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
|
||||
@@ -1509,7 +1528,6 @@ 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" }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1275,6 +1275,963 @@ 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": "탐색기"
|
||||
},
|
||||
|
||||
# --- Wallets dialog: metadata counts + status badges ---
|
||||
"wallets_col_txs": {
|
||||
"es": "tx", "de": "Tx", "fr": "tx", "pt": "tx",
|
||||
"ru": "трз", "zh": "笔交易", "ja": "件", "ko": "건"
|
||||
},
|
||||
"wallets_col_keys": {
|
||||
"es": "claves", "de": "Schlüssel", "fr": "clés", "pt": "chaves",
|
||||
"ru": "ключей", "zh": "个密钥", "ja": "個の鍵", "ko": "개 키"
|
||||
},
|
||||
"wallets_badge_encrypted": {
|
||||
"es": "Cifrada (protegida con contraseña)", "de": "Verschlüsselt (passphrasengeschützt)",
|
||||
"fr": "Chiffré (protégé par phrase secrète)", "pt": "Encriptada (protegida por senha)",
|
||||
"ru": "Зашифрован (защищён паролем)", "zh": "已加密(密码保护)",
|
||||
"ja": "暗号化済み(パスフレーズ保護)", "ko": "암호화됨 (암호로 보호됨)"
|
||||
},
|
||||
"wallets_badge_seed": {
|
||||
"es": "Billetera con frase semilla (HD)", "de": "Seed-Phrase-Wallet (HD)",
|
||||
"fr": "Portefeuille à phrase de récupération (HD)", "pt": "Carteira com frase semente (HD)",
|
||||
"ru": "Кошелёк с seed-фразой (HD)", "zh": "助记词钱包 (HD)",
|
||||
"ja": "シードフレーズウォレット (HD)", "ko": "시드 문구 지갑 (HD)"
|
||||
},
|
||||
"wallets_badge_legacy": {
|
||||
"es": "Billetera heredada (sin frase semilla)", "de": "Legacy-Wallet (keine Seed-Phrase)",
|
||||
"fr": "Portefeuille hérité (sans phrase de récupération)", "pt": "Carteira legada (sem frase semente)",
|
||||
"ru": "Устаревший кошелёк (без seed-фразы)", "zh": "旧版钱包(无助记词)",
|
||||
"ja": "レガシーウォレット(シードフレーズなし)", "ko": "레거시 지갑 (시드 문구 없음)"
|
||||
},
|
||||
"wallets_badge_unknown": {
|
||||
"es": "Tipo de billetera no determinado por completo (archivo grande — abra para confirmar)",
|
||||
"de": "Wallet-Typ nicht vollständig ermittelt (große Datei — zum Bestätigen öffnen)",
|
||||
"fr": "Type de portefeuille non entièrement déterminé (fichier volumineux — ouvrir pour confirmer)",
|
||||
"pt": "Tipo de carteira não totalmente determinado (arquivo grande — abra para confirmar)",
|
||||
"ru": "Тип кошелька определён не полностью (большой файл — откройте для подтверждения)",
|
||||
"zh": "钱包类型未完全确定(文件较大——打开以确认)",
|
||||
"ja": "ウォレットの種類を完全に判定できません(大きなファイル — 開いて確認)",
|
||||
"ko": "지갑 유형을 완전히 확인하지 못함 (큰 파일 — 열어서 확인)"
|
||||
},
|
||||
"wallets_badge_seed_short": {
|
||||
"es": "Frase semilla", "de": "Seed-Phrase", "fr": "Phrase secrète", "pt": "Frase semente",
|
||||
"ru": "Seed-фраза", "zh": "助记词", "ja": "シードフレーズ", "ko": "시드 문구"
|
||||
},
|
||||
"wallets_badge_encrypted_short": {
|
||||
"es": "Cifrada", "de": "Verschlüsselt", "fr": "Chiffré", "pt": "Encriptada",
|
||||
"ru": "Зашифрован", "zh": "已加密", "ja": "暗号化", "ko": "암호화됨"
|
||||
},
|
||||
"wallets_badge_legacy_short": {
|
||||
"es": "Heredada", "de": "Legacy", "fr": "Hérité", "pt": "Legada",
|
||||
"ru": "Устаревший", "zh": "旧版", "ja": "レガシー", "ko": "레거시"
|
||||
},
|
||||
"wallets_badge_unknown_short": {
|
||||
"es": "Desconocido", "de": "Unbekannt", "fr": "Inconnu", "pt": "Desconhecido",
|
||||
"ru": "Неизвестно", "zh": "未知", "ja": "不明", "ko": "알 수 없음"
|
||||
},
|
||||
|
||||
# --- Wallets dialog: created date + sort control ---
|
||||
"wallets_created": {
|
||||
"es": "creada", "de": "erstellt", "fr": "créé", "pt": "criada",
|
||||
"ru": "создан", "zh": "创建于", "ja": "作成", "ko": "생성"
|
||||
},
|
||||
"wallets_sort_by": {
|
||||
"es": "Ordenar:", "de": "Sortieren:", "fr": "Trier :", "pt": "Ordenar:",
|
||||
"ru": "Сортировка:", "zh": "排序:", "ja": "並べ替え:", "ko": "정렬:"
|
||||
},
|
||||
"wallets_sort_created": {
|
||||
"es": "Creado", "de": "Erstellt", "fr": "Créé", "pt": "Criado",
|
||||
"ru": "Создан", "zh": "创建", "ja": "作成", "ko": "생성"
|
||||
},
|
||||
"wallets_sort_addresses": {
|
||||
"es": "Direcciones", "de": "Adressen", "fr": "Adresses", "pt": "Endereços",
|
||||
"ru": "Адреса", "zh": "地址", "ja": "アドレス", "ko": "주소"
|
||||
},
|
||||
"wallets_sort_txs": {
|
||||
"es": "Txs", "de": "Txs", "fr": "Txs", "pt": "Txs",
|
||||
"ru": "Транз.", "zh": "交易", "ja": "取引", "ko": "거래"
|
||||
},
|
||||
"wallets_sort_size": {
|
||||
"es": "Tamaño", "de": "Größe", "fr": "Taille", "pt": "Tamanho",
|
||||
"ru": "Размер", "zh": "大小", "ja": "サイズ", "ko": "크기"
|
||||
},
|
||||
"wallets_sort_asc": {
|
||||
"es": "Ascendente (más antiguo / menos / más pequeño primero)",
|
||||
"de": "Aufsteigend (ältestes / wenigste / kleinstes zuerst)",
|
||||
"fr": "Croissant (plus ancien / moins / plus petit d'abord)",
|
||||
"pt": "Crescente (mais antigo / menos / menor primeiro)",
|
||||
"ru": "По возрастанию (сначала старые / меньше / меньший)",
|
||||
"zh": "升序(最早/最少/最小优先)", "ja": "昇順(古い/少ない/小さい順)", "ko": "오름차순 (오래된/적은/작은 순)"
|
||||
},
|
||||
"wallets_sort_desc": {
|
||||
"es": "Descendente (más reciente / más / más grande primero)",
|
||||
"de": "Absteigend (neuestes / meiste / größtes zuerst)",
|
||||
"fr": "Décroissant (plus récent / plus / plus grand d'abord)",
|
||||
"pt": "Decrescente (mais recente / mais / maior primeiro)",
|
||||
"ru": "По убыванию (сначала новые / больше / больший)",
|
||||
"zh": "降序(最新/最多/最大优先)", "ja": "降順(新しい/多い/大きい順)", "ko": "내림차순 (최신/많은/큰 순)"
|
||||
},
|
||||
"wallets_open_folder": {
|
||||
"es": "Abrir ubicación de la carpeta", "de": "Ordnerpfad öffnen",
|
||||
"fr": "Ouvrir l'emplacement du dossier", "pt": "Abrir local da pasta",
|
||||
"ru": "Открыть расположение папки", "zh": "打开文件夹位置",
|
||||
"ja": "フォルダーの場所を開く", "ko": "폴더 위치 열기"
|
||||
},
|
||||
"wallets_open_inplace_tt": {
|
||||
"es": "Abre esta cartera donde está, enlazándola al directorio de datos (sin copiar)",
|
||||
"de": "Öffnet diese Wallet an ihrem Ort — im Datenverzeichnis verlinkt (keine Kopie)",
|
||||
"fr": "Ouvre ce portefeuille à son emplacement — lié au répertoire de données (sans copie)",
|
||||
"pt": "Abre esta carteira onde está — vinculada ao diretório de dados (sem cópia)",
|
||||
"ru": "Открывает этот кошелёк на месте — по ссылке в каталоге данных (без копирования)",
|
||||
"zh": "在原位置打开此钱包 — 链接到数据目录(不复制)",
|
||||
"ja": "このウォレットをその場で開きます — データディレクトリにリンク(コピーなし)",
|
||||
"ko": "이 지갑을 있는 자리에서 엽니다 — 데이터 디렉터리에 링크 (복사 없음)"
|
||||
},
|
||||
"wallets_open_failed": {
|
||||
"es": "No se pudo abrir esta cartera en su ubicación. Probablemente está en una unidad distinta a tu directorio de datos: muévela a la misma unidad (en Windows, activar el Modo de desarrollador también permite enlazar entre unidades).",
|
||||
"de": "Diese Wallet konnte nicht an ihrem Ort geöffnet werden. Sie liegt vermutlich auf einem anderen Laufwerk als dein Datenverzeichnis — verschiebe sie auf dasselbe Laufwerk (unter Windows erlaubt auch der aktivierte Entwicklermodus laufwerkübergreifende Verknüpfungen).",
|
||||
"fr": "Impossible d'ouvrir ce portefeuille à son emplacement. Il se trouve probablement sur un lecteur différent de votre répertoire de données — déplacez-le sur le même lecteur (sous Windows, activer le mode développeur permet aussi de créer des liens entre lecteurs).",
|
||||
"pt": "Não foi possível abrir esta carteira no lugar. Provavelmente está em uma unidade diferente do seu diretório de dados — mova-a para a mesma unidade (no Windows, ativar o Modo de Desenvolvedor também permite vincular entre unidades).",
|
||||
"ru": "Не удалось открыть этот кошелёк на месте. Вероятно, он на другом диске, чем каталог данных — переместите его на тот же диск (в Windows включённый режим разработчика также позволяет создавать ссылки между дисками).",
|
||||
"zh": "无法在原位置打开此钱包。它可能与数据目录位于不同的驱动器上 — 请将其移动到同一驱动器(在 Windows 上,启用开发者模式也可跨驱动器链接)。",
|
||||
"ja": "このウォレットをその場で開けませんでした。データディレクトリとは別のドライブにある可能性があります — 同じドライブに移動してください(Windows では開発者モードを有効にするとドライブ間のリンクも可能になります)。",
|
||||
"ko": "이 지갑을 제자리에서 열 수 없습니다. 데이터 디렉터리와 다른 드라이브에 있을 가능성이 높습니다 — 같은 드라이브로 옮기세요 (Windows에서는 개발자 모드를 켜면 드라이브 간 링크도 가능합니다)."
|
||||
},
|
||||
"wallets_external_tt": {
|
||||
"es": "Fuera de tu directorio de datos — Abrir lo enlaza en su lugar (sin copiar).",
|
||||
"de": "Außerhalb deines Datenverzeichnisses — Öffnen verlinkt sie an ihrem Ort (keine Kopie).",
|
||||
"fr": "Hors de votre répertoire de données — Ouvrir le lie sur place (sans copie).",
|
||||
"pt": "Fora do seu diretório de dados — Abrir o vincula no lugar (sem cópia).",
|
||||
"ru": "Вне каталога данных — «Открыть» создаёт ссылку на месте (без копирования).",
|
||||
"zh": "在数据目录之外 —「打开」会就地链接(不复制)。",
|
||||
"ja": "データディレクトリの外 —「開く」はその場でリンクします(コピーなし)。",
|
||||
"ko": "데이터 디렉터리 밖 — '열기'는 제자리에 링크합니다 (복사 없음)."
|
||||
},
|
||||
}
|
||||
|
||||
def main():
|
||||
|
||||
801
scripts/build-lite-backend-artifact.sh
Executable file
801
scripts/build-lite-backend-artifact.sh
Executable file
@@ -0,0 +1,801 @@
|
||||
#!/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
|
||||
131
scripts/build_cjk_subset.py
Normal file
131
scripts/build_cjk_subset.py
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build a NotoSansCJK subset font containing all characters used by
|
||||
the zh, ja, and ko translation files, plus common CJK punctuation
|
||||
and symbols.
|
||||
|
||||
Usage:
|
||||
python3 scripts/build_cjk_subset.py
|
||||
|
||||
Requires: pip install fonttools brotli
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools import subset as ftsubset
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
|
||||
SOURCE_FONT = '/tmp/NotoSansCJKsc-Regular.otf'
|
||||
OUTPUT_FONT = os.path.join(ROOT, 'res', 'fonts', 'NotoSansCJK-Subset.ttf')
|
||||
|
||||
# Collect all characters used in CJK translation files
|
||||
needed = set()
|
||||
for lang in ['zh', 'ja', 'ko']:
|
||||
path = os.path.join(LANG_DIR, f'{lang}.json')
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
for v in data.values():
|
||||
if isinstance(v, str):
|
||||
for c in v:
|
||||
cp = ord(c)
|
||||
if cp > 0x7F: # non-ASCII only (ASCII handled by Ubuntu font)
|
||||
needed.add(cp)
|
||||
|
||||
# Also add common CJK ranges that future translations might use:
|
||||
# - CJK punctuation and symbols (3000-303F)
|
||||
# - Hiragana (3040-309F)
|
||||
# - Katakana (30A0-30FF)
|
||||
# - Bopomofo (3100-312F)
|
||||
# - CJK quotation marks, brackets
|
||||
for cp in range(0x3000, 0x3100):
|
||||
needed.add(cp)
|
||||
for cp in range(0x3100, 0x3130):
|
||||
needed.add(cp)
|
||||
# Fullwidth ASCII variants (commonly mixed in CJK text)
|
||||
for cp in range(0xFF01, 0xFF5F):
|
||||
needed.add(cp)
|
||||
|
||||
print(f"Total non-ASCII characters to include: {len(needed)}")
|
||||
|
||||
# Check which of these the source font supports
|
||||
font = TTFont(SOURCE_FONT)
|
||||
cmap = font.getBestCmap()
|
||||
supportable = needed & set(cmap.keys())
|
||||
unsupported = needed - set(cmap.keys())
|
||||
|
||||
print(f"Supported by source font: {len(supportable)}")
|
||||
if unsupported:
|
||||
print(f"Not in source font (will use fallback): {len(unsupported)}")
|
||||
for cp in sorted(unsupported)[:10]:
|
||||
print(f" U+{cp:04X} {chr(cp)}")
|
||||
|
||||
# Build the subset using pyftsubset CLI-style API
|
||||
args = [
|
||||
SOURCE_FONT,
|
||||
f'--output-file={OUTPUT_FONT}',
|
||||
f'--unicodes={",".join(f"U+{cp:04X}" for cp in sorted(supportable))}',
|
||||
'--no-hinting',
|
||||
'--desubroutinize',
|
||||
]
|
||||
|
||||
ftsubset.main(args)
|
||||
|
||||
# Convert CFF outlines to TrueType (glyf) outlines.
|
||||
# stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly.
|
||||
from fontTools.pens.cu2quPen import Cu2QuPen
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||
from fontTools.ttLib import newTable
|
||||
|
||||
tmp_otf = OUTPUT_FONT + '.tmp.otf'
|
||||
os.rename(OUTPUT_FONT, tmp_otf)
|
||||
|
||||
conv = TTFont(tmp_otf)
|
||||
if 'CFF ' in conv:
|
||||
print("Converting CFF -> TrueType outlines...")
|
||||
glyphOrder = conv.getGlyphOrder()
|
||||
glyphSet = conv.getGlyphSet()
|
||||
glyf_table = newTable("glyf")
|
||||
glyf_table.glyphs = {}
|
||||
glyf_table.glyphOrder = glyphOrder
|
||||
loca_table = newTable("loca")
|
||||
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
|
||||
for gname in glyphOrder:
|
||||
try:
|
||||
ttPen = TTGlyphPen(glyphSet)
|
||||
cu2quPen = Cu2QuPen(ttPen, max_err=1.0, reverse_direction=True)
|
||||
glyphSet[gname].draw(cu2quPen)
|
||||
glyf_table.glyphs[gname] = ttPen.glyph()
|
||||
except Exception:
|
||||
glyf_table.glyphs[gname] = TTGlyph()
|
||||
del conv['CFF ']
|
||||
if 'VORG' in conv:
|
||||
del conv['VORG']
|
||||
conv['glyf'] = glyf_table
|
||||
conv['loca'] = loca_table
|
||||
conv['head'].indexToLocFormat = 1
|
||||
if 'maxp' in conv:
|
||||
conv['maxp'].version = 0x00010000
|
||||
conv.sfntVersion = "\x00\x01\x00\x00"
|
||||
conv.save(OUTPUT_FONT)
|
||||
conv.close()
|
||||
os.remove(tmp_otf)
|
||||
|
||||
size = os.path.getsize(OUTPUT_FONT)
|
||||
print(f"\nOutput: {OUTPUT_FONT}")
|
||||
print(f"Size: {size / 1024:.0f} KB")
|
||||
|
||||
# Verify
|
||||
verify = TTFont(OUTPUT_FONT)
|
||||
verify_cmap = set(verify.getBestCmap().keys())
|
||||
still_missing = needed - verify_cmap
|
||||
print(f"Verified glyphs in subset: {len(verify_cmap)}")
|
||||
if still_missing:
|
||||
# These are chars not in the source font - expected for some Hangul/Hiragana
|
||||
print(f"Not coverable by this font: {len(still_missing)} (need additional font)")
|
||||
for cp in sorted(still_missing)[:10]:
|
||||
print(f" U+{cp:04X} {chr(cp)}")
|
||||
else:
|
||||
print("All needed characters are covered!")
|
||||
56
scripts/check-source-hygiene.sh
Executable file
56
scripts/check-source-hygiene.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/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)"
|
||||
64
scripts/check_cjk_coverage.py
Normal file
64
scripts/check_cjk_coverage.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check which characters in translation files fall outside the font glyph ranges."""
|
||||
import json
|
||||
import unicodedata
|
||||
import glob
|
||||
import os
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
LANG_DIR = os.path.join(SCRIPT_DIR, '..', 'res', 'lang')
|
||||
|
||||
# Glyph ranges from typography.cpp (regular font + CJK merge)
|
||||
RANGES = [
|
||||
# Regular font ranges
|
||||
(0x0020, 0x00FF), # Basic Latin + Latin-1 Supplement
|
||||
(0x0100, 0x024F), # Latin Extended-A + B
|
||||
(0x0370, 0x03FF), # Greek and Coptic
|
||||
(0x0400, 0x04FF), # Cyrillic
|
||||
(0x0500, 0x052F), # Cyrillic Supplement
|
||||
(0x2000, 0x206F), # General Punctuation
|
||||
(0x2190, 0x21FF), # Arrows
|
||||
(0x2200, 0x22FF), # Mathematical Operators
|
||||
(0x2600, 0x26FF), # Miscellaneous Symbols
|
||||
# CJK ranges
|
||||
(0x2E80, 0x2FDF), # CJK Radicals
|
||||
(0x3000, 0x30FF), # CJK Symbols, Hiragana, Katakana
|
||||
(0x3100, 0x312F), # Bopomofo
|
||||
(0x31F0, 0x31FF), # Katakana Extensions
|
||||
(0x3400, 0x4DBF), # CJK Extension A
|
||||
(0x4E00, 0x9FFF), # CJK Unified Ideographs
|
||||
(0xAC00, 0xD7AF), # Hangul Syllables
|
||||
(0xFF00, 0xFFEF), # Fullwidth Forms
|
||||
]
|
||||
|
||||
def in_ranges(cp):
|
||||
return any(lo <= cp <= hi for lo, hi in RANGES)
|
||||
|
||||
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
|
||||
lang = os.path.basename(path).replace('.json', '')
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
missing = {}
|
||||
for key, val in data.items():
|
||||
if not isinstance(val, str):
|
||||
continue
|
||||
for c in val:
|
||||
cp = ord(c)
|
||||
if cp > 0x7F and not in_ranges(cp):
|
||||
if c not in missing:
|
||||
missing[c] = []
|
||||
missing[c].append(key)
|
||||
|
||||
if missing:
|
||||
print(f"\n=== {lang}.json: {len(missing)} missing characters ===")
|
||||
for c in sorted(missing, key=lambda x: ord(x)):
|
||||
cp = ord(c)
|
||||
name = unicodedata.name(c, 'UNKNOWN')
|
||||
keys = missing[c][:3]
|
||||
key_str = ', '.join(keys)
|
||||
if len(missing[c]) > 3:
|
||||
key_str += f' (+{len(missing[c])-3} more)'
|
||||
print(f" U+{cp:04X} {c} ({name}) — used in: {key_str}")
|
||||
else:
|
||||
print(f"=== {lang}.json: OK (all characters covered) ===")
|
||||
47
scripts/check_font_coverage.py
Normal file
47
scripts/check_font_coverage.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check which characters needed by translations are missing from bundled fonts."""
|
||||
import json
|
||||
import os
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
FONTS_DIR = os.path.join(ROOT, 'res', 'fonts')
|
||||
LANG_DIR = os.path.join(ROOT, 'res', 'lang')
|
||||
|
||||
# Load font cmaps
|
||||
cjk = TTFont(os.path.join(FONTS_DIR, 'NotoSansCJK-Subset.ttf'))
|
||||
cjk_cmap = set(cjk.getBestCmap().keys())
|
||||
|
||||
ubuntu = TTFont(os.path.join(FONTS_DIR, 'Ubuntu-R.ttf'))
|
||||
ubuntu_cmap = set(ubuntu.getBestCmap().keys())
|
||||
|
||||
combined = cjk_cmap | ubuntu_cmap
|
||||
|
||||
print(f"CJK subset font glyphs: {len(cjk_cmap)}")
|
||||
print(f"Ubuntu font glyphs: {len(ubuntu_cmap)}")
|
||||
print(f"Combined: {len(combined)}")
|
||||
print()
|
||||
|
||||
for lang in ['zh', 'ja', 'ko', 'ru', 'de', 'es', 'fr', 'pt']:
|
||||
path = os.path.join(LANG_DIR, f'{lang}.json')
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
needed = set()
|
||||
for v in data.values():
|
||||
if isinstance(v, str):
|
||||
for c in v:
|
||||
needed.add(ord(c))
|
||||
|
||||
missing = sorted(needed - combined)
|
||||
if missing:
|
||||
print(f"{lang}.json: {len(needed)} chars needed, {len(missing)} MISSING")
|
||||
for cp in missing[:20]:
|
||||
c = chr(cp)
|
||||
print(f" U+{cp:04X} {c}")
|
||||
if len(missing) > 20:
|
||||
print(f" ... and {len(missing) - 20} more")
|
||||
else:
|
||||
print(f"{lang}.json: OK ({len(needed)} chars, all covered)")
|
||||
214
scripts/convert_cjk_to_ttf.py
Normal file
214
scripts/convert_cjk_to_ttf.py
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert CJK subset from CID-keyed CFF/OTF to TrueType/TTF.
|
||||
|
||||
stb_truetype (used by ImGui) doesn't handle CID-keyed CFF fonts properly,
|
||||
so we need glyf-based TrueType outlines instead.
|
||||
|
||||
Two approaches:
|
||||
1. Direct CFF->TTF conversion via cu2qu (fontTools)
|
||||
2. Download NotoSansSC-Regular.ttf (already TTF) and re-subset
|
||||
|
||||
This script tries approach 1 first, falls back to approach 2.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import glob
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
|
||||
FONT_DIR = os.path.join(PROJECT_ROOT, "res", "fonts")
|
||||
LANG_DIR = os.path.join(PROJECT_ROOT, "res", "lang")
|
||||
|
||||
SRC_OTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.otf")
|
||||
DST_TTF = os.path.join(FONT_DIR, "NotoSansCJK-Subset.ttf")
|
||||
|
||||
|
||||
def get_needed_codepoints():
|
||||
"""Collect all unique codepoints from CJK translation files."""
|
||||
codepoints = set()
|
||||
for lang_file in glob.glob(os.path.join(LANG_DIR, "*.json")):
|
||||
with open(lang_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
for value in data.values():
|
||||
if isinstance(value, str):
|
||||
for ch in value:
|
||||
cp = ord(ch)
|
||||
# Include CJK + Hangul + fullwidth + CJK symbols/kana
|
||||
if cp >= 0x2E80:
|
||||
codepoints.add(cp)
|
||||
return codepoints
|
||||
|
||||
|
||||
def convert_cff_to_ttf():
|
||||
"""Convert existing OTF/CFF font to TTF using fontTools cu2qu."""
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.pens.cu2quPen import Cu2QuPen
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||
|
||||
print(f"Loading {SRC_OTF}...")
|
||||
font = TTFont(SRC_OTF)
|
||||
|
||||
# Verify it's CFF
|
||||
if "CFF " not in font:
|
||||
print("Font is not CFF, skipping conversion")
|
||||
return False
|
||||
|
||||
cff = font["CFF "]
|
||||
top = cff.cff.topDictIndex[0]
|
||||
print(f"ROS: {getattr(top, 'ROS', None)}")
|
||||
print(f"CID-keyed: {getattr(top, 'FDSelect', None) is not None}")
|
||||
|
||||
glyphOrder = font.getGlyphOrder()
|
||||
print(f"Glyphs: {len(glyphOrder)}")
|
||||
|
||||
# Use fontTools' built-in otf2ttf if available
|
||||
try:
|
||||
from fontTools.otf2ttf import otf_to_ttf
|
||||
otf_to_ttf(font)
|
||||
font.save(DST_TTF)
|
||||
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
|
||||
font.close()
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Manual conversion using cu2qu
|
||||
print("Using manual CFF->TTF conversion with cu2qu...")
|
||||
|
||||
from fontTools.pens.recordingPen import RecordingPen
|
||||
from fontTools.pens.pointPen import SegmentToPointPen
|
||||
from fontTools import ttLib
|
||||
from fontTools.ttLib.tables._g_l_y_f import Glyph as TTGlyph
|
||||
import struct
|
||||
|
||||
# Get glyph set
|
||||
glyphSet = font.getGlyphSet()
|
||||
|
||||
# Create new glyf table
|
||||
from fontTools.ttLib import newTable
|
||||
|
||||
glyf_table = newTable("glyf")
|
||||
glyf_table.glyphs = {}
|
||||
glyf_table.glyphOrder = glyphOrder
|
||||
|
||||
loca_table = newTable("loca")
|
||||
|
||||
max_error = 1.0 # em-units tolerance for cubic->quadratic
|
||||
|
||||
for gname in glyphOrder:
|
||||
try:
|
||||
ttPen = TTGlyphPen(glyphSet)
|
||||
cu2quPen = Cu2QuPen(ttPen, max_err=max_error, reverse_direction=True)
|
||||
glyphSet[gname].draw(cu2quPen)
|
||||
glyf_table.glyphs[gname] = ttPen.glyph()
|
||||
except Exception as e:
|
||||
# Fallback: empty glyph
|
||||
glyf_table.glyphs[gname] = TTGlyph()
|
||||
|
||||
# Replace CFF with glyf
|
||||
del font["CFF "]
|
||||
if "VORG" in font:
|
||||
del font["VORG"]
|
||||
|
||||
font["glyf"] = glyf_table
|
||||
font["loca"] = loca_table
|
||||
|
||||
# Add required tables for TTF
|
||||
# head table needs indexToLocFormat
|
||||
font["head"].indexToLocFormat = 1 # long format
|
||||
|
||||
# Create maxp for TrueType
|
||||
if "maxp" in font:
|
||||
font["maxp"].version = 0x00010000
|
||||
|
||||
# Update sfntVersion
|
||||
font.sfntVersion = "\x00\x01\x00\x00" # TrueType
|
||||
|
||||
font.save(DST_TTF)
|
||||
print(f"Saved TTF: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
|
||||
font.close()
|
||||
return True
|
||||
|
||||
|
||||
def download_and_subset():
|
||||
"""Download NotoSansSC-Regular.ttf and subset it."""
|
||||
import urllib.request
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools import subset
|
||||
|
||||
# Google Fonts provides static TTF files
|
||||
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/SubsetOTF/SC/NotoSansSC-Regular.otf"
|
||||
# Actually, we want TTF. Let's try the variable font approach.
|
||||
# Or better: use google-fonts API for static TTF
|
||||
|
||||
# NotoSansSC static TTF from Google Fonts CDN
|
||||
tmp_font = "/tmp/NotoSansSC-Regular.ttf"
|
||||
|
||||
if not os.path.exists(tmp_font):
|
||||
print(f"Downloading NotoSansSC-Regular.ttf...")
|
||||
url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTC/NotoSansCJK-Regular.ttc"
|
||||
# This is a TTC (font collection), too large.
|
||||
# Use the OTF we already have and convert it.
|
||||
return False
|
||||
|
||||
print(f"Using {tmp_font}")
|
||||
font = TTFont(tmp_font)
|
||||
cmap = font.getBestCmap()
|
||||
print(f"Source has {len(cmap)} cmap entries")
|
||||
|
||||
needed = get_needed_codepoints()
|
||||
print(f"Need {len(needed)} CJK codepoints")
|
||||
|
||||
# Subset
|
||||
subsetter = subset.Subsetter()
|
||||
subsetter.populate(unicodes=needed)
|
||||
subsetter.subset(font)
|
||||
|
||||
font.save(DST_TTF)
|
||||
print(f"Saved: {DST_TTF} ({os.path.getsize(DST_TTF)} bytes)")
|
||||
font.close()
|
||||
return True
|
||||
|
||||
|
||||
def verify_result():
|
||||
"""Verify the output TTF has glyf outlines and correct characters."""
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
font = TTFont(DST_TTF)
|
||||
cmap = font.getBestCmap()
|
||||
|
||||
print(f"\n--- Verification ---")
|
||||
print(f"Format: {font.sfntVersion!r}")
|
||||
print(f"Has glyf: {'glyf' in font}")
|
||||
print(f"Has CFF: {'CFF ' in font}")
|
||||
print(f"Cmap entries: {len(cmap)}")
|
||||
|
||||
# Check key characters
|
||||
test_chars = {
|
||||
"历": 0x5386, "史": 0x53F2, # Chinese: history
|
||||
"概": 0x6982, "述": 0x8FF0, # Chinese: overview
|
||||
"设": 0x8BBE, "置": 0x7F6E, # Chinese: settings
|
||||
}
|
||||
for name, cp in test_chars.items():
|
||||
status = "YES" if cp in cmap else "NO"
|
||||
print(f" {name} (U+{cp:04X}): {status}")
|
||||
|
||||
size = os.path.getsize(DST_TTF)
|
||||
print(f"File size: {size} bytes ({size/1024:.1f} KB)")
|
||||
font.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== CJK Font CFF -> TTF Converter ===\n")
|
||||
|
||||
if convert_cff_to_ttf():
|
||||
verify_result()
|
||||
else:
|
||||
print("Direct conversion failed, trying download approach...")
|
||||
if download_and_subset():
|
||||
verify_result()
|
||||
else:
|
||||
print("ERROR: Could not convert font")
|
||||
sys.exit(1)
|
||||
@@ -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.1.1"
|
||||
VERSION="1.2.0"
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
|
||||
@@ -50,12 +50,20 @@ if [[ ! -f "$TARBALL" ]]; then
|
||||
curl -fSL -o "$TARBALL" "$SODIUM_URL"
|
||||
fi
|
||||
|
||||
# Verify checksum
|
||||
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
|
||||
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
|
||||
rm -f "$TARBALL"
|
||||
exit 1
|
||||
}
|
||||
# Verify checksum (sha256sum on Linux, shasum on macOS)
|
||||
if command -v sha256sum &>/dev/null; then
|
||||
echo "$SODIUM_SHA256 $TARBALL" | sha256sum -c - || {
|
||||
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
|
||||
rm -f "$TARBALL"
|
||||
exit 1
|
||||
}
|
||||
elif command -v shasum &>/dev/null; then
|
||||
echo "$SODIUM_SHA256 $TARBALL" | shasum -a 256 -c - || {
|
||||
echo "[fetch-libsodium] ERROR: SHA256 mismatch! Removing corrupted download."
|
||||
rm -f "$TARBALL"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
# ── Extract ─────────────────────────────────────────────────────────────────
|
||||
if [[ ! -d "$SRC_DIR" ]]; then
|
||||
@@ -115,6 +123,69 @@ case "$TARGET" in
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Native macOS: build universal binary (arm64 + x86_64) ───────────────────
|
||||
IS_MACOS_NATIVE=false
|
||||
if [[ "$TARGET" == "native" && "$(uname -s)" == "Darwin" ]]; then
|
||||
IS_MACOS_NATIVE=true
|
||||
fi
|
||||
|
||||
if $IS_MACOS_NATIVE; then
|
||||
echo "[fetch-libsodium] Building universal (arm64 + x86_64) for macOS..."
|
||||
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||
|
||||
INSTALL_ARM64="$PROJECT_DIR/libs/libsodium-arm64"
|
||||
INSTALL_X86_64="$PROJECT_DIR/libs/libsodium-x86_64"
|
||||
|
||||
for ARCH in arm64 x86_64; do
|
||||
echo "[fetch-libsodium] Building for $ARCH..."
|
||||
cd "$SRC_DIR"
|
||||
make clean 2>/dev/null || true
|
||||
make distclean 2>/dev/null || true
|
||||
|
||||
if [[ "$ARCH" == "arm64" ]]; then
|
||||
ARCH_INSTALL="$INSTALL_ARM64"
|
||||
HOST_TRIPLE="aarch64-apple-darwin"
|
||||
else
|
||||
ARCH_INSTALL="$INSTALL_X86_64"
|
||||
HOST_TRIPLE="x86_64-apple-darwin"
|
||||
fi
|
||||
|
||||
ARCH_CFLAGS="-arch $ARCH -mmacosx-version-min=11.0"
|
||||
|
||||
./configure \
|
||||
--prefix="$ARCH_INSTALL" \
|
||||
--disable-shared \
|
||||
--enable-static \
|
||||
--with-pic \
|
||||
--host="$HOST_TRIPLE" \
|
||||
CFLAGS="$ARCH_CFLAGS" \
|
||||
LDFLAGS="-arch $ARCH" \
|
||||
> /dev/null
|
||||
|
||||
make -j"$(sysctl -n hw.ncpu 2>/dev/null || echo 4)" > /dev/null 2>&1
|
||||
make install > /dev/null
|
||||
done
|
||||
|
||||
# Merge with lipo
|
||||
echo "[fetch-libsodium] Creating universal binary with lipo..."
|
||||
mkdir -p "$INSTALL_DIR/lib" "$INSTALL_DIR/include"
|
||||
lipo -create \
|
||||
"$INSTALL_ARM64/lib/libsodium.a" \
|
||||
"$INSTALL_X86_64/lib/libsodium.a" \
|
||||
-output "$INSTALL_DIR/lib/libsodium.a"
|
||||
cp -R "$INSTALL_ARM64/include/"* "$INSTALL_DIR/include/"
|
||||
|
||||
# Clean up per-arch builds
|
||||
rm -rf "$INSTALL_ARM64" "$INSTALL_X86_64"
|
||||
cd "$PROJECT_DIR"
|
||||
rm -rf "$SRC_DIR"
|
||||
rm -f "$TARBALL"
|
||||
|
||||
echo "[fetch-libsodium] Done (universal): $INSTALL_DIR/lib/libsodium.a"
|
||||
lipo -info "$INSTALL_DIR/lib/libsodium.a"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "[fetch-libsodium] Configuring for target: $TARGET ..."
|
||||
./configure "${CONFIGURE_ARGS[@]}" > /dev/null
|
||||
|
||||
|
||||
36
scripts/fix_mojibake.py
Normal file
36
scripts/fix_mojibake.py
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fix mojibake en-dash (and other common patterns) in translation JSON files."""
|
||||
import os
|
||||
import glob
|
||||
|
||||
LANG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'res', 'lang')
|
||||
|
||||
# Common mojibake patterns: UTF-8 bytes interpreted as Latin-1
|
||||
MOJIBAKE_FIXES = {
|
||||
'\u00e2\u0080\u0093': '\u2013', # en dash
|
||||
'\u00e2\u0080\u0094': '\u2014', # em dash
|
||||
'\u00e2\u0080\u0099': '\u2019', # right single quote
|
||||
'\u00e2\u0080\u009c': '\u201c', # left double quote
|
||||
'\u00e2\u0080\u009d': '\u201d', # right double quote
|
||||
'\u00e2\u0080\u00a6': '\u2026', # ellipsis
|
||||
}
|
||||
|
||||
total_fixed = 0
|
||||
for path in sorted(glob.glob(os.path.join(LANG_DIR, '*.json'))):
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
raw = f.read()
|
||||
|
||||
original = raw
|
||||
for bad, good in MOJIBAKE_FIXES.items():
|
||||
if bad in raw:
|
||||
count = raw.count(bad)
|
||||
raw = raw.replace(bad, good)
|
||||
lang = os.path.basename(path)
|
||||
print(f" {lang}: fixed {count} x {repr(good)}")
|
||||
total_fixed += count
|
||||
|
||||
if raw != original:
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write(raw)
|
||||
|
||||
print(f"\nTotal fixes: {total_fixed}")
|
||||
35
scripts/gen-lite-checkpoints.sh
Executable file
35
scripts/gen-lite-checkpoints.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/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
|
||||
@@ -1,645 +0,0 @@
|
||||
#!/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)}")
|
||||
@@ -1,665 +0,0 @@
|
||||
#!/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}")
|
||||
@@ -1,646 +0,0 @@
|
||||
#!/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)}")
|
||||
@@ -1,646 +0,0 @@
|
||||
#!/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)}")
|
||||
@@ -1,646 +0,0 @@
|
||||
#!/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)}")
|
||||
@@ -1,646 +0,0 @@
|
||||
#!/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": "NÓ",
|
||||
"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": "há %d dias",
|
||||
"time_hours_ago": "há %d horas",
|
||||
"time_minutes_ago": "há %d minutos",
|
||||
"time_seconds_ago": "há %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)}")
|
||||
@@ -1,646 +0,0 @@
|
||||
#!/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": "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": "Не подключено к 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)}")
|
||||
@@ -1,646 +0,0 @@
|
||||
#!/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)}")
|
||||
@@ -7,7 +7,7 @@ set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BUILD_DIR="${SCRIPT_DIR}/build/linux"
|
||||
VERSION="1.1.1"
|
||||
VERSION="1.2.0"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
|
||||
67
scripts/sign-daemon-release.sh
Executable file
67
scripts/sign-daemon-release.sh
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/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
|
||||
66
scripts/sign-xmrig-release.sh
Executable file
66
scripts/sign-xmrig-release.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/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
|
||||
73
setup.sh
73
setup.sh
@@ -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"
|
||||
autoconf automake libtool wget python3 xxd"
|
||||
|
||||
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"
|
||||
autoconf automake libtool wget python3 vim-common"
|
||||
|
||||
pkgs_core_arch="base-devel cmake git pkg-config
|
||||
mesa libx11 libxcursor libxrandr libxinerama libxi
|
||||
libxkbcommon wayland libsodium curl
|
||||
autoconf automake libtool wget"
|
||||
autoconf automake libtool wget python xxd"
|
||||
|
||||
pkgs_core_macos="cmake"
|
||||
pkgs_core_macos="cmake python xxd"
|
||||
|
||||
# Windows cross-compile (from Linux)
|
||||
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 || NEED_CORE=true
|
||||
has_cmd cmake && has_cmd g++ && has_cmd pkg-config && has_cmd python3 && has_cmd xxd || NEED_CORE=true
|
||||
|
||||
if $NEED_CORE; then
|
||||
install_pkgs "$core_pkgs" "core build"
|
||||
@@ -258,6 +258,8 @@ 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"
|
||||
@@ -440,6 +442,53 @@ 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
|
||||
@@ -459,11 +508,13 @@ 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
|
||||
|
||||
@@ -498,9 +549,11 @@ 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
|
||||
@@ -556,9 +609,11 @@ 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
|
||||
@@ -621,6 +676,14 @@ 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"
|
||||
|
||||
|
||||
2915
src/app.cpp
2915
src/app.cpp
File diff suppressed because it is too large
Load Diff
647
src/app.h
647
src/app.h
@@ -9,11 +9,24 @@
|
||||
#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_index.h"
|
||||
#include "data/wallet_state.h"
|
||||
#include "rpc/connection.h"
|
||||
#include "services/network_refresh_service.h"
|
||||
#include "services/wallet_security_controller.h"
|
||||
#include "services/wallet_security_workflow.h"
|
||||
#include "util/async_task_manager.h"
|
||||
#include "util/pool_stats_service.h"
|
||||
#include "wallet/wallet_capabilities.h"
|
||||
#include "chat/chat_service.h"
|
||||
#include "chat/chat_database.h"
|
||||
#include "ui/sidebar.h"
|
||||
#include "ui/windows/console_tab.h"
|
||||
#include "imgui.h"
|
||||
@@ -25,8 +38,9 @@ namespace dragonx {
|
||||
class RPCWorker;
|
||||
}
|
||||
namespace config { class Settings; }
|
||||
namespace daemon { class EmbeddedDaemon; class XmrigManager; }
|
||||
namespace daemon { class DaemonController; class EmbeddedDaemon; class XmrigManager; }
|
||||
namespace util { class Bootstrap; class SecureVault; }
|
||||
namespace wallet { class LiteWalletController; struct LiteWalletAppRefreshModel; }
|
||||
}
|
||||
|
||||
namespace dragonx {
|
||||
@@ -125,6 +139,13 @@ 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)
|
||||
@@ -140,10 +161,49 @@ 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_; }
|
||||
|
||||
// Hash of the active wallet's identity (derived from its address list), used to scope
|
||||
// per-wallet data (e.g. address-book contacts). Empty until addresses are known (pre-connect).
|
||||
std::string activeWalletIdentityHash() const;
|
||||
|
||||
data::WalletIndex& walletIndex() { return wallet_index_; }
|
||||
const data::WalletIndex& walletIndex() const { return wallet_index_; }
|
||||
|
||||
// Connection state (convenience wrappers)
|
||||
bool isConnected() const { return state_.connected; }
|
||||
@@ -173,6 +233,24 @@ public:
|
||||
// Pool mining (xmrig)
|
||||
void startPoolMining(int threads);
|
||||
void stopPoolMining();
|
||||
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_; }
|
||||
@@ -180,7 +258,9 @@ public:
|
||||
// Peers
|
||||
const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
|
||||
const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; }
|
||||
bool isPeerRefreshInProgress() const { return peer_refresh_in_progress_.load(std::memory_order_relaxed); }
|
||||
bool isPeerRefreshInProgress() const {
|
||||
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Peers);
|
||||
}
|
||||
void banPeer(const std::string& ip, int duration_seconds = 86400);
|
||||
void unbanPeer(const std::string& ip);
|
||||
void clearBans();
|
||||
@@ -199,38 +279,98 @@ 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);
|
||||
void exportAllKeys(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 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) {
|
||||
if (page != current_page_) {
|
||||
current_page_ = page;
|
||||
if (page == ui::NavPage::Peers) refreshPeerInfo();
|
||||
}
|
||||
}
|
||||
void setCurrentPage(ui::NavPage page);
|
||||
ui::NavPage getCurrentPage() const { return current_page_; }
|
||||
|
||||
|
||||
// Debug: screenshot sweep — cycles every skin x every enabled tab, one PNG each, into a
|
||||
// timestamped folder under the config dir. Driven from App::render(); main.cpp polls
|
||||
// wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls
|
||||
// onScreenshotCaptured() to advance. Transient — restores the original skin/page when done.
|
||||
void startScreenshotSweep();
|
||||
// Full UI sweep: like the tab sweep, but also drives every modal / dialog / multi-step flow /
|
||||
// state overlay into view (with injected demo data, offline, firing no live ops) and captures
|
||||
// each under every skin. Output: <config>/screenshots-full/<surface>/<skin>.png + an index.
|
||||
void startFullUiSweep();
|
||||
std::string screenshotFullDir() const;
|
||||
bool isScreenshotSweeping() const { return screenshot_sweep_active_; }
|
||||
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
|
||||
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
|
||||
void onScreenshotCaptured();
|
||||
std::string screenshotDir() const; // <config>/screenshots (fixed; sweeps overwrite in place)
|
||||
|
||||
// Dialog triggers (used by settings page to open modal dialogs)
|
||||
void showImportKeyDialog() { show_import_key_ = true; }
|
||||
void showExportKeyDialog() { show_export_key_ = true; }
|
||||
void showBackupDialog() { show_backup_ = true; }
|
||||
void showSeedBackupDialog() { show_seed_backup_ = true; }
|
||||
void showSeedMigrationDialog(); // opens the migration modal (resumes a pending one at Sweep)
|
||||
// True when the current full-node wallet is a legacy, pre-seed-phrase wallet (no BIP39 mnemonic)
|
||||
// that a capable daemon could migrate — the Migrate-to-seed button glows to nudge the user.
|
||||
bool isPreSeedWallet() const { return wallet_seed_status_ == WalletSeedStatus::NoMnemonic; }
|
||||
void showAboutDialog() { show_about_ = true; }
|
||||
|
||||
// Legacy tab compat — maps int to NavPage
|
||||
@@ -250,10 +390,27 @@ public:
|
||||
bool startEmbeddedDaemon();
|
||||
void stopEmbeddedDaemon();
|
||||
bool isEmbeddedDaemonRunning() const;
|
||||
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
|
||||
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
|
||||
void rescanBlockchain(); // restart daemon with -rescan flag
|
||||
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
|
||||
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)
|
||||
@@ -296,9 +453,19 @@ public:
|
||||
* Shows "Restarting daemon..." in the loading overlay while the daemon cycles.
|
||||
*/
|
||||
void restartDaemon();
|
||||
// Switch the active wallet: persist the new -wallet=<name>, stop the node, restart on it
|
||||
// (rescan only if it was never synced in this datadir). Per-wallet data follows automatically
|
||||
// via the identity-scoped caches (P1). No-op if that wallet is already active.
|
||||
void switchToWallet(const std::string& walletFile);
|
||||
|
||||
// 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);
|
||||
@@ -318,10 +485,7 @@ public:
|
||||
void showChangePassphraseDialog() { show_change_passphrase_ = true; }
|
||||
void showDecryptDialog() {
|
||||
show_decrypt_dialog_ = true;
|
||||
decrypt_phase_ = 0; // passphrase entry
|
||||
decrypt_step_ = 0;
|
||||
decrypt_status_.clear();
|
||||
decrypt_in_progress_ = false;
|
||||
wallet_security_workflow_.reset();
|
||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||
}
|
||||
|
||||
@@ -331,10 +495,112 @@ public:
|
||||
void showPinRemoveDialog() { show_pin_remove_ = true; pin_status_.clear(); }
|
||||
bool hasPinVault() const;
|
||||
|
||||
// Debug-options gate: does revealing the debug dropdown need re-authentication (a PIN vault or
|
||||
// an encrypted wallet)? And verify the entered PIN/passphrase — cb(ok) is invoked on the main
|
||||
// thread (PIN via the vault, else the wallet passphrase via RPC).
|
||||
bool debugGateRequiresAuth() const;
|
||||
void verifyDebugCredential(const std::string& secret, std::function<void(bool)> cb);
|
||||
|
||||
/// @brief Check if RPC worker has queued results waiting to be processed
|
||||
bool hasPendingRPCResults() const;
|
||||
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
|
||||
std::string transactionSendProgressText() const;
|
||||
std::string transactionRefreshProgressText() const;
|
||||
|
||||
// Copy a SECRET (seed phrase, private key) to the clipboard and arm an auto-clear: after a
|
||||
// short delay the clipboard is wiped IF it still holds this secret (so we don't clobber
|
||||
// something the user copied afterwards). Only a hash of the secret is retained, never the
|
||||
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
|
||||
void copySecretToClipboard(const std::string& secret);
|
||||
void pumpSecretClipboardClear();
|
||||
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);
|
||||
// Record the active wallet's cached metadata (balance, address count, identity, size) into the
|
||||
// wallet index (wallets.json). Cheap + throttled: only writes when a value changed. markOpened
|
||||
// stamps last-opened + syncedHere (call once per connect).
|
||||
void updateWalletIndexForActiveWallet(bool markOpened);
|
||||
void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
|
||||
// Shared main-thread continuations for a wallet unlock attempt, so the passphrase
|
||||
// and PIN paths cannot drift. applyUnlockFailure applies the escalating lockout
|
||||
// curve on every failed path (a PIN RPC failure used to skip it).
|
||||
void applyUnlockSuccess(const std::string& passphrase, int timeout);
|
||||
void applyUnlockFailure(const std::string& errorMessage);
|
||||
// Clear all rescan + witness-rebuild progress/accumulator state. Shared by the four
|
||||
// rescan-completion sites so a new witness field can't be forgotten in one copy.
|
||||
void resetWitnessRescanProgress();
|
||||
void loadTransactionHistoryCacheIfAvailable();
|
||||
void storeTransactionHistoryCacheIfAvailable();
|
||||
void wipePendingTransactionHistoryCachePassphrase();
|
||||
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_;
|
||||
@@ -349,8 +615,67 @@ private:
|
||||
rpc::ConnectionConfig saved_config_;
|
||||
|
||||
std::unique_ptr<config::Settings> settings_;
|
||||
std::unique_ptr<daemon::EmbeddedDaemon> embedded_daemon_;
|
||||
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::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
|
||||
@@ -359,16 +684,22 @@ private:
|
||||
// Shutdown state
|
||||
std::atomic<bool> shutting_down_{false};
|
||||
std::atomic<bool> shutdown_complete_{false};
|
||||
std::atomic<bool> refresh_in_progress_{false};
|
||||
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
|
||||
std::string shutdown_status_;
|
||||
std::thread shutdown_thread_;
|
||||
float shutdown_timer_ = 0.0f;
|
||||
bool force_quit_confirm_ = false;
|
||||
std::chrono::steady_clock::time_point shutdown_start_time_;
|
||||
|
||||
// Daemon restart (e.g. after changing debug log categories)
|
||||
std::atomic<bool> daemon_restarting_{false};
|
||||
std::thread daemon_restart_thread_;
|
||||
|
||||
// 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;
|
||||
|
||||
// UI State
|
||||
bool quit_requested_ = false;
|
||||
@@ -378,10 +709,69 @@ private:
|
||||
bool show_import_key_ = false;
|
||||
bool show_export_key_ = false;
|
||||
bool show_backup_ = false;
|
||||
bool show_address_book_ = false;
|
||||
|
||||
bool show_seed_backup_ = false;
|
||||
|
||||
// Seed-phrase backup dialog state. seed_backup_phrase_ holds a SECRET (the revealed
|
||||
// mnemonic) and is wiped with sodium_memzero when the dialog closes.
|
||||
std::string seed_backup_phrase_;
|
||||
std::string seed_backup_status_;
|
||||
bool seed_backup_fetch_started_ = false;
|
||||
bool seed_backup_loading_ = false;
|
||||
bool seed_backup_no_mnemonic_ = false;
|
||||
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
|
||||
|
||||
// Cached mnemonic status of the current wallet, driving the Migrate-to-seed button glow. Probed
|
||||
// once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a
|
||||
// capable daemon can migrate; Incapable = the daemon lacks z_exportmnemonic (can't tell / can't
|
||||
// migrate). Reset to Unknown on wallet switch so it re-probes the new wallet.
|
||||
enum class WalletSeedStatus { Unknown, HasMnemonic, NoMnemonic, Incapable };
|
||||
WalletSeedStatus wallet_seed_status_ = WalletSeedStatus::Unknown;
|
||||
bool wallet_seed_status_in_flight_ = false;
|
||||
int wallet_seed_status_attempts_ = 0; // give up (Incapable) after a few transient probe failures
|
||||
void probeWalletSeedStatus(); // one-shot per connect; classifies the wallet's mnemonic status
|
||||
|
||||
// --- Seed-wallet migration (Phase 1: create; Phase 2: sweep + adopt) ---
|
||||
enum class SeedMigrationStep { Intro, Working, ShowSeed, Sweep, Sweeping, Confirming, Adopting, Done, Error };
|
||||
bool show_seed_migration_ = false;
|
||||
SeedMigrationStep seed_migration_step_ = SeedMigrationStep::Intro;
|
||||
|
||||
// Intro pre-flight: probe the current wallet before offering to create a seed wallet, so we can
|
||||
// skip a pointless migration (AlreadyMnemonic → offer backup) or explain why it can't run
|
||||
// (DaemonTooOld). Set from beginSeedMigrationPrecheck()'s async callback (main thread).
|
||||
enum class SeedMigrationPrecheck { Pending, Legacy, AlreadyMnemonic, DaemonTooOld, CheckFailed };
|
||||
SeedMigrationPrecheck seed_migration_precheck_ = SeedMigrationPrecheck::Pending;
|
||||
bool seed_migration_precheck_started_ = false;
|
||||
bool seed_migration_balance_loaded_ = false; // Sweep step: distinguishes "0 funds" from "not loaded yet"
|
||||
bool seed_migration_nofunds_confirmed_ = false; // "replace my wallet" gate for the no-funds adopt
|
||||
|
||||
// "A newer node is bundled — update?" startup prompt (see maybeOfferDaemonUpdate).
|
||||
bool show_daemon_update_prompt_ = false;
|
||||
unsigned long long daemon_update_bundled_size_ = 0; // bundled daemon size the prompt offers
|
||||
bool seed_migration_in_flight_ = false; // main-thread guard while the bg create task runs
|
||||
std::string seed_migration_seed_; // SECRET — the revealed phrase, wiped on close
|
||||
std::string seed_migration_dest_; // new shielded z-address (Phase 2 sweep target)
|
||||
std::string seed_migration_temp_dir_; // temp datadir root holding the new wallet (kept)
|
||||
bool seed_migration_backed_up_ = false; // "I've written it down" confirmation
|
||||
std::string seed_migration_status_; // main-thread display (progress / error text)
|
||||
double seed_migration_balance_ = 0.0; // legacy total to sweep (shown on the Sweep step)
|
||||
std::string seed_migration_sweep_txid_; // the z_mergetoaddress sweep transaction id
|
||||
int seed_migration_sweep_confs_ = 0; // confirmations of the sweep tx (adopt gate: >= 1)
|
||||
double seed_migration_legacy_remaining_ = -1.0; // legacy balance after sweep (-1 = unknown)
|
||||
float seed_migration_poll_timer_ = 0.0f; // throttles the Confirming-step poll
|
||||
bool seed_migration_confirm_in_flight_ = false; // guards the confirm poll
|
||||
// Cross-thread handoff from the background create task (guarded by seed_migration_mutex_).
|
||||
std::mutex seed_migration_mutex_;
|
||||
std::string seed_migration_progress_; // latest progress line
|
||||
bool seed_migration_done_ = false; // create result ready to consume
|
||||
bool seed_migration_ok_ = false;
|
||||
std::string seed_migration_r_seed_, seed_migration_r_dest_, seed_migration_r_tmp_, seed_migration_r_err_;
|
||||
// Cross-thread handoff from the background adopt task (guarded by seed_migration_mutex_).
|
||||
bool seed_migration_adopt_done_ = false;
|
||||
bool seed_migration_adopt_ok_ = false;
|
||||
std::string seed_migration_adopt_err_;
|
||||
|
||||
// Embedded daemon state
|
||||
bool use_embedded_daemon_ = true;
|
||||
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
|
||||
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)
|
||||
@@ -398,6 +788,16 @@ 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)
|
||||
@@ -405,6 +805,55 @@ private:
|
||||
ui::NavPage prev_page_ = ui::NavPage::Overview;
|
||||
float page_alpha_ = 1.0f; // 0→1 fade on page switch
|
||||
bool sidebar_collapsed_ = false; // true = icon-only mode
|
||||
|
||||
// Debug screenshot sweep state.
|
||||
bool screenshot_sweep_active_ = false;
|
||||
bool sweep_capture_this_frame_ = false;
|
||||
int sweep_skin_idx_ = 0;
|
||||
int sweep_settle_frames_ = 0; // frames to let a new skin/surface settle before capture
|
||||
std::vector<std::string> sweep_skins_; // skin ids to cycle
|
||||
std::string sweep_dir_; // output folder for this sweep
|
||||
std::string sweep_current_path_; // PNG path for the frame about to be captured
|
||||
std::string sweep_saved_skin_; // restore on completion
|
||||
ui::NavPage sweep_saved_page_ = ui::NavPage::Overview;
|
||||
void updateScreenshotSweep(); // called at the top of render() while active
|
||||
void applySweepTarget(); // apply current (skin,page/surface), path, arm settle
|
||||
|
||||
// --- Full UI sweep: the capture unit is a "surface" (a tab, optionally with a modal / step /
|
||||
// state forced on top). A tab is a surface with a null setup. ---
|
||||
struct SweepTarget {
|
||||
std::string name; // fs-safe id, e.g. "modal-seed-backup" / "overview"
|
||||
ui::NavPage page = ui::NavPage::Overview; // base tab under the surface
|
||||
std::function<void(App&)> setup; // reveal the surface (null = plain tab)
|
||||
std::function<void(App&)> teardown; // clear it (null = nothing to undo)
|
||||
int settle = 4;// blur overlays override to 8
|
||||
};
|
||||
std::vector<SweepTarget> sweep_targets_;
|
||||
int sweep_target_idx_ = 0;
|
||||
bool sweep_full_ = false; // full-UI sweep (drives surfaces) vs the legacy tab-only sweep
|
||||
// capture_mode_: set only during a full sweep. CONTRACT: while true, NO live op may fire — no
|
||||
// RPC, no auto-lock, no async pump. New async paths that could run mid-sweep must guard on it.
|
||||
bool capture_mode_ = false;
|
||||
void startSweepImpl(bool full);
|
||||
void buildSweepCatalog();
|
||||
void installDemoWalletData();
|
||||
void clearDemoWalletData();
|
||||
void applyHealthyDemoState(); // reset the connection/encryption flags to the healthy demo values
|
||||
void writeSweepManifest() const;
|
||||
// Snapshot of the state_ fields the demo installer mutates, restored at sweep end. Kept as a
|
||||
// plain struct because WalletState has reference-alias members and isn't copy-assignable.
|
||||
struct SweepStateSnapshot {
|
||||
bool valid = false;
|
||||
bool connected=false, warming_up=false, daemon_initializing=false;
|
||||
bool encrypted=false, locked=false, encryption_state_known=false;
|
||||
std::string warmup_status, warmup_description;
|
||||
SyncInfo sync;
|
||||
double privateBalance=0, transparentBalance=0, totalBalance=0, unconfirmedBalance=0;
|
||||
std::vector<AddressInfo> addresses, z_addresses, t_addresses;
|
||||
std::vector<TransactionInfo> transactions;
|
||||
double market_price_usd=0;
|
||||
double market_change_24h=0;
|
||||
} sweep_state_snapshot_;
|
||||
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
|
||||
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
|
||||
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
|
||||
@@ -425,8 +874,9 @@ private:
|
||||
int coin_logo_h_ = 0;
|
||||
bool coin_logo_loaded_ = false;
|
||||
|
||||
// Console tab
|
||||
// Console tab + its backend executor (full-node RPC or lite backend), created lazily.
|
||||
ui::ConsoleTab console_tab_;
|
||||
std::unique_ptr<ui::ConsoleCommandExecutor> console_exec_;
|
||||
|
||||
// Pending payment from URI
|
||||
bool pending_payment_valid_ = false;
|
||||
@@ -435,47 +885,35 @@ private:
|
||||
std::string pending_memo_;
|
||||
std::string pending_label_;
|
||||
|
||||
// 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};
|
||||
// Per-category refresh timers, policy, and worker queue guards.
|
||||
services::NetworkRefreshService network_refresh_;
|
||||
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};
|
||||
|
||||
// Peer refresh guard (visual feedback for refresh button)
|
||||
std::atomic<bool> peer_refresh_in_progress_{false};
|
||||
// True from a successful startPoolMining() until the miner is confirmed connected/hashing in the
|
||||
// poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay).
|
||||
std::atomic<bool> pool_starting_{false};
|
||||
|
||||
// Auto-shield guard (prevents concurrent auto-shield operations)
|
||||
std::atomic<bool> auto_shield_pending_{false};
|
||||
|
||||
// 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.
|
||||
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_;
|
||||
using ViewTxCacheEntry = services::NetworkRefreshService::TransactionViewCacheEntry;
|
||||
services::NetworkRefreshService::TransactionViewCache viewtx_cache_;
|
||||
|
||||
// P4c: Confirmed transaction cache — deeply-confirmed txns (>= 10 confs)
|
||||
// are accumulated here and reused across refresh cycles. Only
|
||||
@@ -486,14 +924,58 @@ 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
|
||||
float opid_poll_timer_ = 0.0f;
|
||||
static constexpr float OPID_POLL_INTERVAL = 2.0f;
|
||||
|
||||
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_;
|
||||
// Txids from completed z_sendmany operations.
|
||||
// Ensures shielded sends are discoverable by z_viewtransaction
|
||||
// even when they don't appear in listtransactions or
|
||||
@@ -503,21 +985,25 @@ 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
|
||||
|
||||
// Deferred encryption (wizard background task)
|
||||
std::string deferred_encrypt_passphrase_;
|
||||
std::string deferred_encrypt_pin_;
|
||||
bool deferred_encrypt_pending_ = false;
|
||||
// Wallet security flow state shared by wizard/settings encryption paths.
|
||||
services::WalletSecurityController wallet_security_;
|
||||
services::WalletSecurityWorkflow wallet_security_workflow_;
|
||||
|
||||
// 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
|
||||
data::WalletIndex wallet_index_; // per-wallet metadata cache (wallets.json); populated after each load
|
||||
std::string pending_transaction_history_cache_passphrase_;
|
||||
bool transaction_history_cache_loaded_ = false;
|
||||
|
||||
// Lock screen state
|
||||
bool lock_screen_was_visible_ = false; // tracks lock→unlock transitions for auto-focus
|
||||
@@ -559,14 +1045,7 @@ 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] = {};
|
||||
@@ -582,10 +1061,16 @@ private:
|
||||
|
||||
// Private methods - rendering
|
||||
void renderStatusBar();
|
||||
void renderAboutDialog();
|
||||
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
|
||||
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
|
||||
void renderImportKeyDialog();
|
||||
void renderExportKeyDialog();
|
||||
void renderBackupDialog();
|
||||
void renderSeedBackupDialog(); // full-node "Back up seed phrase" modal (z_exportmnemonic)
|
||||
void renderSeedMigrationDialog(); // "Migrate to a seed wallet" guided modal (Phase 1: create)
|
||||
void beginSeedMigrationPrecheck(); // Intro: probe whether the wallet is legacy / already-seeded
|
||||
void maybeOfferDaemonUpdate(); // at startup, flag the prompt if a newer daemon is bundled
|
||||
void renderDaemonUpdatePrompt(); // "a newer node is bundled — update the installed daemon?"
|
||||
void renderFirstRunWizard();
|
||||
void renderLockScreen();
|
||||
void renderEncryptWalletDialog();
|
||||
@@ -598,14 +1083,34 @@ 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();
|
||||
void refreshBalance();
|
||||
void refreshAddresses();
|
||||
void refreshData(); // Orchestrator: dispatches per-category refreshes
|
||||
void refreshCoreData(); // Balance + blockchain info (can use fast_worker_)
|
||||
void refreshAddressData(); // Address lists + balances
|
||||
void refreshTransactionData(); // Transaction list + z_viewtransaction enrichment
|
||||
void refreshRecentTransactionData(); // Lightweight recent/unconfirmed tx poll
|
||||
bool refreshEncryptionState(); // Wallet encryption/lock state
|
||||
void refreshBalance(); // Legacy: balance-only refresh (used by specific callers)
|
||||
void refreshAddresses(); // Legacy: standalone address refresh
|
||||
void refreshPrice();
|
||||
void refreshWalletEncryptionState();
|
||||
void applyRefreshPolicy(ui::NavPage page);
|
||||
bool currentPageNeedsWalletDataRefresh() const;
|
||||
bool shouldRunWalletTransactionRefresh() const;
|
||||
bool shouldRefreshTransactions() const;
|
||||
bool shouldRefreshRecentTransactions() const;
|
||||
void checkAutoLock();
|
||||
void checkIdleMining();
|
||||
};
|
||||
|
||||
4065
src/app_network.cpp
4065
src/app_network.cpp
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
646
src/app_sweep.cpp
Normal file
646
src/app_sweep.cpp
Normal file
@@ -0,0 +1,646 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Screenshot sweep: capture the UI under every skin. Two modes share one state machine:
|
||||
// - startScreenshotSweep() — the legacy tab-only sweep (<config>/screenshots).
|
||||
// - startFullUiSweep() — ALSO drives every modal / dialog / multi-step flow / state overlay
|
||||
// into view (offline, with injected demo data, firing no live ops) and captures each under
|
||||
// every skin (<config>/screenshots-full/<surface>/<skin>.png + index.md).
|
||||
// The capture unit is a "surface" (SweepTarget): a base tab, optionally with a setup() that forces
|
||||
// a modal/step/state on top and a teardown() that clears it. main.cpp reads the framebuffer on the
|
||||
// settled frame and calls onScreenshotCaptured() to advance.
|
||||
|
||||
#include "app.h"
|
||||
|
||||
#include "config/settings.h"
|
||||
#include "ui/schema/skin_manager.h"
|
||||
#include "ui/notifications.h"
|
||||
#include "ui/sidebar.h"
|
||||
#include "ui/windows/send_tab.h"
|
||||
#include "ui/windows/wallets_dialog.h"
|
||||
#include "ui/windows/daemon_download_dialog.h"
|
||||
#include "ui/windows/xmrig_download_dialog.h"
|
||||
#include "ui/pages/settings_page.h"
|
||||
#include "util/platform.h"
|
||||
#include "wallet/wallet_capabilities.h"
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h" // ClosePopupToLevel / OpenPopupStack — flush stuck popups after teardown
|
||||
|
||||
#include <sodium.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
namespace dragonx {
|
||||
|
||||
namespace {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// The real portfolio, snapshotted while demo groups are shown during a full sweep. Settings are
|
||||
// forward-declared in app.h, so this can't live in the app.h SweepStateSnapshot struct.
|
||||
std::vector<config::Settings::PortfolioEntry> g_pfSnapshot;
|
||||
int g_pfStyleSnapshot = 0;
|
||||
bool g_pfSnapshotValid = false;
|
||||
std::vector<double> g_marketHistorySnapshot; // real price history, restored at sweep end
|
||||
|
||||
// Filesystem-safe one-word tab name.
|
||||
const char* sweepPageName(ui::NavPage page)
|
||||
{
|
||||
switch (page) {
|
||||
case ui::NavPage::Overview: return "overview";
|
||||
case ui::NavPage::Send: return "send";
|
||||
case ui::NavPage::Receive: return "receive";
|
||||
case ui::NavPage::History: return "history";
|
||||
case ui::NavPage::Contacts: return "contacts";
|
||||
case ui::NavPage::Chat: return "chat";
|
||||
case ui::NavPage::Mining: return "mining";
|
||||
case ui::NavPage::Market: return "market";
|
||||
case ui::NavPage::Console: return "console";
|
||||
case ui::NavPage::LiteConsole: return "console";
|
||||
case ui::NavPage::Peers: return "network";
|
||||
case ui::NavPage::LiteNetwork: return "network";
|
||||
case ui::NavPage::Explorer: return "explorer";
|
||||
case ui::NavPage::Settings: return "settings";
|
||||
default: return "page";
|
||||
}
|
||||
}
|
||||
|
||||
bool sweepPageEnabled(ui::NavPage page)
|
||||
{
|
||||
return wallet::isUiSurfaceAvailable(wallet::currentWalletCapabilities(), ui::NavPageSurface(page));
|
||||
}
|
||||
|
||||
// Deterministic demo secrets/addresses (NON-secret — safe to hold in memory + display).
|
||||
const char* kDemoMnemonic =
|
||||
"select milk exit banana type alcohol comic moral drama federal just green "
|
||||
"elevator render stumble lesson convince organ category caution panther misery pelican immune";
|
||||
const char* kDemoZAddr =
|
||||
"zs1demoseedwalletreceiveaddressforuisweepcaptures00000000000000000000000000";
|
||||
const char* kDemoTxid = "b647077c471fdd2877d35c9d8b70e3c785547fae618458b4068d913ee3d1dc4f";
|
||||
} // namespace
|
||||
|
||||
std::string App::screenshotDir() const
|
||||
{
|
||||
return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots").string();
|
||||
}
|
||||
|
||||
std::string App::screenshotFullDir() const
|
||||
{
|
||||
return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots-full").string();
|
||||
}
|
||||
|
||||
// ── Demo data (full sweep only): make every data-dependent screen render offline ─────────────
|
||||
void App::installDemoWalletData()
|
||||
{
|
||||
// Snapshot the state_ fields we mutate (WalletState isn't copy-assignable — reference aliases).
|
||||
auto& s = sweep_state_snapshot_;
|
||||
s.connected = state_.connected; s.warming_up = state_.warming_up;
|
||||
s.daemon_initializing = state_.daemon_initializing;
|
||||
s.encrypted = state_.encrypted; s.locked = state_.locked;
|
||||
s.encryption_state_known = state_.encryption_state_known;
|
||||
s.warmup_status = state_.warmup_status; s.warmup_description = state_.warmup_description;
|
||||
s.sync = state_.sync;
|
||||
s.privateBalance = state_.privateBalance; s.transparentBalance = state_.transparentBalance;
|
||||
s.totalBalance = state_.totalBalance; s.unconfirmedBalance = state_.unconfirmedBalance;
|
||||
s.addresses = state_.addresses; s.z_addresses = state_.z_addresses; s.t_addresses = state_.t_addresses;
|
||||
s.transactions = state_.transactions;
|
||||
s.market_price_usd = state_.market.price_usd;
|
||||
s.market_change_24h = state_.market.change_24h;
|
||||
s.valid = true;
|
||||
|
||||
applyHealthyDemoState();
|
||||
state_.sync.blocks = state_.sync.headers = 3124322;
|
||||
state_.sync.verification_progress = 1.0; state_.sync.syncing = false;
|
||||
// Legacy (pre-seed) wallet so the Settings "Migrate to seed" button glows in the sweep.
|
||||
wallet_seed_status_ = WalletSeedStatus::NoMnemonic;
|
||||
|
||||
state_.privateBalance = 12.50000000; state_.transparentBalance = 3.25000000;
|
||||
state_.totalBalance = 15.75000000; state_.unconfirmedBalance = 0.50000000;
|
||||
state_.market.price_usd = 0.01336200;
|
||||
state_.market.change_24h = 5.24000000;
|
||||
// Wavy, gently-rising price history so the row sparklines render (restored at sweep end).
|
||||
g_marketHistorySnapshot = state_.market.price_history;
|
||||
{
|
||||
std::vector<double> hist;
|
||||
for (int i = 0; i < 48; i++) {
|
||||
double t = static_cast<double>(i);
|
||||
hist.push_back(0.01250 + 0.0000180 * t
|
||||
+ 0.00060 * std::sin(t * 0.55) + 0.00025 * std::sin(t * 1.7));
|
||||
}
|
||||
state_.market.price_history = hist;
|
||||
}
|
||||
|
||||
auto zaddr = [](const char* a, double bal, const char* label) {
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.type = "shielded"; i.label = label; return i;
|
||||
};
|
||||
auto taddr = [](const char* a, double bal, const char* label) {
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.type = "transparent"; i.label = label; return i;
|
||||
};
|
||||
state_.z_addresses = {
|
||||
zaddr("zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", 12.0, "Savings"),
|
||||
zaddr("zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", 0.5, ""),
|
||||
};
|
||||
state_.t_addresses = {
|
||||
taddr("t1DemoTransparentAddressForUiSweep00000", 3.25, "Mining payouts"),
|
||||
taddr("t1DemoSecondTransparentAddressForUiSw00", 0.0, ""),
|
||||
};
|
||||
state_.rebuildAddressList();
|
||||
|
||||
auto tx = [](const char* id, const char* type, double amt, int64_t ts, int conf,
|
||||
const char* addr, const char* memo) {
|
||||
TransactionInfo t; t.txid = id; t.type = type; t.amount = amt; t.timestamp = ts;
|
||||
t.confirmations = conf; t.address = addr; t.memo = memo; return t;
|
||||
};
|
||||
state_.transactions = {
|
||||
tx(kDemoTxid, "receive", 15.75000000, 1751286000, 42,
|
||||
"zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", "welcome to DragonX"),
|
||||
tx("a1b2c3d4e5f600000000000000000000000000000000000000000000000000000000", "send", 2.50000000,
|
||||
1751200000, 120, "zs1demopeerreceivingaddressabcdef0000000000000000000000000000000", ""),
|
||||
tx("c0ffee00000000000000000000000000000000000000000000000000000000000000", "mined", 0.30000000,
|
||||
1751100000, 300, "t1DemoTransparentAddressForUiSweep00000", ""),
|
||||
tx("deadbeef000000000000000000000000000000000000000000000000000000000000", "receive", 0.50000000,
|
||||
1751290000, 0, "zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", "pending"),
|
||||
};
|
||||
|
||||
// Demo portfolio groups so the Market tab's row styles render with real-looking data. Snapshot
|
||||
// the user's real portfolio first; setPortfolio* only mutate memory (save() is never called
|
||||
// here), and clearDemoWalletData() restores them at sweep end.
|
||||
if (settings_) {
|
||||
g_pfSnapshot = settings_->getPortfolioEntries();
|
||||
g_pfStyleSnapshot = settings_->getPortfolioStyle();
|
||||
g_pfSnapshotValid = true;
|
||||
auto grp = [](const char* label, const char* icon, uint32_t color, const char* addr,
|
||||
bool drgx, bool value, bool ch, bool spark) {
|
||||
config::Settings::PortfolioEntry e;
|
||||
e.label = label; e.icon = icon; e.color = color; e.outlineOpacity = 30;
|
||||
e.addresses = { addr }; e.priceBasis = 0;
|
||||
e.showDrgx = drgx; e.showValue = value; e.show24h = ch; e.showSparkline = spark;
|
||||
return e;
|
||||
};
|
||||
settings_->setPortfolioEntries({
|
||||
grp("Savings", "savings", 0xFFFF9D4Fu,
|
||||
"zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", true, true, true, true),
|
||||
grp("Mining rewards", "pickaxe", 0xFF3DB0FFu,
|
||||
"t1DemoTransparentAddressForUiSweep00000", true, true, true, true),
|
||||
grp("Cold storage", "diamond", 0xFFFF7BB0u,
|
||||
"zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", true, true, false, false),
|
||||
});
|
||||
settings_->setPortfolioStyle(0);
|
||||
}
|
||||
|
||||
seedChatDemoData();
|
||||
}
|
||||
|
||||
void App::applyHealthyDemoState()
|
||||
{
|
||||
state_.connected = true;
|
||||
state_.warming_up = false;
|
||||
state_.daemon_initializing = false;
|
||||
state_.encryption_state_known = true;
|
||||
state_.encrypted = false;
|
||||
state_.locked = false;
|
||||
state_.warmup_status.clear();
|
||||
state_.warmup_description.clear();
|
||||
// Dismiss any first-run wizard so the base tabs/modals aren't occluded (the wizard targets set
|
||||
// wizard_phase_ themselves and restore None on teardown).
|
||||
wizard_phase_ = WizardPhase::None;
|
||||
}
|
||||
|
||||
void App::clearDemoWalletData()
|
||||
{
|
||||
auto& s = sweep_state_snapshot_;
|
||||
if (!s.valid) return;
|
||||
wallet_seed_status_ = WalletSeedStatus::Unknown; // re-probe on the next real connect
|
||||
state_.connected = s.connected; state_.warming_up = s.warming_up;
|
||||
state_.daemon_initializing = s.daemon_initializing;
|
||||
state_.encrypted = s.encrypted; state_.locked = s.locked;
|
||||
state_.encryption_state_known = s.encryption_state_known;
|
||||
state_.warmup_status = s.warmup_status; state_.warmup_description = s.warmup_description;
|
||||
state_.sync = s.sync;
|
||||
state_.privateBalance = s.privateBalance; state_.transparentBalance = s.transparentBalance;
|
||||
state_.totalBalance = s.totalBalance; state_.unconfirmedBalance = s.unconfirmedBalance;
|
||||
state_.addresses = s.addresses; state_.z_addresses = s.z_addresses; state_.t_addresses = s.t_addresses;
|
||||
state_.transactions = s.transactions;
|
||||
state_.market.price_usd = s.market_price_usd;
|
||||
state_.market.change_24h = s.market_change_24h;
|
||||
state_.market.price_history = g_marketHistorySnapshot;
|
||||
g_marketHistorySnapshot.clear();
|
||||
s = SweepStateSnapshot{}; // invalidate
|
||||
|
||||
// Restore the user's real portfolio (demo groups were in-memory only).
|
||||
if (g_pfSnapshotValid && settings_) {
|
||||
settings_->setPortfolioEntries(g_pfSnapshot);
|
||||
settings_->setPortfolioStyle(g_pfStyleSnapshot);
|
||||
}
|
||||
g_pfSnapshot.clear();
|
||||
g_pfSnapshotValid = false;
|
||||
}
|
||||
|
||||
// ── Catalog ──────────────────────────────────────────────────────────────────────────────────
|
||||
// Lambdas defined in this member function may touch App's private members through the App& arg.
|
||||
void App::buildSweepCatalog()
|
||||
{
|
||||
sweep_targets_.clear();
|
||||
|
||||
// Tabs (both sweeps).
|
||||
for (int p = 0; p < static_cast<int>(ui::NavPage::Count_); ++p) {
|
||||
ui::NavPage pg = static_cast<ui::NavPage>(p);
|
||||
if (sweepPageEnabled(pg)) sweep_targets_.push_back({ sweepPageName(pg), pg, nullptr, nullptr, 4 });
|
||||
}
|
||||
if (!sweep_full_) return;
|
||||
|
||||
// Full sweep: modals / flows / states. Blur overlays need more settle frames.
|
||||
const int kOverlaySettle = 8;
|
||||
auto add = [&](const char* name, ui::NavPage pg, std::function<void(App&)> setup,
|
||||
std::function<void(App&)> teardown) {
|
||||
sweep_targets_.push_back({ name, pg, std::move(setup), std::move(teardown), kOverlaySettle });
|
||||
};
|
||||
|
||||
// Simple bool-flag modals.
|
||||
add("modal-import-key", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; });
|
||||
add("modal-export-key", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_export_key_ = true; }, [](App& a) { a.show_export_key_ = false; });
|
||||
add("modal-backup", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); });
|
||||
add("modal-about", ui::NavPage::Overview,
|
||||
[](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; });
|
||||
add("modal-settings", ui::NavPage::Settings,
|
||||
[](App& a) { a.show_settings_ = true; }, [](App& a) { a.show_settings_ = false; });
|
||||
|
||||
// Seed-backup: pre-set fetch + a demo phrase so it renders the word grid without any RPC.
|
||||
add("modal-seed-backup", ui::NavPage::Overview,
|
||||
[](App& a) {
|
||||
a.show_seed_backup_ = true; a.seed_backup_fetch_started_ = true;
|
||||
a.seed_backup_loading_ = false; a.seed_backup_no_mnemonic_ = false;
|
||||
a.seed_backup_status_.clear();
|
||||
if (a.seed_backup_phrase_.empty()) a.seed_backup_phrase_ = kDemoMnemonic;
|
||||
},
|
||||
[](App& a) {
|
||||
a.show_seed_backup_ = false; a.seed_backup_fetch_started_ = false;
|
||||
if (!a.seed_backup_phrase_.empty())
|
||||
sodium_memzero(&a.seed_backup_phrase_[0], a.seed_backup_phrase_.size());
|
||||
a.seed_backup_phrase_.clear();
|
||||
});
|
||||
|
||||
// Migrate-to-seed — one surface per step (full-node only). Setting the step directly never
|
||||
// fires the async create/sweep/adopt (those are button-triggered).
|
||||
if (supportsFullNodeLifecycleActions()) {
|
||||
auto mig = [&](const char* name, SeedMigrationStep step, std::function<void(App&)> extra) {
|
||||
add(name, ui::NavPage::Settings,
|
||||
[step, extra](App& a) {
|
||||
a.show_seed_migration_ = true; a.seed_migration_step_ = step;
|
||||
a.seed_migration_dest_ = kDemoZAddr;
|
||||
if (extra) extra(a);
|
||||
},
|
||||
[](App& a) {
|
||||
a.show_seed_migration_ = false;
|
||||
if (!a.seed_migration_seed_.empty())
|
||||
sodium_memzero(&a.seed_migration_seed_[0], a.seed_migration_seed_.size());
|
||||
a.seed_migration_seed_.clear(); a.seed_migration_status_.clear();
|
||||
});
|
||||
};
|
||||
// Intro pre-flight branches: pre-set the probe result so the sweep captures each variant
|
||||
// (the probe itself is guarded off during capture_mode_).
|
||||
mig("modal-migrate-intro", SeedMigrationStep::Intro, [](App& a) {
|
||||
a.seed_migration_precheck_ = SeedMigrationPrecheck::Legacy;
|
||||
a.seed_migration_precheck_started_ = true;
|
||||
});
|
||||
mig("modal-migrate-intro-seeded", SeedMigrationStep::Intro, [](App& a) {
|
||||
a.seed_migration_precheck_ = SeedMigrationPrecheck::AlreadyMnemonic;
|
||||
a.seed_migration_precheck_started_ = true;
|
||||
});
|
||||
mig("modal-migrate-intro-oldnode", SeedMigrationStep::Intro, [](App& a) {
|
||||
a.seed_migration_precheck_ = SeedMigrationPrecheck::DaemonTooOld;
|
||||
a.seed_migration_precheck_started_ = true;
|
||||
});
|
||||
mig("modal-migrate-showseed", SeedMigrationStep::ShowSeed,
|
||||
[](App& a) { a.seed_migration_seed_ = kDemoMnemonic; a.seed_migration_backed_up_ = false; });
|
||||
mig("modal-migrate-sweep", SeedMigrationStep::Sweep,
|
||||
[](App& a) { a.seed_migration_balance_ = 15.75000000; a.seed_migration_balance_loaded_ = true; });
|
||||
mig("modal-migrate-nofunds", SeedMigrationStep::Sweep,
|
||||
[](App& a) { a.seed_migration_balance_ = 0.0; a.seed_migration_balance_loaded_ = true; });
|
||||
mig("modal-migrate-confirming", SeedMigrationStep::Confirming, [](App& a) {
|
||||
a.seed_migration_sweep_confs_ = 1; a.seed_migration_sweep_txid_ = kDemoTxid;
|
||||
a.seed_migration_legacy_remaining_ = 0.0;
|
||||
});
|
||||
mig("modal-migrate-done", SeedMigrationStep::Done, nullptr);
|
||||
mig("modal-migrate-error", SeedMigrationStep::Error,
|
||||
[](App& a) { a.seed_migration_status_ = "Example error: the daemon did not respond."; });
|
||||
|
||||
// Multi-wallet: the wallet-files list. Drop a couple of demo wallet files in the (throwaway)
|
||||
// datadir + cache their metadata so the list renders populated.
|
||||
add("modal-wallets", ui::NavPage::Settings,
|
||||
[](App& a) {
|
||||
std::error_code ec;
|
||||
const std::string dd = util::Platform::getDragonXDataDir();
|
||||
fs::create_directories(dd, ec);
|
||||
if (!fs::exists(dd + "/wallet.dat", ec))
|
||||
std::ofstream(dd + "/wallet.dat", std::ios::binary) << std::string(122880, '\0');
|
||||
if (!fs::exists(dd + "/wallet-savings.dat", ec))
|
||||
std::ofstream(dd + "/wallet-savings.dat", std::ios::binary) << std::string(65536, '\0');
|
||||
data::WalletIndexEntry e1; e1.fileName = "wallet.dat"; e1.displayName = "wallet.dat";
|
||||
e1.cachedBalance = 15.7526; e1.cachedAddressCount = 4;
|
||||
e1.lastOpenedEpoch = 1720000000; e1.syncedHere = true;
|
||||
data::WalletIndexEntry e2; e2.fileName = "wallet-savings.dat"; e2.displayName = "wallet-savings.dat";
|
||||
e2.cachedBalance = 250.0; e2.cachedAddressCount = 2;
|
||||
e2.lastOpenedEpoch = 1719400000; e2.syncedHere = true;
|
||||
a.walletIndex().upsert(e1);
|
||||
a.walletIndex().upsert(e2);
|
||||
// An out-of-datadir wallet (in an extra folder) so the Import action shows too.
|
||||
const std::string extra = util::Platform::getConfigDir() + "extra-wallets";
|
||||
fs::create_directories(extra, ec);
|
||||
if (!fs::exists(extra + "/my-wallet.dat", ec))
|
||||
std::ofstream(extra + "/my-wallet.dat", std::ios::binary) << std::string(80000, '\0');
|
||||
a.walletIndex().addExtraFolder(extra);
|
||||
if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
|
||||
ui::WalletsDialog::show(&a);
|
||||
},
|
||||
[](App&) { ui::WalletsDialog::hide(); });
|
||||
|
||||
// Many wallets — exercises the viewport-cap path: the card can't fit all rows, so the list
|
||||
// must scroll internally while the create/scan/footer controls stay pinned (esp. at 150%).
|
||||
// Teardown removes the extra files it dropped so the plain modal-wallets surface stays a
|
||||
// clean 3-wallet reference (both surfaces share the one throwaway datadir).
|
||||
static const char* kManyExtra[] = {"wallet-cold.dat", "wallet-trading.dat", "wallet-mining.dat",
|
||||
"wallet-donations.dat", "wallet-2023.dat", "wallet-payroll.dat"};
|
||||
add("modal-wallets-many", ui::NavPage::Settings,
|
||||
[](App& a) {
|
||||
std::error_code ec;
|
||||
const std::string dd = util::Platform::getDragonXDataDir();
|
||||
fs::create_directories(dd, ec);
|
||||
const char* names[] = {"wallet.dat", "wallet-savings.dat", "wallet-cold.dat",
|
||||
"wallet-trading.dat", "wallet-mining.dat", "wallet-donations.dat",
|
||||
"wallet-2023.dat", "wallet-payroll.dat"};
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (!fs::exists(dd + "/" + names[i], ec))
|
||||
std::ofstream(dd + "/" + names[i], std::ios::binary) << std::string(64000 + i * 4096, '\0');
|
||||
data::WalletIndexEntry e; e.fileName = names[i]; e.displayName = names[i];
|
||||
e.cachedBalance = 5.0 * (i + 1); e.cachedAddressCount = 2 + i;
|
||||
e.lastOpenedEpoch = 1720000000 - i * 86400; e.syncedHere = true;
|
||||
a.walletIndex().upsert(e);
|
||||
}
|
||||
if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat");
|
||||
ui::WalletsDialog::show(&a);
|
||||
},
|
||||
[](App& a) {
|
||||
ui::WalletsDialog::hide();
|
||||
std::error_code ec;
|
||||
const std::string dd = util::Platform::getDragonXDataDir();
|
||||
for (const char* n : kManyExtra) fs::remove(dd + "/" + n, ec);
|
||||
});
|
||||
}
|
||||
|
||||
// First-run wizard — one per meaningful phase (full-node only; blocks all other UI while shown).
|
||||
if (!isLiteBuild()) {
|
||||
auto wiz = [&](const char* name, WizardPhase phase) {
|
||||
add(name, ui::NavPage::Overview,
|
||||
[phase](App& a) { a.wizard_phase_ = phase; },
|
||||
[](App& a) { a.wizard_phase_ = WizardPhase::None; });
|
||||
};
|
||||
wiz("wizard-appearance", WizardPhase::Appearance);
|
||||
wiz("wizard-bootstrap", WizardPhase::BootstrapOffer);
|
||||
wiz("wizard-encrypt", WizardPhase::EncryptOffer);
|
||||
wiz("wizard-pin", WizardPhase::PinSetup);
|
||||
}
|
||||
|
||||
// App-state overlays. Teardown restores the healthy demo flags (full restore at sweep end).
|
||||
add("overlay-lock", ui::NavPage::Overview,
|
||||
[](App& a) { a.state_.encrypted = true; a.state_.locked = true; },
|
||||
[](App& a) { a.applyHealthyDemoState(); });
|
||||
add("overlay-warmup", ui::NavPage::Overview,
|
||||
[](App& a) {
|
||||
a.state_.warming_up = true;
|
||||
a.state_.warmup_status = "Processing blocks…";
|
||||
a.state_.warmup_description = "The node is loading the block index.";
|
||||
},
|
||||
[](App& a) { a.applyHealthyDemoState(); });
|
||||
add("overlay-not-ready", ui::NavPage::Overview,
|
||||
[](App& a) { a.state_.connected = false; a.state_.encryption_state_known = false; },
|
||||
[](App& a) { a.applyHealthyDemoState(); });
|
||||
|
||||
// Send-confirm popup (state lives in send_tab statics → driven via a debug hook).
|
||||
add("popup-send-confirm", ui::NavPage::Send,
|
||||
[](App& a) { ui::SweepShowSendConfirm(&a, true); },
|
||||
[](App& a) { ui::SweepShowSendConfirm(&a, false); });
|
||||
|
||||
// Market portfolio row styles — capture all three so the redesign is reviewable per skin.
|
||||
add("market-rows-compact", ui::NavPage::Market,
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); },
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
||||
add("market-rows-detailed", ui::NavPage::Market,
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(1); },
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
||||
add("market-rows-featured", ui::NavPage::Market,
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(2); },
|
||||
[](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); });
|
||||
|
||||
// Console RPC command-reference popup (fixed-height dialog with a fill-height command list).
|
||||
add("modal-console-commands", ui::NavPage::Console,
|
||||
[](App& a) { a.console_tab_.sweepSetCommandsPopup(true); },
|
||||
[](App& a) { a.console_tab_.sweepSetCommandsPopup(false); });
|
||||
|
||||
// Debug-options gate: confirmation + warning, with the passphrase re-auth field (encrypted).
|
||||
add("modal-debug-gate", ui::NavPage::Settings,
|
||||
[](App& a) { a.state_.encrypted = true; ui::SweepOpenDebugGate(true); },
|
||||
[](App& a) { ui::SweepOpenDebugGate(false); a.applyHealthyDemoState(); });
|
||||
|
||||
// Daemon updater — the two-pane version picker (versions left, selected-version detail right).
|
||||
// Seeds fake releases so the offline sweep renders it without a network fetch / live updater.
|
||||
add("modal-daemon-update", ui::NavPage::Settings,
|
||||
[](App& a) {
|
||||
auto mk = [](const char* tag, const char* name, const char* date, const char* body, bool pre) {
|
||||
util::DaemonRelease r; r.ok = true; r.tag = tag; r.name = name; r.body = body;
|
||||
r.prerelease = pre; r.publishedAt = std::string(date) + "T10:00:00Z";
|
||||
util::DaemonReleaseAsset as;
|
||||
as.name = std::string("dragonx-") + tag + "-linux-amd64.zip";
|
||||
as.downloadUrl = "https://git.dragonx.is/" + as.name; as.size = 43000000;
|
||||
r.assets.push_back(as);
|
||||
return r;
|
||||
};
|
||||
std::vector<util::DaemonRelease> rels;
|
||||
rels.push_back(mk("v1.0.4", "DragonX v1.0.4", "2026-06-28",
|
||||
"## What is DragonX?\n\n"
|
||||
"DragonX is a privacy-focused cryptocurrency built on zero-knowledge mathematics. "
|
||||
"It enforces mandatory z2z (shielded-to-shielded) transactions after block 340,000.\n\n"
|
||||
"## Bug Fixes\n"
|
||||
"* Fix sapling pool persistence \xE2\x80\x94 pool total no longer resets to 0 on restart\n"
|
||||
"* Add `subsidy` and `fees` fields to the `getblock` RPC response\n\n"
|
||||
"## Key Features\n"
|
||||
"* **RandomX Proof-of-Work** \xE2\x80\x94 CPU-mineable, ASIC-resistant\n"
|
||||
"* **Sapling zk-SNARKs** \xE2\x80\x94 zero-knowledge proofs for private transactions\n"
|
||||
"* **Encrypted P2P** \xE2\x80\x94 all connections secured with TLS 1.3 via WolfSSL\n\n"
|
||||
"## Checksums\n| File | SHA-256 |\n"
|
||||
"|---|---|\n| dragonx-v1.0.4-linux-amd64.zip | `ab12cd34` |\n", false));
|
||||
rels.push_back(mk("v1.0.3", "DragonX v1.0.3", "2026-05-14",
|
||||
"## Notes\n- Stability improvements\n- RPC fixes\n", false));
|
||||
rels.push_back(mk("v1.0.2", "DragonX v1.0.2", "2026-04-02",
|
||||
"- First tagged mainnet build\n", false));
|
||||
rels.push_back(mk("v1.1.0-rc1", "DragonX v1.1.0-rc1", "2026-07-01",
|
||||
"Release candidate for the 1.1.0 series. Testing only.\n", true));
|
||||
ui::DaemonUpdateDialog::sweepSeed(&a, rels, util::DaemonUpdater::State::ReleaseList,
|
||||
"v1.0.3-dc45e7d90");
|
||||
},
|
||||
[](App&) { ui::DaemonUpdateDialog::sweepClose(); });
|
||||
|
||||
// Miner (xmrig) updater — same two-pane version picker, seeded with fake releases for the sweep.
|
||||
add("modal-xmrig-update", ui::NavPage::Mining,
|
||||
[](App& a) {
|
||||
auto mk = [](const char* tag, const char* name, const char* date, const char* body, bool pre) {
|
||||
util::XmrigRelease r; r.ok = true; r.tag = tag; r.name = name; r.body = body;
|
||||
r.prerelease = pre; r.publishedAt = std::string(date) + "T10:00:00Z";
|
||||
util::XmrigReleaseAsset as;
|
||||
as.name = std::string("drg-xmrig-") + tag + "-linux-x64.zip";
|
||||
as.downloadUrl = "https://git.dragonx.is/" + as.name; as.size = 8000000;
|
||||
r.assets.push_back(as);
|
||||
return r;
|
||||
};
|
||||
std::vector<util::XmrigRelease> rels;
|
||||
rels.push_back(mk("v6.25.3", "DRG-XMRig v6.25.3", "2026-06-20",
|
||||
"## What's new\n- Rebased on upstream XMRig 6.25.3\n- **RandomX** JIT speedups on modern CPUs\n"
|
||||
"- Fix `--cpu-priority` parsing on Windows\n\n## Checksums\n| File | SHA-256 |\n"
|
||||
"|---|---|\n| drg-xmrig-v6.25.3-linux-x64.zip | `ab12cd34` |\n", false));
|
||||
rels.push_back(mk("v6.24.0", "DRG-XMRig v6.24.0", "2026-04-30",
|
||||
"## Notes\n- Pool TLS fixes\n- Lower idle CPU\n", false));
|
||||
rels.push_back(mk("v6.23.0", "DRG-XMRig v6.23.0", "2026-03-11",
|
||||
"- First DRG-XMRig build\n", false));
|
||||
rels.push_back(mk("v6.26.0-rc1", "DRG-XMRig v6.26.0-rc1", "2026-07-02",
|
||||
"Release candidate. **Testing only.**\n", true));
|
||||
ui::XmrigDownloadDialog::sweepSeed(&a, rels, util::XmrigUpdater::State::ReleaseList, "v6.24.0");
|
||||
},
|
||||
[](App&) { ui::XmrigDownloadDialog::sweepClose(); });
|
||||
|
||||
// In-app folder picker (Wallets → Scan another folder). Populate a small demo tree so the
|
||||
// list shows sub-folders + wallet files, then open the wallets dialog + the picker over it.
|
||||
add("modal-folder-picker", ui::NavPage::Settings,
|
||||
[](App& a) {
|
||||
std::error_code ec;
|
||||
const std::string demo = util::Platform::getConfigDir() + "picker-demo";
|
||||
for (const char* sub : {"Documents", "Downloads", "Backups", "wallet-archive"})
|
||||
fs::create_directories(demo + "/" + sub, ec);
|
||||
for (const char* f : {"wallet-cold.dat", "wallet-2023.dat"})
|
||||
if (!fs::exists(demo + "/" + f, ec))
|
||||
std::ofstream(demo + "/" + f, std::ios::binary) << std::string(66000, '\0');
|
||||
ui::WalletsDialog::show(&a);
|
||||
ui::FolderPicker::open(demo, [](const std::string&) {});
|
||||
},
|
||||
[](App&) { ui::FolderPicker::close(); ui::WalletsDialog::hide(); });
|
||||
}
|
||||
|
||||
// ── State machine ───────────────────────────────────────────────────────────────────────────
|
||||
void App::startSweepImpl(bool full)
|
||||
{
|
||||
if (screenshot_sweep_active_) return;
|
||||
|
||||
sweep_skins_.clear();
|
||||
for (const auto& sk : ui::schema::SkinManager::instance().available())
|
||||
if (sk.valid) sweep_skins_.push_back(sk.id);
|
||||
if (sweep_skins_.empty()) return;
|
||||
|
||||
sweep_full_ = full;
|
||||
if (full) { capture_mode_ = true; installDemoWalletData(); }
|
||||
buildSweepCatalog();
|
||||
if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; }
|
||||
|
||||
sweep_dir_ = full ? screenshotFullDir() : screenshotDir();
|
||||
std::error_code ec; fs::create_directories(sweep_dir_, ec);
|
||||
|
||||
sweep_saved_skin_ = ui::schema::SkinManager::instance().activeSkinId();
|
||||
sweep_saved_page_ = current_page_;
|
||||
sweep_skin_idx_ = 0; sweep_target_idx_ = 0;
|
||||
screenshot_sweep_active_ = true;
|
||||
sweep_capture_this_frame_ = false;
|
||||
|
||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[0]);
|
||||
applySweepTarget();
|
||||
ui::Notifications::instance().info(full ? "Full UI sweep running…" : "Screenshot sweep running…");
|
||||
DEBUG_LOGF("[Sweep] %s -> %s (%d skins x %d surfaces)\n", full ? "FULL" : "tabs",
|
||||
sweep_dir_.c_str(), (int)sweep_skins_.size(), (int)sweep_targets_.size());
|
||||
}
|
||||
|
||||
void App::startScreenshotSweep() { startSweepImpl(false); }
|
||||
void App::startFullUiSweep() { startSweepImpl(true); }
|
||||
|
||||
void App::applySweepTarget()
|
||||
{
|
||||
const SweepTarget& t = sweep_targets_[sweep_target_idx_];
|
||||
current_page_ = t.page;
|
||||
page_alpha_ = 1.0f; // skip the page-switch fade so the shot isn't captured mid-animation
|
||||
if (t.setup) t.setup(*this);
|
||||
// <dir>/<surface>/<skin>.png — one subfolder per surface, one PNG per theme, overwritten in
|
||||
// place next sweep. (writePng in main.cpp creates the parent subfolder.)
|
||||
sweep_current_path_ = (fs::path(sweep_dir_) / t.name / (sweep_skins_[sweep_skin_idx_] + ".png")).string();
|
||||
sweep_settle_frames_ = t.settle;
|
||||
sweep_capture_this_frame_ = false;
|
||||
}
|
||||
|
||||
void App::updateScreenshotSweep()
|
||||
{
|
||||
if (!screenshot_sweep_active_) return;
|
||||
const SweepTarget& t = sweep_targets_[sweep_target_idx_];
|
||||
current_page_ = t.page; // keep the surface pinned each frame
|
||||
page_alpha_ = 1.0f;
|
||||
// Re-run setup every frame: idempotent for flags/enums, and required for OpenPopup-based popups
|
||||
// (must re-fire while open) + to keep the surface state fixed against any refresh.
|
||||
if (t.setup) t.setup(*this);
|
||||
if (sweep_settle_frames_ > 0) { sweep_settle_frames_--; sweep_capture_this_frame_ = false; }
|
||||
else sweep_capture_this_frame_ = true; // settled — main.cpp captures this frame
|
||||
}
|
||||
|
||||
void App::onScreenshotCaptured()
|
||||
{
|
||||
sweep_capture_this_frame_ = false;
|
||||
if (!screenshot_sweep_active_) return;
|
||||
|
||||
{ const SweepTarget& t = sweep_targets_[sweep_target_idx_]; if (t.teardown) t.teardown(*this); }
|
||||
|
||||
// Some surfaces leave an ImGui popup open (e.g. the send-confirm dialog calls OpenPopup but is
|
||||
// torn down by just clearing its flag). The headless sweep never clicks, so ImGui's normal
|
||||
// click-to-dismiss cleanup never runs and the popup lingers on the stack — which keeps
|
||||
// IsPopupOpen(AnyPopup) true forever and disables the smooth-scroll wheel capture on
|
||||
// Settings/Explorer/Console (draw_helpers::ApplySmoothScroll). Flush any lingering popup here so
|
||||
// it can't bleed into the next surface's capture or survive past the sweep.
|
||||
if (ImGuiContext* g = ImGui::GetCurrentContext(); g && g->OpenPopupStack.Size > 0)
|
||||
ImGui::ClosePopupToLevel(0, false);
|
||||
|
||||
if (++sweep_target_idx_ >= static_cast<int>(sweep_targets_.size())) {
|
||||
sweep_target_idx_ = 0;
|
||||
if (++sweep_skin_idx_ >= static_cast<int>(sweep_skins_.size())) {
|
||||
// Done — restore the original skin + page, and (full) the real state.
|
||||
const bool wasFull = sweep_full_;
|
||||
if (wasFull) writeSweepManifest();
|
||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_saved_skin_);
|
||||
current_page_ = sweep_saved_page_;
|
||||
page_alpha_ = 1.0f;
|
||||
if (wasFull) { clearDemoWalletData(); capture_mode_ = false; }
|
||||
sweep_full_ = false;
|
||||
screenshot_sweep_active_ = false;
|
||||
ui::Notifications::instance().success(
|
||||
(wasFull ? std::string("Full UI screenshots saved to ") : std::string("Screenshots saved to ")) + sweep_dir_);
|
||||
DEBUG_LOGF("[Sweep] done -> %s\n", sweep_dir_.c_str());
|
||||
return;
|
||||
}
|
||||
ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[sweep_skin_idx_]);
|
||||
}
|
||||
applySweepTarget();
|
||||
}
|
||||
|
||||
void App::writeSweepManifest() const
|
||||
{
|
||||
std::error_code ec; fs::create_directories(sweep_dir_, ec);
|
||||
std::ofstream out((fs::path(sweep_dir_) / "index.md").string(), std::ios::trunc);
|
||||
if (!out) return;
|
||||
out << "# Full UI sweep\n\n";
|
||||
out << sweep_targets_.size() << " surfaces x " << sweep_skins_.size() << " skins\n\n";
|
||||
for (const auto& t : sweep_targets_) {
|
||||
out << "## " << t.name << " (base: " << sweepPageName(t.page) << ")\n\n";
|
||||
for (const auto& skin : sweep_skins_)
|
||||
out << "- " << skin << ": `" << t.name << "/" << skin << ".png`\n";
|
||||
out << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dragonx
|
||||
@@ -10,6 +10,7 @@
|
||||
#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"
|
||||
@@ -39,13 +40,48 @@ 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 (embedded_daemon_) {
|
||||
embedded_daemon_->resetCrashCount();
|
||||
if (daemon_controller_) {
|
||||
daemon_controller_->resetCrashCount();
|
||||
}
|
||||
|
||||
// Disconnect RPC
|
||||
@@ -56,10 +92,11 @@ 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 (embedded_daemon_ && isEmbeddedDaemonRunning()) {
|
||||
std::thread([this]() {
|
||||
if (daemon_controller_ && isEmbeddedDaemonRunning()) {
|
||||
async_tasks_.submit("wizard-restart-stop-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||
if (token.cancelled()) return;
|
||||
stopEmbeddedDaemon();
|
||||
}).detach();
|
||||
});
|
||||
}
|
||||
|
||||
// Enter wizard — the wizard completion handler already calls
|
||||
@@ -73,6 +110,7 @@ void App::restartWizard()
|
||||
// ===========================================================================
|
||||
|
||||
void App::renderFirstRunWizard() {
|
||||
auto& wizardUi = s_wizardUi;
|
||||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(viewport->WorkPos);
|
||||
ImGui::SetNextWindowSize(viewport->WorkSize);
|
||||
@@ -243,15 +281,14 @@ void App::renderFirstRunWizard() {
|
||||
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
|
||||
cy += 14.0f * dp;
|
||||
|
||||
// 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;
|
||||
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;
|
||||
if (!wiz_appearance_init) {
|
||||
wiz_blur_amount = settings_->getBlurMultiplier();
|
||||
wiz_theme_effects = settings_->getThemeEffectsEnabled();
|
||||
@@ -398,7 +435,7 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
// --- Low-spec mode checkbox ---
|
||||
// Snapshot for restoring settings when low-spec is turned off
|
||||
static struct { bool valid; float blur; float uiOp; bool fx; bool scanline; } wiz_lsSnap = {};
|
||||
WizardLowSpecSnapshot& wiz_lsSnap = wizardUi.low_spec_snapshot;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
|
||||
@@ -574,7 +611,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 (ImGui::Button("Continue##app", ImVec2(btnW, btnH))) {
|
||||
if (ui::material::TactileButton("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
|
||||
@@ -596,7 +633,7 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
cy += cardPad;
|
||||
// Lock card height to the tallest content ever seen
|
||||
static float card0MaxH = 0.0f;
|
||||
float& card0MaxH = wizardUi.card0_max_h;
|
||||
card0MaxH = std::max(card0MaxH, cy - card0Top);
|
||||
card0Bot = card0Top + card0MaxH;
|
||||
|
||||
@@ -726,7 +763,7 @@ void App::renderFirstRunWizard() {
|
||||
float cancelBX = rightX + (colW - cancelW) * 0.5f;
|
||||
ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ImGui::Button("Cancel##bs", ImVec2(cancelW, cancelH))) {
|
||||
if (ui::material::TactileButton("Cancel##bs", ImVec2(cancelW, cancelH))) {
|
||||
bootstrap_->cancel();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
@@ -737,6 +774,8 @@ 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;
|
||||
@@ -772,27 +811,22 @@ 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 (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
|
||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||
if (ui::material::TactileButton("Retry##bs", ImVec2(btnW2, btnH2))) {
|
||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||
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");
|
||||
}
|
||||
stopDaemonForBootstrap();
|
||||
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 (ImGui::Button("Skip##bsfail", ImVec2(btnW2, btnH2))) {
|
||||
if (ui::material::TactileButton("Skip##bsfail", ImVec2(btnW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
@@ -808,17 +842,23 @@ void App::renderFirstRunWizard() {
|
||||
if (isFocused) {
|
||||
static std::atomic<bool> s_extCached{false};
|
||||
static std::atomic<bool> s_checkInFlight{false};
|
||||
static double s_extLastCheck = -10.0;
|
||||
double& s_extLastCheck = wizardUi.external_last_check;
|
||||
double now = ImGui::GetTime();
|
||||
if (now - s_extLastCheck >= 2.0 && !s_checkInFlight.load()) {
|
||||
s_extLastCheck = now;
|
||||
bool embeddedRunning = isEmbeddedDaemonRunning();
|
||||
s_checkInFlight.store(true);
|
||||
std::thread([embeddedRunning]() {
|
||||
async_tasks_.submit("wizard-external-daemon-check", [embeddedRunning](const util::AsyncTaskManager::Token& token) {
|
||||
if (token.cancelled()) {
|
||||
s_checkInFlight.store(false);
|
||||
return;
|
||||
}
|
||||
bool inUse = daemon::EmbeddedDaemon::isRpcPortInUse();
|
||||
s_extCached.store(inUse && !embeddedRunning);
|
||||
if (!token.cancelled()) {
|
||||
s_extCached.store(inUse && !embeddedRunning);
|
||||
}
|
||||
s_checkInFlight.store(false);
|
||||
}).detach();
|
||||
});
|
||||
}
|
||||
externalRunning = s_extCached.load();
|
||||
}
|
||||
@@ -856,22 +896,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 (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
|
||||
if (ui::material::TactileButton("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
|
||||
wizard_stopping_external_ = true;
|
||||
wizard_stop_status_ = "Sending stop command...";
|
||||
if (wizard_stop_thread_.joinable()) wizard_stop_thread_.join();
|
||||
wizard_stop_thread_ = std::thread([this]() {
|
||||
async_tasks_.submit("wizard-stop-external-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||
auto config = rpc::Connection::autoDetectConfig();
|
||||
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
|
||||
auto tmp_rpc = std::make_unique<rpc::RPCClient>();
|
||||
if (tmp_rpc->connect(config.host, config.port,
|
||||
config.rpcuser, config.rpcpassword)) {
|
||||
try { tmp_rpc->call("stop"); } catch (...) {}
|
||||
config.rpcuser, config.rpcpassword,
|
||||
config.use_tls)) {
|
||||
sendStopCommandSafely(*tmp_rpc, "Wizard external daemon stop");
|
||||
tmp_rpc->disconnect();
|
||||
}
|
||||
}
|
||||
wizard_stop_status_ = "Waiting for daemon to shut down...";
|
||||
for (int i = 0; i < 60; i++) {
|
||||
for (int i = 0; i < 60 && !token.cancelled(); i++) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
|
||||
wizard_stop_status_ = "Daemon stopped.";
|
||||
@@ -879,6 +919,7 @@ void App::renderFirstRunWizard() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (token.cancelled()) return;
|
||||
wizard_stop_status_ = "Daemon did not stop — try manually.";
|
||||
wizard_stopping_external_ = false;
|
||||
});
|
||||
@@ -888,7 +929,7 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx + stopW + 12.0f * dp, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ImGui::Button("Skip##extd", ImVec2(skipW2, btnH2))) {
|
||||
if (ui::material::TactileButton("Skip##extd", ImVec2(skipW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
@@ -953,21 +994,16 @@ 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 (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
|
||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||
if (ui::material::TactileButton("Download##bs", ImVec2(dlBtnW, btnH2))) {
|
||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||
if (isEmbeddedDaemonRunning()) {
|
||||
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap...\n");
|
||||
if (rpc_ && rpc_->isConnected()) {
|
||||
try { rpc_->call("stop"); } catch (...) {}
|
||||
rpc_->disconnect();
|
||||
}
|
||||
onDisconnected("Bootstrap");
|
||||
}
|
||||
stopDaemonForBootstrap();
|
||||
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);
|
||||
|
||||
@@ -977,23 +1013,18 @@ 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);
|
||||
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");
|
||||
}
|
||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||
if (ui::material::TactileButton("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
|
||||
stopDaemonForBootstrap();
|
||||
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()) {
|
||||
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
|
||||
ui::material::Tooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
@@ -1001,7 +1032,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 (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
|
||||
if (ui::material::TactileButton("Skip##bs", ImVec2(skipW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
@@ -1012,7 +1043,7 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
cy += cardPad;
|
||||
// Lock card height to the tallest content ever seen (but not when collapsed)
|
||||
static float card1MaxH = 0.0f;
|
||||
float& card1MaxH = wizardUi.card1_max_h;
|
||||
if (isCollapsed) {
|
||||
card1Bot = card1Top + (cy - card1Top);
|
||||
} else {
|
||||
@@ -1037,7 +1068,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) {
|
||||
static bool wiz_daemon_prestarted = false;
|
||||
bool& wiz_daemon_prestarted = wizardUi.daemon_prestarted;
|
||||
if (!wiz_daemon_prestarted) {
|
||||
wiz_daemon_prestarted = true;
|
||||
if (!state_.connected && isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
|
||||
@@ -1096,7 +1127,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 (ImGui::Button("Continue##encok", ImVec2(btnW2, btnH2))) {
|
||||
if (ui::material::TactileButton("Continue##encok", ImVec2(btnW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::Done;
|
||||
settings_->setWizardCompleted(true);
|
||||
settings_->save();
|
||||
@@ -1254,10 +1285,24 @@ 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;
|
||||
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0 &&
|
||||
!passEdgeSpace;
|
||||
// PIN is optional: if entered, must be valid + confirmed
|
||||
std::string pinStr(wizard_pin_buf_);
|
||||
bool pinEntered = !pinStr.empty();
|
||||
@@ -1279,12 +1324,11 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
ImGui::BeginDisabled(!canEncrypt);
|
||||
if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
|
||||
if (ui::material::TactileButton("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
|
||||
// Save passphrase + optional PIN for background processing
|
||||
deferred_encrypt_passphrase_ = std::string(encrypt_pass_buf_);
|
||||
if (pinEntered && pinOk)
|
||||
deferred_encrypt_pin_ = pinStr;
|
||||
deferred_encrypt_pending_ = true;
|
||||
wallet_security_.beginDeferredEncryption(
|
||||
std::string(encrypt_pass_buf_),
|
||||
(pinEntered && pinOk) ? pinStr : std::string());
|
||||
|
||||
// Clear sensitive buffers
|
||||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||
@@ -1294,7 +1338,10 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
// Start daemon + finish wizard immediately
|
||||
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
||||
startEmbeddedDaemon();
|
||||
if (!startEmbeddedDaemon()) {
|
||||
ui::Notifications::instance().warning(
|
||||
TR("wizard_daemon_start_failed"));
|
||||
}
|
||||
}
|
||||
tryConnect();
|
||||
wizard_phase_ = WizardPhase::Done;
|
||||
@@ -1308,14 +1355,25 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ImGui::Button("Skip##enc", ImVec2(skipW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::Done;
|
||||
settings_->setWizardCompleted(true);
|
||||
settings_->save();
|
||||
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
||||
startEmbeddedDaemon();
|
||||
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();
|
||||
}
|
||||
tryConnect();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
cy += btnH2;
|
||||
|
||||
173
src/chat/chat_crypto.cpp
Normal file
173
src/chat/chat_crypto.cpp
Normal file
@@ -0,0 +1,173 @@
|
||||
// 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
|
||||
65
src/chat/chat_crypto.h
Normal file
65
src/chat/chat_crypto.h
Normal file
@@ -0,0 +1,65 @@
|
||||
#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
|
||||
330
src/chat/chat_database.cpp
Normal file
330
src/chat/chat_database.cpp
Normal file
@@ -0,0 +1,330 @@
|
||||
// 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
|
||||
74
src/chat/chat_database.h
Normal file
74
src/chat/chat_database.h
Normal file
@@ -0,0 +1,74 @@
|
||||
#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
|
||||
1594
src/chat/chat_fixture_tooling.cpp
Normal file
1594
src/chat/chat_fixture_tooling.cpp
Normal file
File diff suppressed because it is too large
Load Diff
505
src/chat/chat_fixture_tooling.h
Normal file
505
src/chat/chat_fixture_tooling.h
Normal file
@@ -0,0 +1,505 @@
|
||||
#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
|
||||
70
src/chat/chat_identity.cpp
Normal file
70
src/chat/chat_identity.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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
|
||||
57
src/chat/chat_identity.h
Normal file
57
src/chat/chat_identity.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#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
|
||||
30
src/chat/chat_message.h
Normal file
30
src/chat/chat_message.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#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
|
||||
108
src/chat/chat_outgoing.cpp
Normal file
108
src/chat/chat_outgoing.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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
|
||||
66
src/chat/chat_outgoing.h
Normal file
66
src/chat/chat_outgoing.h
Normal file
@@ -0,0 +1,66 @@
|
||||
#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
|
||||
312
src/chat/chat_protocol.cpp
Normal file
312
src/chat/chat_protocol.cpp
Normal file
@@ -0,0 +1,312 @@
|
||||
#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
|
||||
110
src/chat/chat_protocol.h
Normal file
110
src/chat/chat_protocol.h
Normal file
@@ -0,0 +1,110 @@
|
||||
#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
|
||||
108
src/chat/chat_service.cpp
Normal file
108
src/chat/chat_service.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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
|
||||
85
src/chat/chat_service.h
Normal file
85
src/chat/chat_service.h
Normal file
@@ -0,0 +1,85 @@
|
||||
#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
|
||||
39
src/chat/chat_store.cpp
Normal file
39
src/chat/chat_store.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
// 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
|
||||
38
src/chat/chat_store.h
Normal file
38
src/chat/chat_store.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#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
|
||||
@@ -1,6 +1,9 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// settings.cpp — JSON settings persistence. Loads/saves user preferences
|
||||
// to ~/.config/ObsidianDragon/settings.json (Linux/macOS) or %APPDATA% (Windows).
|
||||
|
||||
#include "settings.h"
|
||||
#include "version.h"
|
||||
@@ -8,8 +11,12 @@
|
||||
#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>
|
||||
@@ -27,35 +34,87 @@ 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()
|
||||
{
|
||||
#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";
|
||||
// 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();
|
||||
fs::create_directories(dir);
|
||||
return dir + "/settings.json";
|
||||
#else
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
home = pw->pw_dir;
|
||||
}
|
||||
std::string dir = std::string(home) + "/.config/ObsidianDragon";
|
||||
fs::create_directories(dir);
|
||||
return dir + "/settings.json";
|
||||
#endif
|
||||
return (fs::path(dir) / "settings.json").string();
|
||||
}
|
||||
|
||||
bool Settings::load()
|
||||
@@ -76,29 +135,30 @@ bool Settings::load(const std::string& path)
|
||||
json j;
|
||||
file >> j;
|
||||
|
||||
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>();
|
||||
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_);
|
||||
// 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
|
||||
}
|
||||
if (j.contains("window_opacity")) window_opacity_ = j["window_opacity"].get<float>();
|
||||
loadScalar(j, "window_opacity", window_opacity_);
|
||||
if (j.contains("balance_layout")) {
|
||||
if (j["balance_layout"].is_string())
|
||||
balance_layout_ = j["balance_layout"].get<std::string>();
|
||||
@@ -112,7 +172,9 @@ bool Settings::load(const std::string& path)
|
||||
if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
|
||||
}
|
||||
}
|
||||
if (j.contains("scanline_enabled")) scanline_enabled_ = j["scanline_enabled"].get<bool>();
|
||||
loadScalar(j, "portfolio_style", portfolio_style_);
|
||||
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
||||
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
||||
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
|
||||
hidden_addresses_.clear();
|
||||
for (const auto& a : j["hidden_addresses"])
|
||||
@@ -123,36 +185,125 @@ 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("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("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, "daemon_update_prompted_size", daemon_update_prompted_size_);
|
||||
loadScalar(j, "active_wallet_file", active_wallet_file_);
|
||||
loadScalar(j, "seed_migration_pending", seed_migration_pending_);
|
||||
loadScalar(j, "seed_migration_dest", seed_migration_dest_);
|
||||
loadScalar(j, "seed_migration_temp_dir", seed_migration_temp_dir_);
|
||||
loadScalar(j, "seed_migration_sweep_txid", seed_migration_sweep_txid_);
|
||||
loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
|
||||
loadScalar(j, "unlock_duration", unlock_duration_);
|
||||
loadScalar(j, "pin_enabled", pin_enabled_);
|
||||
loadScalar(j, "keep_daemon_running", keep_daemon_running_);
|
||||
loadScalar(j, "stop_external_daemon", stop_external_daemon_);
|
||||
loadScalar(j, "max_connections", max_connections_);
|
||||
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
|
||||
const auto& lite = j["lite_wallet"];
|
||||
if (lite.contains("server_selection_mode")) {
|
||||
lite_server_selection_mode_ = parseLiteServerSelectionPreferenceMode(
|
||||
lite["server_selection_mode"]);
|
||||
}
|
||||
if (lite.contains("sticky_server_url") && lite["sticky_server_url"].is_string()) {
|
||||
lite_sticky_server_url_ = lite["sticky_server_url"].get<std::string>();
|
||||
}
|
||||
if (lite.contains("chain_name") && lite["chain_name"].is_string()) {
|
||||
lite_chain_name_ = lite["chain_name"].get<std::string>();
|
||||
}
|
||||
// Migration: the SDXL backend only accepts main/test/regtest and hard-panics
|
||||
// (process abort) on any other chain name. Older builds persisted the "DRAGONX"
|
||||
// ticker here, which crashed the lite backend on launch. Rewrite any invalid
|
||||
// value to "main" and flag a re-save so the corrected setting persists.
|
||||
if (lite_chain_name_ != "main" && lite_chain_name_ != "test" &&
|
||||
lite_chain_name_ != "regtest") {
|
||||
lite_chain_name_ = "main";
|
||||
needs_upgrade_save_ = true;
|
||||
}
|
||||
if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_unsigned()) {
|
||||
lite_random_selection_seed_ = lite["random_selection_seed"].get<std::size_t>();
|
||||
} else if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_integer()) {
|
||||
const auto seed = lite["random_selection_seed"].get<long long>();
|
||||
lite_random_selection_seed_ = seed > 0 ? static_cast<std::size_t>(seed) : 0;
|
||||
}
|
||||
if (lite.contains("persist_selected_server") && lite["persist_selected_server"].is_boolean()) {
|
||||
lite_persist_selected_server_ = lite["persist_selected_server"].get<bool>();
|
||||
}
|
||||
if (lite.contains("servers") && lite["servers"].is_array()) {
|
||||
lite_servers_.clear();
|
||||
for (const auto& server : lite["servers"]) {
|
||||
if (!server.is_object()) continue;
|
||||
LiteServerPreference preference;
|
||||
if (server.contains("url") && server["url"].is_string()) {
|
||||
preference.url = server["url"].get<std::string>();
|
||||
}
|
||||
if (server.contains("label") && server["label"].is_string()) {
|
||||
preference.label = server["label"].get<std::string>();
|
||||
}
|
||||
if (server.contains("enabled") && server["enabled"].is_boolean()) {
|
||||
preference.enabled = server["enabled"].get<bool>();
|
||||
}
|
||||
lite_servers_.push_back(preference);
|
||||
}
|
||||
}
|
||||
if (lite.contains("rollout_override") && lite["rollout_override"].is_string()) {
|
||||
const auto v = lite["rollout_override"].get<std::string>();
|
||||
lite_rollout_override_ = (v == "force_on" || v == "force_off") ? v : "auto";
|
||||
}
|
||||
if (lite.contains("install_id") && lite["install_id"].is_string()) {
|
||||
lite_install_id_ = lite["install_id"].get<std::string>();
|
||||
}
|
||||
if (lite.contains("hidden_servers") && lite["hidden_servers"].is_array()) {
|
||||
lite_hidden_servers_.clear();
|
||||
for (const auto& u : lite["hidden_servers"])
|
||||
if (u.is_string()) lite_hidden_servers_.insert(u.get<std::string>());
|
||||
}
|
||||
}
|
||||
loadScalar(j, "verbose_logging", verbose_logging_);
|
||||
if (j.contains("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>());
|
||||
}
|
||||
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>();
|
||||
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_);
|
||||
// Migrate old default pool URL that was missing the stratum port
|
||||
if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
|
||||
if (j.contains("pool_algo")) pool_algo_ = j["pool_algo"].get<std::string>();
|
||||
if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get<std::string>();
|
||||
if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get<int>();
|
||||
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
|
||||
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
|
||||
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
|
||||
if (j.contains("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>();
|
||||
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("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
|
||||
saved_pool_urls_.clear();
|
||||
for (const auto& u : j["saved_pool_urls"])
|
||||
@@ -163,15 +314,56 @@ 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("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>();
|
||||
if (j.contains("portfolio_entries") && j["portfolio_entries"].is_array()) {
|
||||
portfolio_entries_.clear();
|
||||
for (const auto& e : j["portfolio_entries"]) {
|
||||
if (!e.is_object()) continue;
|
||||
PortfolioEntry entry;
|
||||
if (e.contains("label") && e["label"].is_string())
|
||||
entry.label = e["label"].get<std::string>();
|
||||
if (e.contains("addresses") && e["addresses"].is_array())
|
||||
for (const auto& a : e["addresses"])
|
||||
if (a.is_string()) entry.addresses.push_back(a.get<std::string>());
|
||||
if (e.contains("icon") && e["icon"].is_string())
|
||||
entry.icon = e["icon"].get<std::string>();
|
||||
if (e.contains("color") && e["color"].is_number())
|
||||
entry.color = e["color"].get<unsigned int>();
|
||||
if (e.contains("outline_opacity") && e["outline_opacity"].is_number_integer())
|
||||
entry.outlineOpacity = e["outline_opacity"].get<int>();
|
||||
if (e.contains("price_basis") && e["price_basis"].is_number_integer())
|
||||
entry.priceBasis = e["price_basis"].get<int>();
|
||||
if (e.contains("manual_price") && e["manual_price"].is_number())
|
||||
entry.manualPrice = e["manual_price"].get<double>();
|
||||
if (e.contains("manual_currency") && e["manual_currency"].is_string())
|
||||
entry.manualCurrency = e["manual_currency"].get<std::string>();
|
||||
if (e.contains("show_drgx") && e["show_drgx"].is_boolean())
|
||||
entry.showDrgx = e["show_drgx"].get<bool>();
|
||||
if (e.contains("show_value") && e["show_value"].is_boolean())
|
||||
entry.showValue = e["show_value"].get<bool>();
|
||||
if (e.contains("show_24h") && e["show_24h"].is_boolean())
|
||||
entry.show24h = e["show_24h"].get<bool>();
|
||||
if (e.contains("show_sparkline") && e["show_sparkline"].is_boolean())
|
||||
entry.showSparkline = e["show_sparkline"].get<bool>();
|
||||
if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
|
||||
entry.sparklineInterval = e["sparkline_interval"].get<int>();
|
||||
entry.scope = e.value("scope", "");
|
||||
if (e.contains("grid_col") && e["grid_col"].is_number_integer())
|
||||
entry.gridCol = e["grid_col"].get<int>();
|
||||
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
|
||||
entry.gridRow = e["grid_row"].get<int>();
|
||||
if (e.contains("grid_w") && e["grid_w"].is_number_integer())
|
||||
entry.gridW = e["grid_w"].get<int>();
|
||||
if (e.contains("grid_h") && e["grid_h"].is_number_integer())
|
||||
entry.gridH = e["grid_h"].get<int>();
|
||||
if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
loadClamped(j, "font_scale", font_scale_, 1.0f, 1.5f);
|
||||
loadScalar(j, "window_width", window_width_);
|
||||
loadScalar(j, "window_height", window_height_);
|
||||
|
||||
// Version tracking — detect upgrades so we can re-save with new defaults
|
||||
if (j.contains("settings_version")) settings_version_ = j["settings_version"].get<std::string>();
|
||||
loadScalar(j, "settings_version", settings_version_);
|
||||
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(),
|
||||
@@ -182,6 +374,17 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -209,6 +412,7 @@ 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_;
|
||||
@@ -217,6 +421,7 @@ bool Settings::save(const std::string& path)
|
||||
j["ui_opacity"] = ui_opacity_;
|
||||
j["window_opacity"] = window_opacity_;
|
||||
j["balance_layout"] = balance_layout_; // saved as string ID
|
||||
j["portfolio_style"] = portfolio_style_;
|
||||
j["scanline_enabled"] = scanline_enabled_;
|
||||
j["hidden_addresses"] = json::array();
|
||||
for (const auto& addr : hidden_addresses_)
|
||||
@@ -224,18 +429,61 @@ 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["daemon_update_prompted_size"] = daemon_update_prompted_size_;
|
||||
j["active_wallet_file"] = active_wallet_file_;
|
||||
j["seed_migration_pending"] = seed_migration_pending_;
|
||||
j["seed_migration_dest"] = seed_migration_dest_;
|
||||
j["seed_migration_temp_dir"] = seed_migration_temp_dir_;
|
||||
j["seed_migration_sweep_txid"] = seed_migration_sweep_txid_;
|
||||
j["auto_lock_timeout"] = auto_lock_timeout_;
|
||||
j["unlock_duration"] = unlock_duration_;
|
||||
j["pin_enabled"] = pin_enabled_;
|
||||
j["keep_daemon_running"] = keep_daemon_running_;
|
||||
j["stop_external_daemon"] = stop_external_daemon_;
|
||||
j["max_connections"] = max_connections_;
|
||||
{
|
||||
json lite = json::object();
|
||||
lite["server_selection_mode"] = liteServerSelectionPreferenceModeName(lite_server_selection_mode_);
|
||||
lite["sticky_server_url"] = lite_sticky_server_url_;
|
||||
lite["chain_name"] = lite_chain_name_;
|
||||
lite["random_selection_seed"] = lite_random_selection_seed_;
|
||||
lite["persist_selected_server"] = lite_persist_selected_server_;
|
||||
lite["servers"] = json::array();
|
||||
for (const auto& server : lite_servers_) {
|
||||
json entry = json::object();
|
||||
entry["url"] = server.url;
|
||||
entry["label"] = server.label;
|
||||
entry["enabled"] = server.enabled;
|
||||
lite["servers"].push_back(entry);
|
||||
}
|
||||
lite["rollout_override"] = lite_rollout_override_;
|
||||
lite["install_id"] = lite_install_id_;
|
||||
lite["hidden_servers"] = json::array();
|
||||
for (const auto& u : lite_hidden_servers_) lite["hidden_servers"].push_back(u);
|
||||
j["lite_wallet"] = lite;
|
||||
}
|
||||
j["verbose_logging"] = verbose_logging_;
|
||||
j["debug_categories"] = json::array();
|
||||
for (const auto& cat : debug_categories_)
|
||||
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_;
|
||||
@@ -245,17 +493,44 @@ 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_;
|
||||
j["idle_threads_idle"] = idle_threads_idle_;
|
||||
j["idle_gpu_aware"] = idle_gpu_aware_;
|
||||
j["saved_pool_urls"] = json::array();
|
||||
for (const auto& u : saved_pool_urls_)
|
||||
j["saved_pool_urls"].push_back(u);
|
||||
j["saved_pool_workers"] = json::array();
|
||||
for (const auto& w : saved_pool_workers_)
|
||||
j["saved_pool_workers"].push_back(w);
|
||||
j["portfolio_entries"] = json::array();
|
||||
for (const auto& e : portfolio_entries_) {
|
||||
json entry;
|
||||
entry["label"] = e.label;
|
||||
entry["addresses"] = json::array();
|
||||
for (const auto& a : e.addresses) entry["addresses"].push_back(a);
|
||||
entry["icon"] = e.icon;
|
||||
entry["color"] = e.color;
|
||||
entry["outline_opacity"] = e.outlineOpacity;
|
||||
entry["price_basis"] = e.priceBasis;
|
||||
entry["manual_price"] = e.manualPrice;
|
||||
entry["manual_currency"] = e.manualCurrency;
|
||||
entry["show_drgx"] = e.showDrgx;
|
||||
entry["show_value"] = e.showValue;
|
||||
entry["show_24h"] = e.show24h;
|
||||
entry["show_sparkline"] = e.showSparkline;
|
||||
entry["sparkline_interval"] = e.sparklineInterval;
|
||||
entry["scope"] = e.scope;
|
||||
entry["grid_col"] = e.gridCol;
|
||||
entry["grid_row"] = e.gridRow;
|
||||
entry["grid_w"] = e.gridW;
|
||||
entry["grid_h"] = e.gridH;
|
||||
j["portfolio_entries"].push_back(std::move(entry));
|
||||
}
|
||||
j["font_scale"] = font_scale_;
|
||||
j["settings_version"] = std::string(DRAGONX_VERSION);
|
||||
if (window_width_ > 0 && window_height_ > 0) {
|
||||
@@ -264,17 +539,11 @@ bool Settings::save(const std::string& path)
|
||||
}
|
||||
|
||||
try {
|
||||
// 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;
|
||||
// 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);
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Failed to save settings: %s\n", e.what());
|
||||
return false;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
@@ -53,6 +55,55 @@ 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)
|
||||
// Per-wallet visibility: "" (shown in every wallet — legacy/global) or a wallet-identity
|
||||
// hash (shown only when that wallet is active). New entries are tagged with the current
|
||||
// wallet so a portfolio built for wallet A doesn't clutter wallet B.
|
||||
std::string scope;
|
||||
// Legacy dashboard-grid placement (deprecated by the row layout; kept for back-compat so an
|
||||
// older build's saved positions aren't dropped, but no longer used by the renderer).
|
||||
int gridCol = -1;
|
||||
int gridRow = -1;
|
||||
int gridW = 8;
|
||||
int gridH = 3;
|
||||
};
|
||||
|
||||
// Theme
|
||||
std::string getTheme() const { return theme_; }
|
||||
void setTheme(const std::string& theme) { theme_ = theme; }
|
||||
@@ -61,6 +112,12 @@ 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; }
|
||||
@@ -123,6 +180,11 @@ public:
|
||||
std::string getBalanceLayout() const { return balance_layout_; }
|
||||
void setBalanceLayout(const std::string& v) { balance_layout_ = v; }
|
||||
|
||||
// Market-tab portfolio row style: 0 = single-line, 1 = two-line, 2 = value-hero. Cycled with
|
||||
// Left/Right arrows on the Market tab (like the Overview layouts).
|
||||
int getPortfolioStyle() const { return portfolio_style_; }
|
||||
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
|
||||
// Console scanline effect
|
||||
bool getScanlineEnabled() const { return scanline_enabled_; }
|
||||
void setScanlineEnabled(bool v) { scanline_enabled_ = v; }
|
||||
@@ -141,10 +203,86 @@ 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; }
|
||||
|
||||
// Bundled-daemon size we last prompted to install (see App::renderDaemonUpdatePrompt). Lets the
|
||||
// "a newer node is bundled — update?" prompt fire once per wallet version, never re-nagging.
|
||||
long long getDaemonUpdatePromptedSize() const { return daemon_update_prompted_size_; }
|
||||
void setDaemonUpdatePromptedSize(long long v) { daemon_update_prompted_size_ = v; }
|
||||
|
||||
// Active wallet file the daemon loads via -wallet=<name> (multi-wallet). A plain filename in
|
||||
// the datadir; defaults to the daemon's own default. Used at launch to know which wallet we're
|
||||
// on before connect + to scope per-wallet data.
|
||||
std::string getActiveWalletFile() const { return active_wallet_file_; }
|
||||
void setActiveWalletFile(const std::string& v) { active_wallet_file_ = v; }
|
||||
|
||||
// Pending "migrate to a seed wallet" state (Phase 1 created the wallet; a later sweep/adopt
|
||||
// step consumes it). dest = the new wallet's sweep-target z-address; tempDir = its datadir.
|
||||
bool getSeedMigrationPending() const { return seed_migration_pending_; }
|
||||
void setSeedMigrationPending(bool v) { seed_migration_pending_ = v; }
|
||||
std::string getSeedMigrationDest() const { return seed_migration_dest_; }
|
||||
void setSeedMigrationDest(const std::string& v) { seed_migration_dest_ = v; }
|
||||
std::string getSeedMigrationTempDir() const { return seed_migration_temp_dir_; }
|
||||
void setSeedMigrationTempDir(const std::string& v) { seed_migration_temp_dir_ = v; }
|
||||
// The sweep transaction id, persisted once the sweep is submitted — non-empty means the
|
||||
// migration is past the sweep, so a resume goes to the confirm/adopt stage (not sweep again).
|
||||
std::string getSeedMigrationSweepTxid() const { return seed_migration_sweep_txid_; }
|
||||
void setSeedMigrationSweepTxid(const std::string& v) { seed_migration_sweep_txid_ = v; }
|
||||
|
||||
// Security — auto-lock timeout (seconds; 0 = disabled)
|
||||
int getAutoLockTimeout() const { return auto_lock_timeout_; }
|
||||
void setAutoLockTimeout(int seconds) { auto_lock_timeout_ = seconds; }
|
||||
@@ -165,6 +303,43 @@ 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; }
|
||||
@@ -186,6 +361,10 @@ public:
|
||||
bool getLowSpecMode() const { return low_spec_mode_; }
|
||||
void setLowSpecMode(bool v) { low_spec_mode_ = v; }
|
||||
|
||||
// Reduce motion — disables animated transitions for accessibility
|
||||
bool getReduceMotion() const { return reduce_motion_; }
|
||||
void setReduceMotion(bool v) { reduce_motion_ = v; }
|
||||
|
||||
// Market — last selected exchange + pair
|
||||
std::string getSelectedExchange() const { return selected_exchange_; }
|
||||
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
|
||||
@@ -207,6 +386,12 @@ 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_; }
|
||||
@@ -221,6 +406,8 @@ public:
|
||||
void setIdleThreadsActive(int v) { idle_threads_active_ = std::max(0, v); }
|
||||
int getIdleThreadsIdle() const { return idle_threads_idle_; }
|
||||
void setIdleThreadsIdle(int v) { idle_threads_idle_ = std::max(0, v); }
|
||||
bool getIdleGpuAware() const { return idle_gpu_aware_; }
|
||||
void setIdleGpuAware(bool v) { idle_gpu_aware_ = v; }
|
||||
|
||||
// Saved pool URLs (user-managed favorites dropdown)
|
||||
const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; }
|
||||
@@ -266,6 +453,7 @@ 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;
|
||||
@@ -288,21 +476,54 @@ private:
|
||||
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
||||
#endif
|
||||
std::string balance_layout_ = "classic";
|
||||
int portfolio_style_ = 0; // Market portfolio row style (0 single / 1 two-line / 2 hero)
|
||||
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;
|
||||
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt
|
||||
std::string active_wallet_file_ = "wallet.dat"; // -wallet=<name> the daemon loads (multi-wallet)
|
||||
bool seed_migration_pending_ = false;
|
||||
std::string seed_migration_dest_;
|
||||
std::string seed_migration_temp_dir_;
|
||||
std::string seed_migration_sweep_txid_;
|
||||
int auto_lock_timeout_ = 900; // 15 minutes
|
||||
int unlock_duration_ = 600; // 10 minutes
|
||||
bool pin_enabled_ = false;
|
||||
bool keep_daemon_running_ = false;
|
||||
bool stop_external_daemon_ = false;
|
||||
int max_connections_ = 0; // 0 = daemon default
|
||||
|
||||
// Lite wallet server preferences. These are user/server settings only;
|
||||
// wallet secrets, wallet files, and lifecycle state are never stored here.
|
||||
LiteServerSelectionPreferenceMode lite_server_selection_mode_ = LiteServerSelectionPreferenceMode::Sticky;
|
||||
std::string lite_sticky_server_url_ = "https://lite.dragonx.is";
|
||||
std::string lite_chain_name_ = "main"; // SDXL backend chain id; must be main/test/regtest
|
||||
std::size_t lite_random_selection_seed_ = 0;
|
||||
bool lite_persist_selected_server_ = true;
|
||||
std::string lite_rollout_override_ = "auto"; // auto|force_on|force_off
|
||||
std::string lite_install_id_; // random local-only id; rollout-bucket source
|
||||
std::vector<LiteServerPreference> lite_servers_ = {
|
||||
{"https://lite.dragonx.is", "DragonX Lite", true},
|
||||
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
|
||||
{"https://lite2.dragonx.is", "DragonX Lite 2", true},
|
||||
{"https://lite3.dragonx.is", "DragonX Lite 3", true},
|
||||
{"https://lite4.dragonx.is", "DragonX Lite 4", true},
|
||||
{"https://lite5.dragonx.is", "DragonX Lite 5", true}
|
||||
};
|
||||
std::set<std::string> lite_hidden_servers_; // server URLs hidden from the Network tab
|
||||
|
||||
std::vector<PortfolioEntry> portfolio_entries_; // Market tab custom portfolio groups
|
||||
|
||||
bool verbose_logging_ = false;
|
||||
std::set<std::string> debug_categories_;
|
||||
bool theme_effects_enabled_ = true;
|
||||
bool low_spec_mode_ = false;
|
||||
std::string selected_exchange_ = "TradeOgre";
|
||||
std::string selected_pair_ = "DRGX/BTC";
|
||||
bool reduce_motion_ = false;
|
||||
std::string selected_exchange_ = "Nonkyc.io";
|
||||
std::string selected_pair_ = "DRGX/USDT";
|
||||
|
||||
// Pool mining
|
||||
std::string pool_url_ = "pool.dragonx.is:3433";
|
||||
@@ -312,11 +533,14 @@ 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
|
||||
int idle_threads_active_ = 0; // threads when user active (0 = auto)
|
||||
int idle_threads_idle_ = 0; // threads when idle (0 = auto = all)
|
||||
bool idle_gpu_aware_ = true; // treat GPU activity as non-idle
|
||||
std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites
|
||||
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
|
||||
|
||||
|
||||
@@ -1,31 +1,3 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
// !! 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"
|
||||
#include "dragonx_generated_version.h"
|
||||
|
||||
@@ -4,15 +4,16 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
// !! 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 ...)
|
||||
// !! 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.
|
||||
|
||||
#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_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_APP_NAME "ObsidianDragon"
|
||||
#define DRAGONX_APP_NAME "@DRAGONX_APP_NAME@"
|
||||
#define DRAGONX_ORG_NAME "Hush"
|
||||
|
||||
// Default RPC settings
|
||||
|
||||
143
src/daemon/daemon_controller.cpp
Normal file
143
src/daemon/daemon_controller.cpp
Normal file
@@ -0,0 +1,143 @@
|
||||
#include "daemon_controller.h"
|
||||
#include "../config/settings.h"
|
||||
#include "../util/platform.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
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());
|
||||
|
||||
std::string walletFile = settings->getActiveWalletFile();
|
||||
// The Wallets dialog opens an out-of-datadir wallet by linking it into the datadir under a
|
||||
// "wallet-ip-<hash>.dat" name. If that link went dangling (the external file was moved / a USB was
|
||||
// unplugged between sessions), don't let the daemon create a fresh EMPTY wallet at that name — fall
|
||||
// back to the default this launch. fs::exists follows the link, so it's false for a dangling one.
|
||||
if (walletFile.rfind("wallet-ip-", 0) == 0) {
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(util::Platform::getDragonXDataDir() + "/" + walletFile, ec))
|
||||
walletFile = "wallet.dat";
|
||||
}
|
||||
daemon_->setWalletFile(walletFile);
|
||||
}
|
||||
|
||||
bool DaemonController::start(const config::Settings* settings)
|
||||
{
|
||||
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
|
||||
238
src/daemon/daemon_controller.h
Normal file
238
src/daemon/daemon_controller.h
Normal file
@@ -0,0 +1,238 @@
|
||||
#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
|
||||
@@ -1,6 +1,9 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// embedded_daemon.cpp — Manages the dragonxd child process lifecycle:
|
||||
// binary discovery, process spawn, stdout/stderr monitoring, crash recovery.
|
||||
|
||||
#include "embedded_daemon.h"
|
||||
#include "../config/version.h"
|
||||
@@ -202,9 +205,18 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
|
||||
"-ac_reward=300000000",
|
||||
"-ac_blocktime=36",
|
||||
"-ac_private=1",
|
||||
"-addnode=176.126.87.241",
|
||||
"-addnode=node.dragonx.is",
|
||||
"-addnode=node1.dragonx.is",
|
||||
"-addnode=node2.dragonx.is",
|
||||
"-addnode=node3.dragonx.is",
|
||||
"-addnode=node4.dragonx.is",
|
||||
"-experimentalfeatures",
|
||||
"-developerencryptwallet",
|
||||
// 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
|
||||
};
|
||||
}
|
||||
@@ -436,9 +448,10 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if something is already listening on the RPC port
|
||||
// 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.
|
||||
int rpc_port = std::atoi(DRAGONX_DEFAULT_RPC_PORT);
|
||||
if (isPortInUse(rpc_port)) {
|
||||
if (!skip_port_check_ && 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;
|
||||
@@ -470,12 +483,44 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
||||
args.push_back("-debug=" + cat);
|
||||
}
|
||||
|
||||
// Add -rescan flag if requested (one-shot)
|
||||
if (rescan_on_next_start_.exchange(false)) {
|
||||
// Append max connections if configured (0 = daemon default)
|
||||
if (max_connections_ > 0) {
|
||||
args.push_back("-maxconnections=" + std::to_string(max_connections_));
|
||||
}
|
||||
|
||||
// Active wallet file (multi-wallet). The daemon loads <datadir>/<name>. Only pass it for a
|
||||
// non-default name so the common case's command line is unchanged; skip during an isolated
|
||||
// start (seed migration manages its own throwaway wallet).
|
||||
if (!wallet_file_.empty() && wallet_file_ != "wallet.dat" && override_datadir_.empty()) {
|
||||
DEBUG_LOGF("[INFO] Loading wallet file: %s\n", wallet_file_.c_str());
|
||||
args.push_back("-wallet=" + wallet_file_);
|
||||
}
|
||||
|
||||
// Add wallet-repair flag if requested (one-shot). -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)
|
||||
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");
|
||||
@@ -1197,5 +1242,10 @@ bool EmbeddedDaemon::isRpcPortInUse()
|
||||
return isPortInUse(port);
|
||||
}
|
||||
|
||||
bool EmbeddedDaemon::tcpPortInUse(int port)
|
||||
{
|
||||
return isPortInUse(port);
|
||||
}
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -116,6 +116,26 @@ public:
|
||||
* @brief Get last N lines of daemon output (thread-safe snapshot)
|
||||
*/
|
||||
std::vector<std::string> getRecentLines(int maxLines = 8) const;
|
||||
|
||||
/**
|
||||
* @brief Extract the latest block height from daemon output (thread-safe).
|
||||
* Parses the last "height=N" from UpdateTip lines without copying
|
||||
* the entire output buffer. Returns -1 if no UpdateTip found.
|
||||
*/
|
||||
int getLastBlockHeight() const {
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
// Search backwards from the end for "height="
|
||||
size_t pos = process_output_.rfind("height=");
|
||||
if (pos == std::string::npos) return -1;
|
||||
pos += 7; // skip "height="
|
||||
int h = 0;
|
||||
for (size_t i = pos; i < process_output_.size(); ++i) {
|
||||
char c = process_output_[i];
|
||||
if (c >= '0' && c <= '9') h = h * 10 + (c - '0');
|
||||
else break;
|
||||
}
|
||||
return h > 0 ? h : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Whether start() detected an existing daemon on the RPC port.
|
||||
@@ -152,12 +172,54 @@ 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(); }
|
||||
|
||||
// Active wallet file (multi-wallet). Passed to the daemon as -wallet=<name> so it loads
|
||||
// <datadir>/<name>; empty or "wallet.dat" keeps the daemon default (no arg). Must be a plain
|
||||
// filename in the datadir — the daemon rejects paths.
|
||||
void setWalletFile(const std::string& v) { wallet_file_ = v; }
|
||||
std::string walletFile() const { return wallet_file_; }
|
||||
|
||||
/**
|
||||
* @brief Request a wallet repair (-zapwallettxes=2) on the next daemon start. This deletes all
|
||||
* wallet transaction/note records and rebuilds them from the chain (keys are kept); the
|
||||
* daemon implicitly rescans afterwards. One-shot, like the rescan flag.
|
||||
*/
|
||||
void setZapOnNextStart(bool v) { zap_on_next_start_ = v; }
|
||||
bool zapOnNextStart() const { return zap_on_next_start_.load(); }
|
||||
|
||||
/**
|
||||
* @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) */
|
||||
@@ -194,8 +256,14 @@ 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::string wallet_file_; // -wallet=<name> for the active wallet; empty/"wallet.dat" = default
|
||||
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
||||
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
||||
std::atomic<bool> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start
|
||||
std::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
|
||||
|
||||
93
src/daemon/lifecycle_adapters.cpp
Normal file
93
src/daemon/lifecycle_adapters.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
#include "lifecycle_adapters.h"
|
||||
#include "../util/logger.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
|
||||
AsyncLifecycleTaskContext::AsyncLifecycleTaskContext(
|
||||
const util::AsyncTaskManager::Token& token,
|
||||
const std::atomic<bool>& shuttingDown)
|
||||
: token_(token), shuttingDown_(shuttingDown)
|
||||
{
|
||||
}
|
||||
|
||||
bool AsyncLifecycleTaskContext::cancelled() const
|
||||
{
|
||||
return token_.cancelled();
|
||||
}
|
||||
|
||||
bool AsyncLifecycleTaskContext::shuttingDown() const
|
||||
{
|
||||
return shuttingDown_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void AsyncLifecycleTaskContext::sleepForMs(int milliseconds)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(std::max(0, milliseconds)));
|
||||
}
|
||||
|
||||
bool ImmediateLifecycleTaskContext::cancelled() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ImmediateLifecycleTaskContext::shuttingDown() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void ImmediateLifecycleTaskContext::sleepForMs(int)
|
||||
{
|
||||
}
|
||||
|
||||
int BlockchainDataCleaner::removeBlockchainData(const std::filesystem::path& dataDir)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static constexpr std::array<const char*, 4> directories = {
|
||||
"blocks", "chainstate", "database", "notarizations"
|
||||
};
|
||||
static constexpr std::array<const char*, 5> files = {
|
||||
"peers.dat", "fee_estimates.dat", "banlist.dat", "db.log", ".lock"
|
||||
};
|
||||
|
||||
int removed = 0;
|
||||
for (const char* directoryName : directories) {
|
||||
fs::path path = dataDir / directoryName;
|
||||
std::error_code existsError;
|
||||
if (!fs::exists(path, existsError)) continue;
|
||||
|
||||
std::error_code removeError;
|
||||
auto count = fs::remove_all(path, removeError);
|
||||
if (!removeError) {
|
||||
removed += static_cast<int>(count);
|
||||
DEBUG_LOGF("[DaemonLifecycle] Removed %s (%d entries)\n",
|
||||
directoryName, static_cast<int>(count));
|
||||
} else {
|
||||
DEBUG_LOGF("[DaemonLifecycle] Failed to remove %s: %s\n",
|
||||
directoryName, removeError.message().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
for (const char* fileName : files) {
|
||||
fs::path path = dataDir / fileName;
|
||||
std::error_code removeError;
|
||||
if (fs::remove(path, removeError)) {
|
||||
++removed;
|
||||
DEBUG_LOGF("[DaemonLifecycle] Removed %s\n", fileName);
|
||||
} else if (removeError) {
|
||||
DEBUG_LOGF("[DaemonLifecycle] Failed to remove %s: %s\n",
|
||||
fileName, removeError.message().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
39
src/daemon/lifecycle_adapters.h
Normal file
39
src/daemon/lifecycle_adapters.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "daemon_controller.h"
|
||||
#include "../util/async_task_manager.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
|
||||
class AsyncLifecycleTaskContext final : public DaemonController::LifecycleTaskContext {
|
||||
public:
|
||||
AsyncLifecycleTaskContext(const util::AsyncTaskManager::Token& token,
|
||||
const std::atomic<bool>& shuttingDown);
|
||||
|
||||
bool cancelled() const override;
|
||||
bool shuttingDown() const override;
|
||||
void sleepForMs(int milliseconds) override;
|
||||
|
||||
private:
|
||||
const util::AsyncTaskManager::Token& token_;
|
||||
const std::atomic<bool>& shuttingDown_;
|
||||
};
|
||||
|
||||
class ImmediateLifecycleTaskContext final : public DaemonController::LifecycleTaskContext {
|
||||
public:
|
||||
bool cancelled() const override;
|
||||
bool shuttingDown() const override;
|
||||
void sleepForMs(int milliseconds) override;
|
||||
};
|
||||
|
||||
class BlockchainDataCleaner final {
|
||||
public:
|
||||
static int removeBlockchainData(const std::filesystem::path& dataDir);
|
||||
};
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
150
src/daemon/seed_wallet_creator.cpp
Normal file
150
src/daemon/seed_wallet_creator.cpp
Normal file
@@ -0,0 +1,150 @@
|
||||
#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) {
|
||||
const std::string what = e.what();
|
||||
// "Method not found" (JSON-RPC -32601) means this dragonxd predates mnemonic support —
|
||||
// it has no z_exportmnemonic RPC (the older bundled binary). Migrate-to-seed can't work
|
||||
// until the daemon is updated, so give an actionable message, not the raw RPC error.
|
||||
if (what.find("Method not found") != std::string::npos ||
|
||||
what.find("-32601") != std::string::npos) {
|
||||
r.error = "This DragonX daemon is too old to create a seed wallet — it lacks mnemonic "
|
||||
"support (the z_exportmnemonic RPC). Update to the latest DragonX daemon "
|
||||
"(Settings -> NODE & SECURITY -> Check for updates, or Install bundled), then "
|
||||
"try again.";
|
||||
} else {
|
||||
r.error = std::string("Seed export failed: ") + what;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
32
src/daemon/seed_wallet_creator.h
Normal file
32
src/daemon/seed_wallet_creator.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#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
|
||||
@@ -1,10 +1,14 @@
|
||||
// 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>
|
||||
@@ -19,6 +23,7 @@
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include "../util/logger.h"
|
||||
#include "../util/pool_registry.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
@@ -715,6 +720,11 @@ 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) {
|
||||
@@ -765,10 +775,14 @@ void XmrigManager::fetchStatsHttp() {
|
||||
void XmrigManager::fetchPoolApiStats() {
|
||||
if (state_ != State::Running || pool_host_.empty()) return;
|
||||
|
||||
// Query the pool's public stats API
|
||||
std::string url = "https://" + pool_host_ + "/api/stats";
|
||||
std::string responseData;
|
||||
// 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");
|
||||
|
||||
std::string responseData;
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) return;
|
||||
|
||||
@@ -779,31 +793,99 @@ 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;
|
||||
|
||||
try {
|
||||
json resp = json::parse(responseData);
|
||||
bool ok = false;
|
||||
const double poolHR = util::parsePoolHashrate(
|
||||
known ? known->schema : util::PoolStatsSchema::DragonXIs,
|
||||
responseData, known ? known->miningcorePoolId : std::string{}, ok);
|
||||
if (!ok) return;
|
||||
|
||||
// Pool stats API format: { "pools": { "<name>": { "hashrate": ... } } }
|
||||
double poolHR = 0;
|
||||
if (resp.contains("pools") && resp["pools"].is_object()) {
|
||||
for (auto& [key, pool] : resp["pools"].items()) {
|
||||
if (pool.contains("hashrate") && pool["hashrate"].is_number()) {
|
||||
poolHR = pool["hashrate"].get<double>();
|
||||
break; // Use the first pool entry
|
||||
std::lock_guard<std::mutex> lk(stats_mutex_);
|
||||
stats_.pool_hashrate = poolHR;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Installed-miner version detection (`<binary> --version`, cached)
|
||||
// ============================================================================
|
||||
|
||||
namespace {
|
||||
std::mutex g_installed_ver_mutex;
|
||||
std::string g_installed_ver;
|
||||
std::atomic<bool> g_ver_detect_started{false};
|
||||
|
||||
// Extract the first "D.D[.D...]" version token from `--version` output (skips the
|
||||
// build date, which uses '-' separators). Returns e.g. "6.21.0", or "" if none.
|
||||
std::string parseMinerVersion(const std::string& out)
|
||||
{
|
||||
for (size_t i = 0; i < out.size(); ++i) {
|
||||
if (std::isdigit(static_cast<unsigned char>(out[i]))) {
|
||||
size_t j = i;
|
||||
int dots = 0;
|
||||
while (j < out.size() &&
|
||||
(std::isdigit(static_cast<unsigned char>(out[j])) || out[j] == '.')) {
|
||||
if (out[j] == '.') ++dots;
|
||||
++j;
|
||||
}
|
||||
if (dots >= 1 && (j - i) >= 3) {
|
||||
// Include a trailing build suffix like "-hac" / "-drg1" (e.g. "6.25.1-hac"),
|
||||
// matching what the running miner's API reports.
|
||||
size_t end = j;
|
||||
if (end < out.size() && out[end] == '-') {
|
||||
size_t k = end + 1;
|
||||
while (k < out.size() && std::isalnum(static_cast<unsigned char>(out[k]))) ++k;
|
||||
if (k > end + 1) end = k;
|
||||
}
|
||||
return out.substr(i, end - i);
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void XmrigManager::startVersionDetection()
|
||||
{
|
||||
if (g_ver_detect_started.exchange(true)) return; // one-shot
|
||||
std::thread([]() {
|
||||
const std::string bin = findXmrigBinary();
|
||||
std::string ver;
|
||||
if (!bin.empty()) {
|
||||
const std::string cmd = "\"" + bin + "\" --version 2>&1";
|
||||
#ifdef _WIN32
|
||||
FILE* fp = _popen(cmd.c_str(), "r");
|
||||
#else
|
||||
FILE* fp = popen(cmd.c_str(), "r");
|
||||
#endif
|
||||
if (fp) {
|
||||
std::string out;
|
||||
char buf[256];
|
||||
size_t n;
|
||||
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) out.append(buf, n);
|
||||
#ifdef _WIN32
|
||||
_pclose(fp);
|
||||
#else
|
||||
pclose(fp);
|
||||
#endif
|
||||
ver = parseMinerVersion(out);
|
||||
}
|
||||
}
|
||||
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
|
||||
g_installed_ver = ver;
|
||||
}).detach();
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lk(stats_mutex_);
|
||||
stats_.pool_hashrate = poolHR;
|
||||
} catch (...) {
|
||||
// Malformed response — ignore
|
||||
}
|
||||
std::string XmrigManager::installedVersion()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
|
||||
return g_installed_ver;
|
||||
}
|
||||
|
||||
} // namespace daemon
|
||||
|
||||
@@ -43,6 +43,7 @@ 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
|
||||
@@ -88,6 +89,10 @@ public:
|
||||
const PoolStats& getStats() const { return stats_; }
|
||||
const std::string& getLastError() const { return last_error_; }
|
||||
|
||||
/// Thread count requested at start() — available immediately, unlike
|
||||
/// PoolStats::threads_active which requires an API response.
|
||||
int getRequestedThreads() const { return threads_; }
|
||||
|
||||
/**
|
||||
* @brief Get last N lines of xmrig stdout (thread-safe snapshot).
|
||||
*/
|
||||
@@ -133,6 +138,18 @@ 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);
|
||||
|
||||
@@ -9,13 +9,7 @@
|
||||
#include <filesystem>
|
||||
|
||||
#include "../util/logger.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <shlobj.h>
|
||||
#else
|
||||
#include <pwd.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include "../util/platform.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using json = nlohmann::json;
|
||||
@@ -28,33 +22,11 @@ AddressBook::~AddressBook() = default;
|
||||
|
||||
std::string AddressBook::getDefaultPath()
|
||||
{
|
||||
#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";
|
||||
// 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();
|
||||
fs::create_directories(dir);
|
||||
return dir + "/addressbook.json";
|
||||
#else
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
home = pw->pw_dir;
|
||||
}
|
||||
std::string dir = std::string(home) + "/.config/ObsidianDragon";
|
||||
fs::create_directories(dir);
|
||||
return dir + "/addressbook.json";
|
||||
#endif
|
||||
return (fs::path(dir) / "addressbook.json").string();
|
||||
}
|
||||
|
||||
bool AddressBook::load()
|
||||
@@ -79,7 +51,10 @@ bool AddressBook::load()
|
||||
e.label = entry.value("label", "");
|
||||
e.address = entry.value("address", "");
|
||||
e.notes = entry.value("notes", "");
|
||||
|
||||
// Legacy entries (no "scope") migrate to "global" so nothing disappears when
|
||||
// multi-wallet scoping lands — a contact you already had stays visible everywhere.
|
||||
e.scope = entry.value("scope", "global");
|
||||
|
||||
if (!e.address.empty()) {
|
||||
entries_.push_back(e);
|
||||
}
|
||||
@@ -110,23 +85,20 @@ bool AddressBook::save()
|
||||
e["label"] = entry.label;
|
||||
e["address"] = entry.address;
|
||||
e["notes"] = entry.notes;
|
||||
e["scope"] = entry.scope.empty() ? std::string("global") : entry.scope;
|
||||
j["entries"].push_back(e);
|
||||
}
|
||||
|
||||
// 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());
|
||||
// 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());
|
||||
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;
|
||||
@@ -135,11 +107,12 @@ bool AddressBook::save()
|
||||
|
||||
bool AddressBook::addEntry(const AddressBookEntry& entry)
|
||||
{
|
||||
// Check for duplicate address
|
||||
if (findByAddress(entry.address) >= 0) {
|
||||
// Reject a duplicate only within the same visible set (same wallet or global) — the same
|
||||
// address may legitimately be a contact in two different wallets.
|
||||
if (hasVisibleDuplicate(entry.address, entry.scope)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
entries_.push_back(entry);
|
||||
return save();
|
||||
}
|
||||
@@ -150,12 +123,11 @@ bool AddressBook::updateEntry(size_t index, const AddressBookEntry& entry)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for duplicate address (excluding current entry)
|
||||
int existing = findByAddress(entry.address);
|
||||
if (existing >= 0 && static_cast<size_t>(existing) != index) {
|
||||
// Check for a duplicate visible alongside this entry's scope (excluding the entry being edited)
|
||||
if (hasVisibleDuplicate(entry.address, entry.scope, static_cast<int>(index))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
entries_[index] = entry;
|
||||
return save();
|
||||
}
|
||||
@@ -180,5 +152,19 @@ int AddressBook::findByAddress(const std::string& address) const
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool AddressBook::hasVisibleDuplicate(const std::string& address, const std::string& scope,
|
||||
int excludeIndex) const
|
||||
{
|
||||
AddressBookEntry probe; probe.scope = scope; // reuse the isGlobal()/scope logic
|
||||
for (size_t i = 0; i < entries_.size(); i++) {
|
||||
if (static_cast<int>(i) == excludeIndex) continue;
|
||||
const auto& e = entries_[i];
|
||||
if (e.address != address) continue;
|
||||
// Collides if they'd ever be shown together: same wallet scope, or either is global.
|
||||
if (e.isGlobal() || probe.isGlobal() || e.scope == scope) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
} // namespace dragonx
|
||||
|
||||
@@ -17,10 +17,21 @@ struct AddressBookEntry {
|
||||
std::string label;
|
||||
std::string address;
|
||||
std::string notes;
|
||||
|
||||
// Per-wallet visibility scope: "global" (shown in every wallet) or a wallet-identity hash
|
||||
// (shown only when that wallet is the active one). Empty is treated as "global" so legacy
|
||||
// entries — written before multi-wallet scoping — keep showing everywhere.
|
||||
std::string scope = "global";
|
||||
|
||||
AddressBookEntry() = default;
|
||||
AddressBookEntry(const std::string& l, const std::string& a, const std::string& n = "")
|
||||
: label(l), address(a), notes(n) {}
|
||||
AddressBookEntry(const std::string& l, const std::string& a, const std::string& n = "",
|
||||
const std::string& s = "global")
|
||||
: label(l), address(a), notes(n), scope(s) {}
|
||||
|
||||
bool isGlobal() const { return scope.empty() || scope == "global"; }
|
||||
// Visible under the wallet whose identity hash is walletHash (or globally).
|
||||
bool visibleInWallet(const std::string& walletHash) const {
|
||||
return isGlobal() || scope == walletHash;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -73,12 +84,21 @@ public:
|
||||
bool removeEntry(size_t index);
|
||||
|
||||
/**
|
||||
* @brief Find entry by address
|
||||
* @brief Find entry by address (any scope). Used for contact-label lookups.
|
||||
* @param address Address to search for
|
||||
* @return Index or -1 if not found
|
||||
*/
|
||||
int findByAddress(const std::string& address) const;
|
||||
|
||||
/**
|
||||
* @brief Is there an existing entry with this address that would be visible ALONGSIDE a new
|
||||
* entry of the given scope? (Same wallet, or either side global.) Used to reject
|
||||
* duplicates within a wallet while still allowing the same address in different wallets.
|
||||
* @param excludeIndex Storage index to skip (for edit), or -1.
|
||||
*/
|
||||
bool hasVisibleDuplicate(const std::string& address, const std::string& scope,
|
||||
int excludeIndex = -1) const;
|
||||
|
||||
/**
|
||||
* @brief Get all entries
|
||||
*/
|
||||
|
||||
@@ -4,11 +4,19 @@
|
||||
|
||||
#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",
|
||||
@@ -17,9 +25,68 @@ 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
|
||||
|
||||
@@ -30,9 +30,19 @@ struct ExchangeInfo {
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Returns the static registry of supported exchanges + pairs
|
||||
* @brief Returns the static registry of supported exchanges + pairs.
|
||||
* Used as the offline fallback / seed when the live CoinGecko list is unavailable.
|
||||
*/
|
||||
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
|
||||
|
||||
99
src/data/market_series.h
Normal file
99
src/data/market_series.h
Normal file
@@ -0,0 +1,99 @@
|
||||
// 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
|
||||
61
src/data/portfolio.h
Normal file
61
src/data/portfolio.h
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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
|
||||
538
src/data/transaction_history_cache.cpp
Normal file
538
src/data/transaction_history_cache.cpp
Normal file
@@ -0,0 +1,538 @@
|
||||
#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
|
||||
93
src/data/transaction_history_cache.h
Normal file
93
src/data/transaction_history_cache.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#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
|
||||
159
src/data/wallet_index.cpp
Normal file
159
src/data/wallet_index.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "wallet_index.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include "../util/logger.h"
|
||||
#include "../util/platform.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace dragonx {
|
||||
namespace data {
|
||||
|
||||
namespace {
|
||||
// A field-wise "did anything change" check so we only rewrite wallets.json when needed.
|
||||
bool sameEntry(const WalletIndexEntry& a, const WalletIndexEntry& b)
|
||||
{
|
||||
return a.fileName == b.fileName
|
||||
&& a.displayName == b.displayName
|
||||
&& a.walletIdentityHash == b.walletIdentityHash
|
||||
&& a.cachedBalance == b.cachedBalance
|
||||
&& a.cachedAddressCount == b.cachedAddressCount
|
||||
&& a.lastOpenedEpoch == b.lastOpenedEpoch
|
||||
&& a.sizeBytesAtLastOpen == b.sizeBytesAtLastOpen
|
||||
&& a.syncedHere == b.syncedHere;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string WalletIndex::getDefaultPath()
|
||||
{
|
||||
// Co-located with settings.json / addressbook.json in the per-variant config dir.
|
||||
const std::string dir = util::Platform::getConfigDir();
|
||||
fs::create_directories(dir);
|
||||
return (fs::path(dir) / "wallets.json").string();
|
||||
}
|
||||
|
||||
bool WalletIndex::load()
|
||||
{
|
||||
file_path_ = getDefaultPath();
|
||||
entries_.clear();
|
||||
extra_folders_.clear();
|
||||
|
||||
std::ifstream file(file_path_);
|
||||
if (!file.is_open()) return true; // no file yet is fine
|
||||
|
||||
try {
|
||||
json j;
|
||||
file >> j;
|
||||
|
||||
if (j.contains("entries") && j["entries"].is_array()) {
|
||||
for (const auto& e : j["entries"]) {
|
||||
WalletIndexEntry w;
|
||||
w.fileName = e.value("file", "");
|
||||
if (w.fileName.empty()) continue;
|
||||
w.displayName = e.value("name", w.fileName);
|
||||
w.walletIdentityHash = e.value("identity", "");
|
||||
w.cachedBalance = e.value("balance", -1.0);
|
||||
w.cachedAddressCount = e.value("addresses", (long long)-1);
|
||||
w.lastOpenedEpoch = e.value("lastOpened", (long long)0);
|
||||
w.sizeBytesAtLastOpen = e.value("size", (long long)0);
|
||||
w.syncedHere = e.value("syncedHere", false);
|
||||
entries_.push_back(std::move(w));
|
||||
}
|
||||
}
|
||||
if (j.contains("extraFolders") && j["extraFolders"].is_array()) {
|
||||
for (const auto& d : j["extraFolders"]) {
|
||||
if (d.is_string() && !d.get<std::string>().empty())
|
||||
extra_folders_.push_back(d.get<std::string>());
|
||||
}
|
||||
}
|
||||
DEBUG_LOGF("Wallet index loaded: %zu wallets, %zu extra folders\n",
|
||||
entries_.size(), extra_folders_.size());
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Error loading wallet index: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool WalletIndex::save()
|
||||
{
|
||||
if (file_path_.empty()) file_path_ = getDefaultPath();
|
||||
|
||||
try {
|
||||
json j;
|
||||
j["entries"] = json::array();
|
||||
for (const auto& w : entries_) {
|
||||
json e;
|
||||
e["file"] = w.fileName;
|
||||
e["name"] = w.displayName;
|
||||
e["identity"] = w.walletIdentityHash;
|
||||
e["balance"] = w.cachedBalance;
|
||||
e["addresses"] = w.cachedAddressCount;
|
||||
e["lastOpened"] = w.lastOpenedEpoch;
|
||||
e["size"] = w.sizeBytesAtLastOpen;
|
||||
e["syncedHere"] = w.syncedHere;
|
||||
j["entries"].push_back(std::move(e));
|
||||
}
|
||||
j["extraFolders"] = extra_folders_;
|
||||
|
||||
// No secrets here (file names + coarse metadata) but keep it owner-only for consistency.
|
||||
if (!util::Platform::writeFileAtomically(file_path_, j.dump(2), /*restrictPermissions=*/true)) {
|
||||
DEBUG_LOGF("Could not write wallet index: %s\n", file_path_.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Error saving wallet index: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool WalletIndex::upsert(const WalletIndexEntry& e)
|
||||
{
|
||||
for (auto& w : entries_) {
|
||||
if (w.fileName == e.fileName) {
|
||||
if (sameEntry(w, e)) return false; // nothing changed -> caller can skip save()
|
||||
w = e;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
entries_.push_back(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
const WalletIndexEntry* WalletIndex::find(const std::string& fileName) const
|
||||
{
|
||||
for (const auto& w : entries_) {
|
||||
if (w.fileName == fileName) return &w;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool WalletIndex::addExtraFolder(const std::string& dir)
|
||||
{
|
||||
if (dir.empty()) return false;
|
||||
if (std::find(extra_folders_.begin(), extra_folders_.end(), dir) != extra_folders_.end())
|
||||
return false;
|
||||
extra_folders_.push_back(dir);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WalletIndex::removeExtraFolder(const std::string& dir)
|
||||
{
|
||||
auto it = std::find(extra_folders_.begin(), extra_folders_.end(), dir);
|
||||
if (it == extra_folders_.end()) return false;
|
||||
extra_folders_.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
} // namespace dragonx
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user