Compare commits
191
Commits
dev
..
70fa1ec3bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70fa1ec3bf | ||
|
|
f6440739a5 | ||
|
|
c625a4e31a | ||
|
|
fc85297636 | ||
|
|
95795b1581 | ||
|
|
f9cce04010 | ||
|
|
2624f3dd2e | ||
|
|
d95703a762 | ||
|
|
3a1169018a | ||
|
|
fca3e830fa | ||
|
|
cec6c54137 | ||
|
|
690c4230b3 | ||
|
|
93f7e9ec68 | ||
|
|
d9c05d194e | ||
|
|
e18669daee | ||
|
|
8f95089add | ||
|
|
4e8a39f90e | ||
|
|
2da58a2bfb | ||
|
|
820c5da040 | ||
|
|
923aa4dba3 | ||
|
|
8b5d239f53 | ||
|
|
9b07586b25 | ||
|
|
76f2f3cfe0 | ||
|
|
1fc7f6e22e | ||
|
|
db1b214cc4 | ||
|
|
b5270fa995 | ||
|
|
b4ddf5b146 | ||
|
|
3e1b60b0f5 | ||
|
|
edb1e53380 | ||
|
|
e7ee96aba8 | ||
|
|
dcda46fe2b | ||
|
|
0ffe95d254 | ||
|
|
06a7c8f354 | ||
|
|
e41361dfab | ||
|
|
6f255327a7 | ||
|
|
e9d729a0b4 | ||
|
|
e77e64494e | ||
|
|
76fb423df6 | ||
|
|
bb5874355d | ||
|
|
dab714e0ad | ||
|
|
a93cb7d7cb | ||
|
|
d05d01413c | ||
|
|
3d9c7c99d3 | ||
|
|
a0eb994798 | ||
|
|
72555d5dc0 | ||
|
|
ee798c0117 | ||
|
|
3050920486 | ||
|
|
70fbb4d2fa | ||
|
|
596fe537d6 | ||
|
|
50e5dba689 | ||
|
|
7819d22c0b | ||
|
|
ff95b1bd7b | ||
|
|
af471626c2 | ||
|
|
fb7fb3dee1 | ||
|
|
e9e436f77c | ||
|
|
928c2cbdfe | ||
|
|
70478beb3e | ||
|
|
b3f74e43ac | ||
|
|
3d60de49fe | ||
|
|
2bcf1e6df3 | ||
|
|
b5249fd353 | ||
|
|
37281e8aff | ||
|
|
e278971f72 | ||
|
|
5c36cd0757 | ||
|
|
25c56249eb | ||
|
|
deefd3b845 | ||
|
|
a642e37d9d | ||
|
|
1b4515feab | ||
|
|
02df7c1e83 | ||
|
|
3d78b248bb | ||
|
|
eb820dce21 | ||
|
|
9b4c9b805a | ||
|
|
293533694e | ||
|
|
dd2b49b0cd | ||
|
|
8cf1d20dee | ||
|
|
86efba3933 | ||
|
|
7e013006eb | ||
|
|
4059259a8d | ||
|
|
5c2adc6b0c | ||
|
|
cc507f2c4a | ||
|
|
3110c83665 | ||
|
|
bf384b53ff | ||
|
|
dc26c2952a | ||
|
|
ec8b500a42 | ||
|
|
78770f689e | ||
|
|
97e5a9a4aa | ||
|
|
d188fd9d5f | ||
|
|
54c9e2ba87 | ||
|
|
93d93cfc45 | ||
|
|
62e7ede907 | ||
|
|
a6464fbcf8 | ||
|
|
936070b552 | ||
|
|
b962638198 | ||
|
|
e594ac8c40 | ||
|
|
b7bec1e719 | ||
|
|
4a5497181b | ||
|
|
c818a015a1 | ||
|
|
a36df94b03 | ||
|
|
1683968388 | ||
|
|
b5e72300a3 | ||
|
|
27d11ebe6d | ||
|
|
f169b50e8f | ||
|
|
f84adb206f | ||
|
|
cb6765ae5b | ||
|
|
3bab0ed528 | ||
|
|
2174d880c2 | ||
|
|
424aeb11a6 | ||
|
|
9e991a9c2f | ||
|
|
db49cc8f5b | ||
|
|
335a25d07a | ||
|
|
c997dee9bf | ||
|
|
bf156d0ee9 | ||
|
|
9ce5feeb6a | ||
|
|
ea5749772f | ||
|
|
c198721d54 | ||
|
|
3f0abddf10 | ||
|
|
a97059fd53 | ||
|
|
e906ca58f4 | ||
|
|
95902788f2 | ||
|
|
1fc82addc9 | ||
|
|
096a43ba84 | ||
|
|
3717b231f8 | ||
|
|
6d4ec52331 | ||
|
|
ec075f3db4 | ||
|
|
a5015cfcf2 | ||
|
|
e07abc2e98 | ||
|
|
08b478ed3f | ||
|
|
859405261b | ||
|
|
1b7fcc53ba | ||
|
|
d8a2646747 | ||
|
|
b937364b7c | ||
|
|
648ee2bfdc | ||
|
|
7dd7916b3d | ||
|
|
31ad95bd22 | ||
|
|
10f98ef3fa | ||
|
|
af06b8bf0d | ||
|
|
ffe63d3129 | ||
|
|
99e6444f0d | ||
|
|
229373e937 | ||
|
|
973c390df5 | ||
|
|
d684db446e | ||
|
|
9e1b1397ad | ||
|
|
55a36e0d06 | ||
|
|
7937aad4fb | ||
|
|
077f9a7403 | ||
|
|
9f23b2781c | ||
|
|
79d8f0d809 | ||
|
|
dc4426810f | ||
|
|
915c1b4d23 | ||
|
|
28b9e0dffb | ||
|
|
6be0a58c26 | ||
|
|
fbdba1a001 | ||
|
|
821c54ba2b | ||
|
|
3ff62ca248 | ||
|
|
bbf53a130c | ||
|
|
1f9e43d7b2 | ||
|
|
5ebccceffc | ||
|
|
9d2d581474 | ||
|
|
096f8ee90e | ||
|
|
ca199ef195 | ||
|
|
97bd2f8168 | ||
|
|
09f287fbc5 | ||
|
|
b3d43ba0ad | ||
|
|
430290f97a | ||
|
|
30fc5da520 | ||
|
|
f02c965929 | ||
|
|
f0b7b88ef2 | ||
|
|
53d08de639 | ||
|
|
8645a82e4f | ||
|
|
9e94952e0a | ||
|
|
4a841fd032 | ||
|
|
f0c87e4092 | ||
|
|
c5ef4899bb | ||
|
|
36b67e69d0 | ||
|
|
06c80ef51c | ||
|
|
6bd5341507 | ||
|
|
5284c0dbb6 | ||
|
|
cf520fdf40 | ||
|
|
96c27bb949 | ||
|
|
cc617dd5be | ||
|
|
653a90de62 | ||
|
|
4b16a2a2c4 | ||
|
|
c51d3dafff | ||
|
|
68c2a59d09 | ||
|
|
45a2ccd9f3 | ||
|
|
0ca1caf148 | ||
|
|
7fb1f1de9d | ||
|
|
386cc857b0 | ||
|
|
3e6136983a | ||
|
|
2c1862aed3 | ||
|
|
4b815fc9d1 |
+3
-18
@@ -11,8 +11,8 @@ prebuilt-binaries/dragonxd-win/*
|
||||
!prebuilt-binaries/dragonxd-win/.gitkeep
|
||||
prebuilt-binaries/dragonxd-mac/*
|
||||
!prebuilt-binaries/dragonxd-mac/.gitkeep
|
||||
prebuilt-binaries/drg-xmrig/*
|
||||
!prebuilt-binaries/drg-xmrig/.gitkeep
|
||||
prebuilt-binaries/xmrig-hac/*
|
||||
!prebuilt-binaries/xmrig-hac/.gitkeep
|
||||
|
||||
|
||||
# External sources / toolchains (created by scripts/setup.sh)
|
||||
@@ -33,11 +33,7 @@ imgui.ini
|
||||
*.bak*
|
||||
*.params
|
||||
asmap.dat
|
||||
# Wallet files hold PRIVATE KEYS — never commit them
|
||||
wallet.dat
|
||||
wallet-*.dat
|
||||
wallet.dat.*
|
||||
/external/drg-xmrig
|
||||
/external/xmrig-hac
|
||||
/memory
|
||||
/todo.md
|
||||
/.github/
|
||||
@@ -51,14 +47,3 @@ docs/_archive/
|
||||
# ed25519 release-signing keys — the secret key must NEVER be committed
|
||||
*.ed25519.key
|
||||
*.ed25519.pub.b64
|
||||
|
||||
|
||||
# Lite-backend deps are fetched (or `cargo vendor`-ed locally for offline); not committed.
|
||||
third_party/silentdragonxlite/lib/vendor/
|
||||
|
||||
# Generated by configure_file from res/ObsidianDragon.manifest.in (do not track)
|
||||
res/ObsidianDragon.manifest
|
||||
|
||||
# Cross-built mingw FreeType (color emoji) — regenerated by scripts/build-freetype-mingw.sh
|
||||
third_party/freetype-mingw/
|
||||
third_party/.freetype-mingw-build/
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable user-facing changes to ObsidianDragon are documented here. The format loosely
|
||||
follows [Keep a Changelog](https://keepachangelog.com/); the project uses Conventional Commits.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### ⚠️ Breaking changes
|
||||
|
||||
- **Remote RPC over plain HTTP is now refused by default.** If your wallet is configured to
|
||||
reach a **remote** `rpchost`/`rpcconnect` **without TLS**, it will no longer connect — it
|
||||
previously sent your `rpcuser`/`rpcpassword` in cleartext (capturable by anyone on the
|
||||
network path) after only a dismissible warning. To reconnect, either:
|
||||
- add **`rpctls=1`** to `DRAGONX.conf` (preferred, if your daemon supports TLS), or
|
||||
- add **`rpcallowplaintext=1`** to `DRAGONX.conf` to explicitly accept the plaintext link.
|
||||
|
||||
Local and embedded daemons (`127.0.0.0/8`, `localhost`, `::1`) are unaffected.
|
||||
|
||||
### Security
|
||||
|
||||
- Refuse remote plaintext RPC credential transmission by default (see Breaking changes above).
|
||||
- Tightened localhost detection: a hostname that merely *starts* with `127.` (e.g.
|
||||
`127.evil.com`) is no longer mistaken for a loopback address, so it can no longer bypass the
|
||||
plaintext-RPC protection.
|
||||
- Sapling parameters are now integrity-checked (SHA-256) against pinned canonical digests
|
||||
before use, instead of only checking that the files exist. A truncated or corrupt parameter
|
||||
file is caught up front rather than surfacing later as a confusing shielded-operation failure.
|
||||
(Cached via a `size:mtime` marker so it doesn't re-hash ~48 MB on every launch.)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Daemon crashes are no longer occasionally missed: a race between the UI thread and the
|
||||
process monitor could consume the daemon's exit status, hiding a crash and defeating the
|
||||
automatic-restart cap. The monitor is now the sole reaper.
|
||||
- A daemon that fails to launch (missing execute permission, wrong architecture, corrupt
|
||||
binary) now reports a precise error immediately instead of briefly showing "running" and
|
||||
then a generic "exited unexpectedly (exit code 127)".
|
||||
- A quick stop→start no longer triggers a restart storm: the wallet now waits briefly for a
|
||||
previous daemon to release the data-directory lock and shows a clear, non-crash message
|
||||
instead of exhausting the crash-restart budget.
|
||||
- Failures while writing the daemon binaries or Sapling parameters (disk full, permission
|
||||
denied) are now surfaced clearly up front instead of failing opaquely when the daemon later
|
||||
can't start.
|
||||
- Directory-creation failures on startup (read-only home, permission denied) now produce a
|
||||
clear "Cannot create <dir>" message instead of a confusing downstream "config missing" /
|
||||
"binary not found" error (or, in one path, an uncaught exception).
|
||||
|
||||
### Added
|
||||
|
||||
- A "Taking longer than expected" notice now appears if the daemon is reachable but hasn't
|
||||
finished initializing after ~45 s (configurable via `ui.toml`), with guidance to restart the
|
||||
daemon or open the Console — instead of an indefinite silent spinner. It clears itself
|
||||
automatically once the daemon connects.
|
||||
|
||||
---
|
||||
|
||||
Engineering detail and the finding-by-finding rationale for this batch live in
|
||||
`docs/daemon-startup-hardening.md`.
|
||||
@@ -55,13 +55,13 @@ There is no per-test filtering — it is one binary that runs every assertion. T
|
||||
|
||||
> ⚠️ **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`).
|
||||
**Chat** (`src/chat/chat_protocol.cpp`): experimental HushChat protocol, compiled in only when `DRAGONX_ENABLE_CHAT=ON`.
|
||||
|
||||
## 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_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.
|
||||
- `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`.
|
||||
@@ -79,24 +79,10 @@ The detailed milestone plan and design history (the v2 plan, backend artifact/AB
|
||||
|
||||
## Miner updater (xmrig)
|
||||
|
||||
The mining tab's pool section has an **"Update miner…"** button that downloads/verifies/installs the latest DRG-XMRig from the project Gitea (`util/XmrigUpdater` + `ui/windows/xmrig_download_dialog.h`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest` → pick the asset for this platform (`linux-x64` / `win-x64` / `macos-x86_64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (from the release body) **and** a detached **ed25519 signature** → miniz-extract the binary (flattening the versioned subdir) into `resources::getDaemonDirectory()`. The whole archive is verified, so extracted members are trusted by transitivity (no per-member hash check). The pure, no-I/O core is split into `xmrig_updater_core.cpp` for unit tests; an env-gated (`DRAGONX_TEST_NETWORK=1`) test exercises the worker live. The dialog is a two-pane version picker (every `/releases` entry on the left, newest first, pre-releases included) so users can pin an older or pre-release build — same verify/install path via `startInstallRelease()`. It shares only the `ReleaseRow` row model with the daemon updater (`ui/windows/release_list_view.h`); each dialog renders its own tactile Material list.
|
||||
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.
|
||||
|
||||
**Signature verification is enforced** (`kXmrigRequireSignature = true` in `src/util/xmrig_updater.h`), checked against the public key pinned in `kXmrigSignaturePublicKeyBase64`. **Consequence for releases:** every `drg-xmrig` release MUST ship a detached signature per archive or the in-app updater refuses it. To cut a release: build the archives, then `scripts/sign-xmrig-release.sh sign <secret.key> <archive.zip>...` (OpenSSL-based, no extra deps) and upload each `<archive>.sig` as a release asset alongside its `.zip`. The signing **secret key must stay offline** (it is gitignored: `*.ed25519.key`); only its base64 public key is pinned in the source. To rotate the key, regenerate (`scripts/sign-xmrig-release.sh keygen`) and update `kXmrigSignaturePublicKeyBase64`. An emergency env override is not provided — disabling verification means setting `kXmrigSignaturePublicKeyBase64` empty (and rebuilding).
|
||||
|
||||
## Daemon updater (dragonxd)
|
||||
|
||||
Settings → **NODE & SECURITY → DAEMON BINARY** has a **"Check for updates…"** button that downloads/verifies/installs the latest **dragonxd full node** from the project Gitea — the full-node sibling of the xmrig updater (`util/DaemonUpdater` + `ui/windows/daemon_download_dialog.h`, pure no-I/O core in `daemon_updater_core.cpp`; gated full-node-only via `supportsFullNodeLifecycleActions()`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/latest` → pick the archive for this platform (`linux-amd64` / `macos` / `win64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (parsed from the release body's markdown **checksum table**, not xmrig's `<hash> <name>` lines) **and** a detached **ed25519 signature** → miniz-extract the three executables (`dragonxd`/`dragonx-cli`/`dragonx-tx`, flattening the versioned subdir) into `resources::getDaemonDirectory()`. The archive also bundles Sapling params/asmap, which the updater deliberately leaves to the wallet's own resource extraction. Install is **atomic and safe while the node runs** (POSIX `rename()` replaces the in-use binary; Windows moves the locked `.exe` aside to `.old`); the new binary takes effect on the **next daemon start**, so the Done screen offers **"Restart daemon now"** (`App::restartDaemon()`). The dialog is a two-pane version picker (every `/releases` entry on the left) so users can pin a specific/older/pre-release node build via `startInstallRelease()` — with a downgrade caution, since an older binary may not match current chain data. It shares only the `ReleaseRow` row model (`ui/windows/release_list_view.h`) with the miner updater; each renders its own tactile Material list.
|
||||
|
||||
**Signature verification is enforced** (`kDaemonRequireSignature = true` in `src/util/daemon_updater.h`), checked against `kDaemonSignaturePublicKeyBase64`. **Consequence for releases:** every `dragonx` release MUST ship a detached `<archive>.sig` per platform archive or the in-app updater refuses it (as of v1.0.2 the releases publish SHA-256 but **no** signatures yet — sign + upload them to enable in-app updates). To cut a release: `scripts/sign-daemon-release.sh sign <secret.key> dragonx-<ver>-{linux-amd64,macos,win64}.zip` (OpenSSL-based) and upload each `.sig` next to its `.zip`. The signing **secret key stays offline** (gitignored `*.ed25519.key`; this repo's is `dragonx-daemon.ed25519.key`); only the base64 public key is pinned. To rotate: `scripts/sign-daemon-release.sh keygen` and update `kDaemonSignaturePublicKeyBase64`. The generic SHA-256 / ed25519 primitives are shared with the miner updater (`util::sha256Hex` / `util::verifyXmrigSignature`).
|
||||
|
||||
## Seed phrase & migrate-to-seed (full node)
|
||||
|
||||
Full-node wallets are **BIP39-mnemonic-backed** and can **migrate a legacy (non-mnemonic) wallet into a seed wallet**. All of this **requires the `hd-transparent-keys`/`dev` daemon** (`z_exportmnemonic` + `-usemnemonic`); the older bundled binary lacks those RPCs, so these features degrade gracefully (chat falls back to a `z_exportkey` identity; the backup screen shows a "legacy wallet" note). The daemon source is vendored at `external/dragonx/` (build with its own `./build.sh`); deploy the built `dragonxd`/`dragonx-cli`/`dragonx-tx` into the wallet's daemon dir (`build/*/bin`) or via the daemon updater.
|
||||
|
||||
- **New wallets get a phrase.** `EmbeddedDaemon::getChainParams()` always passes `-usemnemonic=1`. The daemon reads it **only inside `GenerateNewSeed()` when a wallet has no seed yet**, so it is inert on existing wallets (safe to pass unconditionally) and makes every fresh wallet mnemonic-backed.
|
||||
- **Back up seed phrase.** Settings → Backup & Data → "Seed phrase" opens `renderSeedBackupDialog` (`App::exportSeedPhrase` → `z_exportmnemonic`, wiped via `sodium_memzero`). A one-time nudge (`maybeRemindSeedBackup`, settings flag `seed_backup_reminded`) reminds mnemonic-wallet users to back up.
|
||||
- **Migrate-to-seed** (`showSeedMigrationDialog` / `renderSeedMigrationDialog`, state machine `SeedMigrationStep`). **Phase 1 — create:** `daemon::SeedWalletCreator` runs a **second, isolated `dragonxd`** on its own port + throwaway datadir (`<config>/seed-migrate/DRAGONX`, basename MUST be the acname; `-usemnemonic=1 -connect=0`; RPC is plaintext http, not TLS), mints a mnemonic wallet, exports its seed + a sweep-target z-address, then stops. Uses the one-shot `EmbeddedDaemon::setNextStartOverride` + `setSkipPortCheck`. **Phase 2 — sweep + adopt:** `z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"]` sweeps all funds to the new address; a **Confirming gate** (`pollSweepStatus`) only allows adopt once the sweep tx is **mined (≥1 conf) AND the legacy balance is ~0** (offering a remainder re-sweep); adopt (`beginAdoptSeedWallet`, background) stops the daemon, moves `wallet.dat` aside to a **timestamped `.bak` (never deleted, restored on failure)**, installs the new wallet, and restarts with `-rescan`. Fund-moving code — **two rounds of adversarial review + a live mainnet run** gate it; the pending stage persists (`seed_migration_*` settings) so a restart resumes at sweep/confirm.
|
||||
|
||||
## Versioning
|
||||
|
||||
The version has a **single source of truth**: `project(... VERSION 1.2.0 ...)` plus `DRAGONX_VERSION_SUFFIX` in `CMakeLists.txt`. CMake generates `build/.../generated/dragonx_generated_version.h` from `src/config/version.h.in`. Do not hand-edit generated version output or hardcode version strings — bump the `project()` version in `CMakeLists.txt`.
|
||||
@@ -106,6 +92,5 @@ The version has a **single source of truth**: `project(... VERSION 1.2.0 ...)` p
|
||||
- **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.
|
||||
- **i18n:** user-facing strings are translated via `src/util/i18n`; translation JSON lives in `res/lang/` (`de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, `zh`, English fallback in code). Translation/font helper scripts are in `scripts/` (`gen_*.py`, CJK subset tooling).
|
||||
- **Commits:** the history uses Conventional Commits (`feat(scope): …`, `fix(scope): …`). PRs target `master`.
|
||||
|
||||
+64
-372
@@ -15,7 +15,7 @@ if(APPLE)
|
||||
endif()
|
||||
|
||||
project(ObsidianDragon
|
||||
VERSION 2.1.0
|
||||
VERSION 1.3.0
|
||||
LANGUAGES C CXX
|
||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||
)
|
||||
@@ -26,7 +26,7 @@ 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 "2.0.0")
|
||||
set(DRAGONX_LITE_VERSION "1.0.0")
|
||||
set(DRAGONX_LITE_VERSION_SUFFIX "")
|
||||
|
||||
# C++17 standard
|
||||
@@ -44,20 +44,7 @@ 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)
|
||||
option(DRAGONX_BUILD_FAKE_DAEMON "Build the fake slow dragonxd test stub (tools/fake_dragonxd); never shipped" OFF)
|
||||
# The screenshot / full UI sweep injects demo data and touches the wallet's data and config dirs, so it is a
|
||||
# dev tool only. build.sh passes it EXPLICITLY (ON for dev, OFF for every release configure) because build/linux
|
||||
# and build/mac are shared between dev and release builds; dragonx_release refuses to package while it is ON.
|
||||
option(DRAGONX_ENABLE_UI_SWEEP "Compile the UI sweep dev tool (never for shipped builds)" OFF)
|
||||
# Embedded fallback copy of res/lang/*.json (xxd -i -> lang_*.h). Needed only by the Windows single-file exe (no res/ beside it);
|
||||
# Linux zip/AppImage and the macOS .app always ship res/lang, which loadLanguage() reads first.
|
||||
if(WIN32)
|
||||
set(_DRAGONX_EMBED_LANG_DEFAULT ON)
|
||||
else()
|
||||
set(_DRAGONX_EMBED_LANG_DEFAULT OFF)
|
||||
endif()
|
||||
option(DRAGONX_EMBED_LANG "Embed res/lang/*.json in the binary as a fallback" ${_DRAGONX_EMBED_LANG_DEFAULT})
|
||||
option(DRAGONX_ENABLE_CHAT "Enable experimental HushChat protocol/UI integration" OFF)
|
||||
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")
|
||||
@@ -66,6 +53,7 @@ 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
|
||||
@@ -138,24 +126,36 @@ if(DRAGONX_ENABLE_LITE_BACKEND)
|
||||
if(DRAGONX_LITE_BACKEND_MANIFEST AND NOT EXISTS "${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST does not exist: ${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||
endif()
|
||||
# Note (F15-1): the former signature-metadata gate was removed. It trusted a
|
||||
# "verification_status: verified" field that scripts/build-lite-backend-artifact.sh
|
||||
# self-attested with no cryptographic check (the "verified" SHA was just the artifact's
|
||||
# own SHA). The trust root is now build-from-source: that script builds the backend from
|
||||
# the vendored in-tree source and refuses prebuilt artifacts, so the library linked here
|
||||
# is the one built from reviewed source. The required-symbol inventory check above stays.
|
||||
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(APPLE)
|
||||
# The Rust backend's TLS stack (security-framework / core-foundation crates)
|
||||
# references Secure Transport (SSL*) + CoreFoundation symbols. Link the frameworks
|
||||
# that provide them, or the static lib leaves ~130 symbols undefined at link time.
|
||||
set_property(TARGET dragonx_lite_backend APPEND PROPERTY
|
||||
INTERFACE_LINK_LIBRARIES "-framework Security" "-framework CoreFoundation")
|
||||
endif()
|
||||
if(DRAGONX_LITE_BACKEND_INCLUDE_DIR)
|
||||
if(NOT IS_DIRECTORY "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
|
||||
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_INCLUDE_DIR does not exist: ${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
|
||||
@@ -167,11 +167,6 @@ if(DRAGONX_ENABLE_LITE_BACKEND)
|
||||
set(DRAGONX_LITE_BACKEND_READY ON)
|
||||
endif()
|
||||
|
||||
# Windows (mingw cross) builds skip the large test target unless asked for with -DBUILD_TESTING=ON
|
||||
# (scripts/test-windows.sh does that in its own build/windows-tests tree), so release builds are unchanged.
|
||||
if(WIN32 AND NOT DEFINED BUILD_TESTING)
|
||||
set(BUILD_TESTING OFF CACHE BOOL "Build the testing tree.")
|
||||
endif()
|
||||
include(CTest)
|
||||
|
||||
# Output directories
|
||||
@@ -231,7 +226,7 @@ include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
json
|
||||
GIT_REPOSITORY https://github.com/nlohmann/json.git
|
||||
GIT_TAG 9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03 # v3.11.3 — pinned to immutable commit (L-08); tags are mutable
|
||||
GIT_TAG v3.11.3
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(json)
|
||||
@@ -240,7 +235,7 @@ FetchContent_MakeAvailable(json)
|
||||
FetchContent_Declare(
|
||||
tomlplusplus
|
||||
GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git
|
||||
GIT_TAG 30172438cee64926dc41fdd9c11fb3ba5b2ba9de # v3.4.0 — pinned to immutable commit (L-08); tags are mutable
|
||||
GIT_TAG v3.4.0
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(tomlplusplus)
|
||||
@@ -283,11 +278,7 @@ if(WIN32)
|
||||
set(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(CURL_STATICLIB ON CACHE BOOL "" FORCE)
|
||||
# Keep curl's own tests out, without switching off ObsidianDragonTests for every Windows build:
|
||||
# hide BUILD_TESTING behind a normal variable while curl configures, then restore our cache value
|
||||
# (curl's cmake_dependent_option(BUILD_TESTING ...) FORCE-writes the OFF it saw into the cache).
|
||||
set(_dragonx_build_testing "${BUILD_TESTING}")
|
||||
set(BUILD_TESTING OFF)
|
||||
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
||||
set(CURL_DISABLE_LDAP ON CACHE BOOL "" FORCE)
|
||||
set(CURL_DISABLE_LDAPS ON CACHE BOOL "" FORCE)
|
||||
set(CURL_USE_SCHANNEL ON CACHE BOOL "" FORCE) # Use Windows native SSL
|
||||
@@ -296,8 +287,6 @@ if(WIN32)
|
||||
set(CURL_ZLIB OFF CACHE BOOL "" FORCE)
|
||||
set(HTTP_ONLY ON CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(curl)
|
||||
unset(BUILD_TESTING)
|
||||
set(BUILD_TESTING "${_dragonx_build_testing}" CACHE BOOL "Build the testing tree." FORCE)
|
||||
set(CURL_LIBRARIES libcurl_static)
|
||||
set(CURL_INCLUDE_DIRS ${curl_SOURCE_DIR}/include ${curl_BINARY_DIR}/lib/curl)
|
||||
else()
|
||||
@@ -306,38 +295,6 @@ else()
|
||||
set(CURL_INCLUDE_DIRS ${CURL_INCLUDE_DIR})
|
||||
endif()
|
||||
|
||||
# libwebp - WebP decode (still + animated via WebPAnimDecoder). Built from source, static, decode-only
|
||||
# so Linux / mingw-Windows / macOS-osxcross all build it identically (the mingw/osx sysroots have no
|
||||
# webp). Encode tools are disabled to avoid pulling in libpng/zlib that the cross sysroots lack.
|
||||
message(STATUS "Fetching libwebp (decode-only, static)...")
|
||||
FetchContent_Declare(
|
||||
libwebp
|
||||
GIT_REPOSITORY https://github.com/webmproject/libwebp.git
|
||||
GIT_TAG 845d5476a866141ba35ac133f856fa62f0b7445f # v1.4.0 — pinned to immutable commit (L-08); tags are mutable
|
||||
GIT_SHALLOW TRUE
|
||||
# libwebp's cpu.cmake applies -mno-sse2/-mno-sse4.1 to its scalar reference DSP
|
||||
# files when it can't probe SSE support. Under a macOS universal build
|
||||
# (-arch arm64;x86_64) that probe fails, so the flags land on the x86_64 slice,
|
||||
# where -mno-sse2 disables _Float16 and breaks the SDK's <math.h>. Neutralize
|
||||
# those disable flags (SSE2 is x86_64 baseline). Portable + idempotent; a no-op
|
||||
# for single-arch Linux/Windows/x86_64 builds. See cmake/patch-libwebp-simd.cmake.
|
||||
PATCH_COMMAND ${CMAKE_COMMAND}
|
||||
-DCPU_CMAKE=<SOURCE_DIR>/cmake/cpu.cmake
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/patch-libwebp-simd.cmake
|
||||
)
|
||||
set(WEBP_LINK_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_ANIM_UTILS OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_CWEBP OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_DWEBP OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_GIF2WEBP OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_IMG2WEBP OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_VWEBP OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_WEBPINFO OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_LIBWEBPMUX OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_WEBPMUX OFF CACHE BOOL "" FORCE)
|
||||
set(WEBP_BUILD_EXTRAS OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(libwebp)
|
||||
|
||||
# libsodium - platform-specific
|
||||
# Search order per platform:
|
||||
# 1. Local pre-built in libs/libsodium{-mac,-win}/ (downloaded by scripts/fetch-libsodium.sh)
|
||||
@@ -410,12 +367,6 @@ set(IMGUI_SOURCES
|
||||
${IMGUI_DIR}/backends/imgui_impl_sdl3.cpp
|
||||
)
|
||||
|
||||
# The ImGui demo window is unreachable in the wallet, so compile imgui_demo.cpp to its empty stubs
|
||||
# (ShowDemoWindow/ShowStyleEditor/... stay defined; imgui.h declares them unconditionally, so this is ODR-safe).
|
||||
# Do NOT use IMGUI_DISABLE_DEBUG_TOOLS: it changes imgui.h declarations and removes duplicate-ID highlighting.
|
||||
# (Makefiles: a per-source define lands in the target's flags.make, so existing trees do one full app rebuild.)
|
||||
set_source_files_properties(${IMGUI_DIR}/imgui_demo.cpp PROPERTIES COMPILE_DEFINITIONS IMGUI_DISABLE_DEMO_WINDOWS)
|
||||
|
||||
set(IMGUI_HEADERS
|
||||
${IMGUI_DIR}/imgui.h
|
||||
${IMGUI_DIR}/imconfig.h
|
||||
@@ -432,35 +383,6 @@ else()
|
||||
list(APPEND IMGUI_HEADERS ${IMGUI_DIR}/backends/imgui_impl_opengl3.h)
|
||||
endif()
|
||||
|
||||
# Optional FreeType font loader — enables color-emoji rendering (COLR/CPAL Twemoji) when the chat
|
||||
# "color emoji" setting is on; otherwise the wallet falls back to the monochrome emoji subset.
|
||||
# - Native Linux/macOS: use the system FreeType via find_package.
|
||||
# - Windows (mingw cross): the toolchain ships no FreeType, so build.sh --win-release cross-builds a
|
||||
# static one (scripts/build-freetype-mingw.sh) and passes -DDRAGONX_MINGW_FREETYPE_PREFIX here.
|
||||
# - Other cross builds (osxcross) without FreeType: silently fall back to monochrome.
|
||||
set(DRAGONX_FREETYPE OFF)
|
||||
set(DRAGONX_FREETYPE_LIB "")
|
||||
set(DRAGONX_FREETYPE_INC "")
|
||||
if(DEFINED DRAGONX_MINGW_FREETYPE_PREFIX AND EXISTS "${DRAGONX_MINGW_FREETYPE_PREFIX}/lib/libfreetype.a")
|
||||
set(DRAGONX_FREETYPE ON)
|
||||
set(DRAGONX_FREETYPE_LIB "${DRAGONX_MINGW_FREETYPE_PREFIX}/lib/libfreetype.a")
|
||||
set(DRAGONX_FREETYPE_INC "${DRAGONX_MINGW_FREETYPE_PREFIX}/include/freetype2")
|
||||
message(STATUS "FreeType (mingw cross-built) found — chat color emoji enabled")
|
||||
elseif(NOT CMAKE_CROSSCOMPILING)
|
||||
find_package(Freetype QUIET)
|
||||
if(FREETYPE_FOUND)
|
||||
set(DRAGONX_FREETYPE ON)
|
||||
set(DRAGONX_FREETYPE_LIB Freetype::Freetype) # imported target carries include dirs
|
||||
message(STATUS "FreeType ${FREETYPE_VERSION_STRING} found — chat color emoji enabled")
|
||||
endif()
|
||||
endif()
|
||||
if(DRAGONX_FREETYPE)
|
||||
list(APPEND IMGUI_SOURCES ${IMGUI_DIR}/misc/freetype/imgui_freetype.cpp)
|
||||
list(APPEND IMGUI_HEADERS ${IMGUI_DIR}/misc/freetype/imgui_freetype.h)
|
||||
else()
|
||||
message(STATUS "FreeType not found — chat color emoji falls back to monochrome")
|
||||
endif()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# QR Code library (bundled)
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -482,24 +404,13 @@ set(APP_SOURCES
|
||||
src/app.cpp
|
||||
src/app_network.cpp
|
||||
src/app_security.cpp
|
||||
src/app_sweep.cpp
|
||||
src/app_wizard.cpp
|
||||
src/services/history_scan_policy.cpp
|
||||
src/services/network_refresh_service.cpp
|
||||
src/services/note_consolidation.cpp
|
||||
src/services/refresh_scheduler.cpp
|
||||
src/services/wallet_scan_governor.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/chat/chat_sensitive_vault.cpp
|
||||
src/wallet/lite_owned_string.cpp
|
||||
src/wallet/lite_rollout_policy.cpp
|
||||
src/wallet/lite_client_bridge.cpp
|
||||
@@ -512,11 +423,10 @@ set(APP_SOURCES
|
||||
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_server_lifecycle_readiness.cpp
|
||||
src/wallet/lite_wallet_lifecycle_service.cpp
|
||||
src/data/wallet_state.cpp
|
||||
src/data/transaction_csv.cpp
|
||||
src/data/transaction_history_cache.cpp
|
||||
src/data/transaction_history_store_policy.cpp
|
||||
src/ui/theme.cpp
|
||||
src/ui/theme_loader.cpp
|
||||
src/ui/explorer/explorer_block_cache.cpp
|
||||
@@ -542,47 +452,40 @@ set(APP_SOURCES
|
||||
src/ui/windows/mining_tab_helpers.cpp
|
||||
src/ui/windows/peers_tab.cpp
|
||||
src/ui/windows/network_tab.cpp
|
||||
src/ui/windows/lite_console_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
|
||||
src/ui/windows/faq_dialog.cpp
|
||||
src/ui/windows/faq_content.cpp
|
||||
src/ui/windows/key_export_dialog.cpp
|
||||
src/ui/windows/transaction_details_dialog.cpp
|
||||
src/ui/windows/qr_popup_dialog.cpp
|
||||
src/ui/windows/validate_address_dialog.cpp
|
||||
src/ui/windows/contacts_tab.cpp
|
||||
src/ui/windows/chat_tab.cpp
|
||||
src/ui/windows/address_book_dialog.cpp
|
||||
src/ui/windows/shield_dialog.cpp
|
||||
src/ui/windows/request_payment_dialog.cpp
|
||||
src/ui/windows/block_info_dialog.cpp
|
||||
src/ui/windows/import_key_dialog.cpp
|
||||
src/ui/windows/export_all_keys_dialog.cpp
|
||||
src/ui/windows/export_transactions_dialog.cpp
|
||||
src/ui/windows/backup_wallet_dialog.cpp
|
||||
src/ui/widgets/qr_code.cpp
|
||||
src/rpc/rpc_client.cpp
|
||||
src/rpc/rpc_worker.cpp
|
||||
src/rpc/connection.cpp
|
||||
src/config/settings.cpp
|
||||
src/data/address_book.cpp
|
||||
src/data/wallet_index.cpp
|
||||
src/data/exchange_info.cpp
|
||||
src/util/logger.cpp
|
||||
src/util/async_task_manager.cpp
|
||||
src/util/coalescing_writer.cpp
|
||||
src/util/amount_format.cpp
|
||||
src/util/address_validation.cpp
|
||||
src/util/seed_phrase.cpp
|
||||
src/util/base64.cpp
|
||||
src/util/single_instance.cpp
|
||||
src/util/i18n.cpp
|
||||
@@ -590,23 +493,15 @@ set(APP_SOURCES
|
||||
src/util/platform.cpp
|
||||
src/util/payment_uri.cpp
|
||||
src/util/texture_loader.cpp
|
||||
src/util/image_decode_core.cpp
|
||||
src/util/svg_texture.cpp
|
||||
src/util/noise_texture.cpp
|
||||
src/daemon/embedded_daemon.cpp
|
||||
src/daemon/seed_wallet_creator.cpp
|
||||
src/daemon/daemon_controller.cpp
|
||||
src/daemon/lifecycle_adapters.cpp
|
||||
src/daemon/xmrig_manager.cpp
|
||||
src/util/bootstrap.cpp
|
||||
src/util/lite_server_probe.cpp
|
||||
src/util/pool_registry_core.cpp
|
||||
src/util/pool_stats_service.cpp
|
||||
src/util/http_download.cpp
|
||||
src/util/xmrig_updater.cpp
|
||||
src/util/xmrig_updater_core.cpp
|
||||
src/util/daemon_updater.cpp
|
||||
src/util/daemon_updater_core.cpp
|
||||
src/util/secure_vault.cpp
|
||||
src/ui/effects/framebuffer.cpp
|
||||
src/ui/effects/blur_shader.cpp
|
||||
@@ -637,15 +532,8 @@ endif()
|
||||
|
||||
set(APP_HEADERS
|
||||
src/app.h
|
||||
src/services/history_scan_policy.h
|
||||
src/services/network_refresh_service.h
|
||||
src/services/refresh_scheduler.h
|
||||
src/services/wallet_scan_governor.h
|
||||
src/services/connect_scan_gate.h
|
||||
src/services/scan_policy.h
|
||||
src/services/chat_scan_policy.h
|
||||
src/services/mining_thread_commit.h
|
||||
src/services/lowest_block_search.h
|
||||
src/services/wallet_security_controller.h
|
||||
src/services/wallet_security_workflow.h
|
||||
src/services/wallet_security_workflow_executor.h
|
||||
@@ -661,20 +549,12 @@ set(APP_HEADERS
|
||||
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_server_lifecycle_readiness.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/chat/chat_sensitive_vault.h
|
||||
src/config/version.h
|
||||
src/data/wallet_state.h
|
||||
src/data/transaction_history_cache.h
|
||||
src/data/transaction_history_store_policy.h
|
||||
src/ui/theme.h
|
||||
src/ui/theme_loader.h
|
||||
src/ui/explorer/explorer_block_cache.h
|
||||
@@ -683,8 +563,6 @@ set(APP_HEADERS
|
||||
src/ui/windows/balance_tab.h
|
||||
src/ui/windows/balance_address_list.h
|
||||
src/ui/windows/balance_recent_tx.h
|
||||
src/ui/windows/balance_lerp.h
|
||||
src/ui/windows/chat_thread_layout.h
|
||||
src/ui/windows/balance_tab_helpers.h
|
||||
src/ui/windows/send_tab.h
|
||||
src/ui/windows/receive_tab.h
|
||||
@@ -696,47 +574,35 @@ set(APP_HEADERS
|
||||
src/ui/windows/peers_tab.h
|
||||
src/ui/windows/explorer_tab.h
|
||||
src/ui/windows/market_tab.h
|
||||
src/ui/windows/console_channel.h
|
||||
src/ui/windows/console_command_reference.h
|
||||
src/ui/windows/console_input_model.h
|
||||
src/ui/windows/console_model.h
|
||||
src/ui/windows/console_output_model.h
|
||||
src/ui/windows/console_scroll_controller.h
|
||||
src/ui/windows/console_selection_controller.h
|
||||
src/ui/windows/console_tab.h
|
||||
src/ui/windows/console_tab_helpers.h
|
||||
src/ui/windows/settings_window.h
|
||||
src/ui/windows/about_dialog.h
|
||||
src/ui/windows/faq_dialog.h
|
||||
src/ui/windows/faq_content.h
|
||||
src/ui/windows/key_export_dialog.h
|
||||
src/ui/windows/transaction_details_dialog.h
|
||||
src/ui/windows/qr_popup_dialog.h
|
||||
src/ui/windows/validate_address_dialog.h
|
||||
src/ui/windows/contacts_tab.h
|
||||
src/ui/windows/chat_tab.h
|
||||
src/ui/windows/contact_picker.h
|
||||
src/ui/windows/address_book_dialog.h
|
||||
src/ui/windows/shield_dialog.h
|
||||
src/ui/windows/request_payment_dialog.h
|
||||
src/ui/windows/block_info_dialog.h
|
||||
src/ui/windows/import_key_dialog.h
|
||||
src/ui/windows/export_all_keys_dialog.h
|
||||
src/ui/windows/export_transactions_dialog.h
|
||||
src/ui/windows/backup_wallet_dialog.h
|
||||
src/ui/widgets/qr_code.h
|
||||
src/rpc/rpc_client.h
|
||||
src/rpc/rpc_worker.h
|
||||
src/rpc/connection.h
|
||||
src/rpc/long_call_lane.h
|
||||
src/rpc/types.h
|
||||
src/config/settings.h
|
||||
src/data/address_book.h
|
||||
src/data/exchange_info.h
|
||||
src/util/logger.h
|
||||
src/util/async_task_manager.h
|
||||
src/util/serial_job_lane.h
|
||||
src/util/fetch_backoff.h
|
||||
src/util/coalescing_writer.h
|
||||
src/util/local_time.h
|
||||
src/util/background_sampler.h
|
||||
src/util/text_fit.h
|
||||
src/util/amount_format.h
|
||||
src/util/base64.h
|
||||
src/util/single_instance.h
|
||||
@@ -745,7 +611,6 @@ set(APP_HEADERS
|
||||
src/util/payment_uri.h
|
||||
src/util/secure_vault.h
|
||||
src/daemon/embedded_daemon.h
|
||||
src/daemon/seed_wallet_creator.h
|
||||
src/daemon/daemon_controller.h
|
||||
src/daemon/lifecycle_adapters.h
|
||||
src/daemon/xmrig_manager.h
|
||||
@@ -812,29 +677,14 @@ set_source_files_properties(
|
||||
"${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-R.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/UbuntuMono-R.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/NotoEmoji-Subset.ttf;\
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/TwemojiMozilla-Color.ttf"
|
||||
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
|
||||
)
|
||||
|
||||
# Embed ui.toml + default_banlist via INCBIN (same pattern as fonts) - zero configure cost, no
|
||||
# generated header, so editing them re-assembles one tiny TU instead of recompiling every includer.
|
||||
configure_file(
|
||||
${CMAKE_SOURCE_DIR}/src/embedded/embedded_text.cpp.in
|
||||
${CMAKE_BINARY_DIR}/generated/embedded_text.cpp
|
||||
@ONLY)
|
||||
set_source_files_properties(
|
||||
${CMAKE_BINARY_DIR}/generated/embedded_text.cpp
|
||||
PROPERTIES OBJECT_DEPENDS
|
||||
"${CMAKE_SOURCE_DIR}/res/themes/ui.toml;${CMAKE_SOURCE_DIR}/res/default_banlist.txt")
|
||||
|
||||
add_executable(ObsidianDragon
|
||||
${APP_SOURCES}
|
||||
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
|
||||
${CMAKE_BINARY_DIR}/generated/embedded_text.cpp
|
||||
${IMGUI_SOURCES}
|
||||
${QRCODE_SOURCES}
|
||||
${GLAD_SOURCES}
|
||||
@@ -857,7 +707,6 @@ target_include_directories(ObsidianDragon PRIVATE
|
||||
${GLAD_INCLUDE}
|
||||
${CURL_INCLUDE_DIRS}
|
||||
${MINIZ_DIR}
|
||||
${libwebp_SOURCE_DIR}/src # <webp/decode.h>, <webp/demux.h> (FetchContent build tree)
|
||||
)
|
||||
|
||||
target_link_libraries(ObsidianDragon PRIVATE
|
||||
@@ -867,8 +716,6 @@ target_link_libraries(ObsidianDragon PRIVATE
|
||||
sqlite3_amalgamation
|
||||
${CURL_LIBRARIES}
|
||||
${SODIUM_LIBRARY}
|
||||
webp
|
||||
webpdemux # WebPAnimDecoder (animated WebP); transitively pulls in webp + sharpyuv
|
||||
)
|
||||
|
||||
if(DRAGONX_LITE_BACKEND_READY)
|
||||
@@ -897,30 +744,6 @@ if(DRAGONX_LITE_BACKEND_READY)
|
||||
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
|
||||
@@ -955,8 +778,6 @@ target_compile_definitions(ObsidianDragon PRIVATE
|
||||
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}>
|
||||
DRAGONX_EMBED_LANG=$<BOOL:${DRAGONX_EMBED_LANG}>
|
||||
DRAGONX_ENABLE_UI_SWEEP=$<BOOL:${DRAGONX_ENABLE_UI_SWEEP}>
|
||||
)
|
||||
if(WIN32)
|
||||
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11)
|
||||
@@ -964,21 +785,9 @@ else()
|
||||
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAS_GLAD)
|
||||
endif()
|
||||
|
||||
# Color-emoji font loader (FreeType) — linked + flagged only when found (see DRAGONX_FREETYPE above).
|
||||
if(DRAGONX_FREETYPE)
|
||||
target_link_libraries(ObsidianDragon PRIVATE ${DRAGONX_FREETYPE_LIB})
|
||||
if(DRAGONX_FREETYPE_INC)
|
||||
target_include_directories(ObsidianDragon PRIVATE ${DRAGONX_FREETYPE_INC})
|
||||
endif()
|
||||
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAVE_FREETYPE)
|
||||
endif()
|
||||
|
||||
# Dev-only HushChat fixture CLI (nothing runs it; not linked into the app or tests). Not part of `all`:
|
||||
# cmake --build build/linux --target HushChatFixtureCheck (output: build/linux/bin/HushChatFixtureCheck)
|
||||
add_executable(HushChatFixtureCheck EXCLUDE_FROM_ALL
|
||||
add_executable(HushChatFixtureCheck
|
||||
tools/hushchat_fixture_check.cpp
|
||||
src/chat/chat_protocol.cpp
|
||||
src/chat/chat_fixture_tooling.cpp
|
||||
)
|
||||
|
||||
target_include_directories(HushChatFixtureCheck PRIVATE
|
||||
@@ -995,26 +804,6 @@ target_compile_definitions(HushChatFixtureCheck PRIVATE
|
||||
DRAGONX_ENABLE_CHAT=0
|
||||
)
|
||||
|
||||
# Regtest chat smoke driver (dev tool; never shipped). Links the real chat library so a runner
|
||||
# script can verify a chunked message + Control memo round-trip through a live regtest dragonxd.
|
||||
# cmake --build build/linux --target chat_regtest_smoke
|
||||
add_executable(chat_regtest_smoke EXCLUDE_FROM_ALL
|
||||
tools/chat_regtest_smoke.cpp
|
||||
src/chat/chat_crypto.cpp
|
||||
src/chat/chat_identity.cpp
|
||||
src/chat/chat_outgoing.cpp
|
||||
src/chat/chat_protocol.cpp
|
||||
)
|
||||
target_include_directories(chat_regtest_smoke PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${SODIUM_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(chat_regtest_smoke PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
${SODIUM_LIBRARY}
|
||||
)
|
||||
target_compile_definitions(chat_regtest_smoke PRIVATE DRAGONX_ENABLE_CHAT=1)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Copy resources
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1038,11 +827,9 @@ 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)
|
||||
if(DRAGONX_EMBED_LANG)
|
||||
find_program(XXD_EXECUTABLE NAMES xxd)
|
||||
endif()
|
||||
if(DRAGONX_EMBED_LANG AND NOT XXD_EXECUTABLE)
|
||||
message(FATAL_ERROR "DRAGONX_EMBED_LANG is ON but xxd was not found; it generates build/generated/embedded/lang_*.h. Install xxd or configure with -DDRAGONX_EMBED_LANG=OFF")
|
||||
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)
|
||||
|
||||
@@ -1059,7 +846,7 @@ if(LANG_FILES)
|
||||
list(APPEND LANG_OUTPUTS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME})
|
||||
|
||||
# Also regenerate the embedded header so the binary always has fresh translations
|
||||
if(DRAGONX_EMBED_LANG AND XXD_EXECUTABLE)
|
||||
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(
|
||||
@@ -1077,6 +864,20 @@ if(LANG_FILES)
|
||||
message(STATUS " Language files: ${LANG_FILES}")
|
||||
endif()
|
||||
|
||||
# Embed ui.toml into the binary so it's always available at runtime
|
||||
include(${CMAKE_SOURCE_DIR}/cmake/EmbedResources.cmake)
|
||||
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/generated)
|
||||
embed_resource(
|
||||
${CMAKE_SOURCE_DIR}/res/themes/ui.toml
|
||||
${CMAKE_BINARY_DIR}/generated/ui_toml_embedded.h
|
||||
ui_toml
|
||||
)
|
||||
embed_resource(
|
||||
${CMAKE_SOURCE_DIR}/res/default_banlist.txt
|
||||
${CMAKE_BINARY_DIR}/generated/default_banlist_embedded.h
|
||||
default_banlist
|
||||
)
|
||||
|
||||
# Note: xmrig is embedded via build.sh (embedded_data.h) for Windows builds,
|
||||
# following the same pattern as daemon embedding.
|
||||
|
||||
@@ -1157,73 +958,6 @@ install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res
|
||||
OPTIONAL
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# dragonx-wallet-rebuild — offline recovery helper for a BDB-inconsistent wallet.dat.
|
||||
# Bundled next to the daemon; the app spawns it out-of-process. It is the ONLY thing that links
|
||||
# Berkeley DB, so the AGPLv3 BDB never contaminates the GPLv3 GUI (same boundary as the daemon).
|
||||
# Release builds should point DRAGONX_BDB_ROOT at the vendored static libdb (external/dragonx/depends);
|
||||
# a dev build falls back to the system Berkeley DB. Skipped (with a note) if no BDB is found.
|
||||
# -----------------------------------------------------------------------------
|
||||
find_path(BDB_INCLUDE_DIR db.h HINTS ${DRAGONX_BDB_ROOT}/include /usr/include /usr/local/include)
|
||||
find_library(BDB_LIBRARY NAMES db-6.2 db-6.0 db-5.3 db libdb
|
||||
HINTS ${DRAGONX_BDB_ROOT}/lib /usr/lib /usr/local/lib /usr/lib/x86_64-linux-gnu)
|
||||
if(BDB_INCLUDE_DIR AND BDB_LIBRARY)
|
||||
add_executable(dragonx-wallet-rebuild tools/wallet_rebuild/main.cpp)
|
||||
target_include_directories(dragonx-wallet-rebuild PRIVATE ${CMAKE_SOURCE_DIR}/src ${BDB_INCLUDE_DIR})
|
||||
target_link_libraries(dragonx-wallet-rebuild PRIVATE ${BDB_LIBRARY})
|
||||
if(WIN32)
|
||||
target_link_libraries(dragonx-wallet-rebuild PRIVATE ws2_32) # static libdb-6.2 pulls in winsock
|
||||
else()
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(dragonx-wallet-rebuild PRIVATE Threads::Threads ${CMAKE_DL_LIBS}) # static libdb needs pthread/dl
|
||||
endif()
|
||||
set_target_properties(dragonx-wallet-rebuild PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||
message(STATUS "wallet-rebuild helper: ON (Berkeley DB ${BDB_LIBRARY})")
|
||||
else()
|
||||
message(STATUS "wallet-rebuild helper: OFF (no Berkeley DB found; set DRAGONX_BDB_ROOT for release builds)")
|
||||
endif()
|
||||
|
||||
# Release packaging (build.sh --linux-release / --win-release / --mac-release) builds ONLY what ships:
|
||||
# cmake --build . --target dragonx_release
|
||||
# The test binary, the lite smoke tools and the dev CLIs are skipped WITHOUT writing BUILD_TESTING=OFF into the
|
||||
# build/<platform> cache that dev builds share. A stale OFF would leave ctest running an old test binary.
|
||||
# If a release path starts copying a new CMake-built file out of bin/, add its target here.
|
||||
add_custom_target(dragonx_release)
|
||||
add_dependencies(dragonx_release ObsidianDragon)
|
||||
foreach(_dragonx_release_dep dragonx-wallet-rebuild copy_langs copy_themes)
|
||||
if(TARGET ${_dragonx_release_dep})
|
||||
add_dependencies(dragonx_release ${_dragonx_release_dep})
|
||||
endif()
|
||||
endforeach()
|
||||
# Backstop for the UI sweep dev tool: a tree whose cache says ON (e.g. hand-configured) cannot package a release.
|
||||
if(DRAGONX_ENABLE_UI_SWEEP)
|
||||
add_custom_target(dragonx_release_sweep_guard
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "DRAGONX_ENABLE_UI_SWEEP=ON: refusing to package a release"
|
||||
COMMAND ${CMAKE_COMMAND} -E false)
|
||||
add_dependencies(dragonx_release dragonx_release_sweep_guard)
|
||||
endif()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# fake_dragonxd — a deliberately slow, scriptable dragonxd stand-in that reproduces the daemon-stop
|
||||
# incident (usage and the incident it models: tools/fake_dragonxd/README.md).
|
||||
# Test tooling only: OFF by default, never packaged, and written to <build>/fake_dragonxd/ so its
|
||||
# "dragonxd" name can't shadow the real bin/dragonxd.
|
||||
# -----------------------------------------------------------------------------
|
||||
if(DRAGONX_BUILD_FAKE_DAEMON)
|
||||
add_executable(fake_dragonxd tools/fake_dragonxd/fake_dragonxd.cpp)
|
||||
target_link_libraries(fake_dragonxd PRIVATE nlohmann_json::nlohmann_json)
|
||||
if(WIN32)
|
||||
target_link_libraries(fake_dragonxd PRIVATE ws2_32)
|
||||
else()
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(fake_dragonxd PRIVATE Threads::Threads)
|
||||
endif()
|
||||
set_target_properties(fake_dragonxd PROPERTIES
|
||||
OUTPUT_NAME dragonxd
|
||||
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/fake_dragonxd)
|
||||
message(STATUS "fake_dragonxd test stub: ON (${CMAKE_BINARY_DIR}/fake_dragonxd/)")
|
||||
endif()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Tests
|
||||
# -----------------------------------------------------------------------------
|
||||
@@ -1231,22 +965,12 @@ endif()
|
||||
if(BUILD_TESTING)
|
||||
add_executable(ObsidianDragonTests
|
||||
tests/test_phase4.cpp
|
||||
src/services/history_scan_policy.cpp
|
||||
src/services/network_refresh_service.cpp
|
||||
src/services/note_consolidation.cpp
|
||||
src/services/refresh_scheduler.cpp
|
||||
src/services/wallet_scan_governor.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/chat/chat_sensitive_vault.cpp
|
||||
src/wallet/lite_owned_string.cpp
|
||||
src/wallet/lite_rollout_policy.cpp
|
||||
src/wallet/lite_client_bridge.cpp
|
||||
@@ -1259,54 +983,34 @@ if(BUILD_TESTING)
|
||||
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_server_lifecycle_readiness.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/seed_phrase.cpp
|
||||
src/util/i18n.cpp
|
||||
src/util/text_format.cpp
|
||||
src/util/image_decode_core.cpp
|
||||
src/data/wallet_state.cpp
|
||||
src/data/transaction_csv.cpp
|
||||
src/data/transaction_history_cache.cpp
|
||||
src/data/transaction_history_store_policy.cpp
|
||||
src/data/address_book.cpp
|
||||
src/data/wallet_index.cpp
|
||||
src/daemon/lifecycle_adapters.cpp
|
||||
src/daemon/embedded_daemon.cpp
|
||||
src/rpc/connection.cpp
|
||||
src/rpc/rpc_client.cpp
|
||||
src/util/base64.cpp
|
||||
src/config/settings.cpp
|
||||
src/resources/embedded_resources.cpp
|
||||
src/util/secure_vault.cpp
|
||||
src/util/platform.cpp
|
||||
${CMAKE_BINARY_DIR}/generated/embedded_text.cpp
|
||||
src/util/coalescing_writer.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
|
||||
src/util/bootstrap.cpp
|
||||
src/util/single_instance.cpp
|
||||
${MINIZ_SOURCES}
|
||||
)
|
||||
|
||||
@@ -1319,7 +1023,6 @@ if(BUILD_TESTING)
|
||||
${SODIUM_INCLUDE_DIR}
|
||||
${CURL_INCLUDE_DIRS}
|
||||
${MINIZ_DIR}
|
||||
${libwebp_SOURCE_DIR}/src # <webp/decode.h>, <webp/demux.h> for image_decode_core.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(ObsidianDragonTests PRIVATE
|
||||
@@ -1327,8 +1030,6 @@ if(BUILD_TESTING)
|
||||
sqlite3_amalgamation
|
||||
${SODIUM_LIBRARY}
|
||||
${CURL_LIBRARIES}
|
||||
webp
|
||||
webpdemux
|
||||
)
|
||||
|
||||
target_compile_definitions(ObsidianDragonTests PRIVATE
|
||||
@@ -1336,8 +1037,6 @@ if(BUILD_TESTING)
|
||||
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_EMBED_LANG=$<BOOL:${DRAGONX_EMBED_LANG}>
|
||||
DRAGONX_ENABLE_UI_SWEEP=$<BOOL:${DRAGONX_ENABLE_UI_SWEEP}>
|
||||
DRAGONX_TEST_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures"
|
||||
)
|
||||
|
||||
@@ -1348,13 +1047,6 @@ if(BUILD_TESTING)
|
||||
if(UNIX)
|
||||
target_link_libraries(ObsidianDragonTests PRIVATE ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(ObsidianDragonTests PRIVATE ws2_32 crypt32 wldap32 psapi iphlpapi shell32 ole32 advapi32 bcrypt)
|
||||
endif()
|
||||
# i18n.cpp includes the generated embedded/lang_*.h headers; a tree that builds only this target needs them too.
|
||||
if(TARGET copy_langs)
|
||||
add_dependencies(ObsidianDragonTests copy_langs)
|
||||
endif()
|
||||
|
||||
add_test(NAME ObsidianDragonPhase4Tests COMMAND ObsidianDragonTests)
|
||||
endif()
|
||||
@@ -1377,5 +1069,5 @@ message(STATUS " Lite backend: ${DRAGONX_LITE_BACKEND_READY}")
|
||||
message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}")
|
||||
message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
|
||||
message(STATUS " Lite manifest: ${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||
message(STATUS " Lite trust: built-from-source (vendored third_party/silentdragonxlite)")
|
||||
message(STATUS " Lite signature: ${DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE}")
|
||||
message(STATUS "")
|
||||
|
||||
@@ -81,8 +81,8 @@ Download linux and windows binaries of latest releases and place in binary direc
|
||||
- prebuilt-binaries/dragonxd-win/
|
||||
- prebuilt-binaries/dragonxd-mac/
|
||||
|
||||
**DRG-XMRig fork** (https://git.dragonx.is/DragonX/drg-xmrig):
|
||||
- prebuilt-binaries/drg-xmrig/
|
||||
**xmrig HAC fork** (https://git.dragonx.is/dragonx/xmrig-hac):
|
||||
- prebuilt-binaries/xmrig-hac/
|
||||
|
||||
|
||||
## Build Steps
|
||||
|
||||
@@ -1,432 +0,0 @@
|
||||
# Sent-history sweep: GUI watermark fix, then daemon follow-up
|
||||
|
||||
**Audience:** ObsidianDragon (GUI) team and the DragonX daemon maintainer.
|
||||
**Date:** 2026-10-16. **Status:** proposal.
|
||||
|
||||
**Code cited**
|
||||
- GUI: `ObsidianDragon` @ `064c9d1` (unprefixed paths).
|
||||
- Daemon: `dragonx` @ `e9247f5c6` (`perf/history-rpc-copies`), paths prefixed `dx:`. `test/v1.3.1-candidate` @ `ff28e34c8` is the same plus a crash fix; on that branch, `dx:wallet.cpp` lines past ~1263 shift by +10.
|
||||
|
||||
Anything not marked *(inference)* or *(estimate)* was verified in the code or measured.
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem
|
||||
|
||||
**The wallet measured:** an isolated copy of the owner's mainnet wallet, run in a network namespace with no peers.
|
||||
- 16,206 Sapling notes, all unspent.
|
||||
- 5 z-addresses and 11,889 wallet txs.
|
||||
- Unencrypted; `wallet.dat` is about 2.1 GB.
|
||||
|
||||
| Call | v1.3.0 | After daemon PR-A+B+C |
|
||||
|---|---|---|
|
||||
| Balance RPCs | ~14 s | ~50 ms |
|
||||
| Windowed `z_listsentbyaddress` (filter type 2) | 19.5 s | 128 ms |
|
||||
| **Full `z_listsentbyaddress` (no watermark)** | **424 s**; RSS 8 → 19.4 GB, never released | **113 s** |
|
||||
| Full-sweep reply | 26.2 MB (all 11,889 txs) | same |
|
||||
|
||||
### Why every launch pays for the full sweep
|
||||
|
||||
- **The watermark lives only in memory.** `history_sent_scan_height_` starts at `-1` (`src/app.h:1764`).
|
||||
- **Every disconnect resets it to `-1`.** That covers daemon restarts, reconnects and wallet switches. The path is `onDisconnected` → `resetTransactionHistoryCacheSession` → `invalidateShieldedHistoryScanProgress(false)` (`src/app_network.cpp:936`, `:1847`, `:1889-1895`).
|
||||
- **Other resets:**
|
||||
- a locked-wallet connect;
|
||||
- imports (`:4013`);
|
||||
- rescans (`:7279`, `:7302`; `src/app.cpp:6298`) and zap (`app.cpp:6352`);
|
||||
- a new, never-scanned z-address (`app_network.cpp:2593-2597`);
|
||||
- the end of a warmup during which a rescan was observed (`:2186-2195`).
|
||||
- **No watermark means a full sweep.** `sentSweepIsFull` is then true (`src/services/history_scan_policy.cpp:35-38`). The GUI sends `[z,0]` via `callLong(..., kFullSentSweepTimeoutSec = 120)` (`src/services/network_refresh_service.cpp:1324-1325`, `history_scan_policy.h:31`).
|
||||
- **The daemon ignores the address argument.** It walks every wallet tx under `LOCK2(cs_main, cs_wallet)` and calls `zsWalletTxJSON(wtx, ret, "*", false, 0, …)` for each one (`dx:src/wallet/rpcwallet.cpp:3719-3746`; the call is at `:3743`).
|
||||
- **The full sweep barely fits, or never fits, the timeout.**
|
||||
- On the fixed daemon the sweep finishes only **7 s inside** the 120 s timeout.
|
||||
- On v1.3.0 it never finishes. Failures back off 10 min → 1 h → 6 h (`fullSentSweepRetryDelaySec`). That backoff is also kept only in memory (`app.h:1765-1766`).
|
||||
|
||||
### What the GUI uses from the reply
|
||||
|
||||
- **Sweep consumer** (`network_refresh_service.cpp:1328-1332`, `appendSentTransparentOutputs` ~`:1076-1123`):
|
||||
- It reads only `txid`, `confirmations`, `blocktime`/`time` and `sends.transparentSends`.
|
||||
- It never reads `received` (20.8 MB of the 26.2 MB) or `spends`.
|
||||
- **Shielded sends come from enrichment.** Every txid goes into `knownTxids` for `z_viewtransaction` enrichment, capped at 25 per cycle (`app.h:1753`). The rest wait in the in-memory `history_enrich_backlog_`.
|
||||
- **One exception:** `locateSweepTxid` (seed-migration resume) does read `sends.saplingSends` (`app_network.cpp:6394`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Part 1: GUI fix (persist the watermark)
|
||||
|
||||
**Goal:** a relaunch, reconnect or daemon restart runs only the windowed sweep, `[z,0,2,(tip-W)+margin]`. A full sweep then happens only in three cases:
|
||||
- there is no persisted state;
|
||||
- the persisted state fails verification;
|
||||
- the periodic safety-net re-sweep is due.
|
||||
|
||||
### 2.1 Where to store it
|
||||
|
||||
**Use the existing encrypted history snapshot.** Store an optional `sent_sweep` object in its JSON payload, not in `settings.json` or `wallets.json`. Why:
|
||||
- The watermark is only valid together with the rows it produced. Both are written in one `INSERT OR REPLACE` in WAL mode (`src/data/transaction_history_cache.cpp:438-457`), so a crash leaves the previous pair intact.
|
||||
- The snapshot already provides what the watermark needs:
|
||||
- per-wallet keying (BLAKE2b of the sorted z+t address set, `:157-186`);
|
||||
- encryption (Argon2id from the passphrase, or salted BLAKE2b when the wallet is unencrypted);
|
||||
- a tip check on load (`:338-344`);
|
||||
- one-wallet pruning (`pruneOtherWallets`, `:655-672`).
|
||||
- **Keep `kSchemaVersion = 1`.** `load()` rejects any other version (`:351`), so a bump would throw away every user's cache.
|
||||
- Unknown keys are ignored, so older builds read new payloads safely.
|
||||
- A missing `sent_sweep` means `W = -1`, which is today's behaviour.
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `height` | int | `W`: the tip used for the sweep's `maxDepth`. Read in the same worker job, right before the call |
|
||||
| `anchor_height` | int | `A = W - margin`: the bottom edge of the window the sweep covered |
|
||||
| `anchor_hash` | hex | `H_A = getblockhash(A)`, fetched by the worker after the sweep succeeds |
|
||||
| `backlog` | [txid] | `history_enrich_backlog_`. **Required**, see 2.4 |
|
||||
| `full_failures`, `full_retry_delay_s` | int | Full-sweep backoff. The delay is relative to save time, not absolute |
|
||||
| `daemon_id` | string | Daemon version + start time, from `getinfo` / uptime. If it changes, the restored backoff resets (2.5) |
|
||||
| `last_full_height`, `last_full_time` | int | Last successful full sweep; drives the safety net (2.6) |
|
||||
| `txcount`, `k` | int | Optional tripwire (2.3). Both captured **in the sweep's worker job** |
|
||||
|
||||
Do **not** reuse the snapshot's `tip_height`/`tip_hash` columns. They record the tip at store time, not at sweep time (`app_network.cpp:2069-2070`).
|
||||
|
||||
### 2.2 Code changes
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/data/transaction_history_cache.h:20-28`, `:33-48`, `:79-84` | Add a `SentSweepState` struct to `LoadResult`, `SnapshotJob` and `replace()` |
|
||||
| `src/data/transaction_history_cache.cpp:412-426` (`writeSnapshot`) | Emit `sent_sweep` only if `0 < A <= W <= job.tipHeight` |
|
||||
| `src/data/transaction_history_cache.cpp:351-370` (`load`) | Parse with defaults (`W=-1`, empty backlog). Drop the object if it has the wrong type or `W` is above the stored `tip_height` |
|
||||
| `src/data/transaction_history_store_policy.h:35-41` / `.cpp:62-113` | Add `W`/`A`/`H_A` to the `slow` signature. They move every block, so this means a write every 600 s and always on disconnect, lock and exit. Add the backlog to `content` (written within about 20 s). Otherwise an invalidation that only changes the watermark is skipped as "unchanged". Update both callers (`app_network.cpp:2012-2014`, `:2041-2045`). The epoch counter is never persisted, so it doesn't belong in a signature |
|
||||
| `src/app_network.cpp:2066-2079` (`storeTransactionHistoryCacheIfAvailable`) | Fill `job->sentSweep` from the `history_sent_*` and backlog members |
|
||||
| `src/app_network.cpp:1998-2017` (`loadTransactionHistoryCacheIfAvailable`) | Restore the members. `++history_sent_epoch_`. Set `history_sent_verify_pending_ = true`. Apply any **pending rescan action** (2.3) |
|
||||
| `src/app_network.cpp:2028` | The store returns early when there are no rows. On a `persistCache=true` invalidation with no rows, call `clearWallet()` so a stale W can't survive on disk |
|
||||
| `src/services/network_refresh_service.h:256-266`, `:299` | New snapshot/result fields: `anchorHeight`/`anchorHash` in, the verify result and `H_A` out, and the band cursor |
|
||||
| `src/services/network_refresh_service.cpp:1314-1345` | Read the tip immediately before the call. After success, fetch `getblockhash(A)`. Add the verify step (2.3) and the large-window path (2.5) |
|
||||
| `src/app_network.cpp:2784-2792` | Gate both the end-of-pass sweep and the **mid-pass** windowed sweep (`allowMidPassSentSweep`) on the pending verify. Extend the retry gate at `:2792`, which today applies only when `W < 0`, to large windows |
|
||||
| `src/app_network.cpp:2843-2854` | Count large-window failures toward the backoff; today only `sentSweepFull` failures count (`:2849`). Store the band cursor in `App` and reset it with `history_sent_epoch_`, like the watermark |
|
||||
| `src/app.cpp:restoreOriginalWallet` | Call `invalidateShieldedHistoryScanProgress(true)` before `onDisconnected`, as `rescanBlockchain` and zap already do (`app.cpp:6298`, `:6352`) |
|
||||
| `src/services/history_scan_policy.{h,cpp}` | Pure helpers: `sentSweepAnchor(W)`, `sentSweepNeedsLongCall(tip, W)`, `sentSweepMargin(firstAfterLoad)`, `nextSentSweepBand(...)` |
|
||||
|
||||
### 2.3 Invalidation and safety rules
|
||||
|
||||
**Verify at the bottom edge of the window, not at its tip.**
|
||||
- **The check:** before the first windowed sweep after a load, call `getblockhash(A)` and compare it with `H_A`.
|
||||
- **Match:** any fork point lies above `A`, so it is inside the window. Run the windowed sweep.
|
||||
- **Mismatch:** a reorg went deeper than the margin, or this is a different chain or datadir. Drop W and run the banded full sweep (2.5).
|
||||
- **Why not the tip:** checking `getblockhash(W)` would trigger a full sweep on an ordinary one-block reorg of the tip, even though the window already covers that reorg.
|
||||
|
||||
| Event | Action | Mechanism |
|
||||
|---|---|---|
|
||||
| First windowed sweep after a load | Anchor verify (above) | `getblockhash`: O(1), under `cs_main` |
|
||||
| Stored `tip_height` > current tip at load (replay after an unclean stop, older datadir) | **Today:** `load()` deletes the whole snapshot (`cache.cpp:338-344`, `clearWalletByHash` at `:341`). Rows and W are lost, and a full sweep runs | Keep as the default. **Optional improvement:** have `load()` keep the rows and `sent_sweep`, flag "ahead", and defer the sent sweep until `tip >= A`, then verify `H_A`. That requires changes in `sentSweepIsFull` (`history_scan_policy.cpp:37`: `W > tip` means full today), the gate at `app_network.cpp:2789-2792`, and the test at `tests/test_phase4.cpp:15831` |
|
||||
| Wallet switch, or an address added or imported while the GUI was closed | Nothing new | Different address set → different snapshot key → full sweep |
|
||||
| GUI-initiated rescan, zap, import, runtime rescan | Already `invalidate(true)`; with 2.2 it now reaches disk | `app.cpp:6298`, `:6352`; `app_network.cpp:4013`, `:7279`, `:7302` |
|
||||
| New never-scanned z-address during a session | Already resets W (`:2593-2597`); the reset must be persisted | Signature change in 2.2 |
|
||||
| Disconnect, daemon restart, same-wallet reconnect | Keep today's in-memory reset | The disk copy is written first (`:857`) and reloaded after connect |
|
||||
| **Rescan observed in the daemon log** (the daemon's catch-up "Rescanning last N blocks (from block H)", or an external `-rescan`) | **Sticky pending action** (see below) | The warmup-end invalidation at `:2186-2195` runs *before* the snapshot loads, so today it can't reach a persisted W |
|
||||
| Wallet changed outside the GUI with the same address set (Console `z_importviewingkey`, external `-rescan`, same-seed restore) | Optional tripwire: after the first windowed sweep returns R rows, run the banded full sweep if `txcount − txcount_saved > \|R\| − k + slack`. `k` is the rows of the saved sweep within the same `100 + E` depth band the first window uses | `getwalletinfo.txcount` = `mapWallet.size()` (`dx:rpcwallet.cpp:2791`). The GUI already fetches it on every connect (`network_refresh_service.cpp:251-252`, `:295-298`; `app_network.cpp:2140-2143`). Measured cost: ~8-20 ms warm, ~180 ms on the first call after a start |
|
||||
| Daemon version change | Keep W. Reset the persisted backoff | Same filter semantics since 2020; v1.3.0 and the candidate both report 1.3.0.50 |
|
||||
|
||||
**The sticky rescan signal.** Why it's needed:
|
||||
- During warmup no address list is fetched (`scan_policy.h:42-44`, `:64-68`; `app_network.cpp:2492`), and `state_.clear()` has emptied the rows (`wallet_state.h:406-409`).
|
||||
- So when `invalidate(true)` runs at `app_network.cpp:2195`, it finds an empty identity and returns (`:2028`, `:2032`).
|
||||
- The cache then loads afterwards (`:2608`) and would restore the old W unchanged.
|
||||
|
||||
The fix:
|
||||
1. Whenever `rescan_confirmed_active_` is set (`app.cpp:1193`, `:1324`, `:1397`), record a pending action that survives `resetTransactionHistoryCacheSession`:
|
||||
- **clamp** to `H`, parsed from "Rescanning last N blocks (from block H)" or "Rescanning from height H";
|
||||
- **full reset** if `H` is unknown or near genesis.
|
||||
2. `loadTransactionHistoryCacheIfAvailable` applies it to the loaded W (`W = min(W, H)`, and `A` follows) and to the receive-scan heights, then clears it.
|
||||
|
||||
With daemon item D4 (wallet locator on clean shutdown, 3.2) shipped, the daemon's catch-up rescan line disappears from normal starts.
|
||||
|
||||
**Reorg overlap.**
|
||||
- **Window extent.** Filter type 2 drops only txs deeper than N (`dx:rpcwallet.cpp:3740`), and `maxDepth = tip − W + margin`. So the window covers heights ≥ `W − margin + 1`, plus the mempool (depth 0 always passes).
|
||||
- **Margin.**
|
||||
- `MAX_REORG_LENGTH` defaults to 99 (`dx:src/main.h:79`), and a longer reorg shuts the node down (`dx:main.cpp:4314-4333`).
|
||||
- A margin of 100 therefore leaves 1 block of slack, which tip drift between reading the tip and making the call can use up.
|
||||
- Use **margin ≈ 120** in steady state *(recommendation)*, and read the tip in the same worker job as the call.
|
||||
- **First sweep after a load.** Use margin `120 + E` with E ≈ 1000 blocks (~10 h).
|
||||
- This absorbs wallet-notifier lag at save time: `ThreadNotifyWallets` applies blocks asynchronously (`dx:src/validationinterface.cpp:89-262`), and no RPC exposes the wallet's processed tip.
|
||||
- E is an inferred bound, not a measurement.
|
||||
- **Large `-maxreorg`.** A `-maxreorg` set above the margin is caught by the anchor check.
|
||||
- **Conflicted txs.** Conflicted txs (depth −1) are dropped by both the full and the windowed sweep (`dx:rpcwallet.cpp:3725`), so nothing changes there.
|
||||
|
||||
**Locked-wallet rule.**
|
||||
- **The rule:** never advance or persist W from a sweep dispatched or completed while the wallet is locked, and never load or store the snapshot while it is locked.
|
||||
- **What already protects this today:**
|
||||
- History scans don't run while locked (`src/services/scan_policy.h:48-51`).
|
||||
- An encrypted wallet's snapshot opens only after unlock (`app_network.cpp:797-798`, `:1944-1962`).
|
||||
- Locking keeps W and stores immediately (`:1851-1859`).
|
||||
- **What breaks if it's ever violated:** a locked daemon wallet returns empty `saplingReceived` and `saplingSends`, because `GetSaplingHistoryKeys` needs spending keys (`dx:rpchushwallet.cpp:19-39`).
|
||||
- The fields the main consumer reads (txid, `transparentSends`) don't depend on the lock.
|
||||
- `locateSweepTxid` reads `saplingSends`, so it must never run on a locked wallet.
|
||||
|
||||
### 2.4 Backlog rule (required)
|
||||
|
||||
- **The risk.** Enrichment is capped at 25 per cycle, and the txids that miss the cap go to `history_enrich_backlog_` (`app_network.cpp:2843`).
|
||||
- A pure z→z send leaves only a change note, which produces no receive row (`network_refresh_service.cpp:972`).
|
||||
- So its txid exists **only** in a sweep result.
|
||||
- If W is persisted while those txids sit in the backlog, the sends are lost for good once the window moves past them.
|
||||
- **The rule:** persist the backlog together with W.
|
||||
- **Rejected fallback:** "persist `W = -1` while the backlog is non-empty" isn't enough on its own. Enrichment retries are unbounded (`network_refresh_service.cpp:1521`), so one txid that keeps failing would stop W from ever being persisted. If that fallback is wanted as well, cap the retries per txid.
|
||||
|
||||
### 2.5 Timeouts, large windows, banded full sweep
|
||||
|
||||
**Large windows.**
|
||||
- The windowed sweep uses `rpc.call`, which has curl's 30 s timeout (`src/rpc/rpc_client.cpp:223`). Only full-sweep failures count toward the backoff (`app_network.cpp:2846-2853`).
|
||||
- So a watermark saved days ago produces a huge "windowed" call that times out on every block, while the daemon keeps working on each abandoned call.
|
||||
- Rule: if `tip − W > 2400` (about 1 day of 36 s blocks), use `callLong` and count failures the same way as for a full sweep.
|
||||
|
||||
**Banded full sweep.**
|
||||
- **The call:** replace the single 113 s (fixed daemon) or 424 s (v1.3.0) call with depth bands, `z_listsentbyaddress(z, minconf=a, 2, maxDepth=b)`. `minconf` (`dx:rpcwallet.cpp:3732`) and filter type 2 (`:3740`) are independent filters, so each call returns exactly the depth band `[a, b]`.
|
||||
- **Order:**
|
||||
- Go newest band first; the first band uses `minconf=0` so it includes the mempool.
|
||||
- Overlap bands by at least 10 blocks, plus the blocks that arrive between calls, and dedupe by txid.
|
||||
- Size bands adaptively, aiming for about 5 s each.
|
||||
- **Scheduling:** run **one band per history cycle**, using the epoch-guarded band cursor from 2.2. All bands in one job would tie up the shared RPC worker for about 113 s, exactly as today.
|
||||
- **Watermark:** take `W` from the tip at the first band, and set it only after every band has succeeded.
|
||||
- **Don't page with `nCount`.** The iteration order is `nOrderPos`, not height (`:3709`, `:3746`).
|
||||
- **Limits:**
|
||||
- Banding shortens each `LOCK2` hold but not the total CPU.
|
||||
- On v1.3.0 every call has a ~19.5 s floor, because every `CWalletTx` is deep-copied *(inference from the windowed measurement)*. There the bands need `callLong`.
|
||||
|
||||
**Backoff persistence.**
|
||||
- Store the failure count and the relative delay (2.1), so a relaunch doesn't immediately re-run a sweep that keeps failing.
|
||||
- Reset both when `daemon_id` changes. Otherwise a user who installs the fixed daemon could wait up to 6 h for a full sweep that would now succeed.
|
||||
|
||||
### 2.6 Safety net
|
||||
|
||||
- **When:** about every 7 days, run one banded full re-sweep.
|
||||
- **Conditions:** only when synced, unlocked, idle (not on the Mining tab) and admitted by the governor.
|
||||
- **On success**, update `last_full_*`. On failure, back off.
|
||||
- **Why:** it catches what the guards can't see, such as a Console `z_importviewingkey`. That case is also missed within a session today.
|
||||
|
||||
### 2.7 Edge cases
|
||||
|
||||
| Case | Handling |
|
||||
|---|---|
|
||||
| Stored tip < current tip: `load()` doesn't check the hash (`cache.cpp:338-344`) | The anchor verify covers the watermark. Optionally extend `load()` too |
|
||||
| Transparent send rows from `appendSentTransparentOutputs` aren't reproduced by windowed sweeps | Probably dropped on the next clean cycle (`network_refresh_service.cpp:1356-1368`, `:1528-1539`) *(inference)*. Negligible on DragonX, where consensus bans non-notary/burn t-outputs (`dx:main.cpp:1606`, `:1633`, `:1657`). Optional fix: carry over `send` rows that `listtransactions` didn't produce |
|
||||
| `locateSweepTxid` runs a full sweep under the 30 s timeout (`app_network.cpp:6388`) | That times out on this wallet. Switch it to a windowed or banded call |
|
||||
| Watch-only addresses aren't part of the snapshot key | Covered by the tripwire and the safety net |
|
||||
| Switching between two wallets | `pruneOtherWallets` keeps one snapshot, so a switch still costs a full sweep (open question 4) |
|
||||
| GUI crash mid-sweep | W advances only on `sentSweepOk`, and the write is atomic |
|
||||
| Regtest or testnet | Different z-address prefixes, so a different snapshot key |
|
||||
|
||||
### 2.8 Test plan
|
||||
|
||||
**Unit tests** (`tests/test_phase4.cpp`):
|
||||
- In `testTransactionHistoryCache` (`:9057`):
|
||||
- every `sent_sweep` field round-trips;
|
||||
- a missing key reads as −1;
|
||||
- a wrong type, `W > tip_height` or `A > W` reads as −1;
|
||||
- unknown keys are ignored.
|
||||
- Signatures (~`:9244`): a change to W, A or `H_A` changes `slow`; a change to the backlog changes `content`.
|
||||
- Policy (~`:15829`, `:16098-16213`):
|
||||
- the anchor is `W − margin`;
|
||||
- the first window after a load uses `120 + E`;
|
||||
- `tip − W > 2400` uses `callLong` and counts failures;
|
||||
- band planning: boundaries, overlap, dedupe, and cursor reset on an epoch change;
|
||||
- an anchor mismatch means a full sweep;
|
||||
- the pending rescan action clamps W;
|
||||
- `daemon_id` changes reset the backoff.
|
||||
|
||||
**Integration tests** (regtest or `-testnode`):
|
||||
1. Full sweep, quit, relaunch: the log shows only `[z,0,2,N]` calls.
|
||||
2. `invalidateblock` one block above A while the GUI is closed: the windowed sweep runs and returns the right rows.
|
||||
3. `invalidateblock` below A: the verify fails and a banded full sweep runs.
|
||||
4. More than 25 z→z sends, then quit before enrichment finishes: after relaunch every send appears.
|
||||
5. A daemon started with `-rescan` while the GUI was closed: the sticky action clamps or resets W.
|
||||
6. `restoreOriginalWallet`: W is invalidated.
|
||||
7. Locked encrypted wallet: W doesn't advance and the snapshot isn't loaded.
|
||||
|
||||
**Mainnet copy:** relaunch against the isolated owner copy. Expect no `[z,0]` call and a sent sweep well under 1 s.
|
||||
|
||||
### 2.9 Expected effect
|
||||
|
||||
| Scenario | Today | With Part 1 |
|
||||
|---|---|---|
|
||||
| Relaunch or reconnect, fixed daemon | 113 s full sweep under `LOCK2`, 7 s from the timeout | `getblockhash` plus one windowed sweep over `120 + E` + the closure gap. Steady-state windows measure ~128 ms; the first window after a load is larger *(unmeasured)* |
|
||||
| Relaunch, v1.3.0 daemon | 424 s; never fits in 120 s, so it loops in backoff with RSS at 19.4 GB | ~19.5 s+ windowed once a W exists. Seeding the first W needs the banded sweep |
|
||||
| Safety net | — | One banded full sweep a week, each `LOCK2` hold about 5 s |
|
||||
|
||||
---
|
||||
|
||||
## 3. Part 2: daemon follow-up
|
||||
|
||||
### Where the 113 s goes (owner wallet, full sweep)
|
||||
|
||||
| Component | Cost | Basis |
|
||||
|---|---|---|
|
||||
| Trial decryption of outputs that aren't wallet notes: ~172,800 outputs × 5 ivks ≈ 864k failed tries × ~0.13 ms | **~112 s (~99%)** | Output count derived from tx sizes; per-try cost fitted to the full (112.7 s) and windowed (126 ms / 740 tries) measurements *(estimate)*. Code: `OutputDecryptions::Get` (`dx:rpchushwallet.cpp:87-100`), loop at `:303-346` |
|
||||
| `DecryptSent` esk-path decrypts | **0 on this wallet** | No tx spends a wallet note, so the sends helper is gated off (`:586`) |
|
||||
| UniValue build + copy under `LOCK2` | ~0.28 s | Synthetic benchmark on the repo's UniValue |
|
||||
| Reply serialization (after the lock is released) | ~0.31-0.34 s | Same benchmark |
|
||||
|
||||
### Where the foreign outputs come from
|
||||
|
||||
**Sietch padding.** `z_sendmany` pads its z-recipients up to `-sietch-min-zouts` (default 7, clamped to 3-50; `dx:rpcwallet.cpp:5424-5434`), with change on top.
|
||||
- Recipient and dummy outputs take the sender's ovk: the from-address's `fvk.ovk`, or the seed ovk for t-address sources (`dx:asyncrpcoperation_sendmany.cpp:352-368`, applied at `:437`).
|
||||
- Consolidation (`saplingconsolidation.cpp:244`), sweep (`sweep.cpp:299`), shieldcoinbase (`:239-254`) and autoshield (`:435`) also add dummies with the sender's ovk.
|
||||
- So a **sending** wallet's `DecryptSent` opens every one of its own dummies. That is the ~0.36 ms esk-path decrypt each, which is D2.
|
||||
- Every other wallet sees them as foreign outputs and tries them under every ivk, which is D1.
|
||||
|
||||
**On this wallet** most foreign outputs are **not** sietch dummies. About 158.6k of the ~174k sit in the 9,597 txs with 10 or more outputs, which are mostly multi-recipient payouts *(inference from tx sizes)*. That doesn't change D1.
|
||||
|
||||
### Items
|
||||
|
||||
| # | Item | Output | Gain (owner wallet) | Risk | GUI change |
|
||||
|---|---|---|---|---|---|
|
||||
| D1 | Per-output negative ivk cache, plus parallel pre-fill of the txs a call selects | Byte-identical | Warm 113 s → 0.4-1 s. First full sweep after a restart ~20-25 s (pre-fill at the measured 4.75× warm-up scaling). A windowed call stays ~100 ms *(estimate)* | Low-med | None |
|
||||
| D2 | `DecryptSent` cache per (output, ovk) | Byte-identical | 0 here; ~0.36 ms per send output on sending wallets | Low | None |
|
||||
| D3 | `getalldata` balance loop | Byte-identical | ~20-35 s → 10-30 ms *(estimate)* | Low | None (the GUI never calls it) |
|
||||
| D4 | Write the wallet locator on clean shutdown | n/a | Start: −49-62 s (post-rescan locator write, *inferred from the log gap*) and −5-10 s of block rescan; the ~25 s witness-root pass moves to the first live block. Shutdown: +3-60 s *(inferred)* | Medium | Check the stop policy |
|
||||
| D5 | JSON trims | Byte-identical | 0.2-0.4 s | Low | None |
|
||||
| D6 | Sends-only `returnType` (6th parameter) | **Changes output; opt-in** | 113 s → 0.1-0.3 s; 26.2 → ~4.1 MB *(estimate)* | Low | Probe + opt-in |
|
||||
| D7 | Honour the address argument | **Changes output** | None (slower) | Breaks the GUI | Not recommended |
|
||||
|
||||
- **D6 is the single largest win**, but it needs a GUI release.
|
||||
- **D1-D5 are invisible to the GUI.**
|
||||
- **With Part 1 shipped, full sweeps become rare**, so D1 mostly helps older GUIs, other clients and the safety net.
|
||||
|
||||
### 3.1 D1: per-output decryption cache (byte-identical)
|
||||
|
||||
**Shape**
|
||||
- A `mutable std::vector<uint64_t>` on `CWalletTx`, with one mask per `vShieldedOutput` index.
|
||||
- It is sized once, never serialized, and cleared in `Init()`.
|
||||
- Precedents: `cachedPlaintext` (`dx:src/wallet/wallet.h:340-348`) and the mutable caches at `:472-490`.
|
||||
- Bit k means "history key k does not decrypt this output".
|
||||
- An append-only ivk → index registry on `CWallet`, under `cs_wallet`. It is resolved once per RPC into `SaplingHistoryKeys::SpendableAddress` (`dx:rpchushwallet.cpp:19-39`).
|
||||
|
||||
**Logic in `OutputDecryptions::Get`**
|
||||
- If the bit is set, return null without decrypting.
|
||||
- After a failed `Decrypt`, set the bit.
|
||||
- Never cache a success: wallet notes already carry `cachedPlaintext`.
|
||||
- Keys with index ≥ 64 aren't cached.
|
||||
|
||||
**Invariant: the result is a pure function of (txid, n, ivk).** A v4 txid commits to every output byte, so:
|
||||
- nothing ever goes stale;
|
||||
- a newly added key has a clear bit and is still tried;
|
||||
- locking, or losing keys, only means fewer keys are consulted;
|
||||
- reorgs, deletions and reloads are harmless;
|
||||
- output stays byte-identical.
|
||||
|
||||
**Thread safety**
|
||||
- The mask is mutable behind a `const CWalletTx&`, so the compiler won't enforce locking. Every mask read and write therefore happens on the RPC thread under `cs_wallet`, with an `AssertLockHeld` in the accessor.
|
||||
- Pre-fill workers follow `FillSaplingNoteCache`'s rule that workers must not lock, or even assert a lock (`dx:wallet.cpp:5745-5746`). They call only `decrypt_with_note` on pre-resolved (output, ivk) pairs and write private masks, which are merged serially after the join.
|
||||
- Workers must not call `GetSaplingHistoryKeys` or `IsNullifierOfAddress`, because both assert locks.
|
||||
|
||||
**Fill**
|
||||
- Lazily, as the helpers run.
|
||||
- In addition, pre-fill in parallel, but **only the txs the call has already selected**:
|
||||
1. In `z_listsentbyaddress`, first collect the `wtx` pointers that pass the filters at `dx:rpcwallet.cpp:3722-3741`, honouring `nCount`.
|
||||
2. Pre-fill their outputs, using the 128-item threshold and `-witnessbuildthreads` from `dx:wallet.cpp:5739-5797`.
|
||||
3. Then emit.
|
||||
- Pre-fill time is spent under `LOCK2`. For a full sweep that's ~20-25 s once; a windowed call stays small.
|
||||
- Optionally, also record the misses `FindMySaplingNotes` already computes (`:2282-2322`).
|
||||
- A startup precompute is opt-in only: ~112 CPU-s, about 24 s of wall time per start *(estimate)*.
|
||||
|
||||
**Also benefits:** `getalldata`'s tx list (`dx:rpcwallet.cpp:3254`) and `z_listreceivedaddress` (`:3555`).
|
||||
|
||||
**Tests**
|
||||
- Extend the verbatim oracle `dx:src/gtest/test_wallet_history.cpp`: cold, warm and pre-filled runs must match byte for byte.
|
||||
- A key added after the mask was filled, which opens a foreign output, must appear in the output.
|
||||
- More than 64 keys; locked and encrypted wallets; `-witnessbuildthreads=1` vs 24; a `DEBUG_LOCKORDER` build.
|
||||
- The mainnet A/B harness: `cmp` cold and warm replies against the captured 26,233,144-byte reply (`/home/d/dragonx-ab/mainnet-ab/out/B/cap/023_z_listsentbyaddress_addr0_full.json`).
|
||||
|
||||
**D2.** Cache `DecryptSent` results per (n, ovk index): the plaintext and address on success, or "no". That's about 650 B per entry. Key the seed ovk by value, so lock state still decides whether the cache is consulted. Use the same oracle tests as D1.
|
||||
|
||||
### 3.2 Smaller items
|
||||
|
||||
**D3: the `getalldata` balance loop** (`dx:rpcwallet.cpp:3108-3151`, under `LOCK2` at `:2998`).
|
||||
- Today it copies each `SaplingNoteData` by value, witnesses included (`:3110`). For every unspent note and every address it re-derives the spending key and trial-decrypts (`:3117-3121`).
|
||||
- Fix:
|
||||
- build `SaplingHistoryKeys` once, and reuse it at `:3233`;
|
||||
- take note data by `const&`;
|
||||
- use `OutputDecryptions(wtx, op.n).Get(a.ivk)` over `keys.spendable`, moving `OutputDecryptions` out of its anonymous namespace (`rpchushwallet.cpp:56-120`) into `rpchushwallet.h`.
|
||||
- Keep two quirks for byte-identity, and fix them later as a separate change with a release note:
|
||||
- The no-op `txType == 3;` at `:3124` means locked notes are never counted as locked. When fixed, use a per-note local, `noteType = (txType == 0 && IsLockedNote(op)) ? 3 : txType`. `txType` is per tx (`:3043`), so assigning to it would mark every later note of the tx as locked.
|
||||
- Every address whose ivk opens the note is credited, because the `continue` doesn't exit the address loop.
|
||||
- Don't switch to `GetFilteredNotes`: that changes the output.
|
||||
- Test: an oracle gtest with a verbatim copy of the old loop (shared-ivk, locked-note, watch-only and encrypted cases).
|
||||
|
||||
**D4: write the wallet locator on clean shutdown.**
|
||||
- **Evidence:** the copy, taken from a clean Windows shutdown, was 12 blocks behind its chainstate. So every start rescans those blocks.
|
||||
- **Cause:**
|
||||
- `FlushStateToDisk` writes the locator only if `DATABASE_WRITE_INTERVAL` (3600 s, `dx:main.h:145`) has passed, even for `FLUSH_STATE_ALWAYS` (`dx:main.cpp:3838-3841`).
|
||||
- `Shutdown()` calls it at `dx:init.cpp:259`.
|
||||
- `pwalletMain->Flush(true)` at `:280` only flushes BDB.
|
||||
- **Hook:** in `Shutdown()`, between `:259` and `:280`:
|
||||
|
||||
```cpp
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
const CBlockIndex* tip = pwalletMain->GetWalletTip(); // new accessor: pindexWalletTip is private (wallet.h:988-994)
|
||||
if (tip && tip != lastStamped)
|
||||
pwalletMain->SetBestChainINTERNAL(walletdb, chainActive.GetLocator(tip));
|
||||
```
|
||||
|
||||
- **Don't route it through `CWallet::SetBestChain`.** That swaps in `pindexWalletTip` when set but falls back to the passed locator when it's null (`dx:wallet.cpp:829-834`). The null case is exactly "stamp the tip before the init rescan ran", which would skip the unscanned blocks for good.
|
||||
- **The failure path needs the lock too.** AppInit2's failure path calls `Shutdown()` without `join_all` (`dx:bitcoind.cpp:232-243`), so the notifier can still be running. Reading the tip under `LOCK2` keeps that safe.
|
||||
- **Don't drop the interval check globally.** `gettxoutsetinfo` and init also flush with ALWAYS, and each would then rewrite ~2 GB under `cs_main`.
|
||||
- **Trade-off:** one locator write rewrites every note-bearing `CWalletTx`, ~2 GB here (`SetBestChainINTERNAL`, `dx:wallet.h:939-986`, WriteTx loop `:947-962`). The cost moves to shutdown rather than disappearing; see the D4 row in the table above.
|
||||
- **Ship it together with a parallel witness-root check at init.**
|
||||
- **Risk:** the GUI's stop policy (`src/daemon/daemon_controller.h:360`, `evaluateStopWait`) must allow about 16 s plus 3-60 s. Context: the 2026-09-30 Windows kill loop.
|
||||
- **Tests:**
|
||||
- Stop within the hour and restart: no "Rescanning last" line, and identical balances.
|
||||
- Stop during the startup rescan: the locator does not advance.
|
||||
|
||||
**D5: JSON trims.**
|
||||
- Use `static_cast<const CTransaction&>` at `dx:rpchushwallet.cpp:553`. Today it copies 190.6 MB of tx data per full sweep.
|
||||
- Drop the `getValues` / reverse / `push_backV` copy (`dx:rpcwallet.cpp:3749-3755`). Collect the selected `wtx` pointers in the existing reverse loop, which keeps `nCount` picking the newest txs, then build `ret` oldest to newest. The same pattern is at `getalldata` `:3259-3265`.
|
||||
|
||||
**Also found; each needs its own design.**
|
||||
- The hourly `SetBestChain` rewrites ~2 GB while `cs_main` is held (3-47 s gaps in the live log, *inferred from log gaps*).
|
||||
- `FindMySaplingNotes` tries foreign outputs once per viewing key and again per address (`dx:wallet.cpp:2282-2322`). De-duplicating ivks would probably halve the per-block tries *(inference)*.
|
||||
|
||||
### 3.3 Options that change RPC output (need a GUI opt-in)
|
||||
|
||||
**D6: sends-only mode.**
|
||||
- **The parameter:** add an optional 6th parameter `returnType` to `z_listsentbyaddress`, default 0 (unchanged output).
|
||||
- The code's meanings are 1 = spends, 2 = received, 3 = sends. The comment at `dx:rpchushwallet.cpp:561-566` swaps 1 and 3; fix it in the same change.
|
||||
- **With `3` and no address filter:**
|
||||
- the spends helper still runs (`:569`), but its output isn't emitted;
|
||||
- `received`, where all the trial decryption happens, is skipped (`:576-581`);
|
||||
- `sends` is emitted (`:584-592`), and every tx is still pushed (`:594-598`).
|
||||
- The GUI gets the same txids and the same `sends.transparentSends`.
|
||||
- **Daemon changes:**
|
||||
- Raise the check at `dx:rpcwallet.cpp:3606` from `> 5` to `> 6`, keeping the `== 3` rejection.
|
||||
- Change `:3693` to `params.size() >= 5`. Today it reads `nCount` only when `size() == 5`, so every 6-parameter call would silently drop it.
|
||||
- Read `params[5]`, reject anything outside 0-3, and document it in the help text. `dx:src/rpc/client.cpp:165` already converts index 5.
|
||||
- **GUI side:** send 6 parameters. An old daemon throws its help text for more than 5, so fall back to 5 parameters and remember that per connection.
|
||||
- Alternative: a new RPC name, probed through "Method not found" the same way as `z_autoshieldstatus` (`app.h:1744-1747`).
|
||||
- **Owner decision:** a variant that returns only txs with wallet spends would return 0 rows here, but it changes the `knownTxids` set.
|
||||
|
||||
**D7: honour the address argument.**
|
||||
- The GUI passes `shieldedAddresses.front()` (`network_refresh_service.cpp:1319`) and relies on the daemon using `"*"` (`dx:rpcwallet.cpp:3743`). Honouring the argument by default would silently shrink GUI sweeps to one address.
|
||||
- It isn't faster either: with a filter set, `fBool=true` runs `zsTxSendsToJSON` for every tx (`:586`).
|
||||
- Recommendation: document `"*"` as a compatibility contract. If honouring the argument is ever wanted, put it behind a flag.
|
||||
|
||||
**Additive and safe for the GUI: a wallet-tip / history fingerprint.**
|
||||
- Expose `pindexWalletTip`, for example in `getwalletinfo` or a new `z_getwallethistorystate(maxheight)` returning `{count, txid-hash, wallet_tip}`.
|
||||
- The GUI ignores unknown JSON fields.
|
||||
- This would make Part 1's invalidation exact, replacing the E-margin guess and the `txcount` tripwire.
|
||||
|
||||
---
|
||||
|
||||
## 4. Rollout and compatibility
|
||||
|
||||
| Step | Ships in | Works with | Notes |
|
||||
|---|---|---|---|
|
||||
| Part 1: watermark + anchor verify + backlog + sticky rescan + large-window path + banding + safety net | Next GUI release | Every daemon version; it uses only existing `z_listsentbyaddress` parameters, `getblockhash`, `getwalletinfo` and `getinfo` | Old GUI with a new snapshot: the key is ignored. New GUI with an old snapshot: one full sweep, which is today's behaviour. On v1.3.0, seeding the first W needs the banded sweep |
|
||||
| D1, D2, D3, D5 | Daemon v1.3.1 / v1.3.2 | Invisible to the GUI | Gate each on the byte-identical oracle and the mainnet `cmp` |
|
||||
| D6 + GUI probe | Daemon first, then GUI | The GUI falls back on old daemons | Makes the safety net and any full sweep cheap |
|
||||
| D4 | Together with a parallel witness-root check | Invisible apart from shutdown time | Confirm the GUI stop policy first |
|
||||
|
||||
---
|
||||
|
||||
## 5. Open questions
|
||||
|
||||
1. How big is the backlog after the first full sweep on the owner's wallet? Unmeasured; it sizes the persisted `backlog` field.
|
||||
2. What is the full-sweep time on the owner's 24-core Windows node? The 113 s was measured on Linux/WSL, 7 s inside the 120 s timeout.
|
||||
3. How long does the wallet notifier lag after a catch-up on this wallet? This sets E.
|
||||
4. Should the snapshot store keep more than one wallet, so that switching wallets also benefits?
|
||||
5. D6 shape: a 6th parameter or a new RPC? Should a "wallet-spends-only" variant exist?
|
||||
6. Does any client besides ObsidianDragon depend on `returnType 0` output? This sets D1's priority once D6 exists.
|
||||
7. Are the 5 ivks distinct? Assumed. The per-address note counts sum to exactly 16,206, which fits but doesn't prove it.
|
||||
8. Does the owner want the additive wallet-tip / fingerprint RPC, which makes Part 1's invalidation exact?
|
||||
@@ -131,7 +131,7 @@ fi
|
||||
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
|
||||
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
|
||||
_cml="$SCRIPT_DIR/CMakeLists.txt"
|
||||
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]][[:space:]]*\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
|
||||
_full_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)
|
||||
@@ -195,39 +195,6 @@ should_bundle_full_node_assets() {
|
||||
! $DO_LITE
|
||||
}
|
||||
|
||||
# The offline wallet-rebuild helper is the ONLY thing that repairs a genuinely BDB-inconsistent
|
||||
# wallet.dat — plain "Restore" just re-triggers the daemon's salvage cascade. A full-node release must
|
||||
# NEVER ship without it (the in-app "Repair automatically" option silently disappears otherwise), so
|
||||
# treat a missing helper as a HARD build failure instead of degrading recovery to Restore-only.
|
||||
# $1 = built helper path (e.g. bin/dragonx-wallet-rebuild[.exe]); $2 = the BDB depends dir for the hint.
|
||||
require_wallet_rebuild_helper() {
|
||||
local helper="$1" depends="$2"
|
||||
should_bundle_full_node_assets || return 0 # lite builds have no BDB wallet.dat to rebuild
|
||||
if [[ ! -f "$helper" ]]; then
|
||||
err "wallet-rebuild helper was NOT built: $helper"
|
||||
err " → the recovery 'Repair automatically' option would be MISSING from this release."
|
||||
err " Cause: the vendored Berkeley DB depends are absent, so CMake skipped the dragonx-wallet-rebuild target."
|
||||
err " Fix: provide ${depends}/{lib/libdb-6.2.a,include/db.h} (same static libdb the daemon links), then rebuild."
|
||||
exit 1
|
||||
fi
|
||||
info " wallet-rebuild helper present: $helper"
|
||||
}
|
||||
|
||||
# Vendored Berkeley DB depends directory for the macOS wallet-rebuild helper, by TARGET arch. The daemon
|
||||
# links a static libdb-6.2; the helper must match the build arch, so derive the triple from it rather than
|
||||
# hardcoding one. Single-arch: x86_64 -> x86_64-apple-darwin, arm64 -> aarch64-apple-darwin. "universal"
|
||||
# (or anything unexpected) falls back to the host arch — a universal .app needs a universal libdb, so if
|
||||
# only a single-arch libdb is vendored, build single-arch via DRAGONX_MAC_ARCHS.
|
||||
mac_bdb_dir() {
|
||||
local arch="$1" triple
|
||||
case "$arch" in
|
||||
x86_64) triple="x86_64-apple-darwin" ;;
|
||||
arm64) triple="aarch64-apple-darwin" ;;
|
||||
*) [[ "$(uname -m)" == "arm64" ]] && triple="aarch64-apple-darwin" || triple="x86_64-apple-darwin" ;;
|
||||
esac
|
||||
printf '%s/external/dragonx/depends/%s\n' "$SCRIPT_DIR" "$triple"
|
||||
}
|
||||
|
||||
# ── Helper: find resource files ──────────────────────────────────────────────
|
||||
find_sapling_params() {
|
||||
local dirs=(
|
||||
@@ -319,9 +286,6 @@ bundle_linux_daemon() {
|
||||
# asmap.dat
|
||||
find_asmap && cp "$ASMAP_DAT" "$dest/asmap.dat" && info " Bundled asmap.dat"
|
||||
|
||||
# (The dragonx-wallet-rebuild recovery helper is built into bin/ by CMake and packaged explicitly
|
||||
# by each release path — required via require_wallet_rebuild_helper — so it is not copied here.)
|
||||
|
||||
return $found
|
||||
}
|
||||
|
||||
@@ -349,7 +313,6 @@ build_dev() {
|
||||
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=ON \
|
||||
-DDRAGONX_ENABLE_UI_SWEEP=ON \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
|
||||
info "Building with $JOBS jobs ..."
|
||||
@@ -364,8 +327,8 @@ build_dev() {
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
build_release_linux() {
|
||||
header "Release: Linux x86_64"
|
||||
local bd="${DRAGONX_BUILD_ROOT:-$SCRIPT_DIR/build}/linux"
|
||||
local out="${DRAGONX_RELEASE_ROOT:-$SCRIPT_DIR/release}/linux"
|
||||
local bd="$SCRIPT_DIR/build/linux"
|
||||
local out="$SCRIPT_DIR/release/linux"
|
||||
|
||||
if $CLEAN; then
|
||||
info "Cleaning $bd ..."; rm -rf "$bd"
|
||||
@@ -373,39 +336,20 @@ build_release_linux() {
|
||||
mkdir -p "$bd" && cd "$bd"
|
||||
|
||||
# ── Compile ──────────────────────────────────────────────────────────────
|
||||
# Point the wallet-rebuild helper at the vendored static Berkeley DB (same libdb the daemon links)
|
||||
# so its output is a v6.2 btree the bundled dragonxd reads. Pass the paths EXPLICITLY (bypasses
|
||||
# find_library + its cache); the helper target is simply not built if the depends tree is absent.
|
||||
local lin_bdb="$SCRIPT_DIR/external/dragonx/depends/x86_64-unknown-linux-gnu"
|
||||
local BDB_ARGS=()
|
||||
if [[ -f "$lin_bdb/lib/libdb-6.2.a" && -f "$lin_bdb/include/db.h" ]]; then
|
||||
BDB_ARGS=( -DBDB_INCLUDE_DIR="$lin_bdb/include" -DBDB_LIBRARY="$lin_bdb/lib/libdb-6.2.a" )
|
||||
elif should_bundle_full_node_assets; then
|
||||
err "Vendored Berkeley DB depends missing at $lin_bdb — the wallet-rebuild recovery helper cannot be built."
|
||||
err " A full-node release must ship it; aborting rather than degrading recovery to Restore-only."
|
||||
exit 1
|
||||
fi
|
||||
info "Configuring ..."
|
||||
# UI sweep OFF explicitly: build/linux is shared with dev builds, which cache it ON.
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=ON \
|
||||
-DDRAGONX_ENABLE_UI_SWEEP=OFF \
|
||||
"${BDB_ARGS[@]}" \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
|
||||
info "Building with $JOBS jobs ..."
|
||||
cmake --build . -j "$JOBS" --target dragonx_release
|
||||
cmake --build . -j "$JOBS"
|
||||
|
||||
[[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; }
|
||||
|
||||
# A full-node release MUST include the recovery helper — fail loudly, never ship without it.
|
||||
require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild" "$lin_bdb"
|
||||
|
||||
info "Stripping ..."
|
||||
strip "bin/${APP_BASENAME}"
|
||||
[[ -f "bin/dragonx-wallet-rebuild" ]] && strip "bin/dragonx-wallet-rebuild"
|
||||
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||
|
||||
if should_bundle_full_node_assets; then
|
||||
@@ -440,12 +384,9 @@ build_release_linux() {
|
||||
[[ -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/"
|
||||
# Offline wallet-rebuild recovery helper — required (asserted above); ships next to the app.
|
||||
cp bin/dragonx-wallet-rebuild "$dist_dir/" && chmod +x "$dist_dir/dragonx-wallet-rebuild"
|
||||
info " Bundled dragonx-wallet-rebuild"
|
||||
fi
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
|
||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||
|
||||
@@ -476,11 +417,9 @@ build_release_linux() {
|
||||
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
|
||||
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
|
||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
|
||||
# Offline wallet-rebuild recovery helper — required (asserted above); ships next to the app.
|
||||
cp bin/dragonx-wallet-rebuild "$APPDIR/usr/bin/" && chmod +x "$APPDIR/usr/bin/dragonx-wallet-rebuild"
|
||||
fi
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
|
||||
|
||||
# Desktop entry
|
||||
@@ -539,28 +478,18 @@ APPRUN
|
||||
done
|
||||
[[ -f "$bd/_deps/sdl3-build/libSDL3.so" ]] && cp "$bd/_deps/sdl3-build/libSDL3.so"* "$APPDIR/usr/lib/" 2>/dev/null || true
|
||||
|
||||
# appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
|
||||
# The old "continuous" tag is a MOVING build fetched over the network and run on the release
|
||||
# builder; a compromised/MITM'd artifact would execute here. Verify, or refuse to package.
|
||||
local APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
|
||||
local APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
|
||||
# appimagetool
|
||||
local APPIMAGETOOL=""
|
||||
if command -v appimagetool &>/dev/null; then
|
||||
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
|
||||
APPIMAGETOOL="appimagetool"
|
||||
elif [[ -f "$bd/appimagetool-x86_64.AppImage" ]]; then
|
||||
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage"
|
||||
else
|
||||
local at="$bd/appimagetool-x86_64.AppImage"
|
||||
# Re-verify any cached copy too; a stale unverified download must not be trusted.
|
||||
if [[ ! -f "$at" ]] || ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
|
||||
info "Downloading appimagetool 1.9.0 (pinned) ..."
|
||||
wget -q -O "$at" "$APPIMAGETOOL_URL"
|
||||
if ! echo "${APPIMAGETOOL_SHA256} ${at}" | sha256sum -c --status; then
|
||||
err "appimagetool SHA-256 verification failed — refusing to use it"
|
||||
rm -f "$at"
|
||||
return 1
|
||||
fi
|
||||
chmod +x "$at"
|
||||
fi
|
||||
APPIMAGETOOL="$at"
|
||||
info "Downloading appimagetool ..."
|
||||
wget -q -O "$bd/appimagetool-x86_64.AppImage" \
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x "$bd/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="$bd/appimagetool-x86_64.AppImage"
|
||||
fi
|
||||
|
||||
local ARCH
|
||||
@@ -580,8 +509,8 @@ APPRUN
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
build_release_win() {
|
||||
header "Release: Windows x86_64 (cross-compile)"
|
||||
local bd="${DRAGONX_BUILD_ROOT:-$SCRIPT_DIR/build}/windows"
|
||||
local out="${DRAGONX_RELEASE_ROOT:-$SCRIPT_DIR/release}/windows"
|
||||
local bd="$SCRIPT_DIR/build/windows"
|
||||
local out="$SCRIPT_DIR/release/windows"
|
||||
|
||||
if $CLEAN; then
|
||||
info "Cleaning $bd ..."; rm -rf "$bd"
|
||||
@@ -649,16 +578,13 @@ TOOLCHAIN
|
||||
mkdir -p "$RES"
|
||||
|
||||
SAPLING_SPEND="" SAPLING_OUTPUT="" ASMAP_DAT=""
|
||||
local EMBED_NODE_DATA=false
|
||||
should_bundle_full_node_assets && EMBED_NODE_DATA=true
|
||||
if $EMBED_NODE_DATA; then
|
||||
find_sapling_params || true
|
||||
find_asmap || true
|
||||
fi
|
||||
|
||||
# Lite needs no Sapling params/asmap (its backend embeds its own), so it always gets a header for
|
||||
# images/themes/xmrig; the full node still requires the params to be found.
|
||||
if ! $EMBED_NODE_DATA || [[ -n "$SAPLING_SPEND" && -n "$SAPLING_OUTPUT" ]]; then
|
||||
if [[ -n "$SAPLING_SPEND" && -n "$SAPLING_OUTPUT" ]]; then
|
||||
cp -f "$SAPLING_SPEND" "$RES/sapling-spend.params"
|
||||
cp -f "$SAPLING_OUTPUT" "$RES/sapling-output.params"
|
||||
|
||||
cat > "$GEN/embedded_data.h" <<HDR
|
||||
// Auto-generated — DO NOT EDIT (build.sh --win-release)
|
||||
#pragma once
|
||||
@@ -666,14 +592,10 @@ TOOLCHAIN
|
||||
#include <cstddef>
|
||||
#include "incbin.h"
|
||||
|
||||
HDR
|
||||
if $EMBED_NODE_DATA; then
|
||||
cp -f "$SAPLING_SPEND" "$RES/sapling-spend.params"
|
||||
cp -f "$SAPLING_OUTPUT" "$RES/sapling-output.params"
|
||||
cat >> "$GEN/embedded_data.h" <<HDR
|
||||
INCBIN(sapling_spend_params, "$RES/sapling-spend.params");
|
||||
INCBIN(sapling_output_params, "$RES/sapling-output.params");
|
||||
HDR
|
||||
|
||||
if [[ -n "$ASMAP_DAT" ]]; then
|
||||
cp -f "$ASMAP_DAT" "$RES/asmap.dat"
|
||||
echo "INCBIN(asmap_dat, \"$RES/asmap.dat\");" >> "$GEN/embedded_data.h"
|
||||
@@ -681,13 +603,6 @@ HDR
|
||||
echo 'extern "C" { static const uint8_t* g_asmap_dat_data = nullptr; }' >> "$GEN/embedded_data.h"
|
||||
echo 'static const unsigned int g_asmap_dat_size = 0;' >> "$GEN/embedded_data.h"
|
||||
fi
|
||||
else
|
||||
info "Lite mode: not embedding Sapling params/asmap (the SDXL backend embeds its own)"
|
||||
for sym in sapling_spend_params sapling_output_params asmap_dat; do
|
||||
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"
|
||||
done
|
||||
fi
|
||||
|
||||
# ── Daemon binaries ──────────────────────────────────────────────
|
||||
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
||||
@@ -713,32 +628,8 @@ HDR
|
||||
info "Lite mode: skipping embedded daemon binaries"
|
||||
fi
|
||||
|
||||
# ── Wallet-rebuild recovery helper ───────────────────────────────
|
||||
# Built in-tree (not a prebuilt like the daemon), so compile it standalone HERE — before the
|
||||
# main app compiles embedded_resources.cpp — and INCBIN it, so a bare, self-extracting
|
||||
# ObsidianDragon.exe carries the recovery tool exactly like it does the daemon.
|
||||
if should_bundle_full_node_assets; then
|
||||
local WBDB="$SCRIPT_DIR/external/dragonx/depends/x86_64-w64-mingw32"
|
||||
if [[ -f "$WBDB/lib/libdb-6.2.a" && -f "$WBDB/include/db.h" ]]; then
|
||||
info "Compiling + embedding wallet-rebuild helper ..."
|
||||
x86_64-w64-mingw32-g++ -std=c++17 -O2 -static -static-libgcc -static-libstdc++ \
|
||||
-I"$SCRIPT_DIR/src" -I"$WBDB/include" \
|
||||
"$SCRIPT_DIR/tools/wallet_rebuild/main.cpp" \
|
||||
"$WBDB/lib/libdb-6.2.a" -lws2_32 \
|
||||
-o "$RES/dragonx-wallet-rebuild.exe" \
|
||||
|| { err "wallet-rebuild helper failed to compile for embedding"; exit 1; }
|
||||
x86_64-w64-mingw32-strip "$RES/dragonx-wallet-rebuild.exe" 2>/dev/null || true
|
||||
echo -e "\n#define HAS_EMBEDDED_WALLET_REBUILD 1" >> "$GEN/embedded_data.h"
|
||||
echo "INCBIN(dragonx_wallet_rebuild_exe, \"$RES/dragonx-wallet-rebuild.exe\");" >> "$GEN/embedded_data.h"
|
||||
info " Embedded dragonx-wallet-rebuild.exe ($(du -h "$RES/dragonx-wallet-rebuild.exe" | cut -f1))"
|
||||
else
|
||||
err "Vendored mingw Berkeley DB missing at $WBDB — cannot embed the wallet-rebuild recovery helper."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
|
||||
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
# ── 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
|
||||
@@ -826,7 +717,6 @@ HDR
|
||||
info "Embedded resources header generated"
|
||||
else
|
||||
warn "Building WITHOUT embedded resources (Sapling params not found)"
|
||||
rm -f "$GEN/embedded_data.h" # never compile a stale header left by the other variant in this shared build dir
|
||||
fi
|
||||
|
||||
# ── Fetch libsodium for Windows if needed ──────────────────────────────
|
||||
@@ -835,57 +725,20 @@ HDR
|
||||
"$SCRIPT_DIR/scripts/fetch-libsodium.sh" --win
|
||||
fi
|
||||
|
||||
# ── FreeType for Windows (color-emoji rendering) ───────────────────────
|
||||
# The mingw toolchain ships no FreeType; cross-build a minimal static one (COLR/CPAL, no external
|
||||
# deps). Failure is non-fatal — the wallet just falls back to monochrome emoji.
|
||||
local FT_MINGW_PREFIX="$SCRIPT_DIR/third_party/freetype-mingw"
|
||||
if [[ ! -f "$FT_MINGW_PREFIX/lib/libfreetype.a" ]]; then
|
||||
info "Cross-building FreeType for Windows (color emoji) ..."
|
||||
"$SCRIPT_DIR/scripts/build-freetype-mingw.sh" "$FT_MINGW_PREFIX" \
|
||||
|| warn "FreeType cross-build failed — Windows build will use monochrome emoji"
|
||||
fi
|
||||
local FT_CMAKE_ARG=()
|
||||
if [[ -f "$FT_MINGW_PREFIX/lib/libfreetype.a" ]]; then
|
||||
FT_CMAKE_ARG=(-DDRAGONX_MINGW_FREETYPE_PREFIX="$FT_MINGW_PREFIX")
|
||||
fi
|
||||
|
||||
# ── CMake + build ────────────────────────────────────────────────────────
|
||||
# The wallet-rebuild helper links the vendored mingw static Berkeley DB (the mingw toolchain's
|
||||
# find_library is sysroot-only, so pass the depends paths EXPLICITLY to bypass the search). Only
|
||||
# enabled if the depends tree is present; guarded with -DBDB_* left empty otherwise.
|
||||
local win_bdb="$SCRIPT_DIR/external/dragonx/depends/x86_64-w64-mingw32"
|
||||
local BDB_ARGS=()
|
||||
if [[ -f "$win_bdb/lib/libdb-6.2.a" && -f "$win_bdb/include/db.h" ]]; then
|
||||
BDB_ARGS=( -DBDB_INCLUDE_DIR="$win_bdb/include" -DBDB_LIBRARY="$win_bdb/lib/libdb-6.2.a" )
|
||||
elif should_bundle_full_node_assets; then
|
||||
err "Vendored Berkeley DB depends missing at $win_bdb — the wallet-rebuild recovery helper cannot be built."
|
||||
err " A full-node release must ship it; aborting rather than degrading recovery to Restore-only."
|
||||
exit 1
|
||||
fi
|
||||
info "Configuring (cross-compile) ..."
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||
-DDRAGONX_ENABLE_UI_SWEEP=OFF \
|
||||
"${BDB_ARGS[@]}" \
|
||||
"${FT_CMAKE_ARG[@]}" \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
|
||||
info "Building with $JOBS jobs ..."
|
||||
cmake --build . -j "$JOBS" --target dragonx_release
|
||||
cmake --build . -j "$JOBS"
|
||||
|
||||
[[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; }
|
||||
# Strip the app exe — the Linux and macOS release paths already strip theirs, and even the Windows
|
||||
# helper (dragonx-wallet-rebuild.exe) is stripped, but the main app exe was shipping unstripped
|
||||
# (~5MB of symbols on the full node, ~19MB on the params-heavy lite build). Best-effort.
|
||||
x86_64-w64-mingw32-strip --strip-all "bin/${APP_BASENAME}.exe" 2>/dev/null \
|
||||
|| warn " strip unavailable — shipping unstripped ${APP_BASENAME}.exe"
|
||||
info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)"
|
||||
|
||||
# A full-node release MUST include the recovery helper — fail loudly, never ship without it.
|
||||
require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild.exe" "$win_bdb"
|
||||
|
||||
# ── Package: release/windows/ ────────────────────────────────────────────
|
||||
# Remove only THIS variant's prior artifacts so full-node and lite releases coexist here.
|
||||
mkdir -p "$out"
|
||||
@@ -901,9 +754,6 @@ HDR
|
||||
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
|
||||
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
||||
done
|
||||
# dragonx-wallet-rebuild helper (offline recovery for a BDB-inconsistent wallet.dat) — required.
|
||||
cp "bin/dragonx-wallet-rebuild.exe" "$dist_dir/" && info " Bundled dragonx-wallet-rebuild.exe"
|
||||
[[ -f "$dist_dir/dragonx-wallet-rebuild.exe" ]] || { err "Failed to bundle dragonx-wallet-rebuild.exe"; exit 1; }
|
||||
|
||||
# Bundle Sapling params + asmap for the zip distribution
|
||||
# (The single-file exe has these embedded via INCBIN, but the zip
|
||||
@@ -915,14 +765,9 @@ HDR
|
||||
info "Lite mode: skipping daemon and Sapling/asmap assets in Windows zip"
|
||||
fi
|
||||
|
||||
# The exe embeds xmrig (HAS_EMBEDDED_XMRIG) and getXmrigPath() always prefers / re-extracts the copy in
|
||||
# %APPDATA%\<app>\dragonx, so a loose exe-dir xmrig.exe is never used. Ship it loose only when not embedded.
|
||||
if grep -q '^#define HAS_EMBEDDED_XMRIG 1' "$GEN/embedded_data.h" 2>/dev/null; then
|
||||
info "xmrig.exe is embedded in the exe — not copied loose into the zip"
|
||||
else
|
||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig.exe"
|
||||
# Bundle xmrig for mining support
|
||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
|
||||
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
|
||||
fi
|
||||
|
||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||
|
||||
@@ -1031,32 +876,14 @@ build_release_mac() {
|
||||
fi
|
||||
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
|
||||
else
|
||||
# Native macOS: build universal (arm64 + x86_64) by default. Override with
|
||||
# DRAGONX_MAC_ARCHS (e.g. "x86_64").
|
||||
MAC_ARCHS="${DRAGONX_MAC_ARCHS:-arm64;x86_64}"
|
||||
# When linking the real lite backend, the app can only include architectures
|
||||
# the backend static library actually provides. Its pinned ring 0.16.11 has no
|
||||
# Apple-Silicon assembly, so that artifact is x86_64-only — constrain the app
|
||||
# arch to the backend's (unless the user explicitly forced DRAGONX_MAC_ARCHS),
|
||||
# otherwise the arm64 slice fails to link.
|
||||
if $DO_LITE_BACKEND && [[ -z "${DRAGONX_MAC_ARCHS:-}" && -n "${lb_lib:-}" ]] && command -v lipo &>/dev/null; then
|
||||
local _backend_archs; _backend_archs=$(lipo -archs "$lb_lib" 2>/dev/null | tr ' ' ';')
|
||||
if [[ -n "$_backend_archs" && "$_backend_archs" != "$MAC_ARCHS" ]]; then
|
||||
warn "Lite backend provides only [$_backend_archs] — building the app for that instead of universal."
|
||||
MAC_ARCHS="$_backend_archs"
|
||||
fi
|
||||
fi
|
||||
if [[ "$MAC_ARCHS" == *";"* || "$MAC_ARCHS" == *","* ]]; then
|
||||
# Native macOS: build universal binary (arm64 + x86_64)
|
||||
MAC_ARCH="universal"
|
||||
else
|
||||
MAC_ARCH="$MAC_ARCHS"
|
||||
fi
|
||||
export MACOSX_DEPLOYMENT_TARGET="11.0"
|
||||
fi
|
||||
|
||||
header "Release: macOS ($MAC_ARCH$(${IS_CROSS} && echo ' — cross-compile'))"
|
||||
local bd="${DRAGONX_BUILD_ROOT:-$SCRIPT_DIR/build}/mac"
|
||||
local out="${DRAGONX_RELEASE_ROOT:-$SCRIPT_DIR/release}/mac"
|
||||
local bd="$SCRIPT_DIR/build/mac"
|
||||
local out="$SCRIPT_DIR/release/mac"
|
||||
|
||||
if $CLEAN; then
|
||||
info "Cleaning $bd ..."; rm -rf "$bd"
|
||||
@@ -1122,21 +949,13 @@ TOOLCHAIN
|
||||
fi
|
||||
|
||||
info "Configuring (cross-compile via osxcross) ..."
|
||||
# Vendored static libdb for the recovery helper (full-node only), by target arch; see mac_bdb_dir.
|
||||
local mac_bdb; mac_bdb="$(mac_bdb_dir "$MAC_ARCH")"
|
||||
local BDB_ARGS=()
|
||||
if should_bundle_full_node_assets && [[ -f "$mac_bdb/lib/libdb-6.2.a" && -f "$mac_bdb/include/db.h" ]]; then
|
||||
BDB_ARGS=( -DBDB_INCLUDE_DIR="$mac_bdb/include" -DBDB_LIBRARY="$mac_bdb/lib/libdb-6.2.a" )
|
||||
fi
|
||||
cmake "$SCRIPT_DIR" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$bd/osxcross-toolchain.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||
-DDRAGONX_ENABLE_UI_SWEEP=OFF \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} \
|
||||
"${BDB_ARGS[@]}" \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
else
|
||||
# Build libsodium as universal if needed
|
||||
@@ -1146,7 +965,7 @@ TOOLCHAIN
|
||||
need_sodium=true
|
||||
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
|
||||
# Rebuild if existing lib is not universal (single-arch won't link)
|
||||
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -Eq "arm64.*x86_64|x86_64.*arm64"; then
|
||||
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then
|
||||
info "Existing libsodium is not universal — rebuilding ..."
|
||||
rm -rf "$SCRIPT_DIR/libs/libsodium"
|
||||
need_sodium=true
|
||||
@@ -1157,34 +976,21 @@ TOOLCHAIN
|
||||
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
|
||||
fi
|
||||
|
||||
info "Configuring (native macOS, arch: $MAC_ARCHS) ..."
|
||||
# Point CMake at the vendored static libdb for the recovery helper (full-node only); see mac_bdb_dir.
|
||||
local mac_bdb; mac_bdb="$(mac_bdb_dir "$MAC_ARCH")"
|
||||
local BDB_ARGS=()
|
||||
if should_bundle_full_node_assets && [[ -f "$mac_bdb/lib/libdb-6.2.a" && -f "$mac_bdb/include/db.h" ]]; then
|
||||
BDB_ARGS=( -DBDB_INCLUDE_DIR="$mac_bdb/include" -DBDB_LIBRARY="$mac_bdb/lib/libdb-6.2.a" )
|
||||
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 \
|
||||
-DDRAGONX_ENABLE_UI_SWEEP=OFF \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||
-DCMAKE_OSX_ARCHITECTURES="$MAC_ARCHS" \
|
||||
"${BDB_ARGS[@]}" \
|
||||
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
|
||||
"${CMAKE_LITE_ARGS[@]}"
|
||||
fi
|
||||
|
||||
info "Building with $JOBS jobs ..."
|
||||
cmake --build . -j "$JOBS" --target dragonx_release
|
||||
cmake --build . -j "$JOBS"
|
||||
|
||||
[[ -f "bin/${APP_BASENAME}" ]] || { err "macOS build failed"; exit 1; }
|
||||
|
||||
# A full-node release MUST include the recovery helper. macOS needs a static libdb-6.2 (Homebrew
|
||||
# berkeley-db for a native build, or a vendored external/dragonx/depends/<triple>) — otherwise CMake
|
||||
# skips the target and this fails loudly rather than shipping a mac release with no recovery option.
|
||||
require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild" "$(mac_bdb_dir "$MAC_ARCH")"
|
||||
|
||||
# Strip — use osxcross strip for cross-builds
|
||||
if $IS_CROSS; then
|
||||
local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip"
|
||||
@@ -1206,12 +1012,8 @@ TOOLCHAIN
|
||||
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||
|
||||
# ── Create .app bundle ───────────────────────────────────────────────────
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
# Clean only THIS variant's prior artifacts so full-node and lite releases can
|
||||
# coexist in release/mac/ (Linux/Windows scope their cleanup the same way). The
|
||||
# "ObsidianDragon-" glob never matches "ObsidianDragonLite-" (and vice versa),
|
||||
# and the ".app" names are exact.
|
||||
rm -rf "$out/${APP_BASENAME}.app" "$out/${APP_BASENAME}-"*.app.zip "$out/${APP_BASENAME}-"*.dmg
|
||||
|
||||
local APP="$out/${APP_BASENAME}.app"
|
||||
local CONTENTS="$APP/Contents"
|
||||
@@ -1257,14 +1059,12 @@ TOOLCHAIN
|
||||
else
|
||||
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
|
||||
fi
|
||||
# Offline wallet-rebuild recovery helper — required (asserted after build); next to the daemon.
|
||||
cp "bin/dragonx-wallet-rebuild" "$MACOS/" && chmod +x "$MACOS/dragonx-wallet-rebuild" && info " Bundled dragonx-wallet-rebuild"
|
||||
else
|
||||
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
|
||||
fi
|
||||
|
||||
# xmrig binary (from prebuilt-binaries/drg-xmrig/)
|
||||
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig"
|
||||
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
|
||||
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
|
||||
if [[ -f "$XMRIG_MAC" ]]; then
|
||||
cp "$XMRIG_MAC" "$MACOS/xmrig"
|
||||
chmod +x "$MACOS/xmrig"
|
||||
@@ -1413,10 +1213,8 @@ PLIST
|
||||
fi
|
||||
|
||||
# ── Create DMG ───────────────────────────────────────────────────────────
|
||||
# DMG filename matches the app bundle name (ObsidianDragon / ObsidianDragonLite).
|
||||
# The mounted volume + CFBundleName keep the "DragonX Wallet" display branding
|
||||
# (APP_DISPLAY_NAME above).
|
||||
local DMG_BASENAME="${APP_BASENAME}"
|
||||
local DMG_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
|
||||
@@ -1498,9 +1296,3 @@ if $DO_LINUX || $DO_WIN || $DO_MAC; then
|
||||
[[ -d "$SCRIPT_DIR/release/windows" ]] && echo -e " ${CYAN}windows/${NC} — .exe + .zip"
|
||||
[[ -d "$SCRIPT_DIR/release/mac" ]] && echo -e " ${CYAN}mac/${NC} — .app + .dmg"
|
||||
fi
|
||||
|
||||
# Reaching here means the build completed (real failures exit 1 at their point of failure).
|
||||
# Exit 0 explicitly: the final `[[ -d release/mac ]] && echo` above returns non-zero on a
|
||||
# non-mac build — and since set -e exempts the left side of an &&, that status would otherwise
|
||||
# become the script's exit code and make a successful build report failure (e.g. to CI).
|
||||
exit 0
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# CMake script to embed binary files as C arrays
|
||||
|
||||
function(embed_resource RESOURCE_FILE OUTPUT_FILE VAR_NAME)
|
||||
file(READ "${RESOURCE_FILE}" HEX_CONTENT HEX)
|
||||
|
||||
# Convert hex to C array format
|
||||
string(LENGTH "${HEX_CONTENT}" HEX_LENGTH)
|
||||
math(EXPR BYTE_COUNT "${HEX_LENGTH} / 2")
|
||||
|
||||
set(ARRAY_CONTENT "")
|
||||
set(INDEX 0)
|
||||
while(INDEX LESS HEX_LENGTH)
|
||||
string(SUBSTRING "${HEX_CONTENT}" ${INDEX} 2 BYTE)
|
||||
string(APPEND ARRAY_CONTENT "0x${BYTE},")
|
||||
math(EXPR INDEX "${INDEX} + 2")
|
||||
math(EXPR BYTE_NUM "${INDEX} / 2")
|
||||
math(EXPR MOD "${BYTE_NUM} % 16")
|
||||
if(MOD EQUAL 0)
|
||||
string(APPEND ARRAY_CONTENT "\n ")
|
||||
endif()
|
||||
endwhile()
|
||||
|
||||
# Generate header file
|
||||
set(HEADER_CONTENT "// Auto-generated embedded resource - DO NOT EDIT
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace embedded {
|
||||
|
||||
inline constexpr unsigned char ${VAR_NAME}_data[] = {
|
||||
${ARRAY_CONTENT}
|
||||
};
|
||||
|
||||
inline constexpr size_t ${VAR_NAME}_size = ${BYTE_COUNT};
|
||||
|
||||
} // namespace embedded
|
||||
")
|
||||
|
||||
file(WRITE "${OUTPUT_FILE}" "${HEADER_CONTENT}")
|
||||
message(STATUS "Embedded ${RESOURCE_FILE} -> ${OUTPUT_FILE} (${BYTE_COUNT} bytes)")
|
||||
endfunction()
|
||||
@@ -1,31 +0,0 @@
|
||||
# patch-libwebp-simd.cmake — portable, idempotent FetchContent patch for libwebp.
|
||||
#
|
||||
# libwebp's cmake/cpu.cmake compiles its scalar *reference* DSP files with the
|
||||
# SSE-disable flags "-mno-sse4.1;-mno-sse2" whenever it can't positively detect
|
||||
# SSE support. Under a macOS *universal* build (-arch arm64;x86_64) the per-arch
|
||||
# SSE flag probe fails (a flag valid for x86_64 is invalid for arm64), so those
|
||||
# disable flags get applied to the x86_64 slice. clang gates the _Float16 type on
|
||||
# SSE2 for x86_64, and the macOS 15+/26 SDK's <math.h> declares _Float16 math
|
||||
# functions unconditionally — so any TU including <math.h> fails to compile with
|
||||
# "_Float16 is not supported on this target".
|
||||
#
|
||||
# SSE2 is part of the x86_64 baseline ABI, so disabling it on the reference files
|
||||
# is unnecessary on every platform we target. Blanking the SSE entries (indices
|
||||
# must stay aligned with WEBP_SIMD_FLAGS) fixes the universal build and is a no-op
|
||||
# for single-arch Linux/Windows/x86_64 builds. Idempotent: re-running is a no-op.
|
||||
if(NOT DEFINED CPU_CMAKE OR NOT EXISTS "${CPU_CMAKE}")
|
||||
message(FATAL_ERROR "patch-libwebp-simd: cpu.cmake not found at '${CPU_CMAKE}'")
|
||||
endif()
|
||||
|
||||
file(READ "${CPU_CMAKE}" _contents)
|
||||
string(REPLACE
|
||||
"set(SIMD_DISABLE_FLAGS \"-mno-sse4.1;-mno-sse2;;-mno-dspr2;;-mno-msa\")"
|
||||
"set(SIMD_DISABLE_FLAGS \";;;-mno-dspr2;;-mno-msa\")"
|
||||
_patched "${_contents}")
|
||||
|
||||
if(_patched STREQUAL _contents)
|
||||
message(STATUS "patch-libwebp-simd: no change (already patched or pattern absent)")
|
||||
else()
|
||||
file(WRITE "${CPU_CMAKE}" "${_patched}")
|
||||
message(STATUS "patch-libwebp-simd: neutralized x86 SSE-disable flags in cpu.cmake")
|
||||
endif()
|
||||
@@ -1,43 +0,0 @@
|
||||
**DragonX (DRGX) Lite Desktop Wallet — v1.1.1**
|
||||
|
||||
ObsidianDragonLite is the lightweight, node-free companion to ObsidianDragon — it connects to a DragonX lite-wallet server for near-instant startup, with the same native ImGui interface and shielded-first workflow. **v1.1.1 is a maintenance release** centered on an important wallet-encryption fix.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Security fix — please read
|
||||
|
||||
**Creating or restoring a Lite wallet with a passphrase now actually encrypts it.**
|
||||
|
||||
In prior versions, the create / restore / open flow used by the Settings UI **silently discarded the passphrase you entered**:
|
||||
|
||||
- A **newly created or restored** wallet was saved to disk **unencrypted** — the seed and spending keys were stored in the clear even though you set a passphrase.
|
||||
- **Opening** an already-encrypted wallet did not unlock it with the supplied passphrase (it opened locked and re-prompted).
|
||||
|
||||
v1.1.1 fixes this: create/restore now **encrypt** the wallet with your passphrase (the backend encrypts and locks it), and open now **unlocks** it.
|
||||
|
||||
**What you should do:** if you created or restored a Lite wallet **with a passphrase** on an earlier version, treat that wallet's on-disk data as unencrypted. Open it in v1.1.1 and re-apply encryption from **Settings → encrypt** (and, if you're concerned the plaintext wallet file may have been exposed, move the funds to a freshly created encrypted wallet). Wallets you never set a passphrase on are unaffected.
|
||||
|
||||
---
|
||||
|
||||
## Also fixed
|
||||
|
||||
- **Chinese, Japanese, and Korean FAQ text renders correctly.** Several Help/FAQ titles showed `??` instead of characters (e.g. 什么, 门) after the previous translation back-fill; the bundled CJK font subset now includes those glyphs.
|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
macOS builds are x86-64 (Intel; run on Apple Silicon via Rosetta 2).
|
||||
|
||||
| File | SHA-256 |
|
||||
|------|---------|
|
||||
| ObsidianDragonLite-1.1.1.AppImage | `323ff5f8658a5550aa12678ce4be37e32a2bfd3f3b0543074cc86a4267895dcf` |
|
||||
| ObsidianDragonLite-1.1.1-Linux-x64.zip | `e8604e479aed7e020946df45bf0d9373401354907e10565011694efa9978e631` |
|
||||
| ObsidianDragonLite-1.1.1.exe | `4cdcf291fe43c4fed04258aa55bb4e9b591f52780f9db5fb8f8bef522c43a351` |
|
||||
| ObsidianDragonLite-1.1.1-Windows-x64.zip | `c387bfcda508f32195c41fabec9f23d8b094d693a5dba41fbf8ec4abab6f2f6e` |
|
||||
| ObsidianDragonLite-1.1.1-macOS-x86_64.dmg | `bb0c491cb8f90ae55d162ec7c04c8f30988fa2f79c7911471220cddab44f99a5` |
|
||||
| ObsidianDragonLite-1.1.1-macOS-x86_64.app.zip | `a2a75fe40bf1f4db3abed8206dcc40d9fc74d8db2df7704afc9c834106b1c247` |
|
||||
|
||||
---
|
||||
|
||||
For the full feature set (node-free operation, wallet lifecycle, shielded + transparent send/shield/receive, HushChat, server browser with failover, 8-language UI), see the **lite-v1.0.0** release notes — v1.1.1 changes only the items listed above.
|
||||
@@ -1,41 +0,0 @@
|
||||
**DragonX (DRGX) Full-Node Desktop Wallet — v2.0.2**
|
||||
|
||||
ObsidianDragon is a native ImGui-based full-node wallet for the DragonX network, with an embedded `dragonxd` daemon and the integrated DRG-XMRig CPU miner. **v2.0.2 is a maintenance release** — a focused set of transaction-history, sync-safety, and correctness fixes on top of v2.0.x. No configuration changes are required; just replace the binary.
|
||||
|
||||
---
|
||||
|
||||
## What's Fixed in v2.0.2
|
||||
|
||||
### Transaction history
|
||||
|
||||
- **Outgoing shielded sends now appear in History.** Sends *from* a shielded (z-)address — both **z→z** (shielded-to-shielded) and **z→t** (deshield to a transparent address) — were missing from the History tab, because the daemon's `listtransactions` doesn't report them and `z_viewtransaction` doesn't expose the transparent leg. The wallet now enumerates them via `z_listsentbyaddress`, so **your History matches your balance.** (Your funds were never affected — only the display.)
|
||||
- **More accurate send rows** — a single transaction with two equal-value outputs to different recipients is no longer collapsed into one row, and a transparent self-send / change output is no longer double-counted as both sent and received.
|
||||
|
||||
### Sync & fund safety
|
||||
|
||||
- **A node that loses all its peers is no longer reported as "synced."** Previously, a wallet whose node dropped to **0 peers** could keep showing a stale balance as if it were current — and allow a Send against it. The wallet now treats a node with no peer-derived chain tip as *not synced*, and a new **"Connected, but no network peers — the node can't sync"** banner explains the stall instead of silently looking healthy.
|
||||
- **Migrate-to-seed hardening.** If a migrate-to-seed was interrupted (app or daemon restart) *after* the sweep transaction had broadcast but *before* it confirmed, the wallet could mistake the temporarily-zero balance for an empty wallet and adopt the new seed wallet — swapping `wallet.dat` — before the sweep was mined. It now records that a sweep is in flight and **will not swap your wallet until the sweep confirms**, so funds can't be stranded in the timestamped backup.
|
||||
- **Fixed a rare crash** caused by a data race on the daemon-status text (the daemon-monitor thread writing it while the UI thread read it).
|
||||
|
||||
### Display / internationalization
|
||||
|
||||
- **Chinese, Japanese, and Korean FAQ text renders correctly.** Several Help/FAQ titles showed `??` instead of characters (e.g. 什么, 门) after the previous translation back-fill; the bundled CJK font subset now includes those glyphs.
|
||||
|
||||
---
|
||||
|
||||
## Downloads
|
||||
|
||||
macOS builds are x86-64 (Intel; run on Apple Silicon via Rosetta 2).
|
||||
|
||||
| File | SHA-256 |
|
||||
|------|---------|
|
||||
| ObsidianDragon-2.0.2.AppImage | `9bf7328965c6348113ba7a30b0284f15787c635af17efae3dfa8a87e56e10c9b` |
|
||||
| ObsidianDragon-2.0.2-Linux-x64.zip | `1371880c4187a94d4cb56d535471142c5f1a8053ef73fffd99d59e7b78c75c42` |
|
||||
| ObsidianDragon-2.0.2.exe | `673347d2696fb6f5bd09dc25e415d5ae8e532abde75b8d48c3cd26545461de50` |
|
||||
| ObsidianDragon-2.0.2-Windows-x64.zip | `f5ea7c45e66c6c1b90a8127f96dee04235691acb913ddc86327f2060798cd28c` |
|
||||
| ObsidianDragon-2.0.2-macOS-x86_64.dmg | `7038580807a8e93e5443bdc04f2aede3ae1c30e5bc9f8776a207edb2e87771d1` |
|
||||
| ObsidianDragon-2.0.2-macOS-x86_64.app.zip | `78159512ca684b95528231fd46f2d07b5bcc6f2da4d815fc1ecb2ff52127f41c` |
|
||||
|
||||
---
|
||||
|
||||
For the full feature set (HushChat messaging, seed-phrase wallets & migrate-to-seed, in-app node/miner updaters, built-in mining, 8-language UI), see the **v2.0.0** release notes — v2.0.2 changes only the fixes listed above.
|
||||
@@ -67,8 +67,7 @@
|
||||
//#define IMGUI_USE_LEGACY_CRC32_ADLER
|
||||
|
||||
//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...)
|
||||
//---- Enabled so chat can render emoji (U+1F300+, above the BMP) — see Typography::loadFont emoji merge (Q12).
|
||||
#define IMGUI_USE_WCHAR32
|
||||
//#define IMGUI_USE_WCHAR32
|
||||
|
||||
//---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version
|
||||
// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files.
|
||||
|
||||
@@ -1,744 +0,0 @@
|
||||
// dear imgui: FreeType font builder (used as a replacement for the stb_truetype builder)
|
||||
// (code)
|
||||
|
||||
// Get the latest version at https://github.com/ocornut/imgui/tree/master/misc/freetype
|
||||
// Original code by @vuhdo (Aleksei Skriabin) in 2017, with improvements by @mikesart.
|
||||
// Maintained since 2019 by @ocornut.
|
||||
|
||||
// CHANGELOG
|
||||
// (minor and older changes stripped away, please see git history for details)
|
||||
// 2025/06/11: refactored for the new ImFontLoader architecture, and ImGuiBackendFlags_RendererHasTextures support.
|
||||
// 2024/10/17: added plutosvg support for SVG Fonts (seems faster/better than lunasvg). Enable by using '#define IMGUI_ENABLE_FREETYPE_PLUTOSVG'. (#7927)
|
||||
// 2023/11/13: added support for ImFontConfig::RasterizationDensity field for scaling render density without scaling metrics.
|
||||
// 2023/08/01: added support for SVG fonts, enable by using '#define IMGUI_ENABLE_FREETYPE_LUNASVG'. (#6591)
|
||||
// 2023/01/04: fixed a packing issue which in some occurrences would prevent large amount of glyphs from being packed correctly.
|
||||
// 2021/08/23: fixed crash when FT_Render_Glyph() fails to render a glyph and returns nullptr.
|
||||
// 2021/03/05: added ImGuiFreeTypeBuilderFlags_Bitmap to load bitmap glyphs.
|
||||
// 2021/03/02: set 'atlas->TexPixelsUseColors = true' to help some backends with deciding of a preferred texture format.
|
||||
// 2021/01/28: added support for color-layered glyphs via ImGuiFreeTypeBuilderFlags_LoadColor (require Freetype 2.10+).
|
||||
// 2021/01/26: simplified integration by using '#define IMGUI_ENABLE_FREETYPE'. renamed ImGuiFreeType::XXX flags to ImGuiFreeTypeBuilderFlags_XXX for consistency with other API. removed ImGuiFreeType::BuildFontAtlas().
|
||||
// 2020/06/04: fix for rare case where FT_Get_Char_Index() succeed but FT_Load_Glyph() fails.
|
||||
// 2019/02/09: added RasterizerFlags::Monochrome flag to disable font anti-aliasing (combine with ::MonoHinting for best results!)
|
||||
// 2019/01/15: added support for imgui allocators + added FreeType only override function SetAllocatorFunctions().
|
||||
// 2019/01/10: re-factored to match big update in STB builder. fixed texture height waste. fixed redundant glyphs when merging. support for glyph padding.
|
||||
// 2018/06/08: added support for ImFontConfig::GlyphMinAdvanceX, GlyphMaxAdvanceX.
|
||||
// 2018/02/04: moved to main imgui repository (away from http://www.github.com/ocornut/imgui_club)
|
||||
// 2018/01/22: fix for addition of ImFontAtlas::TexUvscale member.
|
||||
// 2017/10/22: minor inconsequential change to match change in master (removed an unnecessary statement).
|
||||
// 2017/09/26: fixes for imgui internal changes.
|
||||
// 2017/08/26: cleanup, optimizations, support for ImFontConfig::RasterizerFlags, ImFontConfig::RasterizerMultiply.
|
||||
// 2017/08/16: imported from https://github.com/Vuhdo/imgui_freetype into http://www.github.com/ocornut/imgui_club, updated for latest changes in ImFontAtlas, minor tweaks.
|
||||
|
||||
// About Gamma Correct Blending:
|
||||
// - FreeType assumes blending in linear space rather than gamma space.
|
||||
// - See https://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#FT_Render_Glyph
|
||||
// - For correct results you need to be using sRGB and convert to linear space in the pixel shader output.
|
||||
// - The default dear imgui styles will be impacted by this change (alpha values will need tweaking).
|
||||
|
||||
// FIXME: cfg.OversampleH, OversampleV are not supported, but generally not necessary with this rasterizer because Hinting makes everything look better.
|
||||
|
||||
#include "imgui.h"
|
||||
#ifndef IMGUI_DISABLE
|
||||
#include "imgui_freetype.h"
|
||||
#include "imgui_internal.h" // ImMin,ImMax,ImFontAtlasBuild*,
|
||||
#include <stdint.h>
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H // <freetype/freetype.h>
|
||||
#include FT_MODULE_H // <freetype/ftmodapi.h>
|
||||
#include FT_GLYPH_H // <freetype/ftglyph.h>
|
||||
#include FT_SIZES_H // <freetype/ftsizes.h>
|
||||
#include FT_SYNTHESIS_H // <freetype/ftsynth.h>
|
||||
|
||||
// Handle LunaSVG and PlutoSVG
|
||||
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) && defined(IMGUI_ENABLE_FREETYPE_PLUTOSVG)
|
||||
#error "Cannot enable both IMGUI_ENABLE_FREETYPE_LUNASVG and IMGUI_ENABLE_FREETYPE_PLUTOSVG"
|
||||
#endif
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
#include FT_OTSVG_H // <freetype/otsvg.h>
|
||||
#include FT_BBOX_H // <freetype/ftbbox.h>
|
||||
#include <lunasvg.h>
|
||||
#endif
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
#include <plutosvg.h>
|
||||
#endif
|
||||
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) || defined (IMGUI_ENABLE_FREETYPE_PLUTOSVG)
|
||||
#if !((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12))
|
||||
#error IMGUI_ENABLE_FREETYPE_PLUTOSVG or IMGUI_ENABLE_FREETYPE_LUNASVG requires FreeType version >= 2.12
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning (push)
|
||||
#pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff)
|
||||
#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3).
|
||||
#endif
|
||||
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind
|
||||
#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used
|
||||
#ifndef __clang__
|
||||
#pragma GCC diagnostic ignored "-Wsubobject-linkage" // warning: 'xxxx' has a field 'xxxx' whose type uses the anonymous namespace
|
||||
#endif
|
||||
#endif
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Data
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
// Default memory allocators
|
||||
static void* ImGuiFreeTypeDefaultAllocFunc(size_t size, void* user_data) { IM_UNUSED(user_data); return IM_ALLOC(size); }
|
||||
static void ImGuiFreeTypeDefaultFreeFunc(void* ptr, void* user_data) { IM_UNUSED(user_data); IM_FREE(ptr); }
|
||||
|
||||
// Current memory allocators
|
||||
static void* (*GImGuiFreeTypeAllocFunc)(size_t size, void* user_data) = ImGuiFreeTypeDefaultAllocFunc;
|
||||
static void (*GImGuiFreeTypeFreeFunc)(void* ptr, void* user_data) = ImGuiFreeTypeDefaultFreeFunc;
|
||||
static void* GImGuiFreeTypeAllocatorUserData = nullptr;
|
||||
|
||||
// Lunasvg support
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
static FT_Error ImGuiLunasvgPortInit(FT_Pointer* state);
|
||||
static void ImGuiLunasvgPortFree(FT_Pointer* state);
|
||||
static FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state);
|
||||
static FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state);
|
||||
#endif
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
// Code
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
#define FT_CEIL(X) (((X + 63) & -64) / 64) // From SDL_ttf: Handy routines for converting from fixed point
|
||||
#define FT_SCALEFACTOR 64.0f
|
||||
|
||||
// Glyph metrics:
|
||||
// --------------
|
||||
//
|
||||
// xmin xmax
|
||||
// | |
|
||||
// |<-------- width -------->|
|
||||
// | |
|
||||
// | +-------------------------+----------------- ymax
|
||||
// | | ggggggggg ggggg | ^ ^
|
||||
// | | g:::::::::ggg::::g | | |
|
||||
// | | g:::::::::::::::::g | | |
|
||||
// | | g::::::ggggg::::::gg | | |
|
||||
// | | g:::::g g:::::g | | |
|
||||
// offsetX -|-------->| g:::::g g:::::g | offsetY |
|
||||
// | | g:::::g g:::::g | | |
|
||||
// | | g::::::g g:::::g | | |
|
||||
// | | g:::::::ggggg:::::g | | |
|
||||
// | | g::::::::::::::::g | | height
|
||||
// | | gg::::::::::::::g | | |
|
||||
// baseline ---*---------|---- gggggggg::::::g-----*-------- |
|
||||
// / | | g:::::g | |
|
||||
// origin | | gggggg g:::::g | |
|
||||
// | | g:::::gg gg:::::g | |
|
||||
// | | g::::::ggg:::::::g | |
|
||||
// | | gg:::::::::::::g | |
|
||||
// | | ggg::::::ggg | |
|
||||
// | | gggggg | v
|
||||
// | +-------------------------+----------------- ymin
|
||||
// | |
|
||||
// |------------- advanceX ----------->|
|
||||
|
||||
// Stored in ImFontAtlas::FontLoaderData. ALLOCATED BY US.
|
||||
struct ImGui_ImplFreeType_Data
|
||||
{
|
||||
FT_Library Library;
|
||||
FT_MemoryRec_ MemoryManager;
|
||||
ImGui_ImplFreeType_Data() { memset((void*)this, 0, sizeof(*this)); }
|
||||
};
|
||||
|
||||
// Stored in ImFontConfig::FontLoaderData. ALLOCATED BY US.
|
||||
struct ImGui_ImplFreeType_FontSrcData
|
||||
{
|
||||
// Initialize from an external data buffer. Doesn't copy data, and you must ensure it stays valid up to this object lifetime.
|
||||
bool InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_user_flags);
|
||||
void CloseFont();
|
||||
ImGui_ImplFreeType_FontSrcData() { memset((void*)this, 0, sizeof(*this)); }
|
||||
~ImGui_ImplFreeType_FontSrcData() { CloseFont(); }
|
||||
|
||||
// Members
|
||||
FT_Face FtFace;
|
||||
ImGuiFreeTypeLoaderFlags UserFlags; // = ImFontConfig::FontLoaderFlags
|
||||
FT_Int32 LoadFlags;
|
||||
ImFontBaked* BakedLastActivated;
|
||||
};
|
||||
|
||||
// Stored in ImFontBaked::FontLoaderDatas: pointer to SourcesCount instances of this. ALLOCATED BY CORE.
|
||||
struct ImGui_ImplFreeType_FontSrcBakedData
|
||||
{
|
||||
FT_Size FtSize; // This represent a FT_Face with a given size.
|
||||
ImGui_ImplFreeType_FontSrcBakedData() { memset((void*)this, 0, sizeof(*this)); }
|
||||
};
|
||||
|
||||
bool ImGui_ImplFreeType_FontSrcData::InitFont(FT_Library ft_library, const ImFontConfig* src, ImGuiFreeTypeLoaderFlags extra_font_loader_flags)
|
||||
{
|
||||
FT_Error error = FT_New_Memory_Face(ft_library, (const FT_Byte*)src->FontData, (FT_Long)src->FontDataSize, (FT_Long)src->FontNo, &FtFace);
|
||||
if (error != 0)
|
||||
return false;
|
||||
error = FT_Select_Charmap(FtFace, FT_ENCODING_UNICODE);
|
||||
if (error != 0)
|
||||
return false;
|
||||
|
||||
// Convert to FreeType flags (NB: Bold and Oblique are processed separately)
|
||||
UserFlags = (ImGuiFreeTypeLoaderFlags)(src->FontLoaderFlags | extra_font_loader_flags);
|
||||
|
||||
LoadFlags = 0;
|
||||
if ((UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) == 0)
|
||||
LoadFlags |= FT_LOAD_NO_BITMAP;
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_NoHinting)
|
||||
LoadFlags |= FT_LOAD_NO_HINTING;
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_NoAutoHint)
|
||||
LoadFlags |= FT_LOAD_NO_AUTOHINT;
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_ForceAutoHint)
|
||||
LoadFlags |= FT_LOAD_FORCE_AUTOHINT;
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_LightHinting)
|
||||
LoadFlags |= FT_LOAD_TARGET_LIGHT;
|
||||
else if (UserFlags & ImGuiFreeTypeLoaderFlags_MonoHinting)
|
||||
LoadFlags |= FT_LOAD_TARGET_MONO;
|
||||
else
|
||||
LoadFlags |= FT_LOAD_TARGET_NORMAL;
|
||||
|
||||
if (UserFlags & ImGuiFreeTypeLoaderFlags_LoadColor)
|
||||
LoadFlags |= FT_LOAD_COLOR;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGui_ImplFreeType_FontSrcData::CloseFont()
|
||||
{
|
||||
if (FtFace)
|
||||
{
|
||||
FT_Done_Face(FtFace);
|
||||
FtFace = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static const FT_Glyph_Metrics* ImGui_ImplFreeType_LoadGlyph(ImGui_ImplFreeType_FontSrcData* src_data, uint32_t codepoint)
|
||||
{
|
||||
uint32_t glyph_index = FT_Get_Char_Index(src_data->FtFace, codepoint);
|
||||
if (glyph_index == 0)
|
||||
return nullptr;
|
||||
|
||||
// If this crash for you: FreeType 2.11.0 has a crash bug on some bitmap/colored fonts.
|
||||
// - https://gitlab.freedesktop.org/freetype/freetype/-/issues/1076
|
||||
// - https://github.com/ocornut/imgui/issues/4567
|
||||
// - https://github.com/ocornut/imgui/issues/4566
|
||||
// You can use FreeType 2.10, or the patched version of 2.11.0 in VcPkg, or probably any upcoming FreeType version.
|
||||
FT_Error error = FT_Load_Glyph(src_data->FtFace, glyph_index, src_data->LoadFlags);
|
||||
if (error)
|
||||
return nullptr;
|
||||
|
||||
// Need an outline for this to work
|
||||
FT_GlyphSlot slot = src_data->FtFace->glyph;
|
||||
#if defined(IMGUI_ENABLE_FREETYPE_LUNASVG) || defined(IMGUI_ENABLE_FREETYPE_PLUTOSVG)
|
||||
IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP || slot->format == FT_GLYPH_FORMAT_SVG);
|
||||
#else
|
||||
#if ((FREETYPE_MAJOR >= 2) && (FREETYPE_MINOR >= 12))
|
||||
IM_ASSERT(slot->format != FT_GLYPH_FORMAT_SVG && "The font contains SVG glyphs, you'll need to enable IMGUI_ENABLE_FREETYPE_PLUTOSVG or IMGUI_ENABLE_FREETYPE_LUNASVG in imconfig.h and install required libraries in order to use this font");
|
||||
#endif
|
||||
IM_ASSERT(slot->format == FT_GLYPH_FORMAT_OUTLINE || slot->format == FT_GLYPH_FORMAT_BITMAP);
|
||||
#endif // IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
|
||||
// Apply convenience transform (this is not picking from real "Bold"/"Italic" fonts! Merely applying FreeType helper transform. Oblique == Slanting)
|
||||
if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bold)
|
||||
FT_GlyphSlot_Embolden(slot);
|
||||
if (src_data->UserFlags & ImGuiFreeTypeLoaderFlags_Oblique)
|
||||
{
|
||||
FT_GlyphSlot_Oblique(slot);
|
||||
//FT_BBox bbox;
|
||||
//FT_Outline_Get_BBox(&slot->outline, &bbox);
|
||||
//slot->metrics.width = bbox.xMax - bbox.xMin;
|
||||
//slot->metrics.height = bbox.yMax - bbox.yMin;
|
||||
}
|
||||
|
||||
return &slot->metrics;
|
||||
}
|
||||
|
||||
static void ImGui_ImplFreeType_BlitGlyph(const FT_Bitmap* ft_bitmap, uint32_t* dst, uint32_t dst_pitch)
|
||||
{
|
||||
IM_ASSERT(ft_bitmap != nullptr);
|
||||
const uint32_t w = ft_bitmap->width;
|
||||
const uint32_t h = ft_bitmap->rows;
|
||||
const uint8_t* src = ft_bitmap->buffer;
|
||||
const uint32_t src_pitch = ft_bitmap->pitch;
|
||||
|
||||
switch (ft_bitmap->pixel_mode)
|
||||
{
|
||||
case FT_PIXEL_MODE_GRAY: // Grayscale image, 1 byte per pixel.
|
||||
{
|
||||
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
|
||||
for (uint32_t x = 0; x < w; x++)
|
||||
dst[x] = IM_COL32(255, 255, 255, src[x]);
|
||||
break;
|
||||
}
|
||||
case FT_PIXEL_MODE_MONO: // Monochrome image, 1 bit per pixel. The bits in each byte are ordered from MSB to LSB.
|
||||
{
|
||||
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
|
||||
{
|
||||
uint8_t bits = 0;
|
||||
const uint8_t* bits_ptr = src;
|
||||
for (uint32_t x = 0; x < w; x++, bits <<= 1)
|
||||
{
|
||||
if ((x & 7) == 0)
|
||||
bits = *bits_ptr++;
|
||||
dst[x] = IM_COL32(255, 255, 255, (bits & 0x80) ? 255 : 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FT_PIXEL_MODE_BGRA:
|
||||
{
|
||||
// FIXME: Converting pre-multiplied alpha to straight. Doesn't smell good.
|
||||
#define DE_MULTIPLY(color, alpha) ImMin((ImU32)(255.0f * (float)color / (float)(alpha + FLT_MIN) + 0.5f), 255u)
|
||||
for (uint32_t y = 0; y < h; y++, src += src_pitch, dst += dst_pitch)
|
||||
for (uint32_t x = 0; x < w; x++)
|
||||
{
|
||||
uint8_t r = src[x * 4 + 2], g = src[x * 4 + 1], b = src[x * 4], a = src[x * 4 + 3];
|
||||
dst[x] = IM_COL32(DE_MULTIPLY(r, a), DE_MULTIPLY(g, a), DE_MULTIPLY(b, a), a);
|
||||
}
|
||||
#undef DE_MULTIPLY
|
||||
break;
|
||||
}
|
||||
default:
|
||||
IM_ASSERT(0 && "FreeTypeFont::BlitGlyph(): Unknown bitmap pixel mode!");
|
||||
}
|
||||
}
|
||||
|
||||
// FreeType memory allocation callbacks
|
||||
static void* FreeType_Alloc(FT_Memory /*memory*/, long size)
|
||||
{
|
||||
return GImGuiFreeTypeAllocFunc((size_t)size, GImGuiFreeTypeAllocatorUserData);
|
||||
}
|
||||
|
||||
static void FreeType_Free(FT_Memory /*memory*/, void* block)
|
||||
{
|
||||
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
|
||||
}
|
||||
|
||||
static void* FreeType_Realloc(FT_Memory /*memory*/, long cur_size, long new_size, void* block)
|
||||
{
|
||||
// Implement realloc() as we don't ask user to provide it.
|
||||
if (block == nullptr)
|
||||
return GImGuiFreeTypeAllocFunc((size_t)new_size, GImGuiFreeTypeAllocatorUserData);
|
||||
|
||||
if (new_size == 0)
|
||||
{
|
||||
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (new_size > cur_size)
|
||||
{
|
||||
void* new_block = GImGuiFreeTypeAllocFunc((size_t)new_size, GImGuiFreeTypeAllocatorUserData);
|
||||
memcpy(new_block, block, (size_t)cur_size);
|
||||
GImGuiFreeTypeFreeFunc(block, GImGuiFreeTypeAllocatorUserData);
|
||||
return new_block;
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreeType_LoaderInit(ImFontAtlas* atlas)
|
||||
{
|
||||
IM_ASSERT(atlas->FontLoaderData == nullptr);
|
||||
ImGui_ImplFreeType_Data* bd = IM_NEW(ImGui_ImplFreeType_Data)();
|
||||
|
||||
// FreeType memory management: https://www.freetype.org/freetype2/docs/design/design-4.html
|
||||
bd->MemoryManager.user = nullptr;
|
||||
bd->MemoryManager.alloc = &FreeType_Alloc;
|
||||
bd->MemoryManager.free = &FreeType_Free;
|
||||
bd->MemoryManager.realloc = &FreeType_Realloc;
|
||||
|
||||
// https://www.freetype.org/freetype2/docs/reference/ft2-module_management.html#FT_New_Library
|
||||
FT_Error error = FT_New_Library(&bd->MemoryManager, &bd->Library);
|
||||
if (error != 0)
|
||||
{
|
||||
IM_DELETE(bd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If you don't call FT_Add_Default_Modules() the rest of code may work, but FreeType won't use our custom allocator.
|
||||
FT_Add_Default_Modules(bd->Library);
|
||||
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
// Install svg hooks for FreeType
|
||||
// https://freetype.org/freetype2/docs/reference/ft2-properties.html#svg-hooks
|
||||
// https://freetype.org/freetype2/docs/reference/ft2-svg_fonts.html#svg_fonts
|
||||
SVG_RendererHooks hooks = { ImGuiLunasvgPortInit, ImGuiLunasvgPortFree, ImGuiLunasvgPortRender, ImGuiLunasvgPortPresetSlot };
|
||||
FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", &hooks);
|
||||
#endif // IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
// With plutosvg, use provided hooks
|
||||
FT_Property_Set(bd->Library, "ot-svg", "svg-hooks", plutosvg_ft_svg_hooks());
|
||||
#endif // IMGUI_ENABLE_FREETYPE_PLUTOSVG
|
||||
|
||||
// Store our data
|
||||
atlas->FontLoaderData = (void*)bd;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ImGui_ImplFreeType_LoaderShutdown(ImFontAtlas* atlas)
|
||||
{
|
||||
ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData;
|
||||
IM_ASSERT(bd != nullptr);
|
||||
FT_Done_Library(bd->Library);
|
||||
IM_DELETE(bd);
|
||||
atlas->FontLoaderData = nullptr;
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreeType_FontSrcInit(ImFontAtlas* atlas, ImFontConfig* src)
|
||||
{
|
||||
ImGui_ImplFreeType_Data* bd = (ImGui_ImplFreeType_Data*)atlas->FontLoaderData;
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = IM_NEW(ImGui_ImplFreeType_FontSrcData);
|
||||
IM_ASSERT(src->FontLoaderData == nullptr);
|
||||
src->FontLoaderData = bd_font_data;
|
||||
|
||||
if (!bd_font_data->InitFont(bd->Library, src, (ImGuiFreeTypeLoaderFlags)atlas->FontLoaderFlags))
|
||||
{
|
||||
IM_DELETE(bd_font_data);
|
||||
src->FontLoaderData = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ImGui_ImplFreeType_FontSrcDestroy(ImFontAtlas* atlas, ImFontConfig* src)
|
||||
{
|
||||
IM_UNUSED(atlas);
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
IM_DELETE(bd_font_data);
|
||||
src->FontLoaderData = nullptr;
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreeType_FontBakedInit(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src)
|
||||
{
|
||||
IM_UNUSED(atlas);
|
||||
float size = baked->Size;
|
||||
if (src->MergeMode && src->SizePixels != 0.0f)
|
||||
size *= (src->SizePixels / baked->OwnerFont->Sources[0]->SizePixels);
|
||||
size *= src->ExtraSizeScale;
|
||||
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
bd_font_data->BakedLastActivated = baked;
|
||||
|
||||
// We use one FT_Size per (source + baked) combination.
|
||||
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
|
||||
IM_ASSERT(bd_baked_data != nullptr);
|
||||
IM_PLACEMENT_NEW(bd_baked_data) ImGui_ImplFreeType_FontSrcBakedData();
|
||||
|
||||
FT_New_Size(bd_font_data->FtFace, &bd_baked_data->FtSize);
|
||||
FT_Activate_Size(bd_baked_data->FtSize);
|
||||
|
||||
// Vuhdo 2017: "I'm not sure how to deal with font sizes properly. As far as I understand, currently ImGui assumes that the 'pixel_height'
|
||||
// is a maximum height of an any given glyph, i.e. it's the sum of font's ascender and descender. Seems strange to me.
|
||||
// FT_Set_Pixel_Sizes() doesn't seem to get us the same result."
|
||||
// (FT_Set_Pixel_Sizes() essentially calls FT_Request_Size() with FT_SIZE_REQUEST_TYPE_NOMINAL)
|
||||
const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
|
||||
FT_Size_RequestRec req;
|
||||
req.type = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Bitmap) ? FT_SIZE_REQUEST_TYPE_NOMINAL : FT_SIZE_REQUEST_TYPE_REAL_DIM;
|
||||
req.width = 0;
|
||||
req.height = (uint32_t)(size * 64 * rasterizer_density);
|
||||
req.horiResolution = 0;
|
||||
req.vertResolution = 0;
|
||||
FT_Request_Size(bd_font_data->FtFace, &req);
|
||||
|
||||
// Output
|
||||
if (src->MergeMode == false)
|
||||
{
|
||||
// Read metrics
|
||||
FT_Size_Metrics metrics = bd_baked_data->FtSize->metrics;
|
||||
const float scale = 1.0f / (rasterizer_density * src->ExtraSizeScale);
|
||||
baked->Ascent = (float)FT_CEIL(metrics.ascender) * scale; // The pixel extents above the baseline in pixels (typically positive).
|
||||
baked->Descent = (float)FT_CEIL(metrics.descender) * scale; // The extents below the baseline in pixels (typically negative).
|
||||
//LineSpacing = (float)FT_CEIL(metrics.height) * scale; // The baseline-to-baseline distance. Note that it usually is larger than the sum of the ascender and descender taken as absolute values. There is also no guarantee that no glyphs extend above or below subsequent baselines when using this distance. Think of it as a value the designer of the font finds appropriate.
|
||||
//LineGap = (float)FT_CEIL(metrics.height - metrics.ascender + metrics.descender) * scale; // The spacing in pixels between one row's descent and the next row's ascent.
|
||||
//MaxAdvanceWidth = (float)FT_CEIL(metrics.max_advance) * scale; // This field gives the maximum horizontal cursor advance for all glyphs in the font.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ImGui_ImplFreeType_FontBakedDestroy(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src)
|
||||
{
|
||||
IM_UNUSED(atlas);
|
||||
IM_UNUSED(baked);
|
||||
IM_UNUSED(src);
|
||||
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
|
||||
IM_ASSERT(bd_baked_data != nullptr);
|
||||
FT_Done_Size(bd_baked_data->FtSize);
|
||||
bd_baked_data->~ImGui_ImplFreeType_FontSrcBakedData(); // ~IM_PLACEMENT_DELETE()
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreeType_FontBakedLoadGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImFontBaked* baked, void* loader_data_for_baked_src, ImWchar codepoint, ImFontGlyph* out_glyph, float* out_advance_x)
|
||||
{
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
uint32_t glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint);
|
||||
if (glyph_index == 0)
|
||||
return false;
|
||||
|
||||
if (bd_font_data->BakedLastActivated != baked) // <-- could use id
|
||||
{
|
||||
// Activate current size
|
||||
ImGui_ImplFreeType_FontSrcBakedData* bd_baked_data = (ImGui_ImplFreeType_FontSrcBakedData*)loader_data_for_baked_src;
|
||||
FT_Activate_Size(bd_baked_data->FtSize);
|
||||
bd_font_data->BakedLastActivated = baked;
|
||||
}
|
||||
|
||||
const FT_Glyph_Metrics* metrics = ImGui_ImplFreeType_LoadGlyph(bd_font_data, codepoint);
|
||||
if (metrics == nullptr)
|
||||
return false;
|
||||
|
||||
FT_Face face = bd_font_data->FtFace;
|
||||
FT_GlyphSlot slot = face->glyph;
|
||||
const float rasterizer_density = src->RasterizerDensity * baked->RasterizerDensity;
|
||||
|
||||
// Load metrics only mode
|
||||
const float advance_x = (slot->advance.x / FT_SCALEFACTOR) / rasterizer_density;
|
||||
if (out_advance_x != NULL)
|
||||
{
|
||||
IM_ASSERT(out_glyph == NULL);
|
||||
*out_advance_x = advance_x;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Render glyph into a bitmap (currently held by FreeType)
|
||||
FT_Render_Mode render_mode = (bd_font_data->UserFlags & ImGuiFreeTypeLoaderFlags_Monochrome) ? FT_RENDER_MODE_MONO : FT_RENDER_MODE_NORMAL;
|
||||
FT_Error error = FT_Render_Glyph(slot, render_mode);
|
||||
const FT_Bitmap* ft_bitmap = &slot->bitmap;
|
||||
if (error != 0 || ft_bitmap == nullptr)
|
||||
return false;
|
||||
|
||||
const int w = (int)ft_bitmap->width;
|
||||
const int h = (int)ft_bitmap->rows;
|
||||
const bool is_visible = (w != 0 && h != 0);
|
||||
|
||||
// Prepare glyph
|
||||
out_glyph->Codepoint = codepoint;
|
||||
out_glyph->AdvanceX = advance_x;
|
||||
|
||||
// Pack and retrieve position inside texture atlas
|
||||
if (is_visible)
|
||||
{
|
||||
ImFontAtlasRectId pack_id = ImFontAtlasPackAddRect(atlas, w, h);
|
||||
if (pack_id == ImFontAtlasRectId_Invalid)
|
||||
{
|
||||
// Pathological out of memory case (TexMaxWidth/TexMaxHeight set too small?)
|
||||
IM_ASSERT(pack_id != ImFontAtlasRectId_Invalid && "Out of texture memory.");
|
||||
return false;
|
||||
}
|
||||
ImTextureRect* r = ImFontAtlasPackGetRect(atlas, pack_id);
|
||||
|
||||
// Render pixels to our temporary buffer
|
||||
atlas->Builder->TempBuffer.resize(w * h * 4);
|
||||
uint32_t* temp_buffer = (uint32_t*)atlas->Builder->TempBuffer.Data;
|
||||
ImGui_ImplFreeType_BlitGlyph(ft_bitmap, temp_buffer, w);
|
||||
|
||||
const float ref_size = baked->OwnerFont->Sources[0]->SizePixels;
|
||||
const float offsets_scale = (ref_size != 0.0f) ? (baked->Size / ref_size) : 1.0f;
|
||||
float font_off_x = ImFloor(src->GlyphOffset.x * offsets_scale + 0.5f); // Snap scaled offset.
|
||||
float font_off_y = ImFloor(src->GlyphOffset.y * offsets_scale + 0.5f) + baked->Ascent;
|
||||
float recip_h = 1.0f / rasterizer_density;
|
||||
float recip_v = 1.0f / rasterizer_density;
|
||||
|
||||
// Register glyph
|
||||
float glyph_off_x = (float)face->glyph->bitmap_left;
|
||||
float glyph_off_y = (float)-face->glyph->bitmap_top;
|
||||
out_glyph->X0 = glyph_off_x * recip_h + font_off_x;
|
||||
out_glyph->Y0 = glyph_off_y * recip_v + font_off_y;
|
||||
out_glyph->X1 = (glyph_off_x + w) * recip_h + font_off_x;
|
||||
out_glyph->Y1 = (glyph_off_y + h) * recip_v + font_off_y;
|
||||
out_glyph->Visible = true;
|
||||
out_glyph->Colored = (ft_bitmap->pixel_mode == FT_PIXEL_MODE_BGRA);
|
||||
out_glyph->PackId = pack_id;
|
||||
ImFontAtlasBakedSetFontGlyphBitmap(atlas, baked, src, out_glyph, r, (const unsigned char*)temp_buffer, ImTextureFormat_RGBA32, w * 4);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ImGui_ImplFreetype_FontSrcContainsGlyph(ImFontAtlas* atlas, ImFontConfig* src, ImWchar codepoint)
|
||||
{
|
||||
IM_UNUSED(atlas);
|
||||
ImGui_ImplFreeType_FontSrcData* bd_font_data = (ImGui_ImplFreeType_FontSrcData*)src->FontLoaderData;
|
||||
int glyph_index = FT_Get_Char_Index(bd_font_data->FtFace, codepoint);
|
||||
return glyph_index != 0;
|
||||
}
|
||||
|
||||
const ImFontLoader* ImGuiFreeType::GetFontLoader()
|
||||
{
|
||||
static ImFontLoader loader;
|
||||
loader.Name = "FreeType";
|
||||
loader.LoaderInit = ImGui_ImplFreeType_LoaderInit;
|
||||
loader.LoaderShutdown = ImGui_ImplFreeType_LoaderShutdown;
|
||||
loader.FontSrcInit = ImGui_ImplFreeType_FontSrcInit;
|
||||
loader.FontSrcDestroy = ImGui_ImplFreeType_FontSrcDestroy;
|
||||
loader.FontSrcContainsGlyph = ImGui_ImplFreetype_FontSrcContainsGlyph;
|
||||
loader.FontBakedInit = ImGui_ImplFreeType_FontBakedInit;
|
||||
loader.FontBakedDestroy = ImGui_ImplFreeType_FontBakedDestroy;
|
||||
loader.FontBakedLoadGlyph = ImGui_ImplFreeType_FontBakedLoadGlyph;
|
||||
loader.FontBakedSrcLoaderDataSize = sizeof(ImGui_ImplFreeType_FontSrcBakedData);
|
||||
return &loader;
|
||||
}
|
||||
|
||||
void ImGuiFreeType::SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data)
|
||||
{
|
||||
GImGuiFreeTypeAllocFunc = alloc_func;
|
||||
GImGuiFreeTypeFreeFunc = free_func;
|
||||
GImGuiFreeTypeAllocatorUserData = user_data;
|
||||
}
|
||||
|
||||
bool ImGuiFreeType::DebugEditFontLoaderFlags(unsigned int* p_font_loader_flags)
|
||||
{
|
||||
bool edited = false;
|
||||
edited |= ImGui::CheckboxFlags("NoHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_NoHinting);
|
||||
edited |= ImGui::CheckboxFlags("NoAutoHint", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_NoAutoHint);
|
||||
edited |= ImGui::CheckboxFlags("ForceAutoHint",p_font_loader_flags, ImGuiFreeTypeLoaderFlags_ForceAutoHint);
|
||||
edited |= ImGui::CheckboxFlags("LightHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_LightHinting);
|
||||
edited |= ImGui::CheckboxFlags("MonoHinting", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_MonoHinting);
|
||||
edited |= ImGui::CheckboxFlags("Bold", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Bold);
|
||||
edited |= ImGui::CheckboxFlags("Oblique", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Oblique);
|
||||
edited |= ImGui::CheckboxFlags("Monochrome", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Monochrome);
|
||||
edited |= ImGui::CheckboxFlags("LoadColor", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_LoadColor);
|
||||
edited |= ImGui::CheckboxFlags("Bitmap", p_font_loader_flags, ImGuiFreeTypeLoaderFlags_Bitmap);
|
||||
return edited;
|
||||
}
|
||||
|
||||
#ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
// For more details, see https://gitlab.freedesktop.org/freetype/freetype-demos/-/blob/master/src/rsvg-port.c
|
||||
// The original code from the demo is licensed under CeCILL-C Free Software License Agreement (https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/LICENSE.TXT)
|
||||
struct LunasvgPortState
|
||||
{
|
||||
FT_Error err = FT_Err_Ok;
|
||||
lunasvg::Matrix matrix;
|
||||
std::unique_ptr<lunasvg::Document> svg = nullptr;
|
||||
};
|
||||
|
||||
static FT_Error ImGuiLunasvgPortInit(FT_Pointer* _state)
|
||||
{
|
||||
*_state = IM_NEW(LunasvgPortState)();
|
||||
return FT_Err_Ok;
|
||||
}
|
||||
|
||||
static void ImGuiLunasvgPortFree(FT_Pointer* _state)
|
||||
{
|
||||
IM_DELETE(*(LunasvgPortState**)_state);
|
||||
}
|
||||
|
||||
static FT_Error ImGuiLunasvgPortRender(FT_GlyphSlot slot, FT_Pointer* _state)
|
||||
{
|
||||
LunasvgPortState* state = *(LunasvgPortState**)_state;
|
||||
|
||||
// If there was an error while loading the svg in ImGuiLunasvgPortPresetSlot(), the renderer hook still get called, so just returns the error.
|
||||
if (state->err != FT_Err_Ok)
|
||||
return state->err;
|
||||
|
||||
// rows is height, pitch (or stride) equals to width * sizeof(int32)
|
||||
lunasvg::Bitmap bitmap((uint8_t*)slot->bitmap.buffer, slot->bitmap.width, slot->bitmap.rows, slot->bitmap.pitch);
|
||||
#if LUNASVG_VERSION_MAJOR >= 3
|
||||
state->svg->render(bitmap, state->matrix); // state->matrix is already scaled and translated
|
||||
#else
|
||||
state->svg->setMatrix(state->svg->matrix().identity()); // Reset the svg matrix to the default value
|
||||
state->svg->render(bitmap, state->matrix); // state->matrix is already scaled and translated
|
||||
#endif
|
||||
state->err = FT_Err_Ok;
|
||||
return state->err;
|
||||
}
|
||||
|
||||
static FT_Error ImGuiLunasvgPortPresetSlot(FT_GlyphSlot slot, FT_Bool cache, FT_Pointer* _state)
|
||||
{
|
||||
FT_SVG_Document document = (FT_SVG_Document)slot->other;
|
||||
LunasvgPortState* state = *(LunasvgPortState**)_state;
|
||||
FT_Size_Metrics& metrics = document->metrics;
|
||||
|
||||
// This function is called twice, once in the FT_Load_Glyph() and another right before ImGuiLunasvgPortRender().
|
||||
// If it's the latter, don't do anything because it's // already done in the former.
|
||||
if (cache)
|
||||
return state->err;
|
||||
|
||||
state->svg = lunasvg::Document::loadFromData((const char*)document->svg_document, document->svg_document_length);
|
||||
if (state->svg == nullptr)
|
||||
{
|
||||
state->err = FT_Err_Invalid_SVG_Document;
|
||||
return state->err;
|
||||
}
|
||||
|
||||
#if LUNASVG_VERSION_MAJOR >= 3
|
||||
lunasvg::Box box = state->svg->boundingBox();
|
||||
#else
|
||||
lunasvg::Box box = state->svg->box();
|
||||
#endif
|
||||
double scale = std::min(metrics.x_ppem / box.w, metrics.y_ppem / box.h);
|
||||
double xx = (double)document->transform.xx / (1 << 16);
|
||||
double xy = -(double)document->transform.xy / (1 << 16);
|
||||
double yx = -(double)document->transform.yx / (1 << 16);
|
||||
double yy = (double)document->transform.yy / (1 << 16);
|
||||
double x0 = (double)document->delta.x / 64 * box.w / metrics.x_ppem;
|
||||
double y0 = -(double)document->delta.y / 64 * box.h / metrics.y_ppem;
|
||||
|
||||
#if LUNASVG_VERSION_MAJOR >= 3
|
||||
// Scale, transform and pre-translate the matrix for the rendering step
|
||||
state->matrix = lunasvg::Matrix::translated(-box.x, -box.y);
|
||||
state->matrix.multiply(lunasvg::Matrix(xx, xy, yx, yy, x0, y0));
|
||||
state->matrix.scale(scale, scale);
|
||||
|
||||
// Apply updated transformation to the bounding box
|
||||
box.transform(state->matrix);
|
||||
#else
|
||||
// Scale and transform, we don't translate the svg yet
|
||||
state->matrix.identity();
|
||||
state->matrix.scale(scale, scale);
|
||||
state->matrix.transform(xx, xy, yx, yy, x0, y0);
|
||||
state->svg->setMatrix(state->matrix);
|
||||
|
||||
// Pre-translate the matrix for the rendering step
|
||||
state->matrix.translate(-box.x, -box.y);
|
||||
|
||||
// Get the box again after the transformation
|
||||
box = state->svg->box();
|
||||
#endif
|
||||
|
||||
// Calculate the bitmap size
|
||||
slot->bitmap_left = FT_Int(box.x);
|
||||
slot->bitmap_top = FT_Int(-box.y);
|
||||
slot->bitmap.rows = (unsigned int)(ImCeil((float)box.h));
|
||||
slot->bitmap.width = (unsigned int)(ImCeil((float)box.w));
|
||||
slot->bitmap.pitch = slot->bitmap.width * 4;
|
||||
slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA;
|
||||
|
||||
// Compute all the bearings and set them correctly. The outline is scaled already, we just need to use the bounding box.
|
||||
double metrics_width = box.w;
|
||||
double metrics_height = box.h;
|
||||
double horiBearingX = box.x;
|
||||
double horiBearingY = -box.y;
|
||||
double vertBearingX = slot->metrics.horiBearingX / 64.0 - slot->metrics.horiAdvance / 64.0 / 2.0;
|
||||
double vertBearingY = (slot->metrics.vertAdvance / 64.0 - slot->metrics.height / 64.0) / 2.0;
|
||||
slot->metrics.width = FT_Pos(IM_ROUND(metrics_width * 64.0)); // Using IM_ROUND() assume width and height are positive
|
||||
slot->metrics.height = FT_Pos(IM_ROUND(metrics_height * 64.0));
|
||||
slot->metrics.horiBearingX = FT_Pos(horiBearingX * 64);
|
||||
slot->metrics.horiBearingY = FT_Pos(horiBearingY * 64);
|
||||
slot->metrics.vertBearingX = FT_Pos(vertBearingX * 64);
|
||||
slot->metrics.vertBearingY = FT_Pos(vertBearingY * 64);
|
||||
|
||||
if (slot->metrics.vertAdvance == 0)
|
||||
slot->metrics.vertAdvance = FT_Pos(metrics_height * 1.2 * 64.0);
|
||||
|
||||
state->err = FT_Err_Ok;
|
||||
return state->err;
|
||||
}
|
||||
|
||||
#endif // #ifdef IMGUI_ENABLE_FREETYPE_LUNASVG
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifdef __GNUC__
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning (pop)
|
||||
#endif
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
@@ -1,83 +0,0 @@
|
||||
// dear imgui: FreeType font builder (used as a replacement for the stb_truetype builder)
|
||||
// (headers)
|
||||
|
||||
#pragma once
|
||||
#include "imgui.h" // IMGUI_API
|
||||
#ifndef IMGUI_DISABLE
|
||||
|
||||
// Usage:
|
||||
// - Add '#define IMGUI_ENABLE_FREETYPE' in your imconfig to automatically enable support
|
||||
// for imgui_freetype in imgui. It is equivalent to selecting the default loader with:
|
||||
// io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader())
|
||||
|
||||
// Optional support for OpenType SVG fonts:
|
||||
// - Add '#define IMGUI_ENABLE_FREETYPE_PLUTOSVG' to use plutosvg (not provided). See #7927.
|
||||
// - Add '#define IMGUI_ENABLE_FREETYPE_LUNASVG' to use lunasvg (not provided). See #6591.
|
||||
|
||||
// Forward declarations
|
||||
struct ImFontAtlas;
|
||||
struct ImFontLoader;
|
||||
|
||||
// Hinting greatly impacts visuals (and glyph sizes).
|
||||
// - By default, hinting is enabled and the font's native hinter is preferred over the auto-hinter.
|
||||
// - When disabled, FreeType generates blurrier glyphs, more or less matches the stb_truetype.h
|
||||
// - The Default hinting mode usually looks good, but may distort glyphs in an unusual way.
|
||||
// - The Light hinting mode generates fuzzier glyphs but better matches Microsoft's rasterizer.
|
||||
// You can set those flags globally in ImFontAtlas::FontLoaderFlags
|
||||
// You can set those flags on a per font basis in ImFontConfig::FontLoaderFlags
|
||||
typedef unsigned int ImGuiFreeTypeLoaderFlags;
|
||||
enum ImGuiFreeTypeLoaderFlags_
|
||||
{
|
||||
ImGuiFreeTypeLoaderFlags_NoHinting = 1 << 0, // Disable hinting. This generally generates 'blurrier' bitmap glyphs when the glyph are rendered in any of the anti-aliased modes.
|
||||
ImGuiFreeTypeLoaderFlags_NoAutoHint = 1 << 1, // Disable auto-hinter.
|
||||
ImGuiFreeTypeLoaderFlags_ForceAutoHint = 1 << 2, // Indicates that the auto-hinter is preferred over the font's native hinter.
|
||||
ImGuiFreeTypeLoaderFlags_LightHinting = 1 << 3, // A lighter hinting algorithm for gray-level modes. Many generated glyphs are fuzzier but better resemble their original shape. This is achieved by snapping glyphs to the pixel grid only vertically (Y-axis), as is done by Microsoft's ClearType and Adobe's proprietary font renderer. This preserves inter-glyph spacing in horizontal text.
|
||||
ImGuiFreeTypeLoaderFlags_MonoHinting = 1 << 4, // Strong hinting algorithm that should only be used for monochrome output.
|
||||
ImGuiFreeTypeLoaderFlags_Bold = 1 << 5, // Styling: Should we artificially embolden the font?
|
||||
ImGuiFreeTypeLoaderFlags_Oblique = 1 << 6, // Styling: Should we slant the font, emulating italic style?
|
||||
ImGuiFreeTypeLoaderFlags_Monochrome = 1 << 7, // Disable anti-aliasing. Combine this with MonoHinting for best results!
|
||||
ImGuiFreeTypeLoaderFlags_LoadColor = 1 << 8, // Enable FreeType color-layered glyphs
|
||||
ImGuiFreeTypeLoaderFlags_Bitmap = 1 << 9, // Enable FreeType bitmap glyphs
|
||||
|
||||
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
ImGuiFreeTypeBuilderFlags_NoHinting = ImGuiFreeTypeLoaderFlags_NoHinting,
|
||||
ImGuiFreeTypeBuilderFlags_NoAutoHint = ImGuiFreeTypeLoaderFlags_NoAutoHint,
|
||||
ImGuiFreeTypeBuilderFlags_ForceAutoHint = ImGuiFreeTypeLoaderFlags_ForceAutoHint,
|
||||
ImGuiFreeTypeBuilderFlags_LightHinting = ImGuiFreeTypeLoaderFlags_LightHinting,
|
||||
ImGuiFreeTypeBuilderFlags_MonoHinting = ImGuiFreeTypeLoaderFlags_MonoHinting,
|
||||
ImGuiFreeTypeBuilderFlags_Bold = ImGuiFreeTypeLoaderFlags_Bold,
|
||||
ImGuiFreeTypeBuilderFlags_Oblique = ImGuiFreeTypeLoaderFlags_Oblique,
|
||||
ImGuiFreeTypeBuilderFlags_Monochrome = ImGuiFreeTypeLoaderFlags_Monochrome,
|
||||
ImGuiFreeTypeBuilderFlags_LoadColor = ImGuiFreeTypeLoaderFlags_LoadColor,
|
||||
ImGuiFreeTypeBuilderFlags_Bitmap = ImGuiFreeTypeLoaderFlags_Bitmap,
|
||||
#endif
|
||||
};
|
||||
|
||||
// Obsolete names (will be removed)
|
||||
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
typedef ImGuiFreeTypeLoaderFlags_ ImGuiFreeTypeBuilderFlags_;
|
||||
#endif
|
||||
|
||||
namespace ImGuiFreeType
|
||||
{
|
||||
// This is automatically assigned when using '#define IMGUI_ENABLE_FREETYPE'.
|
||||
// If you need to dynamically select between multiple builders:
|
||||
// - you can manually assign this builder with 'atlas->SetFontLoader(ImGuiFreeType::GetFontLoader())'
|
||||
// - prefer deep-copying this into your own ImFontLoader instance if you use hot-reloading that messes up static data.
|
||||
IMGUI_API const ImFontLoader* GetFontLoader();
|
||||
|
||||
// Override allocators. By default ImGuiFreeType will use IM_ALLOC()/IM_FREE()
|
||||
// However, as FreeType does lots of allocations we provide a way for the user to redirect it to a separate memory heap if desired.
|
||||
IMGUI_API void SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data = nullptr);
|
||||
|
||||
// Display UI to edit ImFontAtlas::FontLoaderFlags (shared) or ImFontConfig::FontLoaderFlags (single source)
|
||||
IMGUI_API bool DebugEditFontLoaderFlags(ImGuiFreeTypeLoaderFlags* p_font_loader_flags);
|
||||
|
||||
// Obsolete names (will be removed)
|
||||
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
//IMGUI_API const ImFontBuilderIO* GetBuilderForFreeType(); // Renamed/changed in 1.92. Change 'io.Fonts->FontBuilderIO = ImGuiFreeType::GetBuilderForFreeType()' to 'io.Fonts->SetFontLoader(ImGuiFreeType::GetFontLoader())' if you need runtime selection.
|
||||
//static inline bool BuildFontAtlas(ImFontAtlas* atlas, unsigned int flags = 0) { atlas->FontBuilderIO = GetBuilderForFreeType(); atlas->FontLoaderFlags = flags; return atlas->Build(); } // Prefer using '#define IMGUI_ENABLE_FREETYPE'
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<!-- Application identity —————————————————————————————— -->
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="DragonX.ObsidianDragon.Wallet"
|
||||
version="1.2.0.0"
|
||||
processorArchitecture="amd64"
|
||||
/>
|
||||
|
||||
<description>ObsidianDragon Wallet</description>
|
||||
|
||||
<!-- Common Controls v6 (themed buttons, etc.) ————————— -->
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"
|
||||
/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
|
||||
<!-- DPI awareness (Per-Monitor V2) ————————————————————— -->
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
|
||||
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<!-- Supported OS declarations (Windows 7 → 11) ———————— -->
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
<!-- Windows 10 / 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #d82652;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-2" d="M103.98,128s-6.29-24.7-18.73-34.43c-8.53-8.03-15.63-16.49-21.25-24.16-5.62,7.68-12.72,16.17-21.25,24.16-12.4,9.74-18.73,34.43-18.73,34.43-2.38-24.34,7.85-35.82,12.87-41.29,7.82-8.5,15.31-16.56,21.75-25.02-7.64-11.44-11.41-19.72-11.41-19.72-.89-3.27-3.84-6.64-3.84-6.64,6.08-8.1-1.6-16.98-1.6-16.98,5.79-5.62,6.71-10.02,8.32-18.34-1.96,22.35,4.02,39.09,13.93,54.12,9.84-15.03,15.81-31.77,13.86-54.12,1.6,8.35,2.52,12.72,8.32,18.34,0,0-7.68,8.88-1.6,16.98,0,0-2.95,3.38-3.84,6.64,0,0-3.77,8.28-11.37,19.72,6.43,8.45,13.97,16.56,21.75,25.02,4.97,5.47,15.21,16.95,12.83,41.29h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M55.33,61.62c-3.55,4.55-7.39,8.99-11.44,13.47-5.29-4.48-11.23-5.33-11.23-5.33,22.92-7.82,2.81-15.17.28-16.31C9.28,42.78,9.1,14.78,9.1,14.78c11.51,34.15,36.42,32.3,36.42,32.3.35-.21.67-.46.92-.71,1.64,3.27,4.58,8.67,8.88,15.24h0Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M68.62,40.41c-1.35,2.98-2.91,5.83-4.62,8.63-1.71-2.81-3.23-5.69-4.62-8.63,1.74-3.45,4.62-20.58,4.62-20.58,0,0,2.88,17.13,4.62,20.58Z"/>
|
||||
<path class="cls-1" d="M76.01,97.93l-3.48,2.34s-.1-4.44-3.52-1.84c-.42.32-2.38,2.21-.03,4.27,0,0-4.05,4.08-4.97,8.21-.92-4.12-4.97-8.21-4.97-8.21,2.34-2.06.39-3.95-.03-4.27-3.41-2.59-3.52,1.84-3.52,1.84l-3.48-2.34c.28-3.55.1-6.68-.46-9.42,4.69-4.94,8.85-9.88,12.47-14.61,3.66,4.72,7.78,9.67,12.47,14.61-.57,2.74-.75,5.86-.46,9.42Z"/>
|
||||
<path class="cls-1" d="M95.34,69.76s-5.94.85-11.23,5.33c-4.02-4.48-7.89-8.92-11.44-13.47,4.3-6.57,7.25-11.98,8.88-15.24.25.25.57.5.92.71,0,0,24.91,1.84,36.42-32.3,0,0-.18,28-23.84,38.66-2.52,1.14-22.64,8.5.28,16.31h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
+31
-1619
File diff suppressed because it is too large
Load Diff
+31
-1619
File diff suppressed because it is too large
Load Diff
+31
-1619
File diff suppressed because it is too large
Load Diff
+29
-1617
File diff suppressed because it is too large
Load Diff
+31
-1619
File diff suppressed because it is too large
Load Diff
+31
-1619
File diff suppressed because it is too large
Load Diff
+31
-1619
File diff suppressed because it is too large
Load Diff
+29
-1617
File diff suppressed because it is too large
Load Diff
@@ -19,8 +19,8 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
|
||||
--on-secondary = "#FFFFFF"
|
||||
--on-background = "#E8E6F0"
|
||||
--on-surface = "#E8E6F0"
|
||||
--on-surface-medium = "rgba(232,230,240,0.85)"
|
||||
--on-surface-disabled = "rgba(232,230,240,0.58)"
|
||||
--on-surface-medium = "rgba(232,230,240,0.72)"
|
||||
--on-surface-disabled = "rgba(232,230,240,0.40)"
|
||||
--error = "#FF5C72"
|
||||
--on-error = "#000000"
|
||||
--success = "#3DE8A0"
|
||||
@@ -61,7 +61,7 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
|
||||
--sidebar-badge = "rgba(232,230,240,1.0)"
|
||||
--sidebar-divider = "rgba(200,190,240,0.05)"
|
||||
--chart-line = "rgba(124,108,255,0.12)"
|
||||
--window-control = "rgba(232,230,240,0.85)"
|
||||
--window-control = "rgba(232,230,240,0.72)"
|
||||
--window-control-hover = "rgba(124,108,255,0.12)"
|
||||
--window-close-hover = "rgba(255,92,114,0.75)"
|
||||
--spinner-track = "rgba(200,190,240,0.08)"
|
||||
|
||||
@@ -19,16 +19,16 @@ images = { background_image = "backgrounds/texture/pop-light_bg.png", logo = "lo
|
||||
--on-secondary = "#FFFFFF"
|
||||
--on-background = "#1E1E2A"
|
||||
--on-surface = "#1E1E2A"
|
||||
--on-surface-medium = "rgba(30,30,42,0.86)"
|
||||
--on-surface-disabled = "rgba(30,30,42,0.62)"
|
||||
--on-surface-medium = "rgba(30,30,42,0.72)"
|
||||
--on-surface-disabled = "rgba(30,30,42,0.38)"
|
||||
--error = "#E0304A"
|
||||
--on-error = "#FFFFFF"
|
||||
--success = "#18A860"
|
||||
--on-success = "#FFFFFF"
|
||||
--warning = "#E09020"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(30,30,60,0.20)"
|
||||
--outline = "rgba(30,30,60,0.24)"
|
||||
--divider = "rgba(30,30,60,0.12)"
|
||||
--outline = "rgba(30,30,60,0.15)"
|
||||
--scrim = "rgba(0,0,0,0.45)"
|
||||
--surface-hover = "rgba(96,64,224,0.05)"
|
||||
--surface-alt = "rgba(96,64,224,0.02)"
|
||||
@@ -47,14 +47,14 @@ images = { background_image = "backgrounds/texture/pop-light_bg.png", logo = "lo
|
||||
--chart-hover-ring = "rgba(96,64,224,0.30)"
|
||||
--tooltip-bg = "rgba(36,34,52,0.94)"
|
||||
--tooltip-border = "rgba(96,64,224,0.16)"
|
||||
--glass-fill = "rgba(255,255,255,0.20)"
|
||||
--glass-fill = "rgba(255,255,255,0.55)"
|
||||
--glass-border = "rgba(30,30,60,0.10)"
|
||||
--glass-noise-tint = "rgba(96,64,224,0.02)"
|
||||
--tactile-top = "rgba(255,255,255,0.40)"
|
||||
--tactile-bottom = "rgba(255,255,255,0.05)"
|
||||
--hover-overlay = "rgba(96,64,224,0.10)"
|
||||
--hover-overlay = "rgba(96,64,224,0.04)"
|
||||
--active-overlay = "rgba(96,64,224,0.08)"
|
||||
--rim-light = "rgba(30,30,42,0.22)"
|
||||
--rim-light = "rgba(96,64,224,0.06)"
|
||||
--status-divider = "rgba(30,30,60,0.08)"
|
||||
--sidebar-hover = "rgba(96,64,224,0.07)"
|
||||
--sidebar-icon = "rgba(30,30,42,0.50)"
|
||||
|
||||
@@ -19,8 +19,8 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
|
||||
--on-secondary = "#000000"
|
||||
--on-background = "#D0D0D4"
|
||||
--on-surface = "#D0D0D4"
|
||||
--on-surface-medium = "rgba(208,208,212,0.85)"
|
||||
--on-surface-disabled = "rgba(208,208,212,0.58)"
|
||||
--on-surface-medium = "rgba(208,208,212,0.75)"
|
||||
--on-surface-disabled = "rgba(208,208,212,0.45)"
|
||||
--error = "#B07080"
|
||||
--on-error = "#000000"
|
||||
--success = "#7AAE7C"
|
||||
@@ -61,7 +61,7 @@ images = { background_image = "backgrounds/texture/dark_bg.png", logo = "logos/l
|
||||
--sidebar-badge = "rgba(208,208,212,1.0)"
|
||||
--sidebar-divider = "rgba(220,220,225,0.05)"
|
||||
--chart-line = "rgba(220,220,225,0.08)"
|
||||
--window-control = "rgba(208,208,212,0.85)"
|
||||
--window-control = "rgba(208,208,212,0.75)"
|
||||
--window-control-hover = "rgba(220,220,225,0.10)"
|
||||
--window-close-hover = "rgba(200,50,60,0.70)"
|
||||
--spinner-track = "rgba(220,220,225,0.08)"
|
||||
|
||||
@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FDF8F0", --elevation-1 = "#F5EDE0", --elevation-
|
||||
--on-secondary = "#FFFFFF"
|
||||
--on-background = "#3A2E22"
|
||||
--on-surface = "#3A2E22"
|
||||
--on-surface-medium = "rgba(58,46,34,0.86)"
|
||||
--on-surface-disabled = "rgba(58,46,34,0.62)"
|
||||
--on-surface-medium = "rgba(58,46,34,0.68)"
|
||||
--on-surface-disabled = "rgba(58,46,34,0.38)"
|
||||
--error = "#A0524A"
|
||||
--on-error = "#FFFFFF"
|
||||
--success = "#4E8A42"
|
||||
--success = "#6A8A5C"
|
||||
--on-success = "#FFFFFF"
|
||||
--warning = "#C08840"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(140,110,70,0.20)"
|
||||
--outline = "rgba(140,110,70,0.24)"
|
||||
--divider = "rgba(140,110,70,0.14)"
|
||||
--outline = "rgba(140,110,70,0.16)"
|
||||
--scrim = "rgba(30,20,10,0.45)"
|
||||
--surface-hover = "rgba(176,120,64,0.06)"
|
||||
--surface-alt = "rgba(176,120,64,0.03)"
|
||||
@@ -48,14 +48,14 @@ elevation = { --elevation-0 = "#FDF8F0", --elevation-1 = "#F5EDE0", --elevation-
|
||||
--chart-hover-ring = "rgba(176,120,64,0.28)"
|
||||
--tooltip-bg = "rgba(50,38,24,0.94)"
|
||||
--tooltip-border = "rgba(176,120,64,0.12)"
|
||||
--glass-fill = "rgba(255,252,245,0.20)"
|
||||
--glass-fill = "rgba(255,252,245,0.58)"
|
||||
--glass-border = "rgba(176,120,64,0.14)"
|
||||
--glass-noise-tint = "rgba(180,140,80,0.03)"
|
||||
--tactile-top = "rgba(255,255,248,0.50)"
|
||||
--tactile-bottom = "rgba(255,255,248,0.08)"
|
||||
--hover-overlay = "rgba(176,120,64,0.10)"
|
||||
--hover-overlay = "rgba(176,120,64,0.05)"
|
||||
--active-overlay = "rgba(176,120,64,0.10)"
|
||||
--rim-light = "rgba(58,46,34,0.22)"
|
||||
--rim-light = "rgba(212,160,108,0.10)"
|
||||
--status-divider = "rgba(176,120,64,0.10)"
|
||||
--sidebar-hover = "rgba(176,120,64,0.08)"
|
||||
--sidebar-icon = "rgba(58,46,34,0.50)"
|
||||
|
||||
@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FDFBFF", --elevation-1 = "#F5F0FA", --elevation-
|
||||
--on-secondary = "#FFFFFF"
|
||||
--on-background = "#1C1525"
|
||||
--on-surface = "#1C1525"
|
||||
--on-surface-medium = "rgba(28,21,37,0.86)"
|
||||
--on-surface-disabled = "rgba(28,21,37,0.62)"
|
||||
--on-surface-medium = "rgba(28,21,37,0.72)"
|
||||
--on-surface-disabled = "rgba(28,21,37,0.40)"
|
||||
--error = "#C62828"
|
||||
--on-error = "#FFFFFF"
|
||||
--success = "#2E7D32"
|
||||
--on-success = "#FFFFFF"
|
||||
--warning = "#E65100"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(120,80,160,0.20)"
|
||||
--outline = "rgba(120,80,160,0.24)"
|
||||
--divider = "rgba(120,80,160,0.12)"
|
||||
--outline = "rgba(120,80,160,0.14)"
|
||||
--scrim = "rgba(20,10,30,0.45)"
|
||||
--surface-hover = "rgba(140,107,175,0.06)"
|
||||
--surface-alt = "rgba(140,107,175,0.03)"
|
||||
@@ -48,14 +48,14 @@ elevation = { --elevation-0 = "#FDFBFF", --elevation-1 = "#F5F0FA", --elevation-
|
||||
--chart-hover-ring = "rgba(140,107,175,0.28)"
|
||||
--tooltip-bg = "rgba(32,24,48,0.94)"
|
||||
--tooltip-border = "rgba(140,107,175,0.12)"
|
||||
--glass-fill = "rgba(255,255,255,0.20)"
|
||||
--glass-fill = "rgba(255,255,255,0.55)"
|
||||
--glass-border = "rgba(140,107,175,0.14)"
|
||||
--glass-noise-tint = "rgba(180,140,220,0.03)"
|
||||
--tactile-top = "rgba(255,255,255,0.50)"
|
||||
--tactile-bottom = "rgba(255,255,255,0.08)"
|
||||
--hover-overlay = "rgba(140,107,175,0.10)"
|
||||
--hover-overlay = "rgba(140,107,175,0.05)"
|
||||
--active-overlay = "rgba(140,107,175,0.10)"
|
||||
--rim-light = "rgba(28,21,37,0.22)"
|
||||
--rim-light = "rgba(180,140,255,0.10)"
|
||||
--status-divider = "rgba(140,107,175,0.10)"
|
||||
--sidebar-hover = "rgba(140,107,175,0.08)"
|
||||
--sidebar-icon = "rgba(28,21,37,0.50)"
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
[theme]
|
||||
name = "Jade"
|
||||
author = "The Hush Developers"
|
||||
dark = true
|
||||
elevation = { --elevation-0 = "#071210", --elevation-1 = "#0C1A16", --elevation-2 = "#16261F", --elevation-3 = "#1D3128", --elevation-4 = "#243B30" }
|
||||
images = { background_image = "backgrounds/texture/jade_bg.png", logo = "logos/logo_ObsidianDragon_dark.png" }
|
||||
|
||||
[theme.palette]
|
||||
--primary = "#2FA07A"
|
||||
--primary-variant = "#1E7357"
|
||||
--primary-light = "#7FD1B5"
|
||||
--secondary = "#C9A24E"
|
||||
--secondary-variant = "#A8842F"
|
||||
--secondary-light = "#E0C583"
|
||||
--background = "#071210"
|
||||
--surface = "#0C1A16"
|
||||
--surface-variant = "#16261F"
|
||||
--on-primary = "#FFFFFF"
|
||||
--on-secondary = "#000000"
|
||||
--on-background = "#DCEDE4"
|
||||
--on-surface = "#DCEDE4"
|
||||
--on-surface-medium = "rgba(220,237,228,0.85)"
|
||||
--on-surface-disabled = "rgba(220,237,228,0.58)"
|
||||
--error = "#CF6679"
|
||||
--on-error = "#000000"
|
||||
--success = "#81C784"
|
||||
--on-success = "#000000"
|
||||
--warning = "#FFB74D"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(130,205,170,0.14)"
|
||||
--outline = "rgba(130,205,170,0.16)"
|
||||
--scrim = "rgba(0,0,0,0.6)"
|
||||
--surface-hover = "rgba(130,205,170,0.07)"
|
||||
--surface-alt = "rgba(130,205,170,0.05)"
|
||||
--surface-active = "rgba(130,205,170,0.10)"
|
||||
--glass-button = "rgba(130,205,170,0.06)"
|
||||
--glass-button-hover = "rgba(130,205,170,0.12)"
|
||||
--card-border = "rgba(130,205,170,0.26)"
|
||||
--text-shadow = "rgba(0,0,0,0.50)"
|
||||
--input-overlay-text = "rgba(220,237,228,0.30)"
|
||||
--slider-text = "rgba(220,237,228,0.85)"
|
||||
--thumb-fill = "rgba(130,205,170,0.15)"
|
||||
--thumb-border = "rgba(130,205,170,0.50)"
|
||||
--disabled-label = "rgba(130,205,170,0.18)"
|
||||
--chart-grid = "rgba(130,205,170,0.05)"
|
||||
--chart-crosshair = "rgba(130,205,170,0.15)"
|
||||
--chart-hover-ring = "rgba(130,205,170,0.30)"
|
||||
--tooltip-bg = "rgba(9,20,16,0.92)"
|
||||
--tooltip-border = "rgba(130,205,170,0.12)"
|
||||
--glass-fill = "rgba(130,205,170,0.08)"
|
||||
--glass-border = "rgba(47,160,122,0.30)"
|
||||
--glass-noise-tint = "rgba(130,205,170,0.03)"
|
||||
--tactile-top = "rgba(130,205,170,0.06)"
|
||||
--tactile-bottom = "rgba(130,205,170,0.0)"
|
||||
--hover-overlay = "rgba(130,205,170,0.05)"
|
||||
--active-overlay = "rgba(130,205,170,0.10)"
|
||||
--rim-light = "rgba(130,205,170,0.14)"
|
||||
--status-divider = "rgba(130,205,170,0.08)"
|
||||
--sidebar-hover = "rgba(130,205,170,0.10)"
|
||||
--sidebar-icon = "rgba(130,205,170,0.42)"
|
||||
--sidebar-badge = "rgba(220,237,228,1.0)"
|
||||
--sidebar-divider = "rgba(130,205,170,0.06)"
|
||||
--chart-line = "rgba(130,205,170,0.10)"
|
||||
--window-control = "rgba(220,237,228,0.78)"
|
||||
--window-control-hover = "rgba(130,205,170,0.12)"
|
||||
--window-close-hover = "rgba(232,17,35,0.78)"
|
||||
--spinner-track = "rgba(130,205,170,0.10)"
|
||||
--spinner-active = "rgba(79,184,154,0.85)"
|
||||
--shutdown-panel-bg = "rgba(7,18,14,0.90)"
|
||||
--shutdown-panel-border = "rgba(130,205,170,0.07)"
|
||||
--ram-bar-app = "#2FA07A"
|
||||
--ram-bar-system = "rgba(255,255,255,0.18)"
|
||||
--accent-total = "#7FD1B5"
|
||||
--accent-shielded = "#4FB89A"
|
||||
--accent-transparent = "#C9A24E"
|
||||
--accent-action = "#2FA07A"
|
||||
--accent-market = "#4FB89A"
|
||||
--accent-portfolio = "#7FD1B5"
|
||||
--toast-info-accent = "#2FA07A"
|
||||
--toast-info-text = "#7FD1B5"
|
||||
--toast-success-accent = "rgba(50,180,80,1.0)"
|
||||
--toast-success-text = "rgba(180,255,180,1.0)"
|
||||
--toast-warning-accent = "rgba(204,166,50,1.0)"
|
||||
--toast-warning-text = "rgba(255,230,130,1.0)"
|
||||
--toast-error-accent = "rgba(204,64,64,1.0)"
|
||||
--toast-error-text = "rgba(255,153,153,1.0)"
|
||||
--snackbar-bg = "rgba(24,40,34,0.95)"
|
||||
--snackbar-text = "rgba(220,237,228,0.87)"
|
||||
--snackbar-action = "rgba(79,184,154,1.0)"
|
||||
--snackbar-action-hover = "rgba(127,209,181,1.0)"
|
||||
--switch-track-off = "rgba(130,205,170,0.12)"
|
||||
--switch-track-on = "rgba(47,160,122,0.50)"
|
||||
--switch-thumb-off = "#A0C0B4"
|
||||
--switch-thumb-on = "#DCEDE4"
|
||||
--control-shadow = "rgba(0,0,0,0.24)"
|
||||
--checkbox-check = "#000000"
|
||||
--app-bar-shadow = "rgba(0,0,0,0.25)"
|
||||
|
||||
[backdrop]
|
||||
base-color-top = "rgba(14,32,26,210)"
|
||||
base-color-bottom = "rgba(6,18,14,210)"
|
||||
texture-tint-alpha = 120
|
||||
gradient-top-r = 10
|
||||
gradient-top-g = 30
|
||||
gradient-top-b = 22
|
||||
gradient-top-a = 90
|
||||
gradient-bottom-r = 5
|
||||
gradient-bottom-g = 16
|
||||
gradient-bottom-b = 12
|
||||
gradient-bottom-a = 70
|
||||
background-alpha = 0.42
|
||||
surface-alpha = 0.56
|
||||
frame-alpha = 0.78
|
||||
surface-inline-alpha = 0.58
|
||||
background-inline-alpha = 0.40
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Theme Visual Effects — Jade (veins of gold shifting through the stone)
|
||||
# Jade's signature is a slow jade→gold color-shifting border on every glass
|
||||
# panel + the active nav button — a vein of gold surfacing through nephrite.
|
||||
# It's drawn via AddRect so it hugs the real rounded corners (no polygonal
|
||||
# edge-trace). Sparse jade motes drift up the viewport. No other theme turns
|
||||
# gradient-border-panels on, so the panel-wide vein is Jade's own —
|
||||
# deliberately NOT Obsidian's specular glare.
|
||||
# ---------------------------------------------------------------------------
|
||||
[effects]
|
||||
hue-cycle-enabled = { size = 0.0 }
|
||||
rainbow-border-enabled = { size = 0.0 }
|
||||
|
||||
# No shimmer sweep — replaced by specular glare
|
||||
shimmer-enabled = { size = 0.0 }
|
||||
|
||||
positional-hue-enabled = { size = 0.0 }
|
||||
|
||||
glow-pulse-enabled = { size = 0.0 }
|
||||
|
||||
# Edge-trace OFF — its hand-walked perimeter chamfers rounded corners.
|
||||
# Jade's vein is the gradient-border below (corner-clean via AddRect).
|
||||
edge-trace-enabled = { size = 0.0 }
|
||||
edge-trace-speed = { size = 0.16 }
|
||||
edge-trace-length = { size = 0.34 }
|
||||
edge-trace-thickness = { size = 1.6 }
|
||||
edge-trace-alpha = { size = 0.55 }
|
||||
edge-trace-color = { color = "#C9A24E" }
|
||||
|
||||
# Specular glare OFF — that's Obsidian's signature; Jade shouldn't echo it.
|
||||
specular-glare-enabled = { size = 0.0 }
|
||||
specular-glare-speed = { size = 0.018 }
|
||||
specular-glare-intensity = { size = 0.008 }
|
||||
specular-glare-radius = { size = 0.65 }
|
||||
specular-glare-count = { size = 1.0 }
|
||||
specular-glare-color = { color = "rgba(150,220,180,1.0)" }
|
||||
|
||||
# HERO — vein of gold: a slow jade→gold color-shifting border on the active
|
||||
# nav button AND (via gradient-border-panels) every glass panel. Drawn with
|
||||
# AddRect so it follows the rounded corners exactly. Panels drift at a softer
|
||||
# alpha and position-phased offset, so a screenful reads like veins at
|
||||
# different depths rather than one synchronized pulse.
|
||||
gradient-border-enabled = { size = 1.0 }
|
||||
gradient-border-panels = { size = 1.0 }
|
||||
gradient-border-speed = { size = 0.10 }
|
||||
gradient-border-thickness = { size = 1.5 }
|
||||
gradient-border-alpha = { size = 0.55 }
|
||||
gradient-border-color-a = { color = "#7FD1B5" }
|
||||
gradient-border-color-b = { color = "#C9A24E" }
|
||||
|
||||
# Ambient jade motes — sparse, slow, cool green particles drifting up the
|
||||
# viewport (recolored ember-rise; a different mood from dragonx's fire embers).
|
||||
ember-rise-enabled = { size = 1.0 }
|
||||
ember-rise-count = { size = 5.0 }
|
||||
ember-rise-speed = { size = 0.18 }
|
||||
ember-rise-particle-size = { size = 1.4 }
|
||||
ember-rise-alpha = { size = 0.26 }
|
||||
ember-rise-color = { color = "#7FD1B5" }
|
||||
|
||||
# Shader-like viewport overlay — deep green stone atmosphere
|
||||
viewport-wash-enabled = { size = 1.0 }
|
||||
viewport-wash-alpha = { size = 0.05 }
|
||||
viewport-wash-tl = { color = "#12402E" }
|
||||
viewport-wash-tr = { color = "#0E3828" }
|
||||
viewport-wash-bl = { color = "#16442E" }
|
||||
viewport-wash-br = { color = "#1A4A34" }
|
||||
viewport-wash-rotate = { size = 0.015 }
|
||||
viewport-wash-pulse = { size = 0.0 }
|
||||
viewport-wash-pulse-depth = { size = 0.0 }
|
||||
|
||||
viewport-vignette-enabled = { size = 1.0 }
|
||||
viewport-vignette-color = { color = "#04140D" }
|
||||
viewport-vignette-radius = { size = 0.22 }
|
||||
viewport-vignette-alpha = { size = 0.15 }
|
||||
@@ -19,16 +19,16 @@ elevation = { --elevation-0 = "#FAFAFA", --elevation-1 = "#F2F3F5", --elevation-
|
||||
--on-secondary = "#FFFFFF"
|
||||
--on-background = "#2A2C30"
|
||||
--on-surface = "#2A2C30"
|
||||
--on-surface-medium = "rgba(42,44,48,0.86)"
|
||||
--on-surface-disabled = "rgba(42,44,48,0.62)"
|
||||
--on-surface-medium = "rgba(42,44,48,0.68)"
|
||||
--on-surface-disabled = "rgba(42,44,48,0.38)"
|
||||
--error = "#8C5A62"
|
||||
--on-error = "#FFFFFF"
|
||||
--success = "#3D7A42"
|
||||
--success = "#5A7E5C"
|
||||
--on-success = "#FFFFFF"
|
||||
--warning = "#9A7A2E"
|
||||
--warning = "#8A7A52"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(42,44,48,0.20)"
|
||||
--outline = "rgba(42,44,48,0.24)"
|
||||
--divider = "rgba(42,44,48,0.12)"
|
||||
--outline = "rgba(42,44,48,0.14)"
|
||||
--scrim = "rgba(0,0,0,0.42)"
|
||||
--surface-hover = "rgba(42,44,48,0.04)"
|
||||
--surface-alt = "rgba(42,44,48,0.02)"
|
||||
@@ -47,14 +47,14 @@ elevation = { --elevation-0 = "#FAFAFA", --elevation-1 = "#F2F3F5", --elevation-
|
||||
--chart-hover-ring = "rgba(42,44,48,0.24)"
|
||||
--tooltip-bg = "rgba(50,52,58,0.92)"
|
||||
--tooltip-border = "rgba(42,44,48,0.10)"
|
||||
--glass-fill = "rgba(255,255,255,0.20)"
|
||||
--glass-fill = "rgba(255,255,255,0.55)"
|
||||
--glass-border = "rgba(42,44,48,0.10)"
|
||||
--glass-noise-tint = "rgba(42,44,48,0.015)"
|
||||
--tactile-top = "rgba(255,255,255,0.35)"
|
||||
--tactile-bottom = "rgba(255,255,255,0.04)"
|
||||
--hover-overlay = "rgba(42,44,48,0.10)"
|
||||
--hover-overlay = "rgba(42,44,48,0.04)"
|
||||
--active-overlay = "rgba(42,44,48,0.08)"
|
||||
--rim-light = "rgba(42,44,48,0.22)"
|
||||
--rim-light = "rgba(42,44,48,0.06)"
|
||||
--status-divider = "rgba(42,44,48,0.08)"
|
||||
--sidebar-hover = "rgba(42,44,48,0.05)"
|
||||
--sidebar-icon = "rgba(42,44,48,0.45)"
|
||||
|
||||
@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FAFAF8", --elevation-1 = "#F0EEEC", --elevation-
|
||||
--on-secondary = "#FFFFFF"
|
||||
--on-background = "#2C2A28"
|
||||
--on-surface = "#2C2A28"
|
||||
--on-surface-medium = "rgba(44,42,40,0.86)"
|
||||
--on-surface-disabled = "rgba(44,42,40,0.62)"
|
||||
--on-surface-medium = "rgba(44,42,40,0.68)"
|
||||
--on-surface-disabled = "rgba(44,42,40,0.38)"
|
||||
--error = "#8C5250"
|
||||
--on-error = "#FFFFFF"
|
||||
--success = "#3F7A48"
|
||||
--success = "#5C7A62"
|
||||
--on-success = "#FFFFFF"
|
||||
--warning = "#9A7A2E"
|
||||
--warning = "#8A7A4C"
|
||||
--on-warning = "#000000"
|
||||
--divider = "rgba(80,75,68,0.20)"
|
||||
--outline = "rgba(80,75,68,0.24)"
|
||||
--divider = "rgba(80,75,68,0.12)"
|
||||
--outline = "rgba(80,75,68,0.14)"
|
||||
--scrim = "rgba(20,18,16,0.42)"
|
||||
--surface-hover = "rgba(110,117,128,0.05)"
|
||||
--surface-alt = "rgba(110,117,128,0.025)"
|
||||
@@ -48,14 +48,14 @@ elevation = { --elevation-0 = "#FAFAF8", --elevation-1 = "#F0EEEC", --elevation-
|
||||
--chart-hover-ring = "rgba(110,117,128,0.24)"
|
||||
--tooltip-bg = "rgba(44,42,40,0.94)"
|
||||
--tooltip-border = "rgba(110,117,128,0.10)"
|
||||
--glass-fill = "rgba(255,255,254,0.20)"
|
||||
--glass-fill = "rgba(255,255,254,0.62)"
|
||||
--glass-border = "rgba(110,117,128,0.10)"
|
||||
--glass-noise-tint = "rgba(80,75,68,0.02)"
|
||||
--tactile-top = "rgba(255,255,255,0.45)"
|
||||
--tactile-bottom = "rgba(255,255,255,0.06)"
|
||||
--hover-overlay = "rgba(110,117,128,0.10)"
|
||||
--hover-overlay = "rgba(110,117,128,0.04)"
|
||||
--active-overlay = "rgba(110,117,128,0.08)"
|
||||
--rim-light = "rgba(44,42,40,0.22)"
|
||||
--rim-light = "rgba(180,175,168,0.10)"
|
||||
--status-divider = "rgba(110,117,128,0.08)"
|
||||
--sidebar-hover = "rgba(110,117,128,0.06)"
|
||||
--sidebar-icon = "rgba(44,42,40,0.48)"
|
||||
|
||||
@@ -19,8 +19,8 @@ images = { background_image = "backgrounds/texture/obsidian_bg.png", logo = "log
|
||||
--on-secondary = "#000000"
|
||||
--on-background = "#E8E0F0"
|
||||
--on-surface = "#E8E0F0"
|
||||
--on-surface-medium = "rgba(232,224,240,0.85)"
|
||||
--on-surface-disabled = "rgba(232,224,240,0.58)"
|
||||
--on-surface-medium = "rgba(232,224,240,0.75)"
|
||||
--on-surface-disabled = "rgba(232,224,240,0.45)"
|
||||
--error = "#CF6679"
|
||||
--on-error = "#000000"
|
||||
--success = "#81C784"
|
||||
|
||||
+8
-44
@@ -37,8 +37,8 @@ images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/l
|
||||
--on-secondary = "#000000"
|
||||
--on-background = "#F0E0D8"
|
||||
--on-surface = "#F0E0D8"
|
||||
--on-surface-medium = "rgba(240,224,216,0.85)"
|
||||
--on-surface-disabled = "rgba(240,224,216,0.58)"
|
||||
--on-surface-medium = "rgba(240,224,216,0.7)"
|
||||
--on-surface-disabled = "rgba(240,224,216,0.44)"
|
||||
--error = "#FF5252"
|
||||
--on-error = "#000000"
|
||||
--success = "#81C784"
|
||||
@@ -700,13 +700,6 @@ status-pill-bg-alpha = { size = 30 }
|
||||
status-pill-y-offset = { size = 1 }
|
||||
confirmed-threshold = { size = 10 }
|
||||
|
||||
# NOTE: [banners.*] and [layout] are parsed as flat sections (kFlatSections in ui_schema.cpp).
|
||||
# Persistent node/RPC error strip at the top of the content column (see App::renderNodeStatusBanner).
|
||||
# Slightly taller than the per-tab sync banner so it comfortably holds the Reconnect/Restart action.
|
||||
[banners.node-status]
|
||||
min-height = { size = 26.0 }
|
||||
height = { size = 30.0 }
|
||||
|
||||
[tabs.transactions]
|
||||
search-max-width = 300.0
|
||||
search-width-ratio = 0.3
|
||||
@@ -881,7 +874,7 @@ accent-stripe-inset-ratio = { size = 0.0 }
|
||||
accent-stripe-left-offset = { size = 0.0 }
|
||||
accent-stripe-width = { size = 4.0 }
|
||||
accent-stripe-rounding = { size = 1.5 }
|
||||
chart-y-axis-min-padding = { size = 54.0 }
|
||||
chart-y-axis-min-padding = { size = 40.0 }
|
||||
chart-y-axis-padding = { size = 70.0 }
|
||||
chart-dot-min-radius = { size = 1.5 }
|
||||
chart-dot-radius = { size = 2.0 }
|
||||
@@ -915,7 +908,6 @@ scroll-fade-zone = { size = 24.0 }
|
||||
|
||||
[tabs.console]
|
||||
input-area-padding = 8.0
|
||||
bg-darken-alpha = { size = 110.0 } # black overlay alpha (0-255) for the terminal-dark output + input
|
||||
output-line-spacing = 2.0
|
||||
output = { line-spacing = 2 }
|
||||
scroll-multiplier = { size = 3.0 }
|
||||
@@ -938,7 +930,7 @@ scanline-speed = { size = 40.0 }
|
||||
scanline-height = { size = 36.0 }
|
||||
scanline-alpha = { size = 8.0 }
|
||||
scanline-gap = { size = 2.0 }
|
||||
scanline-line-alpha = { size = 2.0 }
|
||||
scanline-line-alpha = { size = 4.0 }
|
||||
scanline-glow-spread = { size = 4.0 }
|
||||
scanline-glow-intensity = { size = 0.6 }
|
||||
scanline-glow-color = { size = 255.0 }
|
||||
@@ -966,11 +958,6 @@ edition-label = { position = 120 }
|
||||
link-button = { width = 100, font = "button-sm" }
|
||||
close-button = { width = 120, font = "button", align = "center" }
|
||||
|
||||
[dialogs.faq]
|
||||
width = 860.0
|
||||
height = 660.0
|
||||
window = { width = 860, height = 660 }
|
||||
|
||||
[dialogs.settings]
|
||||
width = 600.0
|
||||
height = 550.0
|
||||
@@ -1098,7 +1085,6 @@ fee-input = { width = 150 }
|
||||
utxo-limit-input = { width = 150 }
|
||||
shield-button = { width = 120, font = "button" }
|
||||
cancel-button = { width = 100, font = "button" }
|
||||
list-max-rows = { size = 7 } # address picker / run rows shown before the list scrolls
|
||||
|
||||
[dialogs.request-payment]
|
||||
width = 500.0
|
||||
@@ -1204,16 +1190,6 @@ connection-dot = { opacity = 1.0 }
|
||||
height = 30
|
||||
padding = [8, 0]
|
||||
|
||||
[components.startup-files-chip]
|
||||
width = { size = 340.0 }
|
||||
height = { size = 46.0 }
|
||||
bottom-margin = { size = 38.0 }
|
||||
rounding = { size = 10.0 }
|
||||
padding-x = { size = 14.0 }
|
||||
bar-height = { size = 3.0 }
|
||||
bar-bottom = { size = 10.0 }
|
||||
show-delay-seconds = { size = 0.4 }
|
||||
|
||||
[components.notifications]
|
||||
width = 350.0
|
||||
height = 60.0
|
||||
@@ -1225,14 +1201,12 @@ pill-margin-y = { size = 3.0 }
|
||||
pill-rounding = { size = 12.0 }
|
||||
padding-x = { size = 12.0 }
|
||||
glass-fill-alpha = { size = 18.0 }
|
||||
accent-fill-alpha = { size = 0.22 } # severity-tint fill opacity (fraction of the accent alpha)
|
||||
badge-fill-alpha = { size = 0.35 } # "+N more" badge fill opacity (fraction of the accent alpha)
|
||||
glass-border-alpha = { size = 35.0 }
|
||||
progress-bar-height = { size = 2.0 }
|
||||
duration-info = { size = 1.0 }
|
||||
duration-success = { size = 2.5 }
|
||||
duration-warning = { size = 7.0 }
|
||||
duration-error = { size = 9.0 }
|
||||
duration-warning = { size = 3.5 }
|
||||
duration-error = { size = 4.0 }
|
||||
window = { width = 350, height = 60, border-radius = 8, border-width = 1 }
|
||||
viewport-padding = { size = 20.0 }
|
||||
stack-gap = { size = 10.0 }
|
||||
@@ -1244,9 +1218,6 @@ notification-progress = { color = "var(--primary)", height = 4, position = 18 }
|
||||
fill-alpha = { size = 12.0 }
|
||||
noise-alpha = { size = 14.0 }
|
||||
|
||||
[components.overlay-dialog]
|
||||
confirm-btn-height = { size = 40.0 }
|
||||
|
||||
[components.qr-code]
|
||||
module-scale = { size = 4 }
|
||||
border-modules = { size = 2 }
|
||||
@@ -1353,17 +1324,10 @@ wallet-btn-padding = { size = 24.0 }
|
||||
rpc-label-min-width = { size = 70.0 }
|
||||
rpc-label-width = { size = 85.0 }
|
||||
security-combo-width = { size = 120.0 }
|
||||
# Wrap width of the Security band's "PIN out of date" tooltip (a long sentence in every language).
|
||||
pin-tooltip-wrap-width = { size = 420.0 }
|
||||
# Wrap width of the disabled "Use Tor" checkbox's tooltip (Settings page and the legacy Settings window).
|
||||
tor-tooltip-wrap-width = { size = 420.0 }
|
||||
node-grid-breakpoint = { size = 900.0 }
|
||||
port-input-min-width = { size = 60.0 }
|
||||
port-input-width-ratio = { size = 0.4 }
|
||||
idle-combo-width = { size = 64.0 }
|
||||
# Reserved height basis for the About-card logo; the logo is drawn scaled to the
|
||||
# card's actual height (aspect-preserved) and capped to this * aspect in width.
|
||||
about-logo-size = { size = 150.0 }
|
||||
about-logo-size = { size = 64.0 }
|
||||
|
||||
[components.main-layout]
|
||||
app-bar-height = { size = 64.0 }
|
||||
@@ -1532,7 +1496,6 @@ progress-bar = { height = 6.0, radius = 3.0 }
|
||||
progress-width = { size = 260.0 }
|
||||
backdrop-alpha = { opacity = 0.80 }
|
||||
vertical-gap = { size = 8.0 }
|
||||
stall-timeout-sec = { size = 45.0 }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# First-Run Wizard Screens
|
||||
@@ -1558,6 +1521,7 @@ title = { font = "h5" }
|
||||
input = { width = 320.0, height = 40.0 }
|
||||
unlock-button = { width = 320.0, height = 44.0, font = "subtitle1" }
|
||||
error-text = { font = "caption" }
|
||||
backdrop-alpha = { opacity = 0.0 }
|
||||
mode-toggle = { font = "caption" }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -25,644 +25,6 @@ def load_english_keys():
|
||||
|
||||
# All translations for the 158 missing keys
|
||||
TRANSLATIONS = {
|
||||
"merge_where": {
|
||||
"de": "Wohin sollen die Mittel gehen?",
|
||||
"es": "¿Adónde deben ir los fondos?",
|
||||
"fr": "Où doivent aller les fonds ?",
|
||||
"pt": "Para onde devem ir os fundos?",
|
||||
"ru": "Куда направить средства?",
|
||||
"zh": "资金应转到哪里?",
|
||||
"ja": "資金の移動先",
|
||||
"ko": "자금을 어디로 보낼까요?",
|
||||
},
|
||||
"merge_into_one": {
|
||||
"de": "In eine Adresse",
|
||||
"es": "A una sola dirección",
|
||||
"fr": "Vers une seule adresse",
|
||||
"pt": "Para um só endereço",
|
||||
"ru": "На один адрес",
|
||||
"zh": "合并到一个地址",
|
||||
"ja": "1 つのアドレスへ",
|
||||
"ko": "한 주소로",
|
||||
},
|
||||
"merge_keep_each": {
|
||||
"de": "In jeder Adresse belassen",
|
||||
"es": "Mantener en cada dirección",
|
||||
"fr": "Garder dans chaque adresse",
|
||||
"pt": "Manter em cada endereço",
|
||||
"ru": "Оставить в каждом адресе",
|
||||
"zh": "保留在各自地址",
|
||||
"ja": "各アドレスに残す",
|
||||
"ko": "각 주소에 유지",
|
||||
},
|
||||
"merge_keep_each_desc": {
|
||||
"de": "Die Notes jeder Adresse werden in genau diese Adresse zurückgeführt, sodass Mittel, die Sie unter verschiedenen Bezeichnungen getrennt halten, getrennt bleiben. Nur abgeschirmte Adressen – transparente Mittel können nur abgeschirmt werden (In eine Adresse).",
|
||||
"es": "Las notas de cada dirección se fusionan de nuevo en esa misma dirección, así que los fondos que mantienes separados con distintas etiquetas siguen separados. Solo direcciones blindadas: los fondos transparentes solo pueden blindarse (A una sola dirección).",
|
||||
"fr": "Les notes de chaque adresse sont refusionnées dans cette même adresse : les fonds que vous gardez séparés sous des libellés différents restent séparés. Adresses blindées uniquement — les fonds transparents ne peuvent qu'être blindés (Vers une seule adresse).",
|
||||
"pt": "As notas de cada endereço são mescladas de volta nesse mesmo endereço, então os fundos que você mantém separados com rótulos diferentes continuam separados. Só endereços blindados — fundos transparentes só podem ser blindados (Para um só endereço).",
|
||||
"ru": "Заметки каждого адреса объединяются обратно в тот же адрес, поэтому средства, которые вы держите раздельно под разными метками, остаются раздельными. Только экранированные адреса — прозрачные средства можно только экранировать («На один адрес»).",
|
||||
"zh": "每个地址的票据会合并回该地址本身,因此您用不同标签分开存放的资金仍保持分开。仅限屏蔽地址——透明资金只能被屏蔽(合并到一个地址)。",
|
||||
"ja": "各アドレスのノートはそのアドレス自身に統合されるため、ラベルごとに分けている資金は分かれたままです。シールドアドレスのみ対象です。透明アドレスの資金はシールドすることしかできません(1 つのアドレスへ)。",
|
||||
"ko": "각 주소의 노트는 같은 주소로 다시 병합되므로, 다른 라벨로 나눠 둔 자금은 계속 나뉜 상태로 유지됩니다. 차폐 주소만 해당되며, 투명 자금은 차폐만 할 수 있습니다(한 주소로).",
|
||||
},
|
||||
"merge_keep_each_none": {
|
||||
"de": "Jede abgeschirmte Adresse hat bereits nur eine Note – nichts zu konsolidieren.",
|
||||
"es": "Cada dirección blindada ya tiene una sola nota: no hay nada que consolidar.",
|
||||
"fr": "Chaque adresse blindée ne contient déjà qu'une seule note — rien à consolider.",
|
||||
"pt": "Cada endereço blindado já tem uma única nota — nada a consolidar.",
|
||||
"ru": "На каждом экранированном адресе уже одна заметка — объединять нечего.",
|
||||
"zh": "每个屏蔽地址都只有一张票据——无需整合。",
|
||||
"ja": "すべてのシールドアドレスのノートはすでに 1 つです。統合するものはありません。",
|
||||
"ko": "모든 차폐 주소에 노트가 이미 하나뿐입니다. 통합할 것이 없습니다.",
|
||||
},
|
||||
"merge_keep_each_row_fmt": {
|
||||
"de": "%s · %d Notes · %s DRGX",
|
||||
"es": "%s · %d notas · %s DRGX",
|
||||
"fr": "%s · %d notes · %s DRGX",
|
||||
"pt": "%s · %d notas · %s DRGX",
|
||||
"ru": "%s · заметок: %d · %s DRGX",
|
||||
"zh": "%s · %d 张票据 · %s DRGX",
|
||||
"ja": "%s · ノート %d 件 · %s DRGX",
|
||||
"ko": "%s · 노트 %d개 · %s DRGX",
|
||||
},
|
||||
"merge_keep_each_too_small": {
|
||||
"de": "Zu klein zum Konsolidieren: Die Gebühr würde diese Mittel aufbrauchen.",
|
||||
"es": "Demasiado poco para consolidar: la comisión se llevaría estos fondos.",
|
||||
"fr": "Trop faible pour être consolidé : les frais absorberaient ces fonds.",
|
||||
"pt": "Pequeno demais para consolidar: a taxa consumiria esses fundos.",
|
||||
"ru": "Слишком мало для объединения: комиссия съест эти средства.",
|
||||
"zh": "金额太小,无法整合:手续费会耗尽这些资金。",
|
||||
"ja": "少なすぎて統合できません。手数料でこの資金がなくなります。",
|
||||
"ko": "너무 적어서 통합할 수 없습니다. 수수료가 이 자금을 모두 소모합니다.",
|
||||
},
|
||||
"merge_keep_each_batches_fmt": {
|
||||
"de": "Adressen mit mehr als %d Notes werden in mehreren Durchgängen zusammengeführt; führen Sie es nach deren Bestätigung erneut aus, um den Rest zusammenzuführen.",
|
||||
"es": "Las direcciones con más de %d notas se fusionan en varios lotes; vuelve a ejecutarlo cuando se confirmen para fusionar el resto.",
|
||||
"fr": "Les adresses de plus de %d notes sont fusionnées en plusieurs lots ; relancez l'opération une fois ceux-ci confirmés pour fusionner le reste.",
|
||||
"pt": "Endereços com mais de %d notas são mesclados em vários lotes; execute de novo depois que forem confirmados para mesclar o restante.",
|
||||
"ru": "Адреса с более чем %d заметками объединяются в несколько партий; запустите снова после их подтверждения, чтобы объединить остальное.",
|
||||
"zh": "超过 %d 张票据的地址会分多批合并;确认后再运行一次即可合并其余部分。",
|
||||
"ja": "ノートが %d 件を超えるアドレスは複数回に分けて統合されます。承認後にもう一度実行すると残りが統合されます。",
|
||||
"ko": "노트가 %d개를 넘는 주소는 여러 번에 나눠 병합됩니다. 확인된 후 다시 실행하면 나머지가 병합됩니다.",
|
||||
},
|
||||
"merge_keep_each_confirm_fmt": {
|
||||
"de": "%d Notes in %d Adressen konsolidieren? Jede Adresse behält ihre eigenen Mittel. Gebühren: etwa %s DRGX (eine pro Durchgang).",
|
||||
"es": "¿Consolidar %d notas en %d direcciones? Cada dirección conserva sus propios fondos. Comisiones: unos %s DRGX (una por lote).",
|
||||
"fr": "Consolider %d notes dans %d adresses ? Chaque adresse garde ses propres fonds. Frais : environ %s DRGX (un par lot).",
|
||||
"pt": "Consolidar %d notas em %d endereços? Cada endereço mantém seus próprios fundos. Taxas: cerca de %s DRGX (uma por lote).",
|
||||
"ru": "Объединить %d заметок на %d адресах? Каждый адрес сохраняет свои средства. Комиссии: около %s DRGX (по одной на партию).",
|
||||
"zh": "整合 %d 张票据,涉及 %d 个地址?每个地址保留各自的资金。手续费:约 %s DRGX(每批一次)。",
|
||||
"ja": "%d 件のノートを %d 個のアドレスで統合しますか?各アドレスの資金はそのまま残ります。手数料:約 %s DRGX(1 回ごとに 1 件)。",
|
||||
"ko": "노트 %d개를 주소 %d개에서 통합할까요? 각 주소는 자기 자금을 그대로 유지합니다. 수수료: 약 %s DRGX(배치마다 1회).",
|
||||
},
|
||||
"merge_keep_each_select": {
|
||||
"de": "Wählen Sie mindestens eine Adresse zum Konsolidieren aus.",
|
||||
"es": "Marca al menos una dirección para consolidar.",
|
||||
"fr": "Cochez au moins une adresse à consolider.",
|
||||
"pt": "Marque pelo menos um endereço para consolidar.",
|
||||
"ru": "Отметьте хотя бы один адрес для объединения.",
|
||||
"zh": "请至少勾选一个要整合的地址。",
|
||||
"ja": "統合するアドレスを 1 つ以上選んでください。",
|
||||
"ko": "통합할 주소를 하나 이상 선택하세요.",
|
||||
},
|
||||
"merge_keep_each_nothing_submitted": {
|
||||
"de": "Nichts wurde konsolidiert – der Node hat jede Zusammenführung abgelehnt.",
|
||||
"es": "No se consolidó nada: el nodo rechazó todas las fusiones.",
|
||||
"fr": "Rien n'a été consolidé — le nœud a refusé chaque fusion.",
|
||||
"pt": "Nada foi consolidado — o nó recusou todas as mesclagens.",
|
||||
"ru": "Ничего не объединено — узел отклонил все объединения.",
|
||||
"zh": "没有任何整合——节点拒绝了所有合并。",
|
||||
"ja": "何も統合されませんでした。ノードがすべての統合を拒否しました。",
|
||||
"ko": "아무것도 통합되지 않았습니다. 노드가 모든 병합을 거부했습니다.",
|
||||
},
|
||||
"merge_keep_each_some_failed": {
|
||||
"de": "Einige Zusammenführungen sind fehlgeschlagen – siehe Liste oben.",
|
||||
"es": "Algunas fusiones fallaron; consulta la lista de arriba.",
|
||||
"fr": "Certaines fusions ont échoué — voir la liste ci-dessus.",
|
||||
"pt": "Algumas mesclagens falharam — veja a lista acima.",
|
||||
"ru": "Некоторые объединения не удались — см. список выше.",
|
||||
"zh": "部分合并失败——请查看上方列表。",
|
||||
"ja": "一部の統合に失敗しました。上の一覧を確認してください。",
|
||||
"ko": "일부 병합에 실패했습니다. 위 목록을 확인하세요.",
|
||||
},
|
||||
"merge_keep_each_status_not_started": {
|
||||
"de": "%s – nicht gestartet: %s",
|
||||
"es": "%s: no iniciado: %s",
|
||||
"fr": "%s — non démarré : %s",
|
||||
"pt": "%s — não iniciado: %s",
|
||||
"ru": "%s — не запущено: %s",
|
||||
"zh": "%s——未开始:%s",
|
||||
"ja": "%s — 開始されませんでした:%s",
|
||||
"ko": "%s — 시작되지 않음: %s",
|
||||
},
|
||||
"merge_keep_each_status_failed": {
|
||||
"de": "%s – fehlgeschlagen: %s",
|
||||
"es": "%s: falló: %s",
|
||||
"fr": "%s — échec : %s",
|
||||
"pt": "%s — falhou: %s",
|
||||
"ru": "%s — ошибка: %s",
|
||||
"zh": "%s——失败:%s",
|
||||
"ja": "%s — 失敗:%s",
|
||||
"ko": "%s — 실패: %s",
|
||||
},
|
||||
"merge_keep_each_status_done": {
|
||||
"de": "%s – fertig: %d Notes in %d Durchgang/Durchgängen zusammengeführt",
|
||||
"es": "%s: listo: %d notas fusionadas en %d lote(s)",
|
||||
"fr": "%s — terminé : %d notes fusionnées en %d lot(s)",
|
||||
"pt": "%s — concluído: %d notas mescladas em %d lote(s)",
|
||||
"ru": "%s — готово: объединено заметок: %d, партий: %d",
|
||||
"zh": "%s——完成:已合并 %d 张票据,共 %d 批",
|
||||
"ja": "%s — 完了:%d 件のノートを %d 回で統合",
|
||||
"ko": "%s — 완료: 노트 %d개를 %d번에 걸쳐 병합",
|
||||
},
|
||||
"merge_keep_each_status_running": {
|
||||
"de": "%s – %d Notes werden zusammengeführt (%d von %d Durchgängen fertig)…",
|
||||
"es": "%s: fusionando %d notas (%d de %d lotes listos)…",
|
||||
"fr": "%s — fusion de %d notes (%d lots sur %d terminés)…",
|
||||
"pt": "%s — mesclando %d notas (%d de %d lotes concluídos)…",
|
||||
"ru": "%s — объединение %d заметок (готово партий: %d из %d)…",
|
||||
"zh": "%s——正在合并 %d 张票据(已完成 %d/%d 批)…",
|
||||
"ja": "%s — %d 件のノートを統合中(%d / %d 回完了)…",
|
||||
"ko": "%s — 노트 %d개 병합 중(%d/%d 배치 완료)…",
|
||||
},
|
||||
"merge_keep_each_run_again": {
|
||||
"de": "Adressen, die in mehreren Durchgängen zusammengeführt wurden, haben jetzt eine Note pro Durchgang. Führen Sie dies nach deren Bestätigung erneut aus, um sie zu einer zusammenzuführen.",
|
||||
"es": "Las direcciones fusionadas en varios lotes tienen ahora una nota por lote. Vuelve a ejecutarlo cuando se confirmen para fusionarlas en una.",
|
||||
"fr": "Les adresses fusionnées en plusieurs lots contiennent maintenant une note par lot. Relancez l'opération une fois ceux-ci confirmés pour les fusionner en une seule.",
|
||||
"pt": "Endereços mesclados em vários lotes agora têm uma nota por lote. Execute de novo depois que forem confirmados para mesclá-las em uma.",
|
||||
"ru": "На адресах, объединённых в несколько партий, теперь по одной заметке на партию. Запустите снова после их подтверждения, чтобы объединить их в одну.",
|
||||
"zh": "分多批合并的地址现在每批各有一张票据。确认后再运行一次,即可把它们合并为一张。",
|
||||
"ja": "複数回に分けて統合したアドレスには、回数分のノートが残っています。承認後にもう一度実行すると 1 つにまとめられます。",
|
||||
"ko": "여러 번에 나눠 병합된 주소에는 배치마다 노트가 하나씩 남아 있습니다. 확인된 후 다시 실행하면 하나로 병합됩니다.",
|
||||
},
|
||||
"mining_addr_explainer": {
|
||||
"de": "Empfängt nur Solo-Mining-Belohnungen – das Netzwerk lehnt Zahlungen an diese Adresse ab. Belohnungen werden automatisch abgeschirmt, sobald sie gereift sind.",
|
||||
"es": "Solo recibe recompensas de minería en solitario; la red rechaza los pagos a esta dirección. Las recompensas se blindan automáticamente cuando maduran.",
|
||||
"fr": "Ne reçoit que des récompenses de minage en solo — le réseau refuse les paiements vers cette adresse. Les récompenses sont blindées automatiquement une fois arrivées à maturité.",
|
||||
"pt": "Recebe apenas recompensas de mineração solo — a rede rejeita pagamentos para este endereço. As recompensas são blindadas automaticamente quando amadurecem.",
|
||||
"ru": "Принимает только награды за соло-майнинг — сеть отклоняет платежи на этот адрес. Награды экранируются автоматически, когда созревают.",
|
||||
"zh": "仅接收单人挖矿奖励——网络会拒绝向此地址付款。奖励成熟后会自动屏蔽。",
|
||||
"ja": "ソロマイニング報酬のみを受け取ります。ネットワークはこのアドレスへの支払いを拒否します。報酬は成熟すると自動的にシールドされます。",
|
||||
"ko": "솔로 채굴 보상만 받습니다. 네트워크가 이 주소로의 결제를 거부합니다. 보상은 성숙하면 자동으로 차폐됩니다.",
|
||||
},
|
||||
"receive_use_shielded": {
|
||||
"de": "Abgeschirmte Adresse verwenden",
|
||||
"es": "Usar una dirección blindada",
|
||||
"fr": "Utiliser une adresse blindée",
|
||||
"pt": "Usar um endereço blindado",
|
||||
"ru": "Использовать экранированный адрес",
|
||||
"zh": "使用屏蔽地址",
|
||||
"ja": "シールドアドレスを使う",
|
||||
"ko": "차폐 주소 사용",
|
||||
},
|
||||
"receive_not_for_payments": {
|
||||
"de": "Nicht für Zahlungen",
|
||||
"es": "No para pagos",
|
||||
"fr": "Pas pour les paiements",
|
||||
"pt": "Não é para pagamentos",
|
||||
"ru": "Не для платежей",
|
||||
"zh": "不用于付款",
|
||||
"ja": "支払いには使えません",
|
||||
"ko": "결제용이 아님",
|
||||
},
|
||||
"validate_mining_type": {
|
||||
"de": "Mining-Adresse (transparent)",
|
||||
"es": "Dirección de minería (transparente)",
|
||||
"fr": "Adresse de minage (transparente)",
|
||||
"pt": "Endereço de mineração (transparente)",
|
||||
"ru": "Адрес для майнинга (прозрачный)",
|
||||
"zh": "挖矿地址(透明)",
|
||||
"ja": "マイニングアドレス(透明)",
|
||||
"ko": "채굴 주소(투명)",
|
||||
},
|
||||
"send_mining_addr_short": {
|
||||
"de": "Mining-Adresse – kann keine Zahlungen empfangen",
|
||||
"es": "Dirección de minería: no puede recibir pagos",
|
||||
"fr": "Adresse de minage — ne peut pas recevoir de paiements",
|
||||
"pt": "Endereço de mineração — não pode receber pagamentos",
|
||||
"ru": "Адрес для майнинга — не принимает платежи",
|
||||
"zh": "挖矿地址——无法接收付款",
|
||||
"ja": "マイニングアドレス — 支払いを受け取れません",
|
||||
"ko": "채굴 주소 — 결제를 받을 수 없음",
|
||||
},
|
||||
"send_mining_addr_blocked": {
|
||||
"de": "Dies ist eine Mining-Adresse (transparent). Bei DragonX können solche Adressen nur Mining-Belohnungen empfangen – das Netzwerk lehnt Zahlungen an sie ab. Bitten Sie den Empfänger um eine abgeschirmte Adresse (sie beginnt mit zs).",
|
||||
"es": "Esta es una dirección de minería (transparente). En DragonX solo pueden recibir recompensas de minería; la red rechaza los pagos a ellas. Pide al destinatario una dirección blindada (empieza por zs).",
|
||||
"fr": "Ceci est une adresse de minage (transparente). Sur DragonX, elles ne peuvent recevoir que des récompenses de minage — le réseau refuse les paiements vers elles. Demandez au destinataire une adresse blindée (elle commence par zs).",
|
||||
"pt": "Este é um endereço de mineração (transparente). No DragonX eles só podem receber recompensas de mineração — a rede rejeita pagamentos para eles. Peça ao destinatário um endereço blindado (começa com zs).",
|
||||
"ru": "Это адрес для майнинга (прозрачный). В DragonX такие адреса могут получать только награды за майнинг — сеть отклоняет платежи на них. Попросите у получателя экранированный адрес (начинается с zs).",
|
||||
"zh": "这是一个挖矿(透明)地址。在 DragonX 上,这类地址只能接收挖矿奖励——网络会拒绝向其付款。请向收款人索取屏蔽地址(以 zs 开头)。",
|
||||
"ja": "これはマイニング(透明)アドレスです。DragonX ではこのアドレスはマイニング報酬しか受け取れず、ネットワークはこのアドレスへの支払いを拒否します。受取人にシールドアドレス(zs で始まる)を依頼してください。",
|
||||
"ko": "채굴(투명) 주소입니다. DragonX에서 이런 주소는 채굴 보상만 받을 수 있으며, 네트워크가 이 주소로의 결제를 거부합니다. 받는 사람에게 차폐 주소(zs로 시작)를 요청하세요.",
|
||||
},
|
||||
"mining_payout_needs_shielded": {
|
||||
"de": "Pools zahlen mit gewöhnlichen Transaktionen aus, die bei DragonX keine Mining-Adresse (transparent, R…) erreichen können. Verwenden Sie eine abgeschirmte Adresse (sie beginnt mit zs) als Auszahlungsadresse.",
|
||||
"es": "Los pools pagan con transacciones normales, que en DragonX no pueden llegar a una dirección de minería (transparente, R…). Usa una dirección blindada (empieza por zs) como dirección de pago.",
|
||||
"fr": "Les pools paient avec des transactions ordinaires, qui ne peuvent pas atteindre une adresse de minage (transparente, R…) sur DragonX. Utilisez une adresse blindée (elle commence par zs) comme adresse de paiement.",
|
||||
"pt": "Os pools pagam com transações comuns, que no DragonX não conseguem chegar a um endereço de mineração (transparente, R…). Use um endereço blindado (começa com zs) como endereço de pagamento.",
|
||||
"ru": "Пулы выплачивают обычными транзакциями, которые в DragonX не могут дойти до адреса для майнинга (прозрачного, R…). Укажите экранированный адрес (начинается с zs) как адрес для выплат.",
|
||||
"zh": "矿池使用普通交易付款,而在 DragonX 上普通交易无法到达挖矿(透明,R…)地址。请使用屏蔽地址(以 zs 开头)作为收款地址。",
|
||||
"ja": "プールは通常のトランザクションで支払いますが、DragonX ではそれがマイニング(透明、R…)アドレスに届きません。支払い先にはシールドアドレス(zs で始まる)を使用してください。",
|
||||
"ko": "풀은 일반 트랜잭션으로 지급하는데, DragonX에서는 이것이 채굴(투명, R…) 주소에 도달할 수 없습니다. 지급 주소로 차폐 주소(zs로 시작)를 사용하세요.",
|
||||
},
|
||||
"new_mining_address": {
|
||||
"de": "Neue Mining-Adresse",
|
||||
"es": "Nueva dirección de minería",
|
||||
"fr": "Nouvelle adresse de minage",
|
||||
"pt": "Novo endereço de mineração",
|
||||
"ru": "Новый адрес для майнинга",
|
||||
"zh": "新建挖矿地址",
|
||||
"ja": "新しいマイニングアドレス",
|
||||
"ko": "새 채굴 주소",
|
||||
},
|
||||
"tt_new_mining_address": {
|
||||
"de": "Erstellt eine transparente Adresse (R…) für Solo-Mining-Belohnungen. Sie kann keine Zahlungen empfangen – teilen Sie dafür eine abgeschirmte Adresse.",
|
||||
"es": "Crea una dirección transparente (R…) para recompensas de minería en solitario. No puede recibir pagos; para eso comparte una dirección blindada.",
|
||||
"fr": "Crée une adresse transparente (R…) pour les récompenses de minage en solo. Elle ne peut pas recevoir de paiements — partagez une adresse blindée pour cela.",
|
||||
"pt": "Cria um endereço transparente (R…) para recompensas de mineração solo. Ele não pode receber pagamentos — compartilhe um endereço blindado para isso.",
|
||||
"ru": "Создаёт прозрачный адрес (R…) для наград за соло-майнинг. Он не принимает платежи — для них сообщайте экранированный адрес.",
|
||||
"zh": "创建一个用于单人挖矿奖励的透明(R…)地址。它无法接收付款——收款请提供屏蔽地址。",
|
||||
"ja": "ソロマイニング報酬用の透明(R…)アドレスを作成します。支払いは受け取れません。支払いにはシールドアドレスを共有してください。",
|
||||
"ko": "솔로 채굴 보상용 투명(R…) 주소를 만듭니다. 결제는 받을 수 없으니, 결제용으로는 차폐 주소를 공유하세요.",
|
||||
},
|
||||
"mining_address": {
|
||||
"de": "Mining-Adresse",
|
||||
"es": "Dirección de minería",
|
||||
"fr": "Adresse de minage",
|
||||
"pt": "Endereço de mineração",
|
||||
"ru": "Адрес для майнинга",
|
||||
"zh": "挖矿地址",
|
||||
"ja": "マイニングアドレス",
|
||||
"ko": "채굴 주소",
|
||||
},
|
||||
"addr_type_mining": {
|
||||
"de": "Mining",
|
||||
"es": "Minería",
|
||||
"fr": "Minage",
|
||||
"pt": "Mineração",
|
||||
"ru": "Майнинг",
|
||||
"zh": "挖矿",
|
||||
"ja": "マイニング",
|
||||
"ko": "채굴",
|
||||
},
|
||||
"mining_rewards": {
|
||||
"de": "Mining-Belohnungen",
|
||||
"es": "Recompensas de minería",
|
||||
"fr": "Récompenses de minage",
|
||||
"pt": "Recompensas de mineração",
|
||||
"ru": "Награды за майнинг",
|
||||
"zh": "挖矿奖励",
|
||||
"ja": "マイニング報酬",
|
||||
"ko": "채굴 보상",
|
||||
},
|
||||
"mining_rewards_unshielded": {
|
||||
"de": "Noch nicht abgeschirmt",
|
||||
"es": "Aún sin blindar",
|
||||
"fr": "Pas encore blindées",
|
||||
"pt": "Ainda não blindadas",
|
||||
"ru": "Ещё не экранированы",
|
||||
"zh": "尚未屏蔽",
|
||||
"ja": "未シールド",
|
||||
"ko": "아직 차폐되지 않음",
|
||||
},
|
||||
"baltab_mining_addresses_count": {
|
||||
"de": "%d Mining-Adressen",
|
||||
"es": "%d direcciones de minería",
|
||||
"fr": "%d adresses de minage",
|
||||
"pt": "%d endereços de mineração",
|
||||
"ru": "Адресов для майнинга: %d",
|
||||
"zh": "%d 个挖矿地址",
|
||||
"ja": "マイニングアドレス %d 件",
|
||||
"ko": "채굴 주소 %d개",
|
||||
},
|
||||
"baltab_mining_amount": {
|
||||
"de": "Mining-Belohnungen %.8f",
|
||||
"es": "Recompensas de minería %.8f",
|
||||
"fr": "Récompenses de minage %.8f",
|
||||
"pt": "Recompensas de mineração %.8f",
|
||||
"ru": "Награды за майнинг %.8f",
|
||||
"zh": "挖矿奖励 %.8f",
|
||||
"ja": "マイニング報酬 %.8f",
|
||||
"ko": "채굴 보상 %.8f",
|
||||
},
|
||||
"balance_mining_fmt": {
|
||||
"de": "Mining-Belohnungen: %.8f",
|
||||
"es": "Recompensas de minería: %.8f",
|
||||
"fr": "Récompenses de minage : %.8f",
|
||||
"pt": "Recompensas de mineração: %.8f",
|
||||
"ru": "Награды за майнинг: %.8f",
|
||||
"zh": "挖矿奖励:%.8f",
|
||||
"ja": "マイニング報酬:%.8f",
|
||||
"ko": "채굴 보상: %.8f",
|
||||
},
|
||||
"mining_card_tooltip": {
|
||||
"de": "Solo-Mining-Belohnungen gehen an Ihre Mining-Adressen (transparent, R…). Diese Adressen können keine Zahlungen empfangen. Belohnungen werden automatisch abgeschirmt, sobald sie gereift sind.",
|
||||
"es": "Las recompensas de minería en solitario llegan a tus direcciones de minería (transparentes, R…). Esas direcciones no pueden recibir pagos. Las recompensas se blindan automáticamente cuando maduran.",
|
||||
"fr": "Les récompenses de minage en solo arrivent sur vos adresses de minage (transparentes, R…). Ces adresses ne peuvent pas recevoir de paiements. Les récompenses sont blindées automatiquement une fois arrivées à maturité.",
|
||||
"pt": "As recompensas de mineração solo chegam aos seus endereços de mineração (transparentes, R…). Esses endereços não podem receber pagamentos. As recompensas são blindadas automaticamente quando amadurecem.",
|
||||
"ru": "Награды за соло-майнинг поступают на ваши адреса для майнинга (прозрачные, R…). Эти адреса не принимают платежи. Награды экранируются автоматически, когда созревают.",
|
||||
"zh": "单人挖矿奖励会发送到您的挖矿(透明,R…)地址。这些地址无法接收付款。奖励成熟后会自动屏蔽。",
|
||||
"ja": "ソロマイニング報酬はマイニング(透明、R…)アドレスに届きます。これらのアドレスは支払いを受け取れません。報酬は成熟すると自動的にシールドされます。",
|
||||
"ko": "솔로 채굴 보상은 채굴(투명, R…) 주소로 들어옵니다. 이 주소는 결제를 받을 수 없습니다. 보상은 성숙하면 자동으로 차폐됩니다.",
|
||||
},
|
||||
"mining_card_click_to_shield": {
|
||||
"de": "Klicken, um gereifte Belohnungen jetzt abzuschirmen.",
|
||||
"es": "Haz clic para blindar ahora las recompensas maduras.",
|
||||
"fr": "Cliquez pour blinder dès maintenant les récompenses arrivées à maturité.",
|
||||
"pt": "Clique para blindar agora as recompensas amadurecidas.",
|
||||
"ru": "Нажмите, чтобы экранировать созревшие награды сейчас.",
|
||||
"zh": "点击立即屏蔽已成熟的奖励。",
|
||||
"ja": "クリックすると成熟した報酬を今すぐシールドします。",
|
||||
"ko": "클릭하면 성숙한 보상을 지금 차폐합니다.",
|
||||
},
|
||||
"contact_chip_mining": {
|
||||
"de": "Nur Mining",
|
||||
"es": "Solo minería",
|
||||
"fr": "Minage seul",
|
||||
"pt": "Só mineração",
|
||||
"ru": "Только майнинг",
|
||||
"zh": "仅限挖矿",
|
||||
"ja": "マイニング専用",
|
||||
"ko": "채굴 전용",
|
||||
},
|
||||
"failed_create_mining_address": {
|
||||
"de": "Mining-Adresse konnte nicht erstellt werden",
|
||||
"es": "No se pudo crear la dirección de minería",
|
||||
"fr": "Impossible de créer l'adresse de minage",
|
||||
"pt": "Falha ao criar o endereço de mineração",
|
||||
"ru": "Не удалось создать адрес для майнинга",
|
||||
"zh": "创建挖矿地址失败",
|
||||
"ja": "マイニングアドレスを作成できませんでした",
|
||||
"ko": "채굴 주소를 만들지 못했습니다",
|
||||
},
|
||||
"new_mining_address_created": {
|
||||
"de": "Neue Mining-Adresse erstellt",
|
||||
"es": "Nueva dirección de minería creada",
|
||||
"fr": "Nouvelle adresse de minage créée",
|
||||
"pt": "Novo endereço de mineração criado",
|
||||
"ru": "Новый адрес для майнинга создан",
|
||||
"zh": "已创建新的挖矿地址",
|
||||
"ja": "新しいマイニングアドレスを作成しました",
|
||||
"ko": "새 채굴 주소를 만들었습니다",
|
||||
},
|
||||
"appx_node_busy_try_later": {
|
||||
"de": "Der Node ist mit einer langen Aufgabe beschäftigt (etwa dem Neuaufbau der Note-Witnesses). Versuchen Sie es erneut, wenn sie abgeschlossen ist; ein Stoppen jetzt könnte diese Arbeit von vorn beginnen lassen.",
|
||||
"es": "El nodo está ocupado terminando una tarea larga (como reconstruir los testigos de notas). Vuelve a intentarlo cuando termine; detenerlo ahora podría hacer que empiece ese trabajo de nuevo.",
|
||||
"fr": "Le nœud termine une longue tâche (comme la reconstruction des témoins de notes). Réessayez une fois qu'elle est finie ; l'arrêter maintenant pourrait lui faire recommencer ce travail.",
|
||||
"pt": "O nó está ocupado concluindo uma tarefa longa (como reconstruir as testemunhas de notas). Tente novamente quando terminar; pará-lo agora pode fazê-lo recomeçar esse trabalho.",
|
||||
"ru": "Узел занят долгой задачей (например, перестройкой свидетелей заметок). Повторите попытку, когда она завершится; если остановить его сейчас, эту работу, возможно, придётся начать заново.",
|
||||
"zh": "节点正忙于完成一项耗时任务(例如重建票据见证)。请在完成后重试;现在停止它可能导致这项工作从头开始。",
|
||||
"ja": "ノードは長時間の処理(ノートウィットネスの再構築など)を実行中です。完了してからもう一度お試しください。今停止すると、その処理を最初からやり直すことがあります。",
|
||||
"ko": "노드가 긴 작업(노트 위트니스 재구성 등)을 마무리하는 중입니다. 끝난 뒤 다시 시도하세요. 지금 중지하면 그 작업을 처음부터 다시 할 수 있습니다.",
|
||||
},
|
||||
"appx_node_not_stopped": {
|
||||
"de": "Der Node wurde nicht rechtzeitig beendet, daher wurde nichts geändert. Versuchen Sie es in ein paar Minuten erneut.",
|
||||
"es": "El nodo no se detuvo a tiempo, así que no se cambió nada. Vuelve a intentarlo en unos minutos.",
|
||||
"fr": "Le nœud ne s'est pas arrêté à temps, rien n'a donc été modifié. Réessayez dans quelques minutes.",
|
||||
"pt": "O nó não parou a tempo, por isso nada foi alterado. Tente novamente em alguns minutos.",
|
||||
"ru": "Узел не остановился вовремя, поэтому ничего не изменено. Повторите попытку через несколько минут.",
|
||||
"zh": "节点未能及时停止,因此未做任何更改。请几分钟后重试。",
|
||||
"ja": "ノードが時間内に停止しなかったため、何も変更されませんでした。数分後にもう一度お試しください。",
|
||||
"ko": "노드가 제때 중지되지 않아 아무것도 변경되지 않았습니다. 몇 분 후 다시 시도하세요.",
|
||||
},
|
||||
# --- Daemon shutdown hardening: stopping the node on quit (never kill unasked) ---
|
||||
"shutdown_leave_running": {
|
||||
"de": "Wallet schließen – Node weiterlaufen lassen", "es": "Cerrar la cartera y dejar el nodo en marcha",
|
||||
"fr": "Fermer le portefeuille — laisser le nœud actif", "pt": "Fechar a carteira e deixar o nó em execução",
|
||||
"ru": "Закрыть кошелёк — оставить узел работать", "zh": "关闭钱包——保持节点运行",
|
||||
"ja": "ウォレットを閉じる — ノードは実行したまま", "ko": "지갑 닫기 — 노드는 계속 실행"
|
||||
},
|
||||
"shutdown_leave_running_tip": {
|
||||
"de": "Schließt die Wallet sofort. Der Node läuft allein weiter und beendet, was er gerade tut; nichts geht verloren.",
|
||||
"es": "Cierra la cartera ahora. El nodo sigue funcionando por su cuenta y termina lo que está haciendo; no se pierde nada.",
|
||||
"fr": "Ferme le portefeuille maintenant. Le nœud continue de fonctionner seul et termine ce qu'il fait ; rien n'est perdu.",
|
||||
"pt": "Fecha a carteira agora. O nó continua em execução sozinho e termina o que está fazendo; nada é perdido.",
|
||||
"ru": "Закрывает кошелёк сейчас. Узел продолжит работать сам и завершит текущую операцию; ничего не теряется.",
|
||||
"zh": "立即关闭钱包。节点会继续独立运行并完成当前操作,不会丢失任何数据。",
|
||||
"ja": "ウォレットをすぐに閉じます。ノードは単独で実行を続け、処理中の作業を完了します。何も失われません。",
|
||||
"ko": "지갑을 지금 닫습니다. 노드는 혼자 계속 실행되며 진행 중인 작업을 마칩니다. 잃는 것은 없습니다."
|
||||
},
|
||||
"shutdown_kill_node": {
|
||||
"de": "Node erzwungen beenden (unsicher)", "es": "Forzar cierre del nodo (inseguro)",
|
||||
"fr": "Tuer le nœud (risqué)", "pt": "Forçar encerramento do nó (inseguro)",
|
||||
"ru": "Принудительно завершить узел (небезопасно)", "zh": "强制结束节点(不安全)",
|
||||
"ja": "ノードを強制終了(危険)", "ko": "노드 강제 종료(위험)"
|
||||
},
|
||||
"shutdown_kill_node_tip": {
|
||||
"de": "Beendet den Node sofort. Alles, was er noch nicht gespeichert hat, geht verloren, und beim nächsten Start spielt er die Blöcke seit dem letzten Speichern erneut ein.",
|
||||
"es": "Detiene el nodo de inmediato. Se pierde todo lo que no haya guardado, y en el próximo inicio volverá a procesar los bloques desde el último guardado.",
|
||||
"fr": "Arrête le nœud immédiatement. Tout ce qu'il n'a pas enregistré est perdu, et au prochain démarrage il rejouera les blocs depuis son dernier enregistrement.",
|
||||
"pt": "Encerra o nó imediatamente. Tudo o que não foi salvo se perde, e no próximo início ele reprocessa os blocos desde o último salvamento.",
|
||||
"ru": "Немедленно завершает узел. Всё несохранённое будет потеряно, а при следующем запуске он заново обработает блоки с момента последнего сохранения.",
|
||||
"zh": "立即结束节点。所有尚未保存的数据都会丢失,下次启动时会重新处理自上次保存以来的区块。",
|
||||
"ja": "ノードを直ちに終了します。保存されていない内容はすべて失われ、次回の起動時に前回の保存以降のブロックを再処理します。",
|
||||
"ko": "노드를 즉시 종료합니다. 저장되지 않은 내용은 모두 사라지며, 다음 시작 시 마지막 저장 이후의 블록을 다시 처리합니다."
|
||||
},
|
||||
"shutdown_kill_confirm_title": {
|
||||
"de": "Node erzwungen beenden?", "es": "¿Forzar el cierre del nodo?", "fr": "Tuer le nœud ?",
|
||||
"pt": "Forçar o encerramento do nó?", "ru": "Принудительно завершить узел?", "zh": "强制结束节点?",
|
||||
"ja": "ノードを強制終了しますか?", "ko": "노드를 강제 종료할까요?"
|
||||
},
|
||||
"shutdown_kill_confirm_msg": {
|
||||
"de": "Der Node zeigt seit Langem keine CPU- oder Festplattenaktivität und hängt möglicherweise.\nWenn Sie ihn erzwungen beenden, geht alles Ungespeicherte verloren; sein nächster Start kann deutlich länger dauern.",
|
||||
"es": "El nodo lleva mucho tiempo sin actividad de CPU ni de disco y puede estar bloqueado.\nSi fuerzas su cierre se perderá todo lo que no haya guardado; su próximo inicio puede tardar mucho más.",
|
||||
"fr": "Le nœud ne montre plus d'activité processeur ni disque depuis longtemps et est peut-être bloqué.\nLe tuer fait perdre tout ce qu'il n'a pas enregistré ; son prochain démarrage peut être beaucoup plus long.",
|
||||
"pt": "O nó não mostra atividade de CPU nem de disco há muito tempo e pode estar travado.\nForçar o encerramento perde tudo o que não foi salvo; o próximo início pode demorar muito mais.",
|
||||
"ru": "Узел давно не проявляет активности процессора и диска и, возможно, завис.\nПри принудительном завершении всё несохранённое будет потеряно; следующий запуск может занять гораздо больше времени.",
|
||||
"zh": "节点已长时间没有 CPU 或磁盘活动,可能已卡住。\n强制结束会丢失所有未保存的数据;下次启动可能需要更长时间。",
|
||||
"ja": "ノードは長時間 CPU やディスクの活動がなく、停止している可能性があります。\n強制終了すると保存されていない内容はすべて失われ、次回の起動に大幅に時間がかかることがあります。",
|
||||
"ko": "노드가 오랫동안 CPU나 디스크 활동이 없어 멈췄을 수 있습니다.\n강제 종료하면 저장되지 않은 내용이 모두 사라지고, 다음 시작이 훨씬 오래 걸릴 수 있습니다."
|
||||
},
|
||||
"shutdown_kill_confirm_yes": {
|
||||
"de": "Erzwungen beenden", "es": "Forzar cierre", "fr": "Tuer le nœud", "pt": "Forçar encerramento",
|
||||
"ru": "Завершить узел", "zh": "强制结束", "ja": "強制終了", "ko": "강제 종료"
|
||||
},
|
||||
"shutdown_stop_asking": {
|
||||
"de": "Node wird zum Beenden aufgefordert…", "es": "Pidiendo al nodo que se detenga…",
|
||||
"fr": "Demande d'arrêt envoyée au nœud…", "pt": "Pedindo ao nó para parar…",
|
||||
"ru": "Отправка узлу команды остановки…", "zh": "正在请求节点停止…",
|
||||
"ja": "ノードに停止を要求しています…", "ko": "노드에 중지를 요청하는 중…"
|
||||
},
|
||||
"shutdown_stop_queued": {
|
||||
"de": "Der Node hat die Stopp-Anforderung erhalten, ist aber beschäftigt – er stoppt, sobald er kann",
|
||||
"es": "El nodo recibió la orden de detenerse, pero está ocupado; se detendrá en cuanto pueda",
|
||||
"fr": "Le nœud a reçu la demande d'arrêt mais il est occupé — il s'arrêtera dès que possible",
|
||||
"pt": "O nó recebeu o pedido de parada, mas está ocupado; ele vai parar assim que puder",
|
||||
"ru": "Узел получил команду остановки, но занят — он остановится, как только сможет",
|
||||
"zh": "节点已收到停止请求,但正忙——它会尽快停止",
|
||||
"ja": "ノードは停止要求を受け取りましたが処理中です — 可能になり次第停止します",
|
||||
"ko": "노드가 중지 요청을 받았지만 작업 중입니다 — 가능해지면 중지됩니다"
|
||||
},
|
||||
"shutdown_stop_delivered": {
|
||||
"de": "Der Node fährt herunter und speichert seinen Zustand", "es": "El nodo se está cerrando y guardando su estado",
|
||||
"fr": "Le nœud s'arrête et enregistre son état", "pt": "O nó está encerrando e salvando seu estado",
|
||||
"ru": "Узел завершает работу и сохраняет своё состояние", "zh": "节点正在关闭并保存其状态",
|
||||
"ja": "ノードを終了し、状態を保存しています", "ko": "노드가 종료 중이며 상태를 저장하고 있습니다"
|
||||
},
|
||||
"shutdown_stop_cpu_suffix": {
|
||||
"de": " · CPU %d%%", "es": " · CPU %d%%", "fr": " · CPU %d%%", "pt": " · CPU %d%%",
|
||||
"ru": " · ЦП %d%%", "zh": " · CPU %d%%", "ja": " · CPU %d%%", "ko": " · CPU %d%%"
|
||||
},
|
||||
"shutdown_stop_idle": {
|
||||
"de": "Seit %d Min. keine CPU- oder Festplattenaktivität – der Node hängt möglicherweise",
|
||||
"es": "Sin actividad de CPU ni de disco desde hace %d min; puede que el nodo esté bloqueado",
|
||||
"fr": "Aucune activité processeur ni disque depuis %d min — le nœud est peut-être bloqué",
|
||||
"pt": "Sem atividade de CPU ou disco há %d min; o nó pode estar travado",
|
||||
"ru": "Нет активности процессора и диска уже %d мин — возможно, узел завис",
|
||||
"zh": "已 %d 分钟没有 CPU 或磁盘活动——节点可能已卡住",
|
||||
"ja": "%d 分間 CPU やディスクの活動がありません — ノードが停止している可能性があります",
|
||||
"ko": "%d분 동안 CPU나 디스크 활동이 없습니다 — 노드가 멈췄을 수 있습니다"
|
||||
},
|
||||
# --- The node is busy: overlay, banner, status bar ---
|
||||
"node_busy_rebuild_title": {
|
||||
"de": "Node beschäftigt: Note-Witnesses werden neu aufgebaut",
|
||||
"es": "Nodo ocupado: reconstruyendo testigos de notas",
|
||||
"fr": "Nœud occupé : reconstruction des témoins de notes",
|
||||
"pt": "Nó ocupado: reconstruindo testemunhas de notas",
|
||||
"ru": "Узел занят: перестройка свидетелей заметок",
|
||||
"zh": "节点繁忙:正在重建票据见证",
|
||||
"ja": "ノード処理中:ノートウィットネスを再構築しています",
|
||||
"ko": "노드 작업 중: 노트 위트니스 재구성 중",
|
||||
},
|
||||
"node_busy_running": {
|
||||
"de": "Läuft seit %s",
|
||||
"es": "En curso desde hace %s",
|
||||
"fr": "En cours depuis %s",
|
||||
"pt": "Em andamento há %s",
|
||||
"ru": "Выполняется %s",
|
||||
"zh": "已运行 %s",
|
||||
"ja": "経過時間 %s",
|
||||
"ko": "%s 동안 진행 중",
|
||||
},
|
||||
"node_busy_rebuild_desc": {
|
||||
"de": "Läuft seit %s. Bei großen Wallets kann das etwa eine Stunde dauern, und der Node kann der Wallet erst danach antworten.",
|
||||
"es": "En curso desde hace %s. En carteras grandes puede tardar alrededor de una hora, y el nodo no puede responder a la cartera hasta que termine.",
|
||||
"fr": "En cours depuis %s. Pour un gros portefeuille, cela peut prendre environ une heure, et le nœud ne peut pas répondre au portefeuille avant la fin.",
|
||||
"pt": "Em andamento há %s. Em carteiras grandes pode levar cerca de uma hora, e o nó não consegue responder à carteira até terminar.",
|
||||
"ru": "Выполняется %s. Для больших кошельков это может занять около часа, и до окончания узел не может отвечать кошельку.",
|
||||
"zh": "已运行 %s。大型钱包可能需要约一小时,完成前节点无法响应钱包。",
|
||||
"ja": "経過時間 %s。大きなウォレットでは約 1 時間かかることがあり、完了するまでノードはウォレットに応答できません。",
|
||||
"ko": "%s 동안 진행 중입니다. 큰 지갑은 약 1시간이 걸릴 수 있으며, 끝날 때까지 노드가 지갑에 응답할 수 없습니다."
|
||||
},
|
||||
"node_busy_rebuild_reading": {
|
||||
"de": "Blöcke werden gelesen: %ld / %ld", "es": "Leyendo bloques: %ld / %ld", "fr": "Lecture des blocs : %ld / %ld",
|
||||
"pt": "Lendo blocos: %ld / %ld", "ru": "Чтение блоков: %ld / %ld", "zh": "正在读取区块:%ld / %ld",
|
||||
"ja": "ブロックを読み込み中:%ld / %ld", "ko": "블록 읽는 중: %ld / %ld"
|
||||
},
|
||||
"node_busy_rebuild_extending": {
|
||||
"de": "Witnesses werden erweitert (der Node meldet für diesen Schritt keinen Fortschritt).",
|
||||
"es": "Ampliando testigos (el nodo no informa del progreso de este paso).",
|
||||
"fr": "Extension des témoins (le nœud ne signale aucune progression pour cette étape).",
|
||||
"pt": "Estendendo testemunhas (o nó não informa o progresso desta etapa).",
|
||||
"ru": "Расширение свидетелей (узел не сообщает о ходе этого этапа).",
|
||||
"zh": "正在扩展见证(此步骤节点不报告进度)。",
|
||||
"ja": "ウィットネスを拡張しています(この段階ではノードは進捗を報告しません)。",
|
||||
"ko": "위트니스 확장 중(이 단계에서는 노드가 진행 상황을 보고하지 않습니다)."
|
||||
},
|
||||
"node_busy_replay_title": {
|
||||
"de": "Node beschäftigt: Blöcke werden nachgeholt", "es": "Nodo ocupado: poniéndose al día con los bloques",
|
||||
"fr": "Nœud occupé : rattrapage des blocs", "pt": "Nó ocupado: recuperando blocos",
|
||||
"ru": "Узел занят: догоняет блоки", "zh": "节点繁忙:正在追赶区块",
|
||||
"ja": "ノード処理中:ブロックを追いかけています", "ko": "노드 작업 중: 블록 따라잡는 중"
|
||||
},
|
||||
"node_busy_replay_desc": {
|
||||
"de": "Läuft seit %s. Der Node wendet Blöcke erneut an, die er vor dem letzten Beenden empfangen hat.",
|
||||
"es": "En curso desde hace %s. El nodo está volviendo a aplicar bloques que recibió antes de detenerse por última vez.",
|
||||
"fr": "En cours depuis %s. Le nœud réapplique les blocs reçus avant son dernier arrêt.",
|
||||
"pt": "Em andamento há %s. O nó está reaplicando blocos que recebeu antes da última parada.",
|
||||
"ru": "Выполняется %s. Узел повторно применяет блоки, полученные до последней остановки.",
|
||||
"zh": "已运行 %s。节点正在重新应用上次停止前收到的区块。",
|
||||
"ja": "経過時間 %s。ノードは前回停止する前に受信したブロックを再適用しています。",
|
||||
"ko": "%s 동안 진행 중입니다. 노드가 마지막으로 멈추기 전에 받은 블록을 다시 적용하고 있습니다."
|
||||
},
|
||||
"node_busy_rpc_title": {
|
||||
"de": "Node beschäftigt: antwortet noch nicht", "es": "Nodo ocupado: aún no responde",
|
||||
"fr": "Nœud occupé : ne répond pas encore", "pt": "Nó ocupado: ainda não responde",
|
||||
"ru": "Узел занят: пока не отвечает", "zh": "节点繁忙:暂未响应",
|
||||
"ja": "ノード処理中:まだ応答していません", "ko": "노드 작업 중: 아직 응답하지 않음"
|
||||
},
|
||||
"node_busy_rpc_desc": {
|
||||
"de": "Der Node läuft, ist aber seit %s zu beschäftigt, um der Wallet zu antworten – meist wegen eines langen Vorgangs wie dem Neuaufbau der Witnesses.",
|
||||
"es": "El nodo está en marcha, pero lleva %s demasiado ocupado para responder a la cartera, normalmente por una operación larga como la reconstrucción de testigos.",
|
||||
"fr": "Le nœud fonctionne mais est trop occupé pour répondre au portefeuille depuis %s, généralement à cause d'une longue opération comme la reconstruction des témoins.",
|
||||
"pt": "O nó está em execução, mas está ocupado demais para responder à carteira há %s, geralmente por causa de uma operação longa como a reconstrução de testemunhas.",
|
||||
"ru": "Узел работает, но уже %s слишком занят, чтобы отвечать кошельку, обычно из-за долгой операции, например перестройки свидетелей.",
|
||||
"zh": "节点正在运行,但已有 %s 忙于其他任务而无法响应钱包,通常是因为见证重建等耗时操作。",
|
||||
"ja": "ノードは実行中ですが、%s の間ウォレットに応答できないほど処理中です。通常はウィットネスの再構築などの長時間の処理が原因です。",
|
||||
"ko": "노드는 실행 중이지만 %s 동안 지갑에 응답할 수 없을 만큼 바쁩니다. 보통 위트니스 재구성 같은 긴 작업 때문입니다."
|
||||
},
|
||||
"node_starting_title": {
|
||||
"de": "DragonX-Node wird gestartet…", "es": "Iniciando el nodo DragonX…", "fr": "Démarrage du nœud DragonX…",
|
||||
"pt": "Iniciando o nó DragonX…", "ru": "Запуск узла DragonX…", "zh": "正在启动 DragonX 节点…",
|
||||
"ja": "DragonX ノードを起動しています…", "ko": "DragonX 노드 시작 중…"
|
||||
},
|
||||
"node_starting_busy_desc": {
|
||||
"de": "Der Node ist erreichbar, aber beschäftigt und antwortet noch nicht. Das ist nach einem Update oder beim ersten Start normal und kann einige Minuten dauern.",
|
||||
"es": "El nodo está accesible pero ocupado y aún no responde. Es normal tras una actualización o en el primer inicio; puede tardar unos minutos.",
|
||||
"fr": "Le nœud est joignable mais occupé et ne répond pas encore. C'est normal après une mise à jour ou au premier lancement ; cela peut prendre quelques minutes.",
|
||||
"pt": "O nó está acessível, mas ocupado, e ainda não responde. Isso é normal após uma atualização ou na primeira execução; pode levar alguns minutos.",
|
||||
"ru": "Узел доступен, но занят и пока не отвечает. Это нормально после обновления или при первом запуске; может занять несколько минут.",
|
||||
"zh": "节点可以访问,但正忙,暂未响应。更新后或首次启动时属正常现象,可能需要几分钟。",
|
||||
"ja": "ノードには接続できますが処理中のため、まだ応答していません。更新後や初回起動時には正常な状態で、数分かかることがあります。",
|
||||
"ko": "노드에 연결할 수 있지만 바빠서 아직 응답하지 않습니다. 업데이트 후나 처음 실행할 때는 정상이며, 몇 분 걸릴 수 있습니다."
|
||||
},
|
||||
"node_starting_launch_desc": {
|
||||
"de": "dragonxd wird gestartet, warte auf Verbindung…", "es": "Iniciando dragonxd y esperando a que esté en línea…",
|
||||
"fr": "Lancement de dragonxd, en attente de sa disponibilité…", "pt": "Iniciando o dragonxd e aguardando ficar online…",
|
||||
"ru": "Запуск dragonxd и ожидание его готовности…", "zh": "正在启动 dragonxd 并等待其上线…",
|
||||
"ja": "dragonxd を起動し、オンラインになるのを待っています…", "ko": "dragonxd를 시작하고 온라인 상태가 되기를 기다리는 중…"
|
||||
},
|
||||
"sb_node_busy": {
|
||||
"de": "Node beschäftigt", "es": "Nodo ocupado", "fr": "Nœud occupé", "pt": "Nó ocupado",
|
||||
"ru": "Узел занят", "zh": "节点繁忙", "ja": "ノード処理中", "ko": "노드 작업 중"
|
||||
},
|
||||
# --- Quitting while the node is busy ---
|
||||
"stopconfirm_rebuild_title": {
|
||||
"de": "Der Node baut die Note-Witnesses neu auf", "es": "El nodo está reconstruyendo los testigos de notas",
|
||||
"fr": "Le nœud reconstruit les témoins de notes", "pt": "O nó está reconstruindo as testemunhas de notas",
|
||||
"ru": "Узел перестраивает свидетелей заметок", "zh": "节点正在重建票据见证",
|
||||
"ja": "ノードはノートウィットネスを再構築しています", "ko": "노드가 노트 위트니스를 재구성하고 있습니다"
|
||||
},
|
||||
"stopconfirm_rebuild_body": {
|
||||
"de": "Er läuft seit %s, und bei großen Wallets kann das etwa eine Stunde dauern. Wenn Sie den Node jetzt stoppen, wird entweder bis zum Ende des Neuaufbaus gewartet, oder der nächste Start muss den Neuaufbau samt Nachholen wiederholen.\n\nEmpfohlen: Wallet schließen und den Node weiterlaufen lassen. Er beendet die Arbeit selbstständig.",
|
||||
"es": "Lleva %s en curso, y en carteras grandes puede tardar alrededor de una hora. Detener el nodo ahora o bien espera a que termine la reconstrucción, o bien hace que el próximo inicio la repita junto con cualquier puesta al día.\n\nRecomendado: cierra la cartera y deja el nodo en marcha. Terminará por su cuenta.",
|
||||
"fr": "Elle est en cours depuis %s, et pour un gros portefeuille elle peut prendre environ une heure. Arrêter le nœud maintenant attend la fin de la reconstruction, ou oblige son prochain démarrage à la refaire, rattrapage compris.\n\nRecommandé : fermez le portefeuille et laissez le nœud actif. Il terminera seul.",
|
||||
"pt": "Está em andamento há %s, e em carteiras grandes pode levar cerca de uma hora. Parar o nó agora ou espera a reconstrução terminar, ou faz o próximo início refazê-la junto com qualquer recuperação.\n\nRecomendado: feche a carteira e deixe o nó em execução. Ele termina sozinho.",
|
||||
"ru": "Она выполняется уже %s, а для больших кошельков может занять около часа. Если остановить узел сейчас, он либо дождётся окончания перестройки, либо при следующем запуске выполнит её заново вместе с догонянием блоков.\n\nРекомендуется: закройте кошелёк и оставьте узел работать. Он завершит всё сам.",
|
||||
"zh": "已运行 %s,大型钱包可能需要约一小时。现在停止节点,要么会等待重建完成,要么下次启动时需要重新进行重建以及区块追赶。\n\n建议:关闭钱包并保持节点运行,它会自行完成。",
|
||||
"ja": "経過時間は %s で、大きなウォレットでは約 1 時間かかることがあります。今ノードを停止すると、再構築の完了まで待つか、次回の起動時に再構築とブロックの追いつきをやり直すことになります。\n\n推奨:ウォレットを閉じ、ノードは実行したままにしてください。ノードは自動的に完了します。",
|
||||
"ko": "%s 동안 진행 중이며, 큰 지갑은 약 1시간이 걸릴 수 있습니다. 지금 노드를 중지하면 재구성이 끝날 때까지 기다리거나, 다음 시작 시 재구성과 블록 따라잡기를 다시 해야 합니다.\n\n권장: 지갑을 닫고 노드는 계속 실행하세요. 노드가 스스로 마칩니다."
|
||||
},
|
||||
"stopconfirm_busy_title": {
|
||||
"de": "Der Node ist beschäftigt", "es": "El nodo está ocupado", "fr": "Le nœud est occupé",
|
||||
"pt": "O nó está ocupado", "ru": "Узел занят", "zh": "节点正忙", "ja": "ノードは処理中です",
|
||||
"ko": "노드가 작업 중입니다"
|
||||
},
|
||||
"stopconfirm_busy_body": {
|
||||
"de": "Er ist seit %s zu beschäftigt, um der Wallet zu antworten – vermutlich wegen eines langen Vorgangs wie dem Neuaufbau der Witnesses. Wenn Sie ihn jetzt stoppen, muss der nächste Start diese Arbeit eventuell wiederholen.\n\nEmpfohlen: Wallet schließen und den Node weiterlaufen lassen. Er beendet die Arbeit selbstständig.",
|
||||
"es": "Lleva %s demasiado ocupado para responder a la cartera, probablemente por una operación larga como la reconstrucción de testigos. Detenerlo ahora puede hacer que el próximo inicio repita ese trabajo.\n\nRecomendado: cierra la cartera y deja el nodo en marcha. Terminará por su cuenta.",
|
||||
"fr": "Il est trop occupé pour répondre au portefeuille depuis %s, probablement à cause d'une longue opération comme la reconstruction des témoins. L'arrêter maintenant peut obliger son prochain démarrage à refaire ce travail.\n\nRecommandé : fermez le portefeuille et laissez le nœud actif. Il terminera seul.",
|
||||
"pt": "Está ocupado demais para responder à carteira há %s, provavelmente por causa de uma operação longa como a reconstrução de testemunhas. Pará-lo agora pode fazer o próximo início refazer esse trabalho.\n\nRecomendado: feche a carteira e deixe o nó em execução. Ele termina sozinho.",
|
||||
"ru": "Уже %s он слишком занят, чтобы отвечать кошельку, вероятно, из-за долгой операции, например перестройки свидетелей. Если остановить его сейчас, при следующем запуске эту работу, возможно, придётся выполнить заново.\n\nРекомендуется: закройте кошелёк и оставьте узел работать. Он завершит всё сам.",
|
||||
"zh": "节点已有 %s 忙于其他任务而无法响应钱包,可能是因为见证重建等耗时操作。现在停止它,下次启动时可能需要重做这些工作。\n\n建议:关闭钱包并保持节点运行,它会自行完成。",
|
||||
"ja": "%s の間、ウォレットに応答できないほど処理中です。ウィットネスの再構築などの長時間の処理が原因と考えられます。今停止すると、次回の起動時にその処理をやり直すことがあります。\n\n推奨:ウォレットを閉じ、ノードは実行したままにしてください。ノードは自動的に完了します。",
|
||||
"ko": "%s 동안 지갑에 응답할 수 없을 만큼 바빴습니다. 위트니스 재구성 같은 긴 작업 때문일 가능성이 큽니다. 지금 중지하면 다음 시작 시 그 작업을 다시 해야 할 수 있습니다.\n\n권장: 지갑을 닫고 노드는 계속 실행하세요. 노드가 스스로 마칩니다."
|
||||
},
|
||||
"stopconfirm_stop_wait": {
|
||||
"de": "Node stoppen & warten", "es": "Detener el nodo y esperar", "fr": "Arrêter le nœud et attendre",
|
||||
"pt": "Parar o nó e aguardar", "ru": "Остановить узел и подождать", "zh": "停止节点并等待",
|
||||
"ja": "ノードを停止して待つ", "ko": "노드 중지 후 대기"
|
||||
},
|
||||
"appx_daemon_reinstall_node_busy": {
|
||||
"de": "Der Node läuft noch, daher wurde der Daemon nicht neu installiert. Versuchen Sie es erneut, sobald er beendet ist.",
|
||||
"es": "El nodo sigue en marcha, así que no se reinstaló el daemon. Vuelve a intentarlo cuando se haya detenido.",
|
||||
"fr": "Le nœud est toujours actif, le démon n'a donc pas été réinstallé. Réessayez une fois qu'il sera arrêté.",
|
||||
"pt": "O nó ainda está em execução, por isso o daemon não foi reinstalado. Tente novamente quando ele tiver parado.",
|
||||
"ru": "Узел всё ещё работает, поэтому демон не был переустановлен. Повторите попытку, когда он остановится.",
|
||||
"zh": "节点仍在运行,因此未重新安装守护进程。请在它停止后重试。",
|
||||
"ja": "ノードがまだ実行中のため、デーモンは再インストールされませんでした。停止してからもう一度お試しください。",
|
||||
"ko": "노드가 아직 실행 중이라 데몬을 다시 설치하지 않았습니다. 노드가 멈춘 뒤 다시 시도하세요."
|
||||
},
|
||||
# --- Balance tab ---
|
||||
"hide_zero_balances": {
|
||||
"es": "Ocultar saldos 0", "de": "Nullsalden ausblenden", "fr": "Masquer les soldes à 0",
|
||||
@@ -2738,178 +2100,6 @@ TRANSLATIONS = {
|
||||
"pt": "EXPLORADOR", "ru": "ОБОЗРЕВАТЕЛЬ", "zh": "浏览器",
|
||||
"ja": "エクスプローラー", "ko": "탐색기"
|
||||
},
|
||||
|
||||
# --- Wallets dialog: metadata counts + status badges ---
|
||||
"wallets_col_txs": {
|
||||
"es": "tx", "de": "Tx", "fr": "tx", "pt": "tx",
|
||||
"ru": "трз", "zh": "笔交易", "ja": "件", "ko": "건"
|
||||
},
|
||||
"wallets_col_keys": {
|
||||
"es": "claves", "de": "Schlüssel", "fr": "clés", "pt": "chaves",
|
||||
"ru": "ключей", "zh": "个密钥", "ja": "個の鍵", "ko": "개 키"
|
||||
},
|
||||
"wallets_badge_encrypted": {
|
||||
"es": "Cifrada (protegida con contraseña)", "de": "Verschlüsselt (passphrasengeschützt)",
|
||||
"fr": "Chiffré (protégé par phrase secrète)", "pt": "Encriptada (protegida por senha)",
|
||||
"ru": "Зашифрован (защищён паролем)", "zh": "已加密(密码保护)",
|
||||
"ja": "暗号化済み(パスフレーズ保護)", "ko": "암호화됨 (암호로 보호됨)"
|
||||
},
|
||||
"wallets_badge_seed": {
|
||||
"es": "Billetera con frase semilla (HD)", "de": "Seed-Phrase-Wallet (HD)",
|
||||
"fr": "Portefeuille à phrase de récupération (HD)", "pt": "Carteira com frase semente (HD)",
|
||||
"ru": "Кошелёк с seed-фразой (HD)", "zh": "助记词钱包 (HD)",
|
||||
"ja": "シードフレーズウォレット (HD)", "ko": "시드 문구 지갑 (HD)"
|
||||
},
|
||||
"wallets_badge_legacy": {
|
||||
"es": "Billetera heredada (sin frase semilla)", "de": "Legacy-Wallet (keine Seed-Phrase)",
|
||||
"fr": "Portefeuille hérité (sans phrase de récupération)", "pt": "Carteira legada (sem frase semente)",
|
||||
"ru": "Устаревший кошелёк (без seed-фразы)", "zh": "旧版钱包(无助记词)",
|
||||
"ja": "レガシーウォレット(シードフレーズなし)", "ko": "레거시 지갑 (시드 문구 없음)"
|
||||
},
|
||||
"wallets_badge_unknown": {
|
||||
"es": "Tipo de billetera no determinado por completo (archivo grande — abra para confirmar)",
|
||||
"de": "Wallet-Typ nicht vollständig ermittelt (große Datei — zum Bestätigen öffnen)",
|
||||
"fr": "Type de portefeuille non entièrement déterminé (fichier volumineux — ouvrir pour confirmer)",
|
||||
"pt": "Tipo de carteira não totalmente determinado (arquivo grande — abra para confirmar)",
|
||||
"ru": "Тип кошелька определён не полностью (большой файл — откройте для подтверждения)",
|
||||
"zh": "钱包类型未完全确定(文件较大——打开以确认)",
|
||||
"ja": "ウォレットの種類を完全に判定できません(大きなファイル — 開いて確認)",
|
||||
"ko": "지갑 유형을 완전히 확인하지 못함 (큰 파일 — 열어서 확인)"
|
||||
},
|
||||
"wallets_badge_seed_short": {
|
||||
"es": "Frase semilla", "de": "Seed-Phrase", "fr": "Phrase secrète", "pt": "Frase semente",
|
||||
"ru": "Seed-фраза", "zh": "助记词", "ja": "シードフレーズ", "ko": "시드 문구"
|
||||
},
|
||||
"wallets_badge_encrypted_short": {
|
||||
"es": "Cifrada", "de": "Verschlüsselt", "fr": "Chiffré", "pt": "Encriptada",
|
||||
"ru": "Зашифрован", "zh": "已加密", "ja": "暗号化", "ko": "암호화됨"
|
||||
},
|
||||
"wallets_badge_legacy_short": {
|
||||
"es": "Heredada", "de": "Legacy", "fr": "Hérité", "pt": "Legada",
|
||||
"ru": "Устаревший", "zh": "旧版", "ja": "レガシー", "ko": "레거시"
|
||||
},
|
||||
"wallets_badge_unknown_short": {
|
||||
"es": "Desconocido", "de": "Unbekannt", "fr": "Inconnu", "pt": "Desconhecido",
|
||||
"ru": "Неизвестно", "zh": "未知", "ja": "不明", "ko": "알 수 없음"
|
||||
},
|
||||
|
||||
# --- Wallets dialog: created date + sort control ---
|
||||
"wallets_created": {
|
||||
"es": "creada", "de": "erstellt", "fr": "créé", "pt": "criada",
|
||||
"ru": "создан", "zh": "创建于", "ja": "作成", "ko": "생성"
|
||||
},
|
||||
"wallets_sort_by": {
|
||||
"es": "Ordenar:", "de": "Sortieren:", "fr": "Trier :", "pt": "Ordenar:",
|
||||
"ru": "Сортировка:", "zh": "排序:", "ja": "並べ替え:", "ko": "정렬:"
|
||||
},
|
||||
"wallets_sort_created": {
|
||||
"es": "Creado", "de": "Erstellt", "fr": "Créé", "pt": "Criado",
|
||||
"ru": "Создан", "zh": "创建", "ja": "作成", "ko": "생성"
|
||||
},
|
||||
"wallets_sort_addresses": {
|
||||
"es": "Direcciones", "de": "Adressen", "fr": "Adresses", "pt": "Endereços",
|
||||
"ru": "Адреса", "zh": "地址", "ja": "アドレス", "ko": "주소"
|
||||
},
|
||||
"wallets_sort_txs": {
|
||||
"es": "Txs", "de": "Txs", "fr": "Txs", "pt": "Txs",
|
||||
"ru": "Транз.", "zh": "交易", "ja": "取引", "ko": "거래"
|
||||
},
|
||||
"wallets_sort_size": {
|
||||
"es": "Tamaño", "de": "Größe", "fr": "Taille", "pt": "Tamanho",
|
||||
"ru": "Размер", "zh": "大小", "ja": "サイズ", "ko": "크기"
|
||||
},
|
||||
"wallets_sort_asc": {
|
||||
"es": "Ascendente (más antiguo / menos / más pequeño primero)",
|
||||
"de": "Aufsteigend (ältestes / wenigste / kleinstes zuerst)",
|
||||
"fr": "Croissant (plus ancien / moins / plus petit d'abord)",
|
||||
"pt": "Crescente (mais antigo / menos / menor primeiro)",
|
||||
"ru": "По возрастанию (сначала старые / меньше / меньший)",
|
||||
"zh": "升序(最早/最少/最小优先)", "ja": "昇順(古い/少ない/小さい順)", "ko": "오름차순 (오래된/적은/작은 순)"
|
||||
},
|
||||
"wallets_sort_desc": {
|
||||
"es": "Descendente (más reciente / más / más grande primero)",
|
||||
"de": "Absteigend (neuestes / meiste / größtes zuerst)",
|
||||
"fr": "Décroissant (plus récent / plus / plus grand d'abord)",
|
||||
"pt": "Decrescente (mais recente / mais / maior primeiro)",
|
||||
"ru": "По убыванию (сначала новые / больше / больший)",
|
||||
"zh": "降序(最新/最多/最大优先)", "ja": "降順(新しい/多い/大きい順)", "ko": "내림차순 (최신/많은/큰 순)"
|
||||
},
|
||||
"wallets_open_folder": {
|
||||
"es": "Abrir ubicación de la carpeta", "de": "Ordnerpfad öffnen",
|
||||
"fr": "Ouvrir l'emplacement du dossier", "pt": "Abrir local da pasta",
|
||||
"ru": "Открыть расположение папки", "zh": "打开文件夹位置",
|
||||
"ja": "フォルダーの場所を開く", "ko": "폴더 위치 열기"
|
||||
},
|
||||
"wallets_open_inplace_tt": {
|
||||
"es": "Abre esta cartera donde está, enlazándola al directorio de datos (sin copiar)",
|
||||
"de": "Öffnet diese Wallet an ihrem Ort — im Datenverzeichnis verlinkt (keine Kopie)",
|
||||
"fr": "Ouvre ce portefeuille à son emplacement — lié au répertoire de données (sans copie)",
|
||||
"pt": "Abre esta carteira onde está — vinculada ao diretório de dados (sem cópia)",
|
||||
"ru": "Открывает этот кошелёк на месте — по ссылке в каталоге данных (без копирования)",
|
||||
"zh": "在原位置打开此钱包 — 链接到数据目录(不复制)",
|
||||
"ja": "このウォレットをその場で開きます — データディレクトリにリンク(コピーなし)",
|
||||
"ko": "이 지갑을 있는 자리에서 엽니다 — 데이터 디렉터리에 링크 (복사 없음)"
|
||||
},
|
||||
"wallets_open_failed": {
|
||||
"es": "No se pudo abrir esta cartera en su ubicación. Probablemente está en una unidad distinta a tu directorio de datos: muévela a la misma unidad (en Windows, activar el Modo de desarrollador también permite enlazar entre unidades).",
|
||||
"de": "Diese Wallet konnte nicht an ihrem Ort geöffnet werden. Sie liegt vermutlich auf einem anderen Laufwerk als dein Datenverzeichnis — verschiebe sie auf dasselbe Laufwerk (unter Windows erlaubt auch der aktivierte Entwicklermodus laufwerkübergreifende Verknüpfungen).",
|
||||
"fr": "Impossible d'ouvrir ce portefeuille à son emplacement. Il se trouve probablement sur un lecteur différent de votre répertoire de données — déplacez-le sur le même lecteur (sous Windows, activer le mode développeur permet aussi de créer des liens entre lecteurs).",
|
||||
"pt": "Não foi possível abrir esta carteira no lugar. Provavelmente está em uma unidade diferente do seu diretório de dados — mova-a para a mesma unidade (no Windows, ativar o Modo de Desenvolvedor também permite vincular entre unidades).",
|
||||
"ru": "Не удалось открыть этот кошелёк на месте. Вероятно, он на другом диске, чем каталог данных — переместите его на тот же диск (в Windows включённый режим разработчика также позволяет создавать ссылки между дисками).",
|
||||
"zh": "无法在原位置打开此钱包。它可能与数据目录位于不同的驱动器上 — 请将其移动到同一驱动器(在 Windows 上,启用开发者模式也可跨驱动器链接)。",
|
||||
"ja": "このウォレットをその場で開けませんでした。データディレクトリとは別のドライブにある可能性があります — 同じドライブに移動してください(Windows では開発者モードを有効にするとドライブ間のリンクも可能になります)。",
|
||||
"ko": "이 지갑을 제자리에서 열 수 없습니다. 데이터 디렉터리와 다른 드라이브에 있을 가능성이 높습니다 — 같은 드라이브로 옮기세요 (Windows에서는 개발자 모드를 켜면 드라이브 간 링크도 가능합니다)."
|
||||
},
|
||||
"wallets_external_tt": {
|
||||
"es": "Fuera de tu directorio de datos — Abrir lo enlaza en su lugar (sin copiar).",
|
||||
"de": "Außerhalb deines Datenverzeichnisses — Öffnen verlinkt sie an ihrem Ort (keine Kopie).",
|
||||
"fr": "Hors de votre répertoire de données — Ouvrir le lie sur place (sans copie).",
|
||||
"pt": "Fora do seu diretório de dados — Abrir o vincula no lugar (sem cópia).",
|
||||
"ru": "Вне каталога данных — «Открыть» создаёт ссылку на месте (без копирования).",
|
||||
"zh": "在数据目录之外 —「打开」会就地链接(不复制)。",
|
||||
"ja": "データディレクトリの外 —「開く」はその場でリンクします(コピーなし)。",
|
||||
"ko": "데이터 디렉터리 밖 — '열기'는 제자리에 링크합니다 (복사 없음)."
|
||||
},
|
||||
"wallets_scanned_folders": {
|
||||
"es": "Carpetas escaneadas:", "de": "Durchsuchte Ordner:",
|
||||
"fr": "Dossiers analysés :", "pt": "Pastas verificadas:",
|
||||
"ru": "Просканированные папки:", "zh": "已扫描的文件夹:",
|
||||
"ja": "スキャン対象フォルダー:", "ko": "스캔한 폴더:"
|
||||
},
|
||||
"wallets_remove_folder": {
|
||||
"es": "Dejar de escanear esta carpeta", "de": "Diesen Ordner nicht mehr durchsuchen",
|
||||
"fr": "Ne plus analyser ce dossier", "pt": "Parar de verificar esta pasta",
|
||||
"ru": "Больше не сканировать эту папку", "zh": "停止扫描此文件夹",
|
||||
"ja": "このフォルダーのスキャンを停止", "ko": "이 폴더 스캔 중지"
|
||||
},
|
||||
"wallets_empty_hint": {
|
||||
"es": "Escanea una carpeta para encontrar más carteras",
|
||||
"de": "Ordner durchsuchen, um weitere Wallets zu finden",
|
||||
"fr": "Analysez un dossier pour trouver d'autres portefeuilles",
|
||||
"pt": "Verifique uma pasta para encontrar mais carteiras",
|
||||
"ru": "Просканируйте папку, чтобы найти другие кошельки",
|
||||
"zh": "扫描文件夹以查找更多钱包",
|
||||
"ja": "フォルダーをスキャンして他のウォレットを探す",
|
||||
"ko": "폴더를 스캔하여 다른 지갑 찾기"
|
||||
},
|
||||
"market_chart_loading": {
|
||||
"es": "Cargando historial de precios", "de": "Preisverlauf wird geladen",
|
||||
"fr": "Chargement de l'historique des prix", "pt": "Carregando histórico de preços",
|
||||
"ru": "Загрузка истории цен", "zh": "正在加载价格历史",
|
||||
"ja": "価格履歴を読み込み中", "ko": "가격 기록 불러오는 중"
|
||||
},
|
||||
"market_style_line": {
|
||||
"es": "Cambiar a gráfico de líneas", "de": "Zum Liniendiagramm wechseln",
|
||||
"fr": "Passer au graphique en ligne", "pt": "Mudar para gráfico de linhas",
|
||||
"ru": "Переключить на линейный график", "zh": "切换到折线图",
|
||||
"ja": "折れ線チャートに切り替え", "ko": "선형 차트로 전환"
|
||||
},
|
||||
"market_style_candle": {
|
||||
"es": "Cambiar a velas", "de": "Zu Kerzenchart wechseln",
|
||||
"fr": "Passer aux chandeliers", "pt": "Mudar para velas",
|
||||
"ru": "Переключить на свечи", "zh": "切换到蜡烛图",
|
||||
"ja": "ローソク足に切り替え", "ko": "캔들차트로 전환"
|
||||
},
|
||||
}
|
||||
|
||||
def main():
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cross-build a MINIMAL static FreeType for the mingw-w64 (Windows) target.
|
||||
#
|
||||
# Why: the wallet's optional color-emoji rendering needs FreeType (to rasterize the COLR/CPAL Twemoji
|
||||
# font). Native Linux/macOS pick up the system FreeType via find_package; the Debian/Ubuntu mingw-w64
|
||||
# cross toolchain ships no FreeType, so we build one here. The Twemoji font is COLRv0 (layered vector),
|
||||
# which FreeType renders WITHOUT libpng / harfbuzz / brotli / zlib — so this is a dependency-free static
|
||||
# build (no external libs to also cross-compile), producing a self-contained libfreetype.a.
|
||||
#
|
||||
# Output: <prefix>/include/freetype2/... + <prefix>/lib/libfreetype.a (default prefix: third_party/freetype-mingw)
|
||||
# build.sh --win-release runs this automatically and passes -DDRAGONX_MINGW_FREETYPE_PREFIX to CMake.
|
||||
set -euo pipefail
|
||||
|
||||
FT_VERSION="2.13.3"
|
||||
FT_SHA256="5c3a8e78f7b24c20b25b54ee575d6daa40007a5f4eea2845861c3409b3021747" # freetype-2.13.3.tar.gz
|
||||
FT_URL="https://download.savannah.gnu.org/releases/freetype/freetype-${FT_VERSION}.tar.gz"
|
||||
FT_URL_MIRROR="https://downloads.sourceforge.net/project/freetype/freetype2/${FT_VERSION}/freetype-${FT_VERSION}.tar.gz"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PREFIX="${1:-$SCRIPT_DIR/third_party/freetype-mingw}"
|
||||
WORK="$SCRIPT_DIR/third_party/.freetype-mingw-build"
|
||||
|
||||
# Already built? (libfreetype.a present) → nothing to do.
|
||||
if [[ -f "$PREFIX/lib/libfreetype.a" && -d "$PREFIX/include/freetype2" ]]; then
|
||||
echo "FreeType (mingw) already built at: $PREFIX"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Pick the mingw compilers (posix threads variant preferred, matching build.sh).
|
||||
if command -v x86_64-w64-mingw32-gcc-posix &>/dev/null; then
|
||||
MINGW_GCC=x86_64-w64-mingw32-gcc-posix; MINGW_GXX=x86_64-w64-mingw32-g++-posix
|
||||
elif command -v x86_64-w64-mingw32-gcc &>/dev/null; then
|
||||
MINGW_GCC=x86_64-w64-mingw32-gcc; MINGW_GXX=x86_64-w64-mingw32-g++
|
||||
else
|
||||
echo "ERROR: x86_64-w64-mingw32-gcc not found (install mingw-w64)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$WORK"
|
||||
cd "$WORK"
|
||||
|
||||
TARBALL="freetype-${FT_VERSION}.tar.gz"
|
||||
if [[ ! -f "$TARBALL" ]]; then
|
||||
echo "Downloading FreeType ${FT_VERSION} ..."
|
||||
curl -fsSL -o "$TARBALL" "$FT_URL" || curl -fsSL -o "$TARBALL" "$FT_URL_MIRROR"
|
||||
fi
|
||||
|
||||
echo "Verifying SHA-256 ..."
|
||||
echo "${FT_SHA256} ${TARBALL}" | sha256sum -c - || {
|
||||
echo "ERROR: FreeType tarball checksum mismatch (expected ${FT_SHA256})." >&2
|
||||
echo " got: $(sha256sum "$TARBALL" | cut -d' ' -f1)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
rm -rf "freetype-${FT_VERSION}"
|
||||
tar xf "$TARBALL"
|
||||
SRC="$WORK/freetype-${FT_VERSION}"
|
||||
|
||||
# Minimal mingw toolchain for FreeType's own CMake.
|
||||
cat > "$WORK/ft-mingw-toolchain.cmake" <<TOOLCHAIN
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
set(CMAKE_C_COMPILER ${MINGW_GCC})
|
||||
set(CMAKE_CXX_COMPILER ${MINGW_GXX})
|
||||
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
TOOLCHAIN
|
||||
|
||||
echo "Configuring FreeType (static, no external deps) ..."
|
||||
rm -rf "$WORK/build"
|
||||
cmake -S "$SRC" -B "$WORK/build" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$WORK/ft-mingw-toolchain.cmake" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX="$PREFIX" \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
-DFT_DISABLE_ZLIB=ON \
|
||||
-DFT_DISABLE_BZIP2=ON \
|
||||
-DFT_DISABLE_PNG=ON \
|
||||
-DFT_DISABLE_HARFBUZZ=ON \
|
||||
-DFT_DISABLE_BROTLI=ON
|
||||
|
||||
echo "Building + installing FreeType ..."
|
||||
cmake --build "$WORK/build" -j "$(nproc)"
|
||||
cmake --install "$WORK/build"
|
||||
|
||||
if [[ -f "$PREFIX/lib/libfreetype.a" ]]; then
|
||||
echo "OK: mingw FreeType -> $PREFIX/lib/libfreetype.a"
|
||||
else
|
||||
echo "ERROR: build did not produce libfreetype.a" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,17 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script uses bash 4+ features (mapfile, safe empty-array expansion under
|
||||
# `set -u`). macOS ships bash 3.2, so re-exec under a newer bash when one is
|
||||
# present (Homebrew), and fail with a clear message otherwise.
|
||||
if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then
|
||||
for _newer_bash in /opt/homebrew/bin/bash /usr/local/bin/bash; do
|
||||
[ -x "$_newer_bash" ] && exec "$_newer_bash" "$0" "$@"
|
||||
done
|
||||
echo "ERROR: build-lite-backend-artifact.sh requires bash 4+ (found ${BASH_VERSION:-unknown})." >&2
|
||||
echo " On macOS: brew install bash" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -19,7 +7,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
ABI_VERSION="sdxl-c-v1"
|
||||
LINK_MODE="imported"
|
||||
BACKEND_DIR="$PROJECT_ROOT/third_party/silentdragonxlite/lib"
|
||||
BACKEND_DIR="$PROJECT_ROOT/external/SilentDragonXLite/lib"
|
||||
BACKEND_SOURCE_DIR=""
|
||||
BUILD_BACKEND_DIR=""
|
||||
BACKEND_DEPENDENCY_DIR=""
|
||||
@@ -79,9 +67,25 @@ Options:
|
||||
--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.
|
||||
@@ -91,13 +95,9 @@ Outputs:
|
||||
<out>/<platform>/lite-backend-symbols.txt
|
||||
<out>/<platform>/lite-backend-artifact-manifest.json
|
||||
|
||||
The lite backend is always built from the vendored in-tree source
|
||||
(third_party/silentdragonxlite), which is the trust root. Prebuilt artifacts
|
||||
and self-attested signature metadata are NOT accepted (F15-1) — the previous
|
||||
scheme only recorded an unverified "verified" claim. The script captures the
|
||||
freshly-built artifact's symbols and checksum, and records build provenance.
|
||||
It does not load the library, resolve function pointers, call SDXL, sign,
|
||||
upload, or publish artifacts.
|
||||
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
|
||||
}
|
||||
|
||||
@@ -166,8 +166,15 @@ while [[ $# -gt 0 ]]; do
|
||||
OUT_DIR="$(absolute_path "$2")"
|
||||
shift 2
|
||||
;;
|
||||
--artifact|--no-build)
|
||||
die "$1 was removed (F15-1): the lite backend must be built from the vendored in-tree source (third_party/silentdragonxlite); prebuilt artifacts are no longer accepted."
|
||||
--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
|
||||
@@ -184,11 +191,54 @@ while [[ $# -gt 0 ]]; do
|
||||
BUILDER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--signature-required|--signature-file|--signature-path|--signature-format|\
|
||||
--signature-verification-tool|--signature-tool|--signature-verification-command|\
|
||||
--signature-key-fingerprint|--signature-certificate-identity|--signature-certificate-issuer|\
|
||||
--signature-transparency-log-url|--signature-verified-sha256)
|
||||
die "signature-attestation flags were removed (F15-1): they recorded a self-attested \"verified\" claim without running any cryptographic verifier. The lite backend is built from the vendored in-tree source, which is the trust root."
|
||||
--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"
|
||||
@@ -264,33 +314,6 @@ validate_backend_dependency_source() {
|
||||
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"
|
||||
|
||||
@@ -324,13 +347,7 @@ prepare_backend_source() {
|
||||
ln -s "$BACKEND_SOURCE_DIR/src" "$prepared_root/src"
|
||||
[[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock"
|
||||
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
|
||||
# Honor the pinned Rust toolchain (rust-toolchain.toml) inside the prepared root too,
|
||||
# so builds using --silentdragonxlitelib-dir still select rustc 1.63.
|
||||
[[ -f "$BACKEND_SOURCE_DIR/rust-toolchain.toml" ]] && ln -s "$BACKEND_SOURCE_DIR/rust-toolchain.toml" "$prepared_root/rust-toolchain.toml"
|
||||
[[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw"
|
||||
# Vendored crate deps (offline builds): the .cargo/config.toml's vendored-sources directory is
|
||||
# "vendor" relative to the build root, so expose it inside the prepared root too.
|
||||
[[ -d "$BACKEND_SOURCE_DIR/vendor" ]] && ln -s "$BACKEND_SOURCE_DIR/vendor" "$prepared_root/vendor"
|
||||
[[ -f "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" ]] && ln -s "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" "$prepared_root/silentdragonxlitelib.h"
|
||||
|
||||
local replacement="silentdragonxlitelib = { path = \"$BACKEND_DEPENDENCY_DIR\" }"
|
||||
@@ -472,8 +489,6 @@ build_with_cargo() {
|
||||
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")
|
||||
@@ -719,7 +734,6 @@ MANIFEST_FILE="$PLATFORM_OUT_DIR/lite-backend-artifact-manifest.json"
|
||||
printf ' },\n'
|
||||
printf ' "provenance": {\n'
|
||||
printf ' "owner_ready": true,\n'
|
||||
printf ' "built_from_source": true,\n'
|
||||
printf ' "metadata_provided": true,\n'
|
||||
printf ' "source": '; json_escape "$BACKEND_SOURCE_DIR"; printf ',\n'
|
||||
printf ' "cargo_build_source": '; json_escape "$BUILD_BACKEND_DIR"; printf ',\n'
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build a monochrome Noto Emoji subset for the chat/message UI.
|
||||
|
||||
Dear ImGui rasterizes fonts with stb_truetype, which handles only monochrome
|
||||
(outline `glyf`) fonts — NOT color emoji (CBDT/sbix/COLR). So we use Google's
|
||||
*monochrome* Noto Emoji (github.com/google/fonts, ofl/notoemoji, OFL-licensed)
|
||||
and merge it into the text fonts (see Typography::loadFont, the block after the
|
||||
CJK merge). ImGui renders one glyph per codepoint with no shaping, so ZWJ
|
||||
sequences / regional-indicator flags won't compose — single-codepoint emoji
|
||||
(😀 🎉 ❤ 🔥 👍 …) render fine, which covers the overwhelming majority of use.
|
||||
|
||||
Source is the variable font pinned to wght=400 → static, then subset to the
|
||||
emoji planes plus the higher symbol/star ranges the base UI font doesn't cover.
|
||||
The base Ubuntu font already owns U+2600–26FF etc.; ImGui's MergeMode gives the
|
||||
first-loaded glyph precedence, so those stay text-styled and only the codepoints
|
||||
the base lacks fall through to this font.
|
||||
|
||||
Get the source once (OFL, redistributable):
|
||||
curl -fsSL -o /tmp/NotoEmoji-VF.ttf \
|
||||
'https://github.com/google/fonts/raw/main/ofl/notoemoji/NotoEmoji%5Bwght%5D.ttf'
|
||||
|
||||
Then: python3 scripts/build_emoji_subset.py
|
||||
Output: res/fonts/NotoEmoji-Subset.ttf (committed; embedded via INCBIN)
|
||||
"""
|
||||
import os
|
||||
from fontTools import ttLib, subset
|
||||
from fontTools.varLib.instancer import instantiateVariableFont
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SOURCE_VF = '/tmp/NotoEmoji-VF.ttf'
|
||||
STATIC = '/tmp/NotoEmoji-Static.ttf'
|
||||
OUTPUT = os.path.join(ROOT, 'res', 'fonts', 'NotoEmoji-Subset.ttf')
|
||||
|
||||
# Emoji codepoints to keep. Ranges are inclusive.
|
||||
RANGES = [
|
||||
(0x1F000, 0x1FAFF), # all the main emoji planes (emoticons, pictographs, transport, supplement, extended)
|
||||
(0x2600, 0x27BF), # Miscellaneous Symbols + Dingbats
|
||||
(0x2B00, 0x2BFF), # stars (⭐ 2B50) and misc arrows
|
||||
(0xFE00, 0xFE0F), # variation selectors (VS16 emoji-style)
|
||||
(0x2194, 0x21AA), # arrows used as emoji
|
||||
(0x231A, 0x231B), # ⌚ ⌛
|
||||
(0x23E9, 0x23FA), # media-control emoji
|
||||
(0x25AA, 0x25FE), # small squares
|
||||
]
|
||||
SINGLES = [0x200D, 0x2934, 0x2935, 0x3030, 0x303D, 0x3297, 0x3299,
|
||||
0x00A9, 0x00AE, 0x2122, 0x2139, 0x24C2]
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(SOURCE_VF):
|
||||
raise SystemExit(f"missing source font {SOURCE_VF} — see the header for the curl command")
|
||||
|
||||
# 1. Pin the weight axis so stb_truetype rasterizes a clean static instance.
|
||||
f = ttLib.TTFont(SOURCE_VF)
|
||||
if 'fvar' in f:
|
||||
instantiateVariableFont(f, {'wght': 400}, inplace=True)
|
||||
f.save(STATIC)
|
||||
|
||||
unicodes = list(SINGLES)
|
||||
for lo, hi in RANGES:
|
||||
unicodes.extend(range(lo, hi + 1))
|
||||
|
||||
opts = subset.Options()
|
||||
opts.layout_features = [] # ImGui does no shaping — drop GSUB/GPOS
|
||||
opts.name_IDs = []
|
||||
opts.notdef_outline = True
|
||||
opts.glyph_names = False
|
||||
opts.drop_tables = ['GSUB', 'GPOS', 'GDEF', 'morx', 'kern']
|
||||
|
||||
font = subset.load_font(STATIC, opts)
|
||||
ss = subset.Subsetter(options=opts)
|
||||
ss.populate(unicodes=unicodes)
|
||||
ss.subset(font)
|
||||
subset.save_font(font, OUTPUT, opts)
|
||||
|
||||
out = ttLib.TTFont(OUTPUT)
|
||||
cmap = out.getBestCmap()
|
||||
color = any(t in out.reader.keys() for t in ('CBDT', 'sbix', 'COLR'))
|
||||
print(f"Output: {OUTPUT}")
|
||||
print(f"Size: {os.path.getsize(OUTPUT)//1024} KB | glyphs: {len(cmap)} | color tables: {color}")
|
||||
if color:
|
||||
raise SystemExit("ERROR: subset has color tables — stb_truetype cannot render it")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -48,36 +48,6 @@ for f in "${files[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Content guards (UX-hygiene threads) ──────────────────────────────────────
|
||||
# Low-false-positive regression checks for debt the UX audit cleared, so it can't
|
||||
# silently regrow. Each is deliberately narrow; widen only with an allowlist.
|
||||
for f in "${files[@]}"; do
|
||||
[[ "$f" == *.cpp ]] || continue
|
||||
[[ -f "$f" ]] || continue
|
||||
# app_sweep.cpp is the dev screenshot-sweep harness: its fixture strings are never shipped UI.
|
||||
[[ "$(basename "$f")" == "app_sweep.cpp" ]] && continue
|
||||
|
||||
# (1) Tactile button sized with a raw multi-digit pixel literal that ignores HiDPI.
|
||||
# Real sizes multiply by ui::Layout::dpiScale() / dp / hScale, or come from schema.
|
||||
if hits=$(grep -nE 'Tactile(Small)?Button\([^;]*ImVec2\([1-9][0-9]+(\.[0-9]+f?)?,' "$f" \
|
||||
| grep -vE 'dpiScale|\* *dp\b|\* *hdpi|hScale|schema|Scale\('); then
|
||||
while IFS= read -r line; do
|
||||
echo "✗ unscaled button size (multiply the px by ui::Layout::dpiScale()): $f:$line" >&2
|
||||
fail=1
|
||||
done <<< "$hits"
|
||||
fi
|
||||
|
||||
# (2) User-visible migrate/sweep status assigned a bare English literal instead of TR(...).
|
||||
# "__SENTINEL__" comparisons and empty strings are fine.
|
||||
if hits=$(grep -nE '(sweep_status_|seed_migration_status_)[[:space:]]*=[[:space:]]*"[A-Za-z]' "$f" \
|
||||
| grep -vE 'TR\(|"__'); then
|
||||
while IFS= read -r line; do
|
||||
echo "✗ untranslated status string (route through TR(...)): $f:$line" >&2
|
||||
fail=1
|
||||
done <<< "$hits"
|
||||
fi
|
||||
done
|
||||
|
||||
if (( fail )); then
|
||||
echo "" >&2
|
||||
echo "Source hygiene check failed. See docs in scripts/check-source-hygiene.sh." >&2
|
||||
|
||||
@@ -24,27 +24,18 @@ if [ ! -f "${BUILD_DIR}/bin/ObsidianDragon" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for appimagetool — pinned to a tagged release and SHA-256 verified before we exec it.
|
||||
# The old "continuous" tag is a moving, unverified network download that runs on the release
|
||||
# builder; verify it or refuse to package.
|
||||
APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL_SHA256="46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1"
|
||||
# Check for appimagetool
|
||||
APPIMAGETOOL=""
|
||||
if command -v appimagetool &> /dev/null; then
|
||||
APPIMAGETOOL="appimagetool" # maintainer's own trusted system install
|
||||
APPIMAGETOOL="appimagetool"
|
||||
elif [ -f "${BUILD_DIR}/appimagetool-x86_64.AppImage" ]; then
|
||||
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
else
|
||||
AT="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
if [ ! -f "$AT" ] || ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
|
||||
print_status "Downloading appimagetool 1.9.0 (pinned)..."
|
||||
wget -q -O "$AT" "$APPIMAGETOOL_URL"
|
||||
if ! echo "${APPIMAGETOOL_SHA256} ${AT}" | sha256sum -c --status; then
|
||||
print_error "appimagetool SHA-256 verification failed — refusing to use it"
|
||||
rm -f "$AT"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$AT"
|
||||
fi
|
||||
APPIMAGETOOL="$AT"
|
||||
print_status "Downloading appimagetool..."
|
||||
wget -q -O "${BUILD_DIR}/appimagetool-x86_64.AppImage" \
|
||||
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x "${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
APPIMAGETOOL="${BUILD_DIR}/appimagetool-x86_64.AppImage"
|
||||
fi
|
||||
|
||||
print_status "Creating AppDir structure..."
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate SDXL lite-wallet mainnet checkpoint entries from a fully-synced dragonxd.
|
||||
# Each entry is (height,"blockhash","serialized_sapling_tree") in checkpoints.rs format.
|
||||
# Fills the 1,770,000 -> tip gap so wallets reseed close to their birthday on rescan,
|
||||
# bounding the (divergence-prone) compact-block replay span. Usage:
|
||||
# scripts/gen-lite-checkpoints.sh [start] [step] > /tmp/new_checkpoints.txt
|
||||
set -euo pipefail
|
||||
|
||||
CLI=${DRAGONX_CLI:-/home/d/dragonx/src/dragonx-cli}
|
||||
START=${1:-1770000}
|
||||
STEP=${2:-10000}
|
||||
|
||||
tip=$("$CLI" getblockcount)
|
||||
end=$(( (tip / STEP) * STEP ))
|
||||
|
||||
# Sanity: confirm the method reproduces a KNOWN checkpoint tree before trusting it.
|
||||
ref_hash=$("$CLI" getblockhash 1760000 | tr -d '"[:space:]')
|
||||
ref_tree=$("$CLI" getblockmerkletree 1760000 | tr -d '"[:space:]')
|
||||
expect_hash="0000545a45b8d4ee4e4b423cb1ea74d67e3a04c320c6ea2f59ee06c08f91a117"
|
||||
if [ "$ref_hash" != "$expect_hash" ]; then
|
||||
echo "ABORT: getblockhash 1760000 = $ref_hash != known $expect_hash" >&2; exit 1
|
||||
fi
|
||||
echo "# self-check: 1760000 hash matches; tree len=${#ref_tree}" >&2
|
||||
|
||||
n=0
|
||||
h=$START
|
||||
while [ "$h" -le "$end" ]; do
|
||||
hash=$("$CLI" getblockhash "$h" | tr -d '"[:space:]')
|
||||
tree=$("$CLI" getblockmerkletree "$h" | tr -d '"[:space:]')
|
||||
if [ -z "$hash" ] || [ -z "$tree" ]; then echo "ABORT: empty hash/tree at $h" >&2; exit 1; fi
|
||||
printf '\t(%s,"%s",\n\t\t"%s"\n\t),\n' "$h" "$hash" "$tree"
|
||||
n=$((n+1))
|
||||
h=$((h+STEP))
|
||||
done
|
||||
echo "# generated $n checkpoints from $START to $end (tip=$tip)" >&2
|
||||
@@ -0,0 +1,645 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate German (de) translations for ObsidianDragon wallet."""
|
||||
import json, os
|
||||
|
||||
translations = {
|
||||
"24h_change": "24h Änderung",
|
||||
"24h_volume": "24h Volumen",
|
||||
"about": "Über",
|
||||
"about_block_explorer": "Block-Explorer",
|
||||
"about_block_height": "Blockhöhe:",
|
||||
"about_build_date": "Erstellungsdatum:",
|
||||
"about_build_type": "Build-Typ:",
|
||||
"about_chain": "Chain:",
|
||||
"about_connections": "Verbindungen:",
|
||||
"about_credits": "Danksagungen",
|
||||
"about_daemon": "Daemon:",
|
||||
"about_debug": "Debug",
|
||||
"about_dragonx": "Über ObsidianDragon",
|
||||
"about_edition": "ImGui Edition",
|
||||
"about_github": "GitHub",
|
||||
"about_imgui": "ImGui:",
|
||||
"about_license": "Lizenz",
|
||||
"about_license_text": "Diese Software wird unter der GNU General Public License v3 (GPLv3) veröffentlicht. Sie dürfen diese Software gemäß den Lizenzbedingungen frei verwenden, modifizieren und verbreiten.",
|
||||
"about_peers_count": "%zu Peers",
|
||||
"about_release": "Release",
|
||||
"about_title": "Über ObsidianDragon",
|
||||
"about_version": "Version:",
|
||||
"about_website": "Webseite",
|
||||
"acrylic": "Acryl",
|
||||
"add": "Hinzufügen",
|
||||
"address": "Adresse",
|
||||
"address_book_add": "Adresse hinzufügen",
|
||||
"address_book_add_new": "Neue hinzufügen",
|
||||
"address_book_added": "Adresse zum Buch hinzugefügt",
|
||||
"address_book_count": "%zu Adressen gespeichert",
|
||||
"address_book_deleted": "Eintrag gelöscht",
|
||||
"address_book_edit": "Adresse bearbeiten",
|
||||
"address_book_empty": "Keine gespeicherten Adressen. Klicken Sie auf 'Neue hinzufügen', um eine hinzuzufügen.",
|
||||
"address_book_exists": "Adresse existiert bereits im Buch",
|
||||
"address_book_title": "Adressbuch",
|
||||
"address_book_update_failed": "Aktualisierung fehlgeschlagen - Adresse könnte doppelt sein",
|
||||
"address_book_updated": "Adresse aktualisiert",
|
||||
"address_copied": "Adresse in Zwischenablage kopiert",
|
||||
"address_details": "Adressdetails",
|
||||
"address_label": "Adresse:",
|
||||
"address_upper": "ADRESSE",
|
||||
"address_url": "Adress-URL",
|
||||
"addresses_appear_here": "Ihre Empfangsadressen erscheinen hier, sobald Sie verbunden sind.",
|
||||
"advanced": "ERWEITERT",
|
||||
"all_filter": "Alle",
|
||||
"allow_custom_fees": "Benutzerdefinierte Gebühren erlauben",
|
||||
"amount": "Betrag",
|
||||
"amount_details": "BETRAGSDETAILS",
|
||||
"amount_exceeds_balance": "Betrag übersteigt Guthaben",
|
||||
"amount_label": "Betrag:",
|
||||
"appearance": "ERSCHEINUNGSBILD",
|
||||
"auto_shield": "Mining automatisch abschirmen",
|
||||
"available": "Verfügbar",
|
||||
"backup_backing_up": "Sicherung läuft...",
|
||||
"backup_create": "Sicherung erstellen",
|
||||
"backup_created": "Wallet-Sicherung erstellt",
|
||||
"backup_data": "SICHERUNG & DATEN",
|
||||
"backup_description": "Erstellen Sie eine Sicherung Ihrer wallet.dat-Datei. Diese Datei enthält alle Ihre privaten Schlüssel und den Transaktionsverlauf. Bewahren Sie die Sicherung an einem sicheren Ort auf.",
|
||||
"backup_destination": "Sicherungsziel:",
|
||||
"backup_tip_external": "Speichern Sie Sicherungen auf externen Laufwerken oder Cloud-Speicher",
|
||||
"backup_tip_multiple": "Erstellen Sie mehrere Sicherungen an verschiedenen Orten",
|
||||
"backup_tip_test": "Testen Sie regelmäßig die Wiederherstellung aus der Sicherung",
|
||||
"backup_tips": "Tipps:",
|
||||
"backup_title": "Wallet sichern",
|
||||
"backup_wallet": "Wallet sichern...",
|
||||
"backup_wallet_not_found": "Warnung: wallet.dat nicht am erwarteten Speicherort gefunden",
|
||||
"balance": "Guthaben",
|
||||
"balance_layout": "Guthaben-Layout",
|
||||
"ban": "Sperren",
|
||||
"banned_peers": "Gesperrte Peers",
|
||||
"block": "Block",
|
||||
"block_bits": "Bits:",
|
||||
"block_click_next": "Klicken für nächsten Block",
|
||||
"block_click_prev": "Klicken für vorherigen Block",
|
||||
"block_explorer": "Block-Explorer",
|
||||
"block_get_info": "Block-Info abrufen",
|
||||
"block_hash": "Block-Hash:",
|
||||
"block_height": "Blockhöhe:",
|
||||
"block_info_title": "Block-Informationen",
|
||||
"block_merkle_root": "Merkle-Root:",
|
||||
"block_nav_next": "Weiter >>",
|
||||
"block_nav_prev": "<< Zurück",
|
||||
"block_next": "Nächster Block:",
|
||||
"block_previous": "Vorheriger Block:",
|
||||
"block_size": "Größe:",
|
||||
"block_timestamp": "Zeitstempel:",
|
||||
"block_transactions": "Transaktionen:",
|
||||
"blockchain_syncing": "Blockchain synchronisiert (%.1f%%)... Guthaben könnten ungenau sein.",
|
||||
"cancel": "Abbrechen",
|
||||
"characters": "Zeichen",
|
||||
"clear": "Leeren",
|
||||
"clear_all_bans": "Alle Sperren aufheben",
|
||||
"clear_form_confirm": "Alle Formularfelder leeren?",
|
||||
"clear_request": "Anfrage leeren",
|
||||
"click_copy_address": "Klicken zum Kopieren der Adresse",
|
||||
"click_copy_uri": "Klicken zum Kopieren der URI",
|
||||
"close": "Schließen",
|
||||
"conf_count": "%d Best.",
|
||||
"confirm_and_send": "Bestätigen & Senden",
|
||||
"confirm_send": "Senden bestätigen",
|
||||
"confirm_transaction": "Transaktion bestätigen",
|
||||
"confirmations": "Bestätigungen",
|
||||
"confirmations_display": "%d Bestätigungen | %s",
|
||||
"confirmed": "Bestätigt",
|
||||
"connected": "Verbunden",
|
||||
"connected_peers": "Verbundene Peers",
|
||||
"connecting": "Verbinde...",
|
||||
"console": "Konsole",
|
||||
"console_auto_scroll": "Automatisch scrollen",
|
||||
"console_available_commands": "Verfügbare Befehle:",
|
||||
"console_capturing_output": "Erfasse Daemon-Ausgabe...",
|
||||
"console_clear": "Leeren",
|
||||
"console_clear_console": "Konsole leeren",
|
||||
"console_cleared": "Konsole geleert",
|
||||
"console_click_commands": "Befehle oben klicken zum Einfügen",
|
||||
"console_click_insert": "Klicken zum Einfügen",
|
||||
"console_click_insert_params": "Klicken zum Einfügen mit Parametern",
|
||||
"console_close": "Schließen",
|
||||
"console_commands": "Befehle",
|
||||
"console_common_rpc": "Häufige RPC-Befehle:",
|
||||
"console_completions": "Vervollständigungen:",
|
||||
"console_connected": "Verbunden mit Daemon",
|
||||
"console_copy_all": "Alles kopieren",
|
||||
"console_copy_selected": "Kopieren",
|
||||
"console_daemon": "Daemon",
|
||||
"console_daemon_error": "Daemon-Fehler!",
|
||||
"console_daemon_started": "Daemon gestartet",
|
||||
"console_daemon_stopped": "Daemon gestoppt",
|
||||
"console_disconnected": "Vom Daemon getrennt",
|
||||
"console_errors": "Fehler",
|
||||
"console_filter_hint": "Ausgabe filtern...",
|
||||
"console_help_clear": " clear - Konsole leeren",
|
||||
"console_help_getbalance": " getbalance - Transparentes Guthaben anzeigen",
|
||||
"console_help_getblockcount": " getblockcount - Aktuelle Blockhöhe anzeigen",
|
||||
"console_help_getinfo": " getinfo - Knoteninformationen anzeigen",
|
||||
"console_help_getmininginfo": " getmininginfo - Mining-Status anzeigen",
|
||||
"console_help_getpeerinfo": " getpeerinfo - Verbundene Peers anzeigen",
|
||||
"console_help_gettotalbalance": " gettotalbalance - Gesamtguthaben anzeigen",
|
||||
"console_help_help": " help - Diese Hilfe anzeigen",
|
||||
"console_help_setgenerate": " setgenerate - Mining steuern",
|
||||
"console_help_stop": " stop - Daemon stoppen",
|
||||
"console_line_count": "%zu Zeilen",
|
||||
"console_new_lines": "%d neue Zeilen",
|
||||
"console_no_daemon": "Kein Daemon",
|
||||
"console_not_connected": "Fehler: Nicht mit Daemon verbunden",
|
||||
"console_rpc_reference": "RPC-Befehlsreferenz",
|
||||
"console_scanline": "Konsolen-Scanline",
|
||||
"console_search_commands": "Befehle suchen...",
|
||||
"console_select_all": "Alles auswählen",
|
||||
"console_show_daemon_output": "Daemon-Ausgabe anzeigen",
|
||||
"console_show_errors_only": "Nur Fehler anzeigen",
|
||||
"console_show_rpc_ref": "RPC-Befehlsreferenz anzeigen",
|
||||
"console_showing_lines": "Zeige %zu von %zu Zeilen",
|
||||
"console_starting_node": "Knoten wird gestartet...",
|
||||
"console_status_error": "Fehler",
|
||||
"console_status_running": "Läuft",
|
||||
"console_status_starting": "Startet",
|
||||
"console_status_stopped": "Gestoppt",
|
||||
"console_status_stopping": "Stoppt",
|
||||
"console_status_unknown": "Unbekannt",
|
||||
"console_tab_completion": "Tab zur Vervollständigung",
|
||||
"console_type_help": "Geben Sie 'help' ein für verfügbare Befehle",
|
||||
"console_welcome": "Willkommen bei ObsidianDragon Konsole",
|
||||
"console_zoom_in": "Vergrößern",
|
||||
"console_zoom_out": "Verkleinern",
|
||||
"copy": "Kopieren",
|
||||
"copy_address": "Vollständige Adresse kopieren",
|
||||
"copy_error": "Fehler kopieren",
|
||||
"copy_to_clipboard": "In Zwischenablage kopieren",
|
||||
"copy_txid": "TxID kopieren",
|
||||
"copy_uri": "URI kopieren",
|
||||
"current_price": "Aktueller Preis",
|
||||
"custom_fees": "Benutzerdefinierte Gebühren",
|
||||
"dark": "Dunkel",
|
||||
"date": "Datum",
|
||||
"date_label": "Datum:",
|
||||
"delete": "Löschen",
|
||||
"difficulty": "Schwierigkeit",
|
||||
"disconnected": "Getrennt",
|
||||
"dismiss": "Verwerfen",
|
||||
"display": "Anzeige",
|
||||
"dragonx_green": "DragonX (Grün)",
|
||||
"edit": "Bearbeiten",
|
||||
"error": "Fehler",
|
||||
"est_time_to_block": "Gesch. Zeit bis Block",
|
||||
"exit": "Beenden",
|
||||
"explorer": "EXPLORER",
|
||||
"export": "Exportieren",
|
||||
"export_csv": "CSV exportieren",
|
||||
"export_keys_btn": "Schlüssel exportieren",
|
||||
"export_keys_danger": "ACHTUNG: Dies exportiert ALLE privaten Schlüssel aus Ihrer Wallet! Jeder mit Zugriff auf diese Datei kann Ihre Gelder stehlen. Sicher aufbewahren und nach Gebrauch löschen.",
|
||||
"export_keys_include_t": "T-Adressen einschließen (transparent)",
|
||||
"export_keys_include_z": "Z-Adressen einschließen (abgeschirmt)",
|
||||
"export_keys_options": "Export-Optionen:",
|
||||
"export_keys_success": "Schlüssel erfolgreich exportiert",
|
||||
"export_keys_title": "Alle privaten Schlüssel exportieren",
|
||||
"export_private_key": "Privaten Schlüssel exportieren",
|
||||
"export_tx_count": "%zu Transaktionen als CSV exportieren.",
|
||||
"export_tx_file_fail": "CSV-Datei konnte nicht erstellt werden",
|
||||
"export_tx_none": "Keine Transaktionen zum Exportieren",
|
||||
"export_tx_success": "Transaktionen erfolgreich exportiert",
|
||||
"export_tx_title": "Transaktionen als CSV exportieren",
|
||||
"export_viewing_key": "Betrachtungsschlüssel exportieren",
|
||||
"failed_create_shielded": "Abgeschirmte Adresse konnte nicht erstellt werden",
|
||||
"failed_create_transparent": "Transparente Adresse konnte nicht erstellt werden",
|
||||
"fee": "Gebühr",
|
||||
"fee_high": "Hoch",
|
||||
"fee_label": "Gebühr:",
|
||||
"fee_low": "Niedrig",
|
||||
"fee_normal": "Normal",
|
||||
"fetch_prices": "Preise abrufen",
|
||||
"file": "Datei",
|
||||
"file_save_location": "Datei wird gespeichert in: ~/.config/ObsidianDragon/",
|
||||
"font_scale": "Schriftgröße",
|
||||
"from": "Von",
|
||||
"from_upper": "VON",
|
||||
"full_details": "Alle Details",
|
||||
"general": "Allgemein",
|
||||
"go_to_receive": "Zum Empfangen",
|
||||
"height": "Höhe",
|
||||
"help": "Hilfe",
|
||||
"hide": "Ausblenden",
|
||||
"history": "Verlauf",
|
||||
"immature_type": "Unreif",
|
||||
"import": "Importieren",
|
||||
"import_key_btn": "Schlüssel importieren",
|
||||
"import_key_formats": "Unterstützte Schlüsselformate:",
|
||||
"import_key_full_rescan": "(0 = vollständiger Rescan)",
|
||||
"import_key_label": "Privater Schlüssel:",
|
||||
"import_key_no_valid": "Keine gültigen Schlüssel in der Eingabe gefunden",
|
||||
"import_key_rescan": "Blockchain nach Import neu scannen",
|
||||
"import_key_start_height": "Starthöhe:",
|
||||
"import_key_success": "Schlüssel erfolgreich importiert",
|
||||
"import_key_t_format": "T-Adresse WIF private Schlüssel",
|
||||
"import_key_title": "Privaten Schlüssel importieren",
|
||||
"import_key_tooltip": "Geben Sie einen oder mehrere private Schlüssel ein, einen pro Zeile.\nUnterstützt sowohl z-Adresse als auch t-Adresse Schlüssel.\nZeilen die mit # beginnen werden als Kommentare behandelt.",
|
||||
"import_key_warning": "Warnung: Teilen Sie niemals Ihre privaten Schlüssel! Das Importieren von Schlüsseln aus nicht vertrauenswürdigen Quellen kann Ihr Wallet gefährden.",
|
||||
"import_key_z_format": "Z-Adresse Ausgabeschlüssel (secret-extended-key-...)",
|
||||
"import_private_key": "Privaten Schlüssel importieren...",
|
||||
"invalid_address": "Ungültiges Adressformat",
|
||||
"ip_address": "IP-Adresse",
|
||||
"keep": "Behalten",
|
||||
"keep_daemon": "Daemon weiterlaufen lassen",
|
||||
"key_export_fetching": "Schlüssel wird aus Wallet abgerufen...",
|
||||
"key_export_private_key": "Privater Schlüssel:",
|
||||
"key_export_private_warning": "Halten Sie diesen Schlüssel GEHEIM! Jeder mit diesem Schlüssel kann Ihre Gelder ausgeben. Teilen Sie ihn niemals online oder mit nicht vertrauenswürdigen Parteien.",
|
||||
"key_export_reveal": "Schlüssel anzeigen",
|
||||
"key_export_viewing_key": "Betrachtungsschlüssel:",
|
||||
"key_export_viewing_warning": "Dieser Betrachtungsschlüssel ermöglicht es anderen, Ihre eingehenden Transaktionen und Ihr Guthaben zu sehen, aber NICHT Ihre Gelder auszugeben. Teilen Sie ihn nur mit vertrauenswürdigen Parteien.",
|
||||
"label": "Bezeichnung:",
|
||||
"language": "Sprache",
|
||||
"light": "Hell",
|
||||
"loading": "Laden...",
|
||||
"loading_addresses": "Adressen werden geladen...",
|
||||
"local_hashrate": "Lokale Hashrate",
|
||||
"low_spec_mode": "Energiesparmodus",
|
||||
"market": "Markt",
|
||||
"market_12h": "12h",
|
||||
"market_18h": "18h",
|
||||
"market_24h": "24h",
|
||||
"market_24h_volume": "24H VOLUMEN",
|
||||
"market_6h": "6h",
|
||||
"market_attribution": "Preisdaten von NonKYC",
|
||||
"market_btc_price": "BTC PREIS",
|
||||
"market_cap": "Marktkapitalisierung",
|
||||
"market_no_history": "Kein Preisverlauf verfügbar",
|
||||
"market_no_price": "Keine Preisdaten",
|
||||
"market_now": "Jetzt",
|
||||
"market_pct_shielded": "%.0f%% Abgeschirmt",
|
||||
"market_portfolio": "PORTFOLIO",
|
||||
"market_price_unavailable": "Preisdaten nicht verfügbar",
|
||||
"market_refresh_price": "Preisdaten aktualisieren",
|
||||
"market_trade_on": "Handeln auf %s",
|
||||
"mature": "Reif",
|
||||
"max": "Max",
|
||||
"memo": "Memo (optional, verschlüsselt)",
|
||||
"memo_label": "Memo:",
|
||||
"memo_optional": "MEMO (OPTIONAL)",
|
||||
"memo_upper": "MEMO",
|
||||
"memo_z_only": "Hinweis: Memos sind nur beim Senden an abgeschirmte (z) Adressen verfügbar",
|
||||
"merge_description": "Mehrere UTXOs zu einer einzelnen abgeschirmten Adresse zusammenführen. Dies kann die Wallet-Größe reduzieren und die Privatsphäre verbessern.",
|
||||
"merge_funds": "Gelder zusammenführen",
|
||||
"merge_started": "Zusammenführung gestartet",
|
||||
"merge_title": "An Adresse zusammenführen",
|
||||
"mine_when_idle": "Im Leerlauf minen",
|
||||
"mined": "gemined",
|
||||
"mined_filter": "Gemined",
|
||||
"mined_type": "Gemined",
|
||||
"mined_upper": "GEMINED",
|
||||
"miner_fee": "Miner-Gebühr",
|
||||
"mining": "Mining",
|
||||
"mining_active": "Aktiv",
|
||||
"mining_address_copied": "Mining-Adresse kopiert",
|
||||
"mining_all_time": "Gesamt",
|
||||
"mining_already_saved": "Pool-URL bereits gespeichert",
|
||||
"mining_block_copied": "Block-Hash kopiert",
|
||||
"mining_chart_1m_ago": "vor 1m",
|
||||
"mining_chart_5m_ago": "vor 5m",
|
||||
"mining_chart_now": "Jetzt",
|
||||
"mining_click": "Klicken",
|
||||
"mining_click_copy_address": "Klicken zum Kopieren der Adresse",
|
||||
"mining_click_copy_block": "Klicken zum Kopieren des Block-Hash",
|
||||
"mining_click_copy_difficulty": "Klicken zum Kopieren der Schwierigkeit",
|
||||
"mining_connected": "Verbunden",
|
||||
"mining_connecting": "Verbinde...",
|
||||
"mining_control": "Mining-Steuerung",
|
||||
"mining_difficulty_copied": "Schwierigkeit kopiert",
|
||||
"mining_est_block": "Gesch. Block",
|
||||
"mining_est_daily": "Gesch. täglich",
|
||||
"mining_filter_all": "Alle",
|
||||
"mining_filter_tip_all": "Alle Einnahmen anzeigen",
|
||||
"mining_filter_tip_pool": "Nur Pool-Einnahmen anzeigen",
|
||||
"mining_filter_tip_solo": "Nur Solo-Einnahmen anzeigen",
|
||||
"mining_idle_off_tooltip": "Leerlauf-Mining aktivieren",
|
||||
"mining_idle_on_tooltip": "Leerlauf-Mining deaktivieren",
|
||||
"mining_local_hashrate": "Lokale Hashrate",
|
||||
"mining_mine": "Minen",
|
||||
"mining_mining_addr": "Mining-Adr.",
|
||||
"mining_network": "Netzwerk",
|
||||
"mining_no_blocks_yet": "Noch keine Blöcke gefunden",
|
||||
"mining_no_payouts_yet": "Noch keine Pool-Auszahlungen",
|
||||
"mining_no_saved_addresses": "Keine gespeicherten Adressen",
|
||||
"mining_no_saved_pools": "Keine gespeicherten Pools",
|
||||
"mining_off": "Mining ist AUS",
|
||||
"mining_on": "Mining ist AN",
|
||||
"mining_open_in_explorer": "Im Explorer öffnen",
|
||||
"mining_payout_address": "Auszahlungsadresse",
|
||||
"mining_payout_tooltip": "Adresse für Mining-Belohnungen",
|
||||
"mining_pool": "Pool",
|
||||
"mining_pool_hashrate": "Pool-Hashrate",
|
||||
"mining_pool_url": "Pool-URL",
|
||||
"mining_recent_blocks": "LETZTE BLÖCKE",
|
||||
"mining_recent_payouts": "LETZTE POOL-AUSZAHLUNGEN",
|
||||
"mining_remove": "Entfernen",
|
||||
"mining_reset_defaults": "Standardwerte zurücksetzen",
|
||||
"mining_save_payout_address": "Auszahlungsadresse speichern",
|
||||
"mining_save_pool_url": "Pool-URL speichern",
|
||||
"mining_saved_addresses": "Gespeicherte Adressen:",
|
||||
"mining_saved_pools": "Gespeicherte Pools:",
|
||||
"mining_shares": "Shares",
|
||||
"mining_show_chart": "Diagramm",
|
||||
"mining_show_log": "Protokoll",
|
||||
"mining_solo": "Solo",
|
||||
"mining_starting": "Startet...",
|
||||
"mining_starting_tooltip": "Miner startet...",
|
||||
"mining_statistics": "Mining-Statistiken",
|
||||
"mining_stop": "Stopp",
|
||||
"mining_stop_solo_for_pool": "Solo-Mining stoppen bevor Pool-Mining gestartet wird",
|
||||
"mining_stop_solo_for_pool_settings": "Solo-Mining stoppen um Pool-Einstellungen zu ändern",
|
||||
"mining_stopping": "Stoppt...",
|
||||
"mining_stopping_tooltip": "Miner stoppt...",
|
||||
"mining_syncing_tooltip": "Blockchain synchronisiert...",
|
||||
"mining_threads": "Mining-Threads",
|
||||
"mining_to_save": "zum Speichern",
|
||||
"mining_today": "Heute",
|
||||
"mining_uptime": "Laufzeit",
|
||||
"mining_yesterday": "Gestern",
|
||||
"network": "Netzwerk",
|
||||
"network_fee": "NETZWERKGEBÜHR",
|
||||
"network_hashrate": "Netzwerk-Hashrate",
|
||||
"new": "+ Neu",
|
||||
"new_shielded_created": "Neue abgeschirmte Adresse erstellt",
|
||||
"new_t_address": "Neue T-Adresse",
|
||||
"new_t_transparent": "Neue t-Adresse (Transparent)",
|
||||
"new_transparent_created": "Neue transparente Adresse erstellt",
|
||||
"new_z_address": "Neue Z-Adresse",
|
||||
"new_z_shielded": "Neue z-Adresse (Abgeschirmt)",
|
||||
"no_addresses": "Keine Adressen gefunden. Erstellen Sie eine mit den Schaltflächen oben.",
|
||||
"no_addresses_available": "Keine Adressen verfügbar",
|
||||
"no_addresses_match": "Keine Adressen passen zum Filter",
|
||||
"no_addresses_with_balance": "Keine Adressen mit Guthaben",
|
||||
"no_matching": "Keine passenden Transaktionen",
|
||||
"no_recent_receives": "Keine kürzlichen Empfänge",
|
||||
"no_recent_sends": "Keine kürzlichen Sendungen",
|
||||
"no_transactions": "Keine Transaktionen gefunden",
|
||||
"node": "KNOTEN",
|
||||
"node_security": "KNOTEN & SICHERHEIT",
|
||||
"noise": "Rauschen",
|
||||
"not_connected": "Nicht mit Daemon verbunden...",
|
||||
"not_connected_to_daemon": "Nicht mit Daemon verbunden",
|
||||
"notes": "Notizen",
|
||||
"notes_optional": "Notizen (optional):",
|
||||
"output_filename": "Ausgabedateiname:",
|
||||
"overview": "Übersicht",
|
||||
"paste": "Einfügen",
|
||||
"paste_from_clipboard": "Aus Zwischenablage einfügen",
|
||||
"pay_from": "Zahlen von",
|
||||
"payment_request": "ZAHLUNGSANFRAGE",
|
||||
"payment_request_copied": "Zahlungsanfrage kopiert",
|
||||
"payment_uri_copied": "Zahlungs-URI kopiert",
|
||||
"peers": "Peers",
|
||||
"peers_avg_ping": "Durchschn. Ping",
|
||||
"peers_ban_24h": "Peer 24h sperren",
|
||||
"peers_ban_score": "Sperr-Score: %d",
|
||||
"peers_banned": "Gesperrt",
|
||||
"peers_banned_count": "Gesperrt: %d",
|
||||
"peers_best_block": "Bester Block",
|
||||
"peers_blockchain": "BLOCKCHAIN",
|
||||
"peers_blocks": "Blöcke",
|
||||
"peers_blocks_left": "%d Blöcke übrig",
|
||||
"peers_clear_all_bans": "Alle Sperren aufheben",
|
||||
"peers_click_copy": "Klicken zum Kopieren",
|
||||
"peers_connected": "Verbunden",
|
||||
"peers_connected_count": "Verbunden: %d",
|
||||
"peers_copy_ip": "IP kopieren",
|
||||
"peers_dir_in": "Ein",
|
||||
"peers_dir_out": "Aus",
|
||||
"peers_hash_copied": "Hash kopiert",
|
||||
"peers_hashrate": "Hashrate",
|
||||
"peers_in_out": "Ein/Aus",
|
||||
"peers_longest": "Längste",
|
||||
"peers_longest_chain": "Längste Chain",
|
||||
"peers_memory": "Speicher",
|
||||
"peers_no_banned": "Keine gesperrten Peers",
|
||||
"peers_no_connected": "Keine verbundenen Peers",
|
||||
"peers_no_tls": "Kein TLS",
|
||||
"peers_notarized": "Notarisiert",
|
||||
"peers_p2p_port": "P2P-Port",
|
||||
"peers_protocol": "Protokoll",
|
||||
"peers_received": "Empfangen",
|
||||
"peers_refresh": "Aktualisieren",
|
||||
"peers_refresh_tooltip": "Peer-Liste aktualisieren",
|
||||
"peers_refreshing": "Aktualisiere...",
|
||||
"peers_sent": "Gesendet",
|
||||
"peers_tt_id": "ID: %d",
|
||||
"peers_tt_received": "Empfangen: %s",
|
||||
"peers_tt_sent": "Gesendet: %s",
|
||||
"peers_tt_services": "Dienste: %s",
|
||||
"peers_tt_start_height": "Starthöhe: %d",
|
||||
"peers_tt_synced": "Synchronisiert H/B: %d/%d",
|
||||
"peers_tt_tls_cipher": "TLS: %s",
|
||||
"peers_unban": "Entsperren",
|
||||
"peers_upper": "PEERS",
|
||||
"peers_version": "Version",
|
||||
"pending": "Ausstehend",
|
||||
"ping": "Ping",
|
||||
"price_chart": "Preisdiagramm",
|
||||
"qr_code": "QR-Code",
|
||||
"qr_failed": "QR-Code konnte nicht generiert werden",
|
||||
"qr_title": "QR-Code",
|
||||
"qr_unavailable": "QR nicht verfügbar",
|
||||
"receive": "Empfangen",
|
||||
"received": "empfangen",
|
||||
"received_filter": "Empfangen",
|
||||
"received_label": "Empfangen",
|
||||
"received_upper": "EMPFANGEN",
|
||||
"receiving_addresses": "Ihre Empfangsadressen",
|
||||
"recent_received": "KÜRZLICH EMPFANGEN",
|
||||
"recent_sends": "KÜRZLICH GESENDET",
|
||||
"recipient": "EMPFÄNGER",
|
||||
"recv_type": "Empf.",
|
||||
"refresh": "Aktualisieren",
|
||||
"refresh_now": "Jetzt aktualisieren",
|
||||
"report_bug": "Fehler melden",
|
||||
"request_amount": "Betrag (optional):",
|
||||
"request_copy_uri": "URI kopieren",
|
||||
"request_description": "Erstellen Sie eine Zahlungsanfrage, die andere scannen oder kopieren können. Der QR-Code enthält Ihre Adresse und optionalen Betrag/Memo.",
|
||||
"request_label": "Bezeichnung (optional):",
|
||||
"request_memo": "Memo (optional):",
|
||||
"request_payment": "Zahlung anfordern",
|
||||
"request_payment_uri": "Zahlungs-URI:",
|
||||
"request_receive_address": "Empfangsadresse:",
|
||||
"request_select_address": "Adresse auswählen...",
|
||||
"request_shielded_addrs": "-- Abgeschirmte Adressen --",
|
||||
"request_title": "Zahlung anfordern",
|
||||
"request_transparent_addrs": "-- Transparente Adressen --",
|
||||
"request_uri_copied": "Zahlungs-URI in Zwischenablage kopiert",
|
||||
"rescan": "Neu scannen",
|
||||
"reset_to_defaults": "Standardwerte zurücksetzen",
|
||||
"review_send": "Senden prüfen",
|
||||
"rpc_host": "RPC-Host",
|
||||
"rpc_pass": "Passwort",
|
||||
"rpc_port": "Port",
|
||||
"rpc_user": "Benutzername",
|
||||
"save": "Speichern",
|
||||
"save_settings": "Einstellungen speichern",
|
||||
"save_z_transactions": "Z-Tx in Tx-Liste speichern",
|
||||
"search_placeholder": "Suchen...",
|
||||
"security": "SICHERHEIT",
|
||||
"select_address": "Adresse auswählen...",
|
||||
"select_receiving_address": "Empfangsadresse auswählen...",
|
||||
"select_source_address": "Quelladresse auswählen...",
|
||||
"send": "Senden",
|
||||
"send_amount": "Betrag",
|
||||
"send_amount_details": "BETRAGSDETAILS",
|
||||
"send_amount_upper": "BETRAG",
|
||||
"send_clear_fields": "Alle Formularfelder leeren?",
|
||||
"send_copy_error": "Fehler kopieren",
|
||||
"send_dismiss": "Verwerfen",
|
||||
"send_error_copied": "Fehler in Zwischenablage kopiert",
|
||||
"send_error_prefix": "Fehler: %s",
|
||||
"send_exceeds_available": "Übersteigt verfügbar (%.8f)",
|
||||
"send_fee": "Gebühr",
|
||||
"send_fee_high": "Hoch",
|
||||
"send_fee_low": "Niedrig",
|
||||
"send_fee_normal": "Normal",
|
||||
"send_form_restored": "Formular wiederhergestellt",
|
||||
"send_from_this_address": "Von dieser Adresse senden",
|
||||
"send_go_to_receive": "Zum Empfangen",
|
||||
"send_keep": "Behalten",
|
||||
"send_network_fee": "NETZWERKGEBÜHR",
|
||||
"send_no_balance": "Kein Guthaben",
|
||||
"send_no_recent": "Keine kürzlichen Sendungen",
|
||||
"send_recent_sends": "KÜRZLICH GESENDET",
|
||||
"send_recipient": "EMPFÄNGER",
|
||||
"send_select_source": "Quelladresse auswählen...",
|
||||
"send_sending_from": "SENDEN VON",
|
||||
"send_submitting": "Transaktion wird übermittelt...",
|
||||
"send_switch_to_receive": "Wechseln Sie zu Empfangen, um Ihre Adresse zu erhalten und Gelder zu empfangen.",
|
||||
"send_to": "Senden an",
|
||||
"send_tooltip_enter_amount": "Geben Sie einen Betrag zum Senden ein",
|
||||
"send_tooltip_exceeds_balance": "Betrag übersteigt verfügbares Guthaben",
|
||||
"send_tooltip_in_progress": "Transaktion bereits in Bearbeitung",
|
||||
"send_tooltip_invalid_address": "Geben Sie eine gültige Empfängeradresse ein",
|
||||
"send_tooltip_not_connected": "Nicht mit Daemon verbunden",
|
||||
"send_tooltip_select_source": "Wählen Sie zuerst eine Quelladresse",
|
||||
"send_tooltip_syncing": "Warten Sie auf die Blockchain-Synchronisierung",
|
||||
"send_total": "Gesamt",
|
||||
"send_transaction": "Transaktion senden",
|
||||
"send_tx_failed": "Transaktion fehlgeschlagen",
|
||||
"send_tx_sent": "Transaktion gesendet!",
|
||||
"send_tx_success": "Transaktion erfolgreich gesendet!",
|
||||
"send_txid_copied": "TxID in Zwischenablage kopiert",
|
||||
"send_txid_label": "TxID: %s",
|
||||
"send_valid_shielded": "Gültige abgeschirmte Adresse",
|
||||
"send_valid_transparent": "Gültige transparente Adresse",
|
||||
"send_wallet_empty": "Ihre Wallet ist leer",
|
||||
"send_yes_clear": "Ja, leeren",
|
||||
"sending": "Transaktion wird gesendet",
|
||||
"sending_from": "SENDEN VON",
|
||||
"sent": "gesendet",
|
||||
"sent_filter": "Gesendet",
|
||||
"sent_type": "Gesendet",
|
||||
"sent_upper": "GESENDET",
|
||||
"settings": "Einstellungen",
|
||||
"setup_wizard": "Einrichtungsassistent",
|
||||
"share": "Teilen",
|
||||
"shield_check_status": "Status prüfen",
|
||||
"shield_completed": "Vorgang erfolgreich abgeschlossen!",
|
||||
"shield_description": "Schirmen Sie Ihre Mining-Belohnungen ab, indem Sie Coinbase-Ausgaben von transparenten Adressen an eine abgeschirmte Adresse senden. Dies verbessert die Privatsphäre, indem Ihre Mining-Einkünfte verborgen werden.",
|
||||
"shield_from_address": "Von Adresse:",
|
||||
"shield_funds": "Gelder abschirmen",
|
||||
"shield_in_progress": "Vorgang läuft...",
|
||||
"shield_max_utxos": "Max. UTXOs pro Vorgang",
|
||||
"shield_merge_done": "Abschirmung/Zusammenführung abgeschlossen!",
|
||||
"shield_select_z": "z-Adresse auswählen...",
|
||||
"shield_started": "Abschirmvorgang gestartet",
|
||||
"shield_title": "Coinbase-Belohnungen abschirmen",
|
||||
"shield_to_address": "An Adresse (Abgeschirmt):",
|
||||
"shield_utxo_limit": "UTXO-Limit:",
|
||||
"shield_wildcard_hint": "Verwenden Sie '*' um von allen transparenten Adressen abzuschirmen",
|
||||
"shielded": "Abgeschirmt",
|
||||
"shielded_to": "ABGESCHIRMT AN",
|
||||
"shielded_type": "Abgeschirmt",
|
||||
"show": "Anzeigen",
|
||||
"show_qr_code": "QR-Code anzeigen",
|
||||
"showing_transactions": "Zeige %d\xe2\x80\x93%d von %d Transaktionen (gesamt: %zu)",
|
||||
"simple_background": "Einfacher Hintergrund",
|
||||
"start_mining": "Mining starten",
|
||||
"status": "Status",
|
||||
"stop_external": "Externen Daemon stoppen",
|
||||
"stop_mining": "Mining stoppen",
|
||||
"submitting_transaction": "Transaktion wird übermittelt...",
|
||||
"success": "Erfolg",
|
||||
"summary": "Zusammenfassung",
|
||||
"syncing": "Synchronisiere...",
|
||||
"t_addresses": "T-Adressen",
|
||||
"test_connection": "Testen",
|
||||
"theme": "Design",
|
||||
"theme_effects": "Design-Effekte",
|
||||
"time_days_ago": "vor %d Tagen",
|
||||
"time_hours_ago": "vor %d Stunden",
|
||||
"time_minutes_ago": "vor %d Minuten",
|
||||
"time_seconds_ago": "vor %d Sekunden",
|
||||
"to": "An",
|
||||
"to_upper": "AN",
|
||||
"tools": "WERKZEUGE",
|
||||
"total": "Gesamt",
|
||||
"transaction_id": "TRANSAKTIONS-ID",
|
||||
"transaction_sent": "Transaktion erfolgreich gesendet",
|
||||
"transaction_sent_msg": "Transaktion gesendet!",
|
||||
"transaction_url": "Transaktions-URL",
|
||||
"transactions": "Transaktionen",
|
||||
"transactions_upper": "TRANSAKTIONEN",
|
||||
"transparent": "Transparent",
|
||||
"tx_confirmations": "%d Bestätigungen",
|
||||
"tx_details_title": "Transaktionsdetails",
|
||||
"tx_from_address": "Von Adresse:",
|
||||
"tx_id_label": "Transaktions-ID:",
|
||||
"tx_immature": "UNREIF",
|
||||
"tx_mined": "GEMINED",
|
||||
"tx_received": "EMPFANGEN",
|
||||
"tx_sent": "GESENDET",
|
||||
"tx_to_address": "An Adresse:",
|
||||
"tx_view_explorer": "Im Explorer anzeigen",
|
||||
"txs_count": "%d Txs",
|
||||
"type": "Typ",
|
||||
"ui_opacity": "UI-Transparenz",
|
||||
"unban": "Entsperren",
|
||||
"unconfirmed": "Unbestätigt",
|
||||
"undo_clear": "Leeren rückgängig",
|
||||
"unknown": "Unbekannt",
|
||||
"use_embedded_daemon": "Eingebetteten dragonxd verwenden",
|
||||
"use_tor": "Tor verwenden",
|
||||
"validate_btn": "Validieren",
|
||||
"validate_description": "Geben Sie eine DragonX-Adresse ein, um zu prüfen, ob sie gültig ist und ob sie zu dieser Wallet gehört.",
|
||||
"validate_invalid": "UNGÜLTIG",
|
||||
"validate_is_mine": "Diese Wallet besitzt diese Adresse",
|
||||
"validate_not_mine": "Nicht im Besitz dieser Wallet",
|
||||
"validate_ownership": "Eigentum:",
|
||||
"validate_results": "Ergebnisse:",
|
||||
"validate_shielded_type": "Abgeschirmt (z-Adresse)",
|
||||
"validate_status": "Status:",
|
||||
"validate_title": "Adresse validieren",
|
||||
"validate_transparent_type": "Transparent (t-Adresse)",
|
||||
"validate_type": "Typ:",
|
||||
"validate_valid": "GÜLTIG",
|
||||
"validating": "Validiere...",
|
||||
"verbose_logging": "Ausführliches Logging",
|
||||
"version": "Version",
|
||||
"view": "Ansicht",
|
||||
"view_details": "Details anzeigen",
|
||||
"view_on_explorer": "Im Explorer anzeigen",
|
||||
"waiting_for_daemon": "Warte auf Daemon-Verbindung...",
|
||||
"wallet": "WALLET",
|
||||
"wallet_empty": "Ihre Wallet ist leer",
|
||||
"wallet_empty_hint": "Wechseln Sie zu Empfangen, um Ihre Adresse zu erhalten und Gelder zu empfangen.",
|
||||
"warning": "Warnung",
|
||||
"warning_upper": "WARNUNG!",
|
||||
"website": "Webseite",
|
||||
"window_opacity": "Fenster-Transparenz",
|
||||
"yes_clear": "Ja, leeren",
|
||||
"your_addresses": "Ihre Adressen",
|
||||
"z_addresses": "Z-Adressen",
|
||||
}
|
||||
|
||||
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "de.json")
|
||||
with open(out, "w", encoding="utf-8") as f:
|
||||
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
|
||||
print(f"Wrote {len(translations)} German translations to {os.path.abspath(out)}")
|
||||
@@ -0,0 +1,665 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate complete Spanish (es.json) translations for ObsidianDragon wallet."""
|
||||
import json
|
||||
|
||||
es = {
|
||||
# ---- Navigation & Tabs ----
|
||||
"overview": "Resumen",
|
||||
"balance": "Saldo",
|
||||
"send": "Enviar",
|
||||
"receive": "Recibir",
|
||||
"transactions": "Transacciones",
|
||||
"history": "Historial",
|
||||
"mining": "Minería",
|
||||
"peers": "Nodos",
|
||||
"market": "Mercado",
|
||||
"settings": "Configuración",
|
||||
"console": "Consola",
|
||||
"tools": "HERRAMIENTAS",
|
||||
"advanced": "AVANZADO",
|
||||
"network": "Red",
|
||||
|
||||
# ---- Settings sections ----
|
||||
"appearance": "APARIENCIA",
|
||||
"wallet": "CARTERA",
|
||||
"node_security": "NODO Y SEGURIDAD",
|
||||
"node": "NODO",
|
||||
"security": "SEGURIDAD",
|
||||
"explorer": "EXPLORADOR",
|
||||
"about": "Acerca de",
|
||||
"backup_data": "RESPALDO Y DATOS",
|
||||
"general": "General",
|
||||
|
||||
# ---- Settings options ----
|
||||
"balance_layout": "Diseño de Saldo",
|
||||
"low_spec_mode": "Modo bajo rendimiento",
|
||||
"simple_background": "Fondo simple",
|
||||
"console_scanline": "Líneas de consola",
|
||||
"theme_effects": "Efectos de tema",
|
||||
"language": "Idioma",
|
||||
"save_z_transactions": "Guardar Z-tx en lista",
|
||||
"allow_custom_fees": "Permitir comisiones personalizadas",
|
||||
"custom_fees": "Comisiones personalizadas",
|
||||
"auto_shield": "Auto-proteger minería",
|
||||
"fetch_prices": "Obtener precios",
|
||||
"use_tor": "Usar Tor",
|
||||
"font_scale": "Escala de fuente",
|
||||
"keep_daemon": "Mantener daemon activo",
|
||||
"stop_external": "Detener daemon externo",
|
||||
"mine_when_idle": "Minar en reposo",
|
||||
"verbose_logging": "Registro detallado",
|
||||
"acrylic": "Acrílico",
|
||||
"noise": "Ruido",
|
||||
"ui_opacity": "Opacidad de UI",
|
||||
"window_opacity": "Opacidad de ventana",
|
||||
|
||||
# ---- Settings buttons ----
|
||||
"save_settings": "Guardar Configuración",
|
||||
"reset_to_defaults": "Restablecer Valores",
|
||||
"report_bug": "Reportar Error",
|
||||
"website": "Sitio Web",
|
||||
"setup_wizard": "Asistente de Configuración",
|
||||
"rescan": "Re-escanear",
|
||||
"test_connection": "Probar",
|
||||
|
||||
# ---- Settings fields ----
|
||||
"rpc_host": "Host RPC",
|
||||
"rpc_port": "Puerto",
|
||||
"rpc_user": "Usuario",
|
||||
"rpc_pass": "Contraseña",
|
||||
"transaction_url": "URL de Transacción",
|
||||
"address_url": "URL de Dirección",
|
||||
"block_explorer": "Explorador de Bloques",
|
||||
|
||||
# ---- Common actions ----
|
||||
"add": "Agregar",
|
||||
"edit": "Editar",
|
||||
"delete": "Eliminar",
|
||||
"cancel": "Cancelar",
|
||||
"close": "Cerrar",
|
||||
"clear": "Limpiar",
|
||||
"copy": "Copiar",
|
||||
"paste": "Pegar",
|
||||
"save": "Guardar",
|
||||
"refresh": "Actualizar",
|
||||
"export": "Exportar",
|
||||
"import": "Importar",
|
||||
"show": "Mostrar",
|
||||
"hide": "Ocultar",
|
||||
"share": "Compartir",
|
||||
"confirm_and_send": "Confirmar y Enviar",
|
||||
"confirm_send": "Confirmar Envío",
|
||||
"confirm_transaction": "Confirmar Transacción",
|
||||
"review_send": "Revisar Envío",
|
||||
"copy_address": "Copiar Dirección Completa",
|
||||
"copy_to_clipboard": "Copiar al Portapapeles",
|
||||
"paste_from_clipboard": "Pegar del Portapapeles",
|
||||
"copy_txid": "Copiar TxID",
|
||||
"copy_uri": "Copiar URI",
|
||||
"copy_error": "Copiar Error",
|
||||
"search_placeholder": "Buscar...",
|
||||
"exit": "Salir",
|
||||
"help": "Ayuda",
|
||||
"file": "Archivo",
|
||||
"display": "Pantalla",
|
||||
"new": "+ Nuevo",
|
||||
"dismiss": "Descartar",
|
||||
"keep": "Mantener",
|
||||
"yes_clear": "Sí, Limpiar",
|
||||
"undo_clear": "Deshacer Limpieza",
|
||||
|
||||
# ---- Common labels ----
|
||||
"address": "Dirección",
|
||||
"address_label": "Dirección:",
|
||||
"amount": "Cantidad",
|
||||
"amount_label": "Cantidad:",
|
||||
"date": "Fecha",
|
||||
"date_label": "Fecha:",
|
||||
"fee": "Comisión",
|
||||
"fee_label": "Comisión:",
|
||||
"label": "Etiqueta:",
|
||||
"memo": "Memo (opcional, encriptado)",
|
||||
"memo_label": "Memo:",
|
||||
"notes": "Notas",
|
||||
"notes_optional": "Notas (opcional):",
|
||||
"total": "Total",
|
||||
"from": "Desde",
|
||||
"to_upper": "PARA",
|
||||
"from_upper": "DESDE",
|
||||
"max": "Máximo",
|
||||
"characters": "caracteres",
|
||||
"ping": "Ping",
|
||||
"height": "Altura",
|
||||
"block": "Bloque",
|
||||
"available": "Disponible",
|
||||
"unknown": "Desconocido",
|
||||
"loading": "Cargando...",
|
||||
"validating": "Validando...",
|
||||
"warning_upper": "¡ADVERTENCIA!",
|
||||
"output_filename": "Nombre del archivo:",
|
||||
"file_save_location": "El archivo se guardará en: ~/.config/ObsidianDragon/",
|
||||
"light": "Claro",
|
||||
"dark": "Oscuro",
|
||||
|
||||
# ---- Status ----
|
||||
"connected": "Conectado",
|
||||
"disconnected": "Desconectado",
|
||||
"connecting": "Conectando...",
|
||||
"confirmed": "Confirmada",
|
||||
"confirmations": "Confirmaciones",
|
||||
"not_connected_to_daemon": "No conectado al daemon",
|
||||
"waiting_for_daemon": "Esperando conexión al daemon...",
|
||||
"blockchain_syncing": "Sincronizando blockchain (%.1f%%)... Los saldos pueden ser inexactos.",
|
||||
"error": "Error",
|
||||
"success": "Éxito",
|
||||
"warning": "Advertencia",
|
||||
|
||||
# ---- Time ----
|
||||
"time_days_ago": "hace %d días",
|
||||
"time_hours_ago": "hace %d horas",
|
||||
"time_minutes_ago": "hace %d minutos",
|
||||
"time_seconds_ago": "hace %d segundos",
|
||||
|
||||
# ---- Transaction types/filters ----
|
||||
"sent_type": "Enviado",
|
||||
"sent_filter": "Enviado",
|
||||
"sent_upper": "ENVIADO",
|
||||
"received_label": "Recibido",
|
||||
"received_filter": "Recibido",
|
||||
"received_upper": "RECIBIDO",
|
||||
"mined_type": "Minado",
|
||||
"mined_filter": "Minado",
|
||||
"mined_upper": "MINADO",
|
||||
"immature_type": "Inmaduro",
|
||||
"mature": "Maduro",
|
||||
"recv_type": "Recibido",
|
||||
"all_filter": "Todos",
|
||||
"shielded_type": "Protegido",
|
||||
|
||||
# ---- Balance / Overview ----
|
||||
"address_upper": "DIRECCIÓN",
|
||||
"address_details": "Detalles de Dirección",
|
||||
"amount_details": "DETALLES DE CANTIDAD",
|
||||
"transactions_upper": "TRANSACCIONES",
|
||||
"addresses_appear_here": "Tus direcciones de recepción aparecerán aquí una vez conectado.",
|
||||
"wallet_empty": "Tu cartera está vacía",
|
||||
"wallet_empty_hint": "Cambia a Recibir para obtener tu dirección y empezar a recibir fondos.",
|
||||
"loading_addresses": "Cargando direcciones...",
|
||||
"no_addresses_match": "No hay direcciones que coincidan con el filtro",
|
||||
"no_addresses_with_balance": "No hay direcciones con saldo",
|
||||
"click_copy_address": "Clic para copiar dirección",
|
||||
"click_copy_uri": "Clic para copiar URI",
|
||||
"address_copied": "Dirección copiada al portapapeles",
|
||||
"about_dragonx": "Acerca de DragonX",
|
||||
"dragonx_green": "DragonX (Verde)",
|
||||
|
||||
# ---- Transactions tab ----
|
||||
"no_transactions": "No se encontraron transacciones",
|
||||
"no_matching": "No hay transacciones coincidentes",
|
||||
"showing_transactions": "Mostrando %d\u2013%d de %d transacciones (total: %zu)",
|
||||
"conf_count": "%d conf",
|
||||
"confirmations_display": "%d confirmaciones | %s",
|
||||
"txs_count": "%d txs",
|
||||
"view_details": "Ver Detalles",
|
||||
"full_details": "Detalles Completos",
|
||||
"export_csv": "Exportar CSV",
|
||||
"transaction_id": "ID DE TRANSACCIÓN",
|
||||
|
||||
# ---- Receive tab ----
|
||||
"select_receiving_address": "Seleccionar dirección de recepción...",
|
||||
"payment_request": "SOLICITUD DE PAGO",
|
||||
"recent_received": "RECIBIDOS RECIENTES",
|
||||
"no_recent_receives": "No hay recepciones recientes",
|
||||
"qr_unavailable": "QR no disponible",
|
||||
"clear_request": "Limpiar Solicitud",
|
||||
"clear_form_confirm": "¿Limpiar todos los campos del formulario?",
|
||||
"payment_request_copied": "Solicitud de pago copiada",
|
||||
"payment_uri_copied": "URI de pago copiada",
|
||||
"failed_create_shielded": "Error al crear dirección protegida",
|
||||
"failed_create_transparent": "Error al crear dirección transparente",
|
||||
"new_shielded_created": "Nueva dirección protegida creada",
|
||||
"new_transparent_created": "Nueva dirección transparente creada",
|
||||
|
||||
# ---- Send tab ----
|
||||
"send_sending_from": "ENVIANDO DESDE",
|
||||
"send_select_source": "Seleccionar dirección de origen...",
|
||||
"send_no_balance": "Sin saldo",
|
||||
"send_recipient": "DESTINATARIO",
|
||||
"send_amount_upper": "CANTIDAD",
|
||||
"send_amount": "Cantidad",
|
||||
"send_amount_details": "DETALLES DE CANTIDAD",
|
||||
"send_fee": "Comisión",
|
||||
"send_fee_low": "Baja",
|
||||
"send_fee_normal": "Normal",
|
||||
"send_fee_high": "Alta",
|
||||
"send_network_fee": "COMISIÓN DE RED",
|
||||
"send_total": "Total",
|
||||
"send_recent_sends": "ENVÍOS RECIENTES",
|
||||
"send_no_recent": "No hay envíos recientes",
|
||||
"send_clear_fields": "¿Limpiar todos los campos del formulario?",
|
||||
"send_yes_clear": "Sí, Limpiar",
|
||||
"send_keep": "Mantener",
|
||||
"send_form_restored": "Formulario restaurado",
|
||||
"send_valid_shielded": "Dirección protegida válida",
|
||||
"send_valid_transparent": "Dirección transparente válida",
|
||||
"send_exceeds_available": "Excede disponible (%.8f)",
|
||||
"send_submitting": "Enviando transacción...",
|
||||
"send_tx_sent": "¡Transacción enviada!",
|
||||
"send_tx_success": "¡Transacción enviada exitosamente!",
|
||||
"send_tx_failed": "Error en la transacción",
|
||||
"send_error_prefix": "Error: %s",
|
||||
"send_error_copied": "Error copiado al portapapeles",
|
||||
"send_copy_error": "Copiar Error",
|
||||
"send_dismiss": "Descartar",
|
||||
"send_txid_copied": "TxID copiado al portapapeles",
|
||||
"send_txid_label": "TxID: %s",
|
||||
"send_go_to_receive": "Ir a Recibir",
|
||||
"send_wallet_empty": "Tu cartera está vacía",
|
||||
"send_switch_to_receive": "Cambia a Recibir para obtener tu dirección y empezar a recibir fondos.",
|
||||
"send_tooltip_select_source": "Selecciona una dirección de origen primero",
|
||||
"send_tooltip_invalid_address": "Ingresa una dirección de destinatario válida",
|
||||
"send_tooltip_enter_amount": "Ingresa una cantidad a enviar",
|
||||
"send_tooltip_exceeds_balance": "La cantidad excede el saldo disponible",
|
||||
"send_tooltip_not_connected": "No conectado al daemon",
|
||||
"send_tooltip_syncing": "Espera a que se sincronice el blockchain",
|
||||
"send_tooltip_in_progress": "Transacción ya en progreso",
|
||||
"sending_from": "ENVIANDO DESDE",
|
||||
"select_source_address": "Seleccionar dirección de origen...",
|
||||
"recipient": "DESTINATARIO",
|
||||
"memo_optional": "MEMO (OPCIONAL)",
|
||||
"memo_upper": "MEMO",
|
||||
"network_fee": "COMISIÓN DE RED",
|
||||
"fee_low": "Baja",
|
||||
"fee_normal": "Normal",
|
||||
"fee_high": "Alta",
|
||||
"recent_sends": "ENVÍOS RECIENTES",
|
||||
"no_recent_sends": "No hay envíos recientes",
|
||||
"shielded_to": "PROTEGIDA PARA",
|
||||
"submitting_transaction": "Enviando transacción...",
|
||||
"transaction_sent_msg": "¡Transacción enviada!",
|
||||
"amount_exceeds_balance": "La cantidad excede el saldo",
|
||||
|
||||
# ---- Mining tab ----
|
||||
"mining_solo": "Solo",
|
||||
"mining_pool": "Pool",
|
||||
"mining_pool_url": "URL del Pool",
|
||||
"mining_pool_hashrate": "Hashrate del Pool",
|
||||
"mining_local_hashrate": "Hashrate Local",
|
||||
"mining_payout_address": "Dirección de Pago",
|
||||
"mining_payout_tooltip": "Dirección para recibir recompensas de minería",
|
||||
"mining_mine": "Minar",
|
||||
"mining_stop": "Detener",
|
||||
"mining_starting": "Iniciando...",
|
||||
"mining_stopping": "Deteniendo...",
|
||||
"mining_active": "Activo",
|
||||
"mining_connected": "Conectado",
|
||||
"mining_connecting": "Conectando...",
|
||||
"mining_network": "Red",
|
||||
"mining_shares": "Shares",
|
||||
"mining_uptime": "Tiempo activo",
|
||||
"mining_mining_addr": "Dir. Minería",
|
||||
"mining_est_block": "Bloque Est.",
|
||||
"mining_est_daily": "Diario Est.",
|
||||
"mining_today": "Hoy",
|
||||
"mining_yesterday": "Ayer",
|
||||
"mining_all_time": "Todo el Tiempo",
|
||||
"mining_recent_blocks": "BLOQUES RECIENTES",
|
||||
"mining_recent_payouts": "PAGOS DE POOL RECIENTES",
|
||||
"mining_no_blocks_yet": "Aún no se han encontrado bloques",
|
||||
"mining_no_payouts_yet": "Aún no hay pagos del pool",
|
||||
"mining_show_chart": "Gráfico",
|
||||
"mining_show_log": "Registro",
|
||||
"mining_chart_now": "Ahora",
|
||||
"mining_chart_start": "Inicio",
|
||||
"mining_chart_1m_ago": "hace 1m",
|
||||
"mining_chart_5m_ago": "hace 5m",
|
||||
"mining_save_pool_url": "Guardar URL del pool",
|
||||
"mining_save_payout_address": "Guardar dirección de pago",
|
||||
"mining_saved_pools": "Pools Guardados:",
|
||||
"mining_saved_addresses": "Direcciones Guardadas:",
|
||||
"mining_no_saved_pools": "No hay pools guardados",
|
||||
"mining_no_saved_addresses": "No hay direcciones guardadas",
|
||||
"mining_already_saved": "URL del pool ya guardada",
|
||||
"mining_remove": "Eliminar",
|
||||
"mining_reset_defaults": "Restablecer Valores",
|
||||
"mining_click": "Clic",
|
||||
"mining_to_save": "para guardar",
|
||||
"mining_click_copy_address": "Clic para copiar dirección",
|
||||
"mining_click_copy_block": "Clic para copiar hash de bloque",
|
||||
"mining_click_copy_difficulty": "Clic para copiar dificultad",
|
||||
"mining_address_copied": "Dirección de minería copiada",
|
||||
"mining_block_copied": "Hash de bloque copiado",
|
||||
"mining_difficulty_copied": "Dificultad copiada",
|
||||
"mining_open_in_explorer": "Abrir en explorador",
|
||||
"mining_starting_tooltip": "El minero está iniciando...",
|
||||
"mining_stopping_tooltip": "El minero está deteniéndose...",
|
||||
"mining_syncing_tooltip": "El blockchain está sincronizando...",
|
||||
"mining_idle_on_tooltip": "Desactivar minería en reposo",
|
||||
"mining_idle_off_tooltip": "Activar minería en reposo",
|
||||
"mining_stop_solo_for_pool": "Detener minería solo antes de iniciar minería en pool",
|
||||
"mining_stop_solo_for_pool_settings": "Detener minería solo para cambiar configuración del pool",
|
||||
"mining_filter_all": "Todos",
|
||||
"mining_filter_tip_all": "Mostrar todas las ganancias",
|
||||
"mining_filter_tip_solo": "Mostrar solo ganancias solo",
|
||||
"mining_filter_tip_pool": "Mostrar solo ganancias del pool",
|
||||
"local_hashrate": "Tasa Hash Local",
|
||||
"est_time_to_block": "Tiempo Est. al Bloque",
|
||||
"difficulty": "Dificultad",
|
||||
"current_price": "Precio Actual",
|
||||
"market_cap": "Cap. de Mercado",
|
||||
|
||||
# ---- Peers tab ----
|
||||
"peers_blockchain": "BLOCKCHAIN",
|
||||
"peers_blocks": "Bloques",
|
||||
"peers_connected": "Conectados",
|
||||
"peers_connected_count": "Conectados: %d",
|
||||
"peers_banned": "Bloqueados",
|
||||
"peers_banned_count": "Bloqueados: %d",
|
||||
"peers_upper": "NODOS",
|
||||
"peers_avg_ping": "Ping Prom.",
|
||||
"peers_best_block": "Mejor Bloque",
|
||||
"peers_hashrate": "Hashrate",
|
||||
"peers_longest": "Más Larga",
|
||||
"peers_longest_chain": "Cadena Más Larga",
|
||||
"peers_memory": "Memoria",
|
||||
"peers_notarized": "Notarizado",
|
||||
"peers_p2p_port": "Puerto P2P",
|
||||
"peers_protocol": "Protocolo",
|
||||
"peers_version": "Versión",
|
||||
"peers_in_out": "Ent/Sal",
|
||||
"peers_dir_in": "Ent",
|
||||
"peers_dir_out": "Sal",
|
||||
"peers_received": "Recibido",
|
||||
"peers_sent": "Enviado",
|
||||
"peers_refresh": "Actualizar",
|
||||
"peers_refresh_tooltip": "Actualizar lista de nodos",
|
||||
"peers_refreshing": "Actualizando...",
|
||||
"peers_no_connected": "No hay nodos conectados",
|
||||
"peers_no_banned": "No hay nodos bloqueados",
|
||||
"peers_ban_24h": "Bloquear Nodo 24h",
|
||||
"peers_unban": "Desbloquear",
|
||||
"peers_clear_all_bans": "Limpiar Todos los Bloqueos",
|
||||
"peers_copy_ip": "Copiar IP",
|
||||
"peers_click_copy": "Clic para copiar",
|
||||
"peers_hash_copied": "Hash copiado",
|
||||
"peers_no_tls": "Sin TLS",
|
||||
"peers_blocks_left": "%d bloques restantes",
|
||||
"peers_ban_score": "Puntuación: %d",
|
||||
"peers_tt_id": "ID: %d",
|
||||
"peers_tt_sent": "Enviado: %s",
|
||||
"peers_tt_received": "Recibido: %s",
|
||||
"peers_tt_services": "Servicios: %s",
|
||||
"peers_tt_start_height": "Altura Inicial: %d",
|
||||
"peers_tt_synced": "Sinc H/B: %d/%d",
|
||||
"peers_tt_tls_cipher": "TLS: %s",
|
||||
"connected_peers": "Nodos Conectados",
|
||||
"banned_peers": "Nodos Bloqueados",
|
||||
"ban": "Bloquear",
|
||||
"clear_all_bans": "Limpiar Todos los Bloqueos",
|
||||
"ip_address": "Dirección IP",
|
||||
|
||||
# ---- Market tab ----
|
||||
"market_btc_price": "PRECIO BTC",
|
||||
"market_24h_volume": "VOLUMEN 24H",
|
||||
"market_portfolio": "PORTAFOLIO",
|
||||
"market_pct_shielded": "%.0f%% Protegido",
|
||||
"market_attribution": "Datos de precios de NonKYC",
|
||||
"market_no_price": "Sin datos de precio",
|
||||
"market_no_history": "No hay historial de precios disponible",
|
||||
"market_price_unavailable": "Datos de precio no disponibles",
|
||||
"market_refresh_price": "Actualizar datos de precio",
|
||||
"market_trade_on": "Operar en %s",
|
||||
"market_now": "Ahora",
|
||||
"market_6h": "6h",
|
||||
"market_12h": "12h",
|
||||
"market_18h": "18h",
|
||||
"market_24h": "24h",
|
||||
"24h_change": "Cambio 24h",
|
||||
"24h_volume": "Volumen 24h",
|
||||
|
||||
# ---- Console tab ----
|
||||
"console_welcome": "Bienvenido a la Consola de ObsidianDragon",
|
||||
"console_type_help": "Escribe 'help' para ver los comandos disponibles",
|
||||
"console_available_commands": "Comandos disponibles:",
|
||||
"console_common_rpc": "Comandos RPC comunes:",
|
||||
"console_rpc_reference": "Referencia de Comandos RPC",
|
||||
"console_auto_scroll": "Auto-desplazamiento",
|
||||
"console_clear": "Limpiar",
|
||||
"console_clear_console": "Limpiar Consola",
|
||||
"console_cleared": "Consola limpiada",
|
||||
"console_commands": "Comandos",
|
||||
"console_completions": "Completaciones:",
|
||||
"console_tab_completion": "Tab para completar",
|
||||
"console_connected": "Conectado al daemon",
|
||||
"console_disconnected": "Desconectado del daemon",
|
||||
"console_not_connected": "Error: No conectado al daemon",
|
||||
"console_no_daemon": "Sin daemon",
|
||||
"console_daemon": "Daemon",
|
||||
"console_daemon_error": "¡Error del daemon!",
|
||||
"console_daemon_started": "Daemon iniciado",
|
||||
"console_daemon_stopped": "Daemon detenido",
|
||||
"console_errors": "Errores",
|
||||
"console_filter_hint": "Filtrar salida...",
|
||||
"console_search_commands": "Buscar comandos...",
|
||||
"console_copy_all": "Copiar Todo",
|
||||
"console_copy_selected": "Copiar",
|
||||
"console_select_all": "Seleccionar Todo",
|
||||
"console_zoom_in": "Acercar",
|
||||
"console_zoom_out": "Alejar",
|
||||
"console_show_daemon_output": "Mostrar salida del daemon",
|
||||
"console_show_errors_only": "Mostrar solo errores",
|
||||
"console_show_rpc_ref": "Mostrar referencia de comandos RPC",
|
||||
"console_capturing_output": "Capturando salida del daemon...",
|
||||
"console_starting_node": "Iniciando nodo...",
|
||||
"console_line_count": "%zu líneas",
|
||||
"console_new_lines": "%d nuevas líneas",
|
||||
"console_showing_lines": "Mostrando %zu de %zu líneas",
|
||||
"console_click_commands": "Clic en los comandos de arriba para insertarlos",
|
||||
"console_click_insert": "Clic para insertar",
|
||||
"console_click_insert_params": "Clic para insertar con parámetros",
|
||||
"console_close": "Cerrar",
|
||||
"console_status_running": "Ejecutando",
|
||||
"console_status_stopped": "Detenido",
|
||||
"console_status_starting": "Iniciando",
|
||||
"console_status_stopping": "Deteniendo",
|
||||
"console_status_error": "Error",
|
||||
"console_status_unknown": "Desconocido",
|
||||
"console_help_help": " help - Mostrar este mensaje de ayuda",
|
||||
"console_help_getinfo": " getinfo - Mostrar información del nodo",
|
||||
"console_help_getblockcount": " getblockcount - Mostrar altura actual del bloque",
|
||||
"console_help_getbalance": " getbalance - Mostrar saldo transparente",
|
||||
"console_help_gettotalbalance": " gettotalbalance - Mostrar saldo total",
|
||||
"console_help_getmininginfo": " getmininginfo - Mostrar estado de minería",
|
||||
"console_help_getpeerinfo": " getpeerinfo - Mostrar nodos conectados",
|
||||
"console_help_setgenerate": " setgenerate - Controlar minería",
|
||||
"console_help_stop": " stop - Detener el daemon",
|
||||
"console_help_clear": " clear - Limpiar la consola",
|
||||
|
||||
# ---- About dialog ----
|
||||
"about_title": "Acerca de ObsidianDragon",
|
||||
"about_edition": "Edición ImGui",
|
||||
"about_version": "Versión:",
|
||||
"about_imgui": "ImGui:",
|
||||
"about_build_date": "Fecha de Compilación:",
|
||||
"about_build_type": "Tipo de Compilación:",
|
||||
"about_debug": "Depuración",
|
||||
"about_release": "Producción",
|
||||
"about_daemon": "Daemon:",
|
||||
"about_chain": "Cadena:",
|
||||
"about_block_height": "Altura de Bloque:",
|
||||
"about_connections": "Conexiones:",
|
||||
"about_peers_count": "%zu nodos",
|
||||
"about_credits": "Créditos",
|
||||
"about_license": "Licencia",
|
||||
"about_license_text": "Este software se distribuye bajo la Licencia Pública General de GNU v3 (GPLv3). Usted es libre de usar, modificar y distribuir este software bajo los términos de la licencia.",
|
||||
"about_website": "Sitio Web",
|
||||
"about_github": "GitHub",
|
||||
"about_block_explorer": "Explorador de Bloques",
|
||||
|
||||
# ---- Address Book dialog ----
|
||||
"address_book_title": "Libreta de Direcciones",
|
||||
"address_book_add_new": "Agregar Nueva",
|
||||
"address_book_add": "Agregar Dirección",
|
||||
"address_book_edit": "Editar Dirección",
|
||||
"address_book_empty": "No hay direcciones guardadas. Haz clic en 'Agregar Nueva' para añadir una.",
|
||||
"address_book_count": "%zu direcciones guardadas",
|
||||
"address_book_deleted": "Entrada eliminada",
|
||||
"address_book_added": "Dirección agregada a la libreta",
|
||||
"address_book_exists": "La dirección ya existe en la libreta",
|
||||
"address_book_updated": "Dirección actualizada",
|
||||
"address_book_update_failed": "Error al actualizar - la dirección puede estar duplicada",
|
||||
|
||||
# ---- Backup dialog ----
|
||||
"backup_title": "Respaldar Cartera",
|
||||
"backup_description": "Crea un respaldo de tu archivo wallet.dat. Este archivo contiene todas tus claves privadas e historial de transacciones. Guarda el respaldo en un lugar seguro.",
|
||||
"backup_destination": "Destino del respaldo:",
|
||||
"backup_wallet_not_found": "Advertencia: wallet.dat no encontrado en la ubicación esperada",
|
||||
"backup_create": "Crear Respaldo",
|
||||
"backup_created": "Respaldo de cartera creado",
|
||||
"backup_backing_up": "Respaldando...",
|
||||
"backup_tips": "Consejos:",
|
||||
"backup_tip_external": "Guarda respaldos en unidades externas o almacenamiento en la nube",
|
||||
"backup_tip_multiple": "Crea múltiples respaldos en diferentes ubicaciones",
|
||||
"backup_tip_test": "Prueba restaurar desde el respaldo periódicamente",
|
||||
"backup_wallet": "Respaldar Cartera...",
|
||||
|
||||
# ---- Block Info dialog ----
|
||||
"block_info_title": "Información del Bloque",
|
||||
"block_height": "Altura del Bloque:",
|
||||
"block_get_info": "Obtener Info del Bloque",
|
||||
"block_hash": "Hash del Bloque:",
|
||||
"block_timestamp": "Fecha y Hora:",
|
||||
"block_transactions": "Transacciones:",
|
||||
"block_size": "Tamaño:",
|
||||
"block_bits": "Bits:",
|
||||
"block_merkle_root": "Raíz Merkle:",
|
||||
"block_previous": "Bloque Anterior:",
|
||||
"block_next": "Bloque Siguiente:",
|
||||
"block_click_prev": "Clic para ver bloque anterior",
|
||||
"block_click_next": "Clic para ver bloque siguiente",
|
||||
"block_nav_prev": "<< Anterior",
|
||||
"block_nav_next": "Siguiente >>",
|
||||
|
||||
# ---- Export Keys dialog ----
|
||||
"export_keys_title": "Exportar Todas las Claves Privadas",
|
||||
"export_keys_danger": "PELIGRO: ¡Esto exportará TODAS las claves privadas de tu cartera! Cualquiera con acceso a este archivo puede robar tus fondos. Guárdalo de forma segura y elimínalo después de usar.",
|
||||
"export_keys_options": "Opciones de exportación:",
|
||||
"export_keys_include_z": "Incluir direcciones Z (protegidas)",
|
||||
"export_keys_include_t": "Incluir direcciones T (transparentes)",
|
||||
"export_keys_btn": "Exportar Claves",
|
||||
"export_keys_success": "Claves exportadas exitosamente",
|
||||
"export_private_key": "Exportar Clave Privada",
|
||||
"export_viewing_key": "Exportar Clave de Vista",
|
||||
|
||||
# ---- Export Transactions dialog ----
|
||||
"export_tx_title": "Exportar Transacciones a CSV",
|
||||
"export_tx_count": "Exportar %zu transacciones a archivo CSV.",
|
||||
"export_tx_none": "No hay transacciones para exportar",
|
||||
"export_tx_file_fail": "Error al crear archivo CSV",
|
||||
"export_tx_success": "Transacciones exportadas exitosamente",
|
||||
|
||||
# ---- Import Key dialog ----
|
||||
"import_key_title": "Importar Clave Privada",
|
||||
"import_key_warning": "Advertencia: ¡Nunca compartas tus claves privadas! Importar claves de fuentes no confiables puede comprometer tu cartera.",
|
||||
"import_key_label": "Clave(s) Privada(s):",
|
||||
"import_key_tooltip": "Ingresa una o más claves privadas, una por línea.\nSoporta claves de direcciones z y t.\nLas líneas que empiezan con # se tratan como comentarios.",
|
||||
"import_key_btn": "Importar Clave(s)",
|
||||
"import_key_no_valid": "No se encontraron claves válidas en la entrada",
|
||||
"import_key_success": "Claves importadas exitosamente",
|
||||
"import_key_rescan": "Re-escanear blockchain después de importar",
|
||||
"import_key_start_height": "Altura inicial:",
|
||||
"import_key_full_rescan": "(0 = re-escaneo completo)",
|
||||
"import_key_formats": "Formatos de clave soportados:",
|
||||
"import_key_z_format": "Claves de gasto de direcciones Z (secret-extended-key-...)",
|
||||
"import_key_t_format": "Claves privadas WIF de direcciones T",
|
||||
"import_private_key": "Importar Clave Privada...",
|
||||
"invalid_address": "Formato de dirección inválido",
|
||||
|
||||
# ---- Key Export dialog ----
|
||||
"key_export_private_key": "Clave Privada:",
|
||||
"key_export_viewing_key": "Clave de Vista:",
|
||||
"key_export_private_warning": "¡Mantén esta clave en SECRETO! Cualquiera con esta clave puede gastar tus fondos. Nunca la compartas en línea ni con personas no confiables.",
|
||||
"key_export_viewing_warning": "Esta clave de vista permite a otros ver tus transacciones entrantes y saldo, pero NO gastar tus fondos. Comparte solo con personas de confianza.",
|
||||
"key_export_fetching": "Obteniendo clave de la cartera...",
|
||||
"key_export_reveal": "Revelar Clave",
|
||||
|
||||
# ---- QR dialog ----
|
||||
"qr_title": "Código QR",
|
||||
"qr_failed": "Error al generar código QR",
|
||||
|
||||
# ---- Request Payment dialog ----
|
||||
"request_title": "Solicitar Pago",
|
||||
"request_description": "Genera una solicitud de pago que otros pueden escanear o copiar. El código QR contiene tu dirección y cantidad/memo opcionales.",
|
||||
"request_receive_address": "Dirección de Recepción:",
|
||||
"request_select_address": "Seleccionar dirección...",
|
||||
"request_shielded_addrs": "-- Direcciones Protegidas --",
|
||||
"request_transparent_addrs": "-- Direcciones Transparentes --",
|
||||
"request_amount": "Cantidad (opcional):",
|
||||
"request_label": "Etiqueta (opcional):",
|
||||
"request_memo": "Memo (opcional):",
|
||||
"request_payment_uri": "URI de Pago:",
|
||||
"request_copy_uri": "Copiar URI",
|
||||
"request_uri_copied": "URI de pago copiada al portapapeles",
|
||||
|
||||
# ---- Shield dialog ----
|
||||
"shield_title": "Proteger Recompensas de Coinbase",
|
||||
"shield_description": "Protege tus recompensas de minería enviando salidas coinbase de direcciones transparentes a una dirección protegida. Esto mejora la privacidad ocultando tus ingresos de minería.",
|
||||
"shield_from_address": "Dirección de Origen:",
|
||||
"shield_wildcard_hint": "Usa '*' para proteger desde todas las direcciones transparentes",
|
||||
"shield_to_address": "Dirección Destino (Protegida):",
|
||||
"shield_select_z": "Seleccionar dirección z...",
|
||||
"shield_utxo_limit": "Límite UTXO:",
|
||||
"shield_max_utxos": "UTXOs máximos por operación",
|
||||
"shield_funds": "Proteger Fondos",
|
||||
"shield_started": "Operación de protección iniciada",
|
||||
"shield_check_status": "Verificar Estado",
|
||||
"shield_completed": "¡Operación completada exitosamente!",
|
||||
"shield_merge_done": "¡Protección/fusión completada!",
|
||||
"shield_in_progress": "Operación en progreso...",
|
||||
"merge_title": "Fusionar a Dirección",
|
||||
"merge_description": "Fusiona múltiples UTXOs en una sola dirección protegida. Esto puede ayudar a reducir el tamaño de la cartera y mejorar la privacidad.",
|
||||
"merge_funds": "Fusionar Fondos",
|
||||
"merge_started": "Operación de fusión iniciada",
|
||||
|
||||
# ---- Transaction Details dialog ----
|
||||
"tx_details_title": "Detalles de Transacción",
|
||||
"tx_received": "RECIBIDO",
|
||||
"tx_sent": "ENVIADO",
|
||||
"tx_mined": "MINADO",
|
||||
"tx_immature": "INMADURO",
|
||||
"tx_confirmations": "%d confirmaciones",
|
||||
"tx_id_label": "ID de Transacción:",
|
||||
"tx_to_address": "Dirección Destino:",
|
||||
"tx_from_address": "Dirección Origen:",
|
||||
"tx_view_explorer": "Ver en Explorador",
|
||||
"pending": "Pendiente",
|
||||
|
||||
# ---- Validate Address dialog ----
|
||||
"validate_title": "Validar Dirección",
|
||||
"validate_description": "Ingresa una dirección DragonX para verificar si es válida y si pertenece a esta cartera.",
|
||||
"validate_btn": "Validar",
|
||||
"validate_results": "Resultados:",
|
||||
"validate_status": "Estado:",
|
||||
"validate_valid": "VÁLIDA",
|
||||
"validate_invalid": "INVÁLIDA",
|
||||
"validate_type": "Tipo:",
|
||||
"validate_ownership": "Propiedad:",
|
||||
"validate_is_mine": "Esta cartera es dueña de esta dirección",
|
||||
"validate_not_mine": "No es propiedad de esta cartera",
|
||||
"validate_shielded_type": "Protegida (dirección z)",
|
||||
"validate_transparent_type": "Transparente (dirección t)",
|
||||
|
||||
# ---- Misc ----
|
||||
"transaction_sent": "Transacción enviada exitosamente",
|
||||
}
|
||||
|
||||
# Load existing to preserve anything we might have missed
|
||||
import os
|
||||
existing_path = os.path.join(os.path.dirname(__file__), '..', 'res', 'lang', 'es.json')
|
||||
out_path = existing_path
|
||||
|
||||
with open(out_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(dict(sorted(es.items())), f, indent=4, ensure_ascii=False)
|
||||
f.write('\n')
|
||||
|
||||
print(f"Wrote {len(es)} Spanish translations to {out_path}")
|
||||
@@ -0,0 +1,646 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate French (fr) translations for ObsidianDragon wallet."""
|
||||
import json, os
|
||||
|
||||
translations = {
|
||||
"24h_change": "Variation 24h",
|
||||
"24h_volume": "Volume 24h",
|
||||
"about": "À propos",
|
||||
"about_block_explorer": "Explorateur de blocs",
|
||||
"about_block_height": "Hauteur de bloc :",
|
||||
"about_build_date": "Date de compilation :",
|
||||
"about_build_type": "Type de build :",
|
||||
"about_chain": "Chaîne :",
|
||||
"about_connections": "Connexions :",
|
||||
"about_credits": "Crédits",
|
||||
"about_daemon": "Daemon :",
|
||||
"about_debug": "Débogage",
|
||||
"about_dragonx": "À propos d'ObsidianDragon",
|
||||
"about_edition": "Édition ImGui",
|
||||
"about_github": "GitHub",
|
||||
"about_imgui": "ImGui :",
|
||||
"about_license": "Licence",
|
||||
"about_license_text": "Ce logiciel est publié sous la licence publique générale GNU v3 (GPLv3). Vous êtes libre d'utiliser, de modifier et de distribuer ce logiciel selon les termes de la licence.",
|
||||
"about_peers_count": "%zu pairs",
|
||||
"about_release": "Version",
|
||||
"about_title": "À propos d'ObsidianDragon",
|
||||
"about_version": "Version :",
|
||||
"about_website": "Site web",
|
||||
"acrylic": "Acrylique",
|
||||
"add": "Ajouter",
|
||||
"address": "Adresse",
|
||||
"address_book_add": "Ajouter une adresse",
|
||||
"address_book_add_new": "Ajouter",
|
||||
"address_book_added": "Adresse ajoutée au carnet",
|
||||
"address_book_count": "%zu adresses enregistrées",
|
||||
"address_book_deleted": "Entrée supprimée",
|
||||
"address_book_edit": "Modifier l'adresse",
|
||||
"address_book_empty": "Aucune adresse enregistrée. Cliquez sur 'Ajouter' pour en créer une.",
|
||||
"address_book_exists": "L'adresse existe déjà dans le carnet",
|
||||
"address_book_title": "Carnet d'adresses",
|
||||
"address_book_update_failed": "Échec de la mise à jour - l'adresse est peut-être en double",
|
||||
"address_book_updated": "Adresse mise à jour",
|
||||
"address_copied": "Adresse copiée dans le presse-papiers",
|
||||
"address_details": "Détails de l'adresse",
|
||||
"address_label": "Adresse :",
|
||||
"address_upper": "ADRESSE",
|
||||
"address_url": "URL de l'adresse",
|
||||
"addresses_appear_here": "Vos adresses de réception apparaîtront ici une fois connecté.",
|
||||
"advanced": "AVANCÉ",
|
||||
"all_filter": "Tout",
|
||||
"allow_custom_fees": "Autoriser les frais personnalisés",
|
||||
"amount": "Montant",
|
||||
"amount_details": "DÉTAILS DU MONTANT",
|
||||
"amount_exceeds_balance": "Le montant dépasse le solde",
|
||||
"amount_label": "Montant :",
|
||||
"appearance": "APPARENCE",
|
||||
"auto_shield": "Auto-blindage du minage",
|
||||
"available": "Disponible",
|
||||
"backup_backing_up": "Sauvegarde en cours...",
|
||||
"backup_create": "Créer une sauvegarde",
|
||||
"backup_created": "Sauvegarde du portefeuille créée",
|
||||
"backup_data": "SAUVEGARDE & DONNÉES",
|
||||
"backup_description": "Créez une sauvegarde de votre fichier wallet.dat. Ce fichier contient toutes vos clés privées et l'historique des transactions. Conservez la sauvegarde dans un endroit sûr.",
|
||||
"backup_destination": "Destination de sauvegarde :",
|
||||
"backup_tip_external": "Stockez les sauvegardes sur des disques externes ou un stockage cloud",
|
||||
"backup_tip_multiple": "Créez plusieurs sauvegardes à différents endroits",
|
||||
"backup_tip_test": "Testez périodiquement la restauration à partir de la sauvegarde",
|
||||
"backup_tips": "Conseils :",
|
||||
"backup_title": "Sauvegarder le portefeuille",
|
||||
"backup_wallet": "Sauvegarder le portefeuille...",
|
||||
"backup_wallet_not_found": "Attention : wallet.dat introuvable à l'emplacement prévu",
|
||||
"balance": "Solde",
|
||||
"balance_layout": "Disposition du solde",
|
||||
"ban": "Bannir",
|
||||
"banned_peers": "Pairs bannis",
|
||||
"block": "Bloc",
|
||||
"block_bits": "Bits :",
|
||||
"block_click_next": "Cliquez pour voir le bloc suivant",
|
||||
"block_click_prev": "Cliquez pour voir le bloc précédent",
|
||||
"block_explorer": "Explorateur de blocs",
|
||||
"block_get_info": "Obtenir les infos du bloc",
|
||||
"block_hash": "Hash du bloc :",
|
||||
"block_height": "Hauteur du bloc :",
|
||||
"block_info_title": "Informations sur le bloc",
|
||||
"block_merkle_root": "Racine de Merkle :",
|
||||
"block_nav_next": "Suivant >>",
|
||||
"block_nav_prev": "<< Précédent",
|
||||
"block_next": "Bloc suivant :",
|
||||
"block_previous": "Bloc précédent :",
|
||||
"block_size": "Taille :",
|
||||
"block_timestamp": "Horodatage :",
|
||||
"block_transactions": "Transactions :",
|
||||
"blockchain_syncing": "Synchronisation de la blockchain (%.1f%%)... Les soldes peuvent être inexacts.",
|
||||
"cancel": "Annuler",
|
||||
"characters": "caractères",
|
||||
"clear": "Effacer",
|
||||
"clear_all_bans": "Lever tous les bannissements",
|
||||
"clear_form_confirm": "Effacer tous les champs du formulaire ?",
|
||||
"clear_request": "Effacer la demande",
|
||||
"click_copy_address": "Cliquez pour copier l'adresse",
|
||||
"click_copy_uri": "Cliquez pour copier l'URI",
|
||||
"close": "Fermer",
|
||||
"conf_count": "%d conf.",
|
||||
"confirm_and_send": "Confirmer & Envoyer",
|
||||
"confirm_send": "Confirmer l'envoi",
|
||||
"confirm_transaction": "Confirmer la transaction",
|
||||
"confirmations": "Confirmations",
|
||||
"confirmations_display": "%d confirmations | %s",
|
||||
"confirmed": "Confirmé",
|
||||
"connected": "Connecté",
|
||||
"connected_peers": "Pairs connectés",
|
||||
"connecting": "Connexion...",
|
||||
"console": "Console",
|
||||
"console_auto_scroll": "Défilement auto",
|
||||
"console_available_commands": "Commandes disponibles :",
|
||||
"console_capturing_output": "Capture de la sortie du daemon...",
|
||||
"console_clear": "Effacer",
|
||||
"console_clear_console": "Effacer la console",
|
||||
"console_cleared": "Console effacée",
|
||||
"console_click_commands": "Cliquez sur les commandes ci-dessus pour les insérer",
|
||||
"console_click_insert": "Cliquez pour insérer",
|
||||
"console_click_insert_params": "Cliquez pour insérer avec paramètres",
|
||||
"console_close": "Fermer",
|
||||
"console_commands": "Commandes",
|
||||
"console_common_rpc": "Commandes RPC courantes :",
|
||||
"console_completions": "Complétions :",
|
||||
"console_connected": "Connecté au daemon",
|
||||
"console_copy_all": "Tout copier",
|
||||
"console_copy_selected": "Copier",
|
||||
"console_daemon": "Daemon",
|
||||
"console_daemon_error": "Erreur du daemon !",
|
||||
"console_daemon_started": "Daemon démarré",
|
||||
"console_daemon_stopped": "Daemon arrêté",
|
||||
"console_disconnected": "Déconnecté du daemon",
|
||||
"console_errors": "Erreurs",
|
||||
"console_filter_hint": "Filtrer la sortie...",
|
||||
"console_help_clear": " clear - Effacer la console",
|
||||
"console_help_getbalance": " getbalance - Afficher le solde transparent",
|
||||
"console_help_getblockcount": " getblockcount - Afficher la hauteur de bloc actuelle",
|
||||
"console_help_getinfo": " getinfo - Afficher les infos du nœud",
|
||||
"console_help_getmininginfo": " getmininginfo - Afficher le statut du minage",
|
||||
"console_help_getpeerinfo": " getpeerinfo - Afficher les pairs connectés",
|
||||
"console_help_gettotalbalance": " gettotalbalance - Afficher le solde total",
|
||||
"console_help_help": " help - Afficher ce message d'aide",
|
||||
"console_help_setgenerate": " setgenerate - Contrôler le minage",
|
||||
"console_help_stop": " stop - Arrêter le daemon",
|
||||
"console_line_count": "%zu lignes",
|
||||
"console_new_lines": "%d nouvelles lignes",
|
||||
"console_no_daemon": "Pas de daemon",
|
||||
"console_not_connected": "Erreur : Non connecté au daemon",
|
||||
"console_rpc_reference": "Référence des commandes RPC",
|
||||
"console_scanline": "Scanline de la console",
|
||||
"console_search_commands": "Rechercher des commandes...",
|
||||
"console_select_all": "Tout sélectionner",
|
||||
"console_show_daemon_output": "Afficher la sortie du daemon",
|
||||
"console_show_errors_only": "Afficher uniquement les erreurs",
|
||||
"console_show_rpc_ref": "Afficher la référence des commandes RPC",
|
||||
"console_showing_lines": "Affichage de %zu sur %zu lignes",
|
||||
"console_starting_node": "Démarrage du nœud...",
|
||||
"console_status_error": "Erreur",
|
||||
"console_status_running": "En cours",
|
||||
"console_status_starting": "Démarrage",
|
||||
"console_status_stopped": "Arrêté",
|
||||
"console_status_stopping": "Arrêt",
|
||||
"console_status_unknown": "Inconnu",
|
||||
"console_tab_completion": "Tab pour compléter",
|
||||
"console_type_help": "Tapez 'help' pour les commandes disponibles",
|
||||
"console_welcome": "Bienvenue dans la console ObsidianDragon",
|
||||
"console_zoom_in": "Agrandir",
|
||||
"console_zoom_out": "Réduire",
|
||||
"copy": "Copier",
|
||||
"copy_address": "Copier l'adresse complète",
|
||||
"copy_error": "Copier l'erreur",
|
||||
"copy_to_clipboard": "Copier dans le presse-papiers",
|
||||
"copy_txid": "Copier le TxID",
|
||||
"copy_uri": "Copier l'URI",
|
||||
"current_price": "Prix actuel",
|
||||
"custom_fees": "Frais personnalisés",
|
||||
"dark": "Sombre",
|
||||
"date": "Date",
|
||||
"date_label": "Date :",
|
||||
"delete": "Supprimer",
|
||||
"difficulty": "Difficulté",
|
||||
"disconnected": "Déconnecté",
|
||||
"dismiss": "Ignorer",
|
||||
"display": "Affichage",
|
||||
"dragonx_green": "DragonX (Vert)",
|
||||
"edit": "Modifier",
|
||||
"error": "Erreur",
|
||||
"est_time_to_block": "Temps est. par bloc",
|
||||
"exit": "Quitter",
|
||||
"explorer": "EXPLORATEUR",
|
||||
"export": "Exporter",
|
||||
"export_csv": "Exporter en CSV",
|
||||
"export_keys_btn": "Exporter les clés",
|
||||
"export_keys_danger": "DANGER : Ceci exportera TOUTES les clés privées de votre portefeuille ! Toute personne ayant accès à ce fichier peut voler vos fonds. Conservez-le en sécurité et supprimez-le après utilisation.",
|
||||
"export_keys_include_t": "Inclure les adresses T (transparentes)",
|
||||
"export_keys_include_z": "Inclure les adresses Z (blindées)",
|
||||
"export_keys_options": "Options d'exportation :",
|
||||
"export_keys_success": "Clés exportées avec succès",
|
||||
"export_keys_title": "Exporter toutes les clés privées",
|
||||
"export_private_key": "Exporter la clé privée",
|
||||
"export_tx_count": "Exporter %zu transactions en fichier CSV.",
|
||||
"export_tx_file_fail": "Impossible de créer le fichier CSV",
|
||||
"export_tx_none": "Aucune transaction à exporter",
|
||||
"export_tx_success": "Transactions exportées avec succès",
|
||||
"export_tx_title": "Exporter les transactions en CSV",
|
||||
"export_viewing_key": "Exporter la clé de visualisation",
|
||||
"failed_create_shielded": "Échec de la création de l'adresse blindée",
|
||||
"failed_create_transparent": "Échec de la création de l'adresse transparente",
|
||||
"fee": "Frais",
|
||||
"fee_high": "Élevés",
|
||||
"fee_label": "Frais :",
|
||||
"fee_low": "Faibles",
|
||||
"fee_normal": "Normal",
|
||||
"fetch_prices": "Récupérer les prix",
|
||||
"file": "Fichier",
|
||||
"file_save_location": "Le fichier sera enregistré dans : ~/.config/ObsidianDragon/",
|
||||
"font_scale": "Taille de police",
|
||||
"from": "De",
|
||||
"from_upper": "DE",
|
||||
"full_details": "Tous les détails",
|
||||
"general": "Général",
|
||||
"go_to_receive": "Aller à Recevoir",
|
||||
"height": "Hauteur",
|
||||
"help": "Aide",
|
||||
"hide": "Masquer",
|
||||
"history": "Historique",
|
||||
"immature_type": "Immature",
|
||||
"import": "Importer",
|
||||
"import_key_btn": "Importer clé(s)",
|
||||
"import_key_formats": "Formats de clés pris en charge :",
|
||||
"import_key_full_rescan": "(0 = rescan complet)",
|
||||
"import_key_label": "Clé(s) privée(s) :",
|
||||
"import_key_no_valid": "Aucune clé valide trouvée dans l'entrée",
|
||||
"import_key_rescan": "Re-scanner la blockchain après l'importation",
|
||||
"import_key_start_height": "Hauteur de départ :",
|
||||
"import_key_success": "Clés importées avec succès",
|
||||
"import_key_t_format": "Clés privées WIF d'adresses T",
|
||||
"import_key_title": "Importer une clé privée",
|
||||
"import_key_tooltip": "Entrez une ou plusieurs clés privées, une par ligne.\nPrend en charge les clés z-adresse et t-adresse.\nLes lignes commençant par # sont traitées comme des commentaires.",
|
||||
"import_key_warning": "Attention : Ne partagez jamais vos clés privées ! L'importation de clés provenant de sources non fiables peut compromettre votre portefeuille.",
|
||||
"import_key_z_format": "Clés de dépenses z-adresse (secret-extended-key-...)",
|
||||
"import_private_key": "Importer une clé privée...",
|
||||
"invalid_address": "Format d'adresse invalide",
|
||||
"ip_address": "Adresse IP",
|
||||
"keep": "Conserver",
|
||||
"keep_daemon": "Garder le daemon en marche",
|
||||
"key_export_fetching": "Récupération de la clé depuis le portefeuille...",
|
||||
"key_export_private_key": "Clé privée :",
|
||||
"key_export_private_warning": "Gardez cette clé SECRÈTE ! Toute personne possédant cette clé peut dépenser vos fonds. Ne la partagez jamais en ligne ou avec des tiers non fiables.",
|
||||
"key_export_reveal": "Révéler la clé",
|
||||
"key_export_viewing_key": "Clé de visualisation :",
|
||||
"key_export_viewing_warning": "Cette clé de visualisation permet à d'autres de voir vos transactions entrantes et votre solde, mais PAS de dépenser vos fonds. Ne la partagez qu'avec des personnes de confiance.",
|
||||
"label": "Libellé :",
|
||||
"language": "Langue",
|
||||
"light": "Clair",
|
||||
"loading": "Chargement...",
|
||||
"loading_addresses": "Chargement des adresses...",
|
||||
"local_hashrate": "Hashrate local",
|
||||
"low_spec_mode": "Mode économie",
|
||||
"market": "Marché",
|
||||
"market_12h": "12h",
|
||||
"market_18h": "18h",
|
||||
"market_24h": "24h",
|
||||
"market_24h_volume": "VOLUME 24H",
|
||||
"market_6h": "6h",
|
||||
"market_attribution": "Données de prix de NonKYC",
|
||||
"market_btc_price": "PRIX BTC",
|
||||
"market_cap": "Capitalisation",
|
||||
"market_no_history": "Aucun historique de prix disponible",
|
||||
"market_no_price": "Pas de données de prix",
|
||||
"market_now": "Maintenant",
|
||||
"market_pct_shielded": "%.0f%% Blindé",
|
||||
"market_portfolio": "PORTEFEUILLE",
|
||||
"market_price_unavailable": "Données de prix indisponibles",
|
||||
"market_refresh_price": "Actualiser les données de prix",
|
||||
"market_trade_on": "Échanger sur %s",
|
||||
"mature": "Mature",
|
||||
"max": "Max",
|
||||
"memo": "Mémo (optionnel, chiffré)",
|
||||
"memo_label": "Mémo :",
|
||||
"memo_optional": "MÉMO (OPTIONNEL)",
|
||||
"memo_upper": "MÉMO",
|
||||
"memo_z_only": "Note : Les mémos ne sont disponibles que lors de l'envoi vers des adresses blindées (z)",
|
||||
"merge_description": "Fusionnez plusieurs UTXOs en une seule adresse blindée. Cela peut réduire la taille du portefeuille et améliorer la confidentialité.",
|
||||
"merge_funds": "Fusionner les fonds",
|
||||
"merge_started": "Opération de fusion démarrée",
|
||||
"merge_title": "Fusionner vers une adresse",
|
||||
"mine_when_idle": "Miner au repos",
|
||||
"mined": "miné",
|
||||
"mined_filter": "Miné",
|
||||
"mined_type": "Miné",
|
||||
"mined_upper": "MINÉ",
|
||||
"miner_fee": "Frais de mineur",
|
||||
"mining": "Minage",
|
||||
"mining_active": "Actif",
|
||||
"mining_address_copied": "Adresse de minage copiée",
|
||||
"mining_all_time": "Tout le temps",
|
||||
"mining_already_saved": "URL du pool déjà enregistrée",
|
||||
"mining_block_copied": "Hash du bloc copié",
|
||||
"mining_chart_1m_ago": "il y a 1m",
|
||||
"mining_chart_5m_ago": "il y a 5m",
|
||||
"mining_chart_now": "Maintenant",
|
||||
"mining_chart_start": "Début",
|
||||
"mining_click": "Cliquer",
|
||||
"mining_click_copy_address": "Cliquez pour copier l'adresse",
|
||||
"mining_click_copy_block": "Cliquez pour copier le hash du bloc",
|
||||
"mining_click_copy_difficulty": "Cliquez pour copier la difficulté",
|
||||
"mining_connected": "Connecté",
|
||||
"mining_connecting": "Connexion...",
|
||||
"mining_control": "Contrôle du minage",
|
||||
"mining_difficulty_copied": "Difficulté copiée",
|
||||
"mining_est_block": "Bloc est.",
|
||||
"mining_est_daily": "Est. quotidien",
|
||||
"mining_filter_all": "Tout",
|
||||
"mining_filter_tip_all": "Afficher tous les gains",
|
||||
"mining_filter_tip_pool": "Afficher uniquement les gains du pool",
|
||||
"mining_filter_tip_solo": "Afficher uniquement les gains solo",
|
||||
"mining_idle_off_tooltip": "Activer le minage au repos",
|
||||
"mining_idle_on_tooltip": "Désactiver le minage au repos",
|
||||
"mining_local_hashrate": "Hashrate local",
|
||||
"mining_mine": "Miner",
|
||||
"mining_mining_addr": "Adr. minage",
|
||||
"mining_network": "Réseau",
|
||||
"mining_no_blocks_yet": "Aucun bloc trouvé pour l'instant",
|
||||
"mining_no_payouts_yet": "Aucun paiement de pool pour l'instant",
|
||||
"mining_no_saved_addresses": "Aucune adresse enregistrée",
|
||||
"mining_no_saved_pools": "Aucun pool enregistré",
|
||||
"mining_off": "Le minage est DÉSACTIVÉ",
|
||||
"mining_on": "Le minage est ACTIVÉ",
|
||||
"mining_open_in_explorer": "Ouvrir dans l'explorateur",
|
||||
"mining_payout_address": "Adresse de paiement",
|
||||
"mining_payout_tooltip": "Adresse pour recevoir les récompenses de minage",
|
||||
"mining_pool": "Pool",
|
||||
"mining_pool_hashrate": "Hashrate du pool",
|
||||
"mining_pool_url": "URL du pool",
|
||||
"mining_recent_blocks": "BLOCS RÉCENTS",
|
||||
"mining_recent_payouts": "PAIEMENTS DE POOL RÉCENTS",
|
||||
"mining_remove": "Supprimer",
|
||||
"mining_reset_defaults": "Réinitialiser les paramètres",
|
||||
"mining_save_payout_address": "Enregistrer l'adresse de paiement",
|
||||
"mining_save_pool_url": "Enregistrer l'URL du pool",
|
||||
"mining_saved_addresses": "Adresses enregistrées :",
|
||||
"mining_saved_pools": "Pools enregistrés :",
|
||||
"mining_shares": "Parts",
|
||||
"mining_show_chart": "Graphique",
|
||||
"mining_show_log": "Journal",
|
||||
"mining_solo": "Solo",
|
||||
"mining_starting": "Démarrage...",
|
||||
"mining_starting_tooltip": "Le mineur démarre...",
|
||||
"mining_statistics": "Statistiques de minage",
|
||||
"mining_stop": "Arrêter",
|
||||
"mining_stop_solo_for_pool": "Arrêtez le minage solo avant de démarrer le minage en pool",
|
||||
"mining_stop_solo_for_pool_settings": "Arrêtez le minage solo pour modifier les paramètres du pool",
|
||||
"mining_stopping": "Arrêt...",
|
||||
"mining_stopping_tooltip": "Le mineur s'arrête...",
|
||||
"mining_syncing_tooltip": "La blockchain se synchronise...",
|
||||
"mining_threads": "Threads de minage",
|
||||
"mining_to_save": "pour enregistrer",
|
||||
"mining_today": "Aujourd'hui",
|
||||
"mining_uptime": "Temps de fonctionnement",
|
||||
"mining_yesterday": "Hier",
|
||||
"network": "Réseau",
|
||||
"network_fee": "FRAIS RÉSEAU",
|
||||
"network_hashrate": "Hashrate du réseau",
|
||||
"new": "+ Nouveau",
|
||||
"new_shielded_created": "Nouvelle adresse blindée créée",
|
||||
"new_t_address": "Nouvelle adresse T",
|
||||
"new_t_transparent": "Nouvelle adresse t (Transparente)",
|
||||
"new_transparent_created": "Nouvelle adresse transparente créée",
|
||||
"new_z_address": "Nouvelle adresse Z",
|
||||
"new_z_shielded": "Nouvelle adresse z (Blindée)",
|
||||
"no_addresses": "Aucune adresse trouvée. Créez-en une avec les boutons ci-dessus.",
|
||||
"no_addresses_available": "Aucune adresse disponible",
|
||||
"no_addresses_match": "Aucune adresse ne correspond au filtre",
|
||||
"no_addresses_with_balance": "Aucune adresse avec solde",
|
||||
"no_matching": "Aucune transaction correspondante",
|
||||
"no_recent_receives": "Aucune réception récente",
|
||||
"no_recent_sends": "Aucun envoi récent",
|
||||
"no_transactions": "Aucune transaction trouvée",
|
||||
"node": "NŒUD",
|
||||
"node_security": "NŒUD & SÉCURITÉ",
|
||||
"noise": "Bruit",
|
||||
"not_connected": "Non connecté au daemon...",
|
||||
"not_connected_to_daemon": "Non connecté au daemon",
|
||||
"notes": "Notes",
|
||||
"notes_optional": "Notes (optionnel) :",
|
||||
"output_filename": "Nom du fichier de sortie :",
|
||||
"overview": "Aperçu",
|
||||
"paste": "Coller",
|
||||
"paste_from_clipboard": "Coller depuis le presse-papiers",
|
||||
"pay_from": "Payer depuis",
|
||||
"payment_request": "DEMANDE DE PAIEMENT",
|
||||
"payment_request_copied": "Demande de paiement copiée",
|
||||
"payment_uri_copied": "URI de paiement copiée",
|
||||
"peers": "Pairs",
|
||||
"peers_avg_ping": "Ping moyen",
|
||||
"peers_ban_24h": "Bannir le pair 24h",
|
||||
"peers_ban_score": "Score de ban : %d",
|
||||
"peers_banned": "Bannis",
|
||||
"peers_banned_count": "Bannis : %d",
|
||||
"peers_best_block": "Meilleur bloc",
|
||||
"peers_blockchain": "BLOCKCHAIN",
|
||||
"peers_blocks": "Blocs",
|
||||
"peers_blocks_left": "%d blocs restants",
|
||||
"peers_clear_all_bans": "Lever tous les bannissements",
|
||||
"peers_click_copy": "Cliquez pour copier",
|
||||
"peers_connected": "Connectés",
|
||||
"peers_connected_count": "Connectés : %d",
|
||||
"peers_copy_ip": "Copier l'IP",
|
||||
"peers_dir_in": "Ent.",
|
||||
"peers_dir_out": "Sort.",
|
||||
"peers_hash_copied": "Hash copié",
|
||||
"peers_hashrate": "Hashrate",
|
||||
"peers_in_out": "Ent./Sort.",
|
||||
"peers_longest": "Plus longue",
|
||||
"peers_longest_chain": "Plus longue chaîne",
|
||||
"peers_memory": "Mémoire",
|
||||
"peers_no_banned": "Aucun pair banni",
|
||||
"peers_no_connected": "Aucun pair connecté",
|
||||
"peers_no_tls": "Pas de TLS",
|
||||
"peers_notarized": "Notarisé",
|
||||
"peers_p2p_port": "Port P2P",
|
||||
"peers_protocol": "Protocole",
|
||||
"peers_received": "Reçu",
|
||||
"peers_refresh": "Actualiser",
|
||||
"peers_refresh_tooltip": "Actualiser la liste des pairs",
|
||||
"peers_refreshing": "Actualisation...",
|
||||
"peers_sent": "Envoyé",
|
||||
"peers_tt_id": "ID : %d",
|
||||
"peers_tt_received": "Reçu : %s",
|
||||
"peers_tt_sent": "Envoyé : %s",
|
||||
"peers_tt_services": "Services : %s",
|
||||
"peers_tt_start_height": "Hauteur de départ : %d",
|
||||
"peers_tt_synced": "Synchronisé H/B : %d/%d",
|
||||
"peers_tt_tls_cipher": "TLS : %s",
|
||||
"peers_unban": "Débannir",
|
||||
"peers_upper": "PAIRS",
|
||||
"peers_version": "Version",
|
||||
"pending": "En attente",
|
||||
"ping": "Ping",
|
||||
"price_chart": "Graphique des prix",
|
||||
"qr_code": "Code QR",
|
||||
"qr_failed": "Échec de la génération du code QR",
|
||||
"qr_title": "Code QR",
|
||||
"qr_unavailable": "QR indisponible",
|
||||
"receive": "Recevoir",
|
||||
"received": "reçu",
|
||||
"received_filter": "Reçu",
|
||||
"received_label": "Reçu",
|
||||
"received_upper": "REÇU",
|
||||
"receiving_addresses": "Vos adresses de réception",
|
||||
"recent_received": "REÇUS RÉCENTS",
|
||||
"recent_sends": "ENVOIS RÉCENTS",
|
||||
"recipient": "DESTINATAIRE",
|
||||
"recv_type": "Reçu",
|
||||
"refresh": "Actualiser",
|
||||
"refresh_now": "Actualiser maintenant",
|
||||
"report_bug": "Signaler un bug",
|
||||
"request_amount": "Montant (optionnel) :",
|
||||
"request_copy_uri": "Copier l'URI",
|
||||
"request_description": "Générez une demande de paiement que d'autres peuvent scanner ou copier. Le code QR contient votre adresse et un montant/mémo optionnel.",
|
||||
"request_label": "Libellé (optionnel) :",
|
||||
"request_memo": "Mémo (optionnel) :",
|
||||
"request_payment": "Demander un paiement",
|
||||
"request_payment_uri": "URI de paiement :",
|
||||
"request_receive_address": "Adresse de réception :",
|
||||
"request_select_address": "Sélectionner une adresse...",
|
||||
"request_shielded_addrs": "-- Adresses blindées --",
|
||||
"request_title": "Demander un paiement",
|
||||
"request_transparent_addrs": "-- Adresses transparentes --",
|
||||
"request_uri_copied": "URI de paiement copiée dans le presse-papiers",
|
||||
"rescan": "Re-scanner",
|
||||
"reset_to_defaults": "Réinitialiser les paramètres",
|
||||
"review_send": "Vérifier l'envoi",
|
||||
"rpc_host": "Hôte RPC",
|
||||
"rpc_pass": "Mot de passe",
|
||||
"rpc_port": "Port",
|
||||
"rpc_user": "Nom d'utilisateur",
|
||||
"save": "Enregistrer",
|
||||
"save_settings": "Enregistrer les paramètres",
|
||||
"save_z_transactions": "Enregistrer les Z-tx dans la liste",
|
||||
"search_placeholder": "Rechercher...",
|
||||
"security": "SÉCURITÉ",
|
||||
"select_address": "Sélectionner une adresse...",
|
||||
"select_receiving_address": "Sélectionner une adresse de réception...",
|
||||
"select_source_address": "Sélectionner une adresse source...",
|
||||
"send": "Envoyer",
|
||||
"send_amount": "Montant",
|
||||
"send_amount_details": "DÉTAILS DU MONTANT",
|
||||
"send_amount_upper": "MONTANT",
|
||||
"send_clear_fields": "Effacer tous les champs du formulaire ?",
|
||||
"send_copy_error": "Copier l'erreur",
|
||||
"send_dismiss": "Ignorer",
|
||||
"send_error_copied": "Erreur copiée dans le presse-papiers",
|
||||
"send_error_prefix": "Erreur : %s",
|
||||
"send_exceeds_available": "Dépasse le disponible (%.8f)",
|
||||
"send_fee": "Frais",
|
||||
"send_fee_high": "Élevés",
|
||||
"send_fee_low": "Faibles",
|
||||
"send_fee_normal": "Normal",
|
||||
"send_form_restored": "Formulaire restauré",
|
||||
"send_from_this_address": "Envoyer depuis cette adresse",
|
||||
"send_go_to_receive": "Aller à Recevoir",
|
||||
"send_keep": "Conserver",
|
||||
"send_network_fee": "FRAIS RÉSEAU",
|
||||
"send_no_balance": "Pas de solde",
|
||||
"send_no_recent": "Aucun envoi récent",
|
||||
"send_recent_sends": "ENVOIS RÉCENTS",
|
||||
"send_recipient": "DESTINATAIRE",
|
||||
"send_select_source": "Sélectionner une adresse source...",
|
||||
"send_sending_from": "ENVOI DEPUIS",
|
||||
"send_submitting": "Soumission de la transaction...",
|
||||
"send_switch_to_receive": "Passez à Recevoir pour obtenir votre adresse et commencer à recevoir des fonds.",
|
||||
"send_to": "Envoyer à",
|
||||
"send_tooltip_enter_amount": "Entrez un montant à envoyer",
|
||||
"send_tooltip_exceeds_balance": "Le montant dépasse le solde disponible",
|
||||
"send_tooltip_in_progress": "Transaction déjà en cours",
|
||||
"send_tooltip_invalid_address": "Entrez une adresse de destinataire valide",
|
||||
"send_tooltip_not_connected": "Non connecté au daemon",
|
||||
"send_tooltip_select_source": "Sélectionnez d'abord une adresse source",
|
||||
"send_tooltip_syncing": "Attendez la synchronisation de la blockchain",
|
||||
"send_total": "Total",
|
||||
"send_transaction": "Envoyer la transaction",
|
||||
"send_tx_failed": "Transaction échouée",
|
||||
"send_tx_sent": "Transaction envoyée !",
|
||||
"send_tx_success": "Transaction envoyée avec succès !",
|
||||
"send_txid_copied": "TxID copié dans le presse-papiers",
|
||||
"send_txid_label": "TxID : %s",
|
||||
"send_valid_shielded": "Adresse blindée valide",
|
||||
"send_valid_transparent": "Adresse transparente valide",
|
||||
"send_wallet_empty": "Votre portefeuille est vide",
|
||||
"send_yes_clear": "Oui, effacer",
|
||||
"sending": "Envoi de la transaction",
|
||||
"sending_from": "ENVOI DEPUIS",
|
||||
"sent": "envoyé",
|
||||
"sent_filter": "Envoyé",
|
||||
"sent_type": "Envoyé",
|
||||
"sent_upper": "ENVOYÉ",
|
||||
"settings": "Paramètres",
|
||||
"setup_wizard": "Assistant de configuration",
|
||||
"share": "Partager",
|
||||
"shield_check_status": "Vérifier le statut",
|
||||
"shield_completed": "Opération terminée avec succès !",
|
||||
"shield_description": "Blindez vos récompenses de minage en envoyant les sorties coinbase des adresses transparentes vers une adresse blindée. Cela améliore la confidentialité en masquant vos revenus de minage.",
|
||||
"shield_from_address": "Depuis l'adresse :",
|
||||
"shield_funds": "Blinder les fonds",
|
||||
"shield_in_progress": "Opération en cours...",
|
||||
"shield_max_utxos": "UTXOs max par opération",
|
||||
"shield_merge_done": "Blindage/fusion terminé !",
|
||||
"shield_select_z": "Sélectionner une z-adresse...",
|
||||
"shield_started": "Opération de blindage démarrée",
|
||||
"shield_title": "Blinder les récompenses coinbase",
|
||||
"shield_to_address": "Vers l'adresse (blindée) :",
|
||||
"shield_utxo_limit": "Limite UTXO :",
|
||||
"shield_wildcard_hint": "Utilisez '*' pour blinder depuis toutes les adresses transparentes",
|
||||
"shielded": "Blindé",
|
||||
"shielded_to": "BLINDÉ VERS",
|
||||
"shielded_type": "Blindé",
|
||||
"show": "Afficher",
|
||||
"show_qr_code": "Afficher le code QR",
|
||||
"showing_transactions": "Affichage %d\xe2\x80\x93%d sur %d transactions (total : %zu)",
|
||||
"simple_background": "Arrière-plan simple",
|
||||
"start_mining": "Démarrer le minage",
|
||||
"status": "Statut",
|
||||
"stop_external": "Arrêter le daemon externe",
|
||||
"stop_mining": "Arrêter le minage",
|
||||
"submitting_transaction": "Soumission de la transaction...",
|
||||
"success": "Succès",
|
||||
"summary": "Résumé",
|
||||
"syncing": "Synchronisation...",
|
||||
"t_addresses": "Adresses T",
|
||||
"test_connection": "Tester",
|
||||
"theme": "Thème",
|
||||
"theme_effects": "Effets de thème",
|
||||
"time_days_ago": "il y a %d jours",
|
||||
"time_hours_ago": "il y a %d heures",
|
||||
"time_minutes_ago": "il y a %d minutes",
|
||||
"time_seconds_ago": "il y a %d secondes",
|
||||
"to": "À",
|
||||
"to_upper": "À",
|
||||
"tools": "OUTILS",
|
||||
"total": "Total",
|
||||
"transaction_id": "ID DE TRANSACTION",
|
||||
"transaction_sent": "Transaction envoyée avec succès",
|
||||
"transaction_sent_msg": "Transaction envoyée !",
|
||||
"transaction_url": "URL de transaction",
|
||||
"transactions": "Transactions",
|
||||
"transactions_upper": "TRANSACTIONS",
|
||||
"transparent": "Transparent",
|
||||
"tx_confirmations": "%d confirmations",
|
||||
"tx_details_title": "Détails de la transaction",
|
||||
"tx_from_address": "Adresse d'origine :",
|
||||
"tx_id_label": "ID de transaction :",
|
||||
"tx_immature": "IMMATURE",
|
||||
"tx_mined": "MINÉ",
|
||||
"tx_received": "REÇU",
|
||||
"tx_sent": "ENVOYÉ",
|
||||
"tx_to_address": "Adresse de destination :",
|
||||
"tx_view_explorer": "Voir dans l'explorateur",
|
||||
"txs_count": "%d txs",
|
||||
"type": "Type",
|
||||
"ui_opacity": "Opacité de l'interface",
|
||||
"unban": "Débannir",
|
||||
"unconfirmed": "Non confirmé",
|
||||
"undo_clear": "Annuler l'effacement",
|
||||
"unknown": "Inconnu",
|
||||
"use_embedded_daemon": "Utiliser le dragonxd intégré",
|
||||
"use_tor": "Utiliser Tor",
|
||||
"validate_btn": "Valider",
|
||||
"validate_description": "Entrez une adresse DragonX pour vérifier si elle est valide et si elle appartient à ce portefeuille.",
|
||||
"validate_invalid": "INVALIDE",
|
||||
"validate_is_mine": "Ce portefeuille possède cette adresse",
|
||||
"validate_not_mine": "N'appartient pas à ce portefeuille",
|
||||
"validate_ownership": "Propriété :",
|
||||
"validate_results": "Résultats :",
|
||||
"validate_shielded_type": "Blindée (z-adresse)",
|
||||
"validate_status": "Statut :",
|
||||
"validate_title": "Valider l'adresse",
|
||||
"validate_transparent_type": "Transparente (t-adresse)",
|
||||
"validate_type": "Type :",
|
||||
"validate_valid": "VALIDE",
|
||||
"validating": "Validation...",
|
||||
"verbose_logging": "Journalisation détaillée",
|
||||
"version": "Version",
|
||||
"view": "Afficher",
|
||||
"view_details": "Voir les détails",
|
||||
"view_on_explorer": "Voir dans l'explorateur",
|
||||
"waiting_for_daemon": "En attente de la connexion au daemon...",
|
||||
"wallet": "PORTEFEUILLE",
|
||||
"wallet_empty": "Votre portefeuille est vide",
|
||||
"wallet_empty_hint": "Passez à Recevoir pour obtenir votre adresse et commencer à recevoir des fonds.",
|
||||
"warning": "Attention",
|
||||
"warning_upper": "ATTENTION !",
|
||||
"website": "Site web",
|
||||
"window_opacity": "Opacité de la fenêtre",
|
||||
"yes_clear": "Oui, effacer",
|
||||
"your_addresses": "Vos adresses",
|
||||
"z_addresses": "Adresses Z",
|
||||
}
|
||||
|
||||
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "fr.json")
|
||||
with open(out, "w", encoding="utf-8") as f:
|
||||
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
|
||||
print(f"Wrote {len(translations)} French translations to {os.path.abspath(out)}")
|
||||
@@ -0,0 +1,646 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate Japanese (ja) translations for ObsidianDragon wallet."""
|
||||
import json, os
|
||||
|
||||
translations = {
|
||||
"24h_change": "24時間変動",
|
||||
"24h_volume": "24時間出来高",
|
||||
"about": "概要",
|
||||
"about_block_explorer": "ブロックエクスプローラー",
|
||||
"about_block_height": "ブロック高:",
|
||||
"about_build_date": "ビルド日:",
|
||||
"about_build_type": "ビルドタイプ:",
|
||||
"about_chain": "チェーン:",
|
||||
"about_connections": "接続数:",
|
||||
"about_credits": "クレジット",
|
||||
"about_daemon": "デーモン:",
|
||||
"about_debug": "デバッグ",
|
||||
"about_dragonx": "ObsidianDragonについて",
|
||||
"about_edition": "ImGui エディション",
|
||||
"about_github": "GitHub",
|
||||
"about_imgui": "ImGui:",
|
||||
"about_license": "ライセンス",
|
||||
"about_license_text": "本ソフトウェアはGNU General Public License v3 (GPLv3)の下で公開されています。ライセンス条項に従い、自由に使用、変更、配布できます。",
|
||||
"about_peers_count": "%zu ピア",
|
||||
"about_release": "リリース",
|
||||
"about_title": "ObsidianDragonについて",
|
||||
"about_version": "バージョン:",
|
||||
"about_website": "ウェブサイト",
|
||||
"acrylic": "アクリル",
|
||||
"add": "追加",
|
||||
"address": "アドレス",
|
||||
"address_book_add": "アドレスを追加",
|
||||
"address_book_add_new": "新規追加",
|
||||
"address_book_added": "アドレスをアドレス帳に追加しました",
|
||||
"address_book_count": "%zu 件のアドレスを保存済み",
|
||||
"address_book_deleted": "エントリを削除しました",
|
||||
"address_book_edit": "アドレスを編集",
|
||||
"address_book_empty": "保存されたアドレスがありません。「新規追加」をクリックして追加してください。",
|
||||
"address_book_exists": "アドレスは既にアドレス帳に存在します",
|
||||
"address_book_title": "アドレス帳",
|
||||
"address_book_update_failed": "更新に失敗しました — アドレスが重複している可能性があります",
|
||||
"address_book_updated": "アドレスを更新しました",
|
||||
"address_copied": "アドレスをクリップボードにコピーしました",
|
||||
"address_details": "アドレス詳細",
|
||||
"address_label": "アドレス:",
|
||||
"address_upper": "アドレス",
|
||||
"address_url": "アドレスURL",
|
||||
"addresses_appear_here": "接続後、受信アドレスがここに表示されます。",
|
||||
"advanced": "詳細設定",
|
||||
"all_filter": "すべて",
|
||||
"allow_custom_fees": "カスタム手数料を許可",
|
||||
"amount": "金額",
|
||||
"amount_details": "金額の詳細",
|
||||
"amount_exceeds_balance": "金額が残高を超えています",
|
||||
"amount_label": "金額:",
|
||||
"appearance": "外観",
|
||||
"auto_shield": "マイニング自動シールド",
|
||||
"available": "利用可能",
|
||||
"backup_backing_up": "バックアップ中...",
|
||||
"backup_create": "バックアップを作成",
|
||||
"backup_created": "ウォレットのバックアップを作成しました",
|
||||
"backup_data": "バックアップとデータ",
|
||||
"backup_description": "wallet.datファイルのバックアップを作成します。このファイルにはすべての秘密鍵と取引履歴が含まれています。バックアップは安全な場所に保管してください。",
|
||||
"backup_destination": "バックアップ先:",
|
||||
"backup_tip_external": "外部ドライブまたはクラウドストレージにバックアップを保存",
|
||||
"backup_tip_multiple": "異なる場所に複数のバックアップを作成",
|
||||
"backup_tip_test": "定期的にバックアップからの復元をテスト",
|
||||
"backup_tips": "ヒント:",
|
||||
"backup_title": "ウォレットのバックアップ",
|
||||
"backup_wallet": "ウォレットをバックアップ...",
|
||||
"backup_wallet_not_found": "警告:予想される場所にwallet.datが見つかりません",
|
||||
"balance": "残高",
|
||||
"balance_layout": "残高レイアウト",
|
||||
"ban": "ブロック",
|
||||
"banned_peers": "ブロック済みピア",
|
||||
"block": "ブロック",
|
||||
"block_bits": "ビット:",
|
||||
"block_click_next": "クリックして次のブロックを表示",
|
||||
"block_click_prev": "クリックして前のブロックを表示",
|
||||
"block_explorer": "ブロックエクスプローラー",
|
||||
"block_get_info": "ブロック情報を取得",
|
||||
"block_hash": "ブロックハッシュ:",
|
||||
"block_height": "ブロック高:",
|
||||
"block_info_title": "ブロック情報",
|
||||
"block_merkle_root": "マークルルート:",
|
||||
"block_nav_next": "次へ >>",
|
||||
"block_nav_prev": "<< 前へ",
|
||||
"block_next": "次のブロック:",
|
||||
"block_previous": "前のブロック:",
|
||||
"block_size": "サイズ:",
|
||||
"block_timestamp": "タイムスタンプ:",
|
||||
"block_transactions": "トランザクション:",
|
||||
"blockchain_syncing": "ブロックチェーン同期中 (%.1f%%)... 残高が不正確な場合があります。",
|
||||
"cancel": "キャンセル",
|
||||
"characters": "文字",
|
||||
"clear": "クリア",
|
||||
"clear_all_bans": "すべてのブロックを解除",
|
||||
"clear_form_confirm": "すべてのフォームフィールドをクリアしますか?",
|
||||
"clear_request": "リクエストをクリア",
|
||||
"click_copy_address": "クリックしてアドレスをコピー",
|
||||
"click_copy_uri": "クリックしてURIをコピー",
|
||||
"close": "閉じる",
|
||||
"conf_count": "%d 確認",
|
||||
"confirm_and_send": "確認して送金",
|
||||
"confirm_send": "送金を確認",
|
||||
"confirm_transaction": "取引を確認",
|
||||
"confirmations": "確認数",
|
||||
"confirmations_display": "%d 確認 | %s",
|
||||
"confirmed": "確認済み",
|
||||
"connected": "接続済み",
|
||||
"connected_peers": "接続中のピア",
|
||||
"connecting": "接続中...",
|
||||
"console": "コンソール",
|
||||
"console_auto_scroll": "自動スクロール",
|
||||
"console_available_commands": "利用可能なコマンド:",
|
||||
"console_capturing_output": "デーモン出力をキャプチャ中...",
|
||||
"console_clear": "クリア",
|
||||
"console_clear_console": "コンソールをクリア",
|
||||
"console_cleared": "コンソールをクリアしました",
|
||||
"console_click_commands": "上のコマンドをクリックして挿入",
|
||||
"console_click_insert": "クリックして挿入",
|
||||
"console_click_insert_params": "クリックしてパラメータ付きで挿入",
|
||||
"console_close": "閉じる",
|
||||
"console_commands": "コマンド",
|
||||
"console_common_rpc": "一般的なRPCコマンド:",
|
||||
"console_completions": "補完:",
|
||||
"console_connected": "デーモンに接続済み",
|
||||
"console_copy_all": "すべてコピー",
|
||||
"console_copy_selected": "コピー",
|
||||
"console_daemon": "デーモン",
|
||||
"console_daemon_error": "デーモンエラー!",
|
||||
"console_daemon_started": "デーモンが起動しました",
|
||||
"console_daemon_stopped": "デーモンが停止しました",
|
||||
"console_disconnected": "デーモンから切断されました",
|
||||
"console_errors": "エラー",
|
||||
"console_filter_hint": "出力をフィルタ...",
|
||||
"console_help_clear": " clear - コンソールをクリア",
|
||||
"console_help_getbalance": " getbalance - 透明残高を表示",
|
||||
"console_help_getblockcount": " getblockcount - 現在のブロック高を表示",
|
||||
"console_help_getinfo": " getinfo - ノード情報を表示",
|
||||
"console_help_getmininginfo": " getmininginfo - マイニング状況を表示",
|
||||
"console_help_getpeerinfo": " getpeerinfo - 接続中のピアを表示",
|
||||
"console_help_gettotalbalance": " gettotalbalance - 合計残高を表示",
|
||||
"console_help_help": " help - このヘルプを表示",
|
||||
"console_help_setgenerate": " setgenerate - マイニングを制御",
|
||||
"console_help_stop": " stop - デーモンを停止",
|
||||
"console_line_count": "%zu 行",
|
||||
"console_new_lines": "%d 新しい行",
|
||||
"console_no_daemon": "デーモンなし",
|
||||
"console_not_connected": "エラー:デーモンに接続されていません",
|
||||
"console_rpc_reference": "RPCコマンドリファレンス",
|
||||
"console_scanline": "コンソールスキャンライン",
|
||||
"console_search_commands": "コマンドを検索...",
|
||||
"console_select_all": "すべて選択",
|
||||
"console_show_daemon_output": "デーモン出力を表示",
|
||||
"console_show_errors_only": "エラーのみ表示",
|
||||
"console_show_rpc_ref": "RPCコマンドリファレンスを表示",
|
||||
"console_showing_lines": "%zu / %zu 行を表示中",
|
||||
"console_starting_node": "ノードを起動中...",
|
||||
"console_status_error": "エラー",
|
||||
"console_status_running": "実行中",
|
||||
"console_status_starting": "起動中",
|
||||
"console_status_stopped": "停止済み",
|
||||
"console_status_stopping": "停止中",
|
||||
"console_status_unknown": "不明",
|
||||
"console_tab_completion": "Tabで補完",
|
||||
"console_type_help": "'help'と入力して利用可能なコマンドを表示",
|
||||
"console_welcome": "ObsidianDragonコンソールへようこそ",
|
||||
"console_zoom_in": "拡大",
|
||||
"console_zoom_out": "縮小",
|
||||
"copy": "コピー",
|
||||
"copy_address": "完全なアドレスをコピー",
|
||||
"copy_error": "エラーをコピー",
|
||||
"copy_to_clipboard": "クリップボードにコピー",
|
||||
"copy_txid": "TxIDをコピー",
|
||||
"copy_uri": "URIをコピー",
|
||||
"current_price": "現在の価格",
|
||||
"custom_fees": "カスタム手数料",
|
||||
"dark": "ダーク",
|
||||
"date": "日付",
|
||||
"date_label": "日付:",
|
||||
"delete": "削除",
|
||||
"difficulty": "難易度",
|
||||
"disconnected": "切断済み",
|
||||
"dismiss": "閉じる",
|
||||
"display": "表示",
|
||||
"dragonx_green": "DragonX(グリーン)",
|
||||
"edit": "編集",
|
||||
"error": "エラー",
|
||||
"est_time_to_block": "予測ブロック時間",
|
||||
"exit": "終了",
|
||||
"explorer": "エクスプローラー",
|
||||
"export": "エクスポート",
|
||||
"export_csv": "CSVエクスポート",
|
||||
"export_keys_btn": "鍵をエクスポート",
|
||||
"export_keys_danger": "危険:ウォレットからすべての秘密鍵がエクスポートされます!このファイルにアクセスできる人は誰でもあなたの資金を盗めます。安全に保管し、使用後は削除してください。",
|
||||
"export_keys_include_t": "Tアドレスを含める(透明)",
|
||||
"export_keys_include_z": "Zアドレスを含める(シールド)",
|
||||
"export_keys_options": "エクスポートオプション:",
|
||||
"export_keys_success": "鍵のエクスポートに成功しました",
|
||||
"export_keys_title": "すべての秘密鍵をエクスポート",
|
||||
"export_private_key": "秘密鍵をエクスポート",
|
||||
"export_tx_count": "%zu件の取引をCSVファイルにエクスポート。",
|
||||
"export_tx_file_fail": "CSVファイルの作成に失敗しました",
|
||||
"export_tx_none": "エクスポートする取引がありません",
|
||||
"export_tx_success": "取引のエクスポートに成功しました",
|
||||
"export_tx_title": "取引をCSVにエクスポート",
|
||||
"export_viewing_key": "閲覧鍵をエクスポート",
|
||||
"failed_create_shielded": "シールドアドレスの作成に失敗しました",
|
||||
"failed_create_transparent": "透明アドレスの作成に失敗しました",
|
||||
"fee": "手数料",
|
||||
"fee_high": "高い",
|
||||
"fee_label": "手数料:",
|
||||
"fee_low": "低い",
|
||||
"fee_normal": "通常",
|
||||
"fetch_prices": "価格を取得",
|
||||
"file": "ファイル",
|
||||
"file_save_location": "ファイルの保存先:~/.config/ObsidianDragon/",
|
||||
"font_scale": "フォントサイズ",
|
||||
"from": "送信元",
|
||||
"from_upper": "送信元",
|
||||
"full_details": "詳細情報",
|
||||
"general": "一般",
|
||||
"go_to_receive": "受信へ移動",
|
||||
"height": "高さ",
|
||||
"help": "ヘルプ",
|
||||
"hide": "非表示",
|
||||
"history": "履歴",
|
||||
"immature_type": "未成熟",
|
||||
"import": "インポート",
|
||||
"import_key_btn": "鍵をインポート",
|
||||
"import_key_formats": "サポートされる鍵形式:",
|
||||
"import_key_full_rescan": "(0 = 完全再スキャン)",
|
||||
"import_key_label": "秘密鍵:",
|
||||
"import_key_no_valid": "入力に有効な鍵が見つかりません",
|
||||
"import_key_rescan": "インポート後にブロックチェーンを再スキャン",
|
||||
"import_key_start_height": "開始高:",
|
||||
"import_key_success": "鍵のインポートに成功しました",
|
||||
"import_key_t_format": "TアドレスWIF秘密鍵",
|
||||
"import_key_title": "秘密鍵をインポート",
|
||||
"import_key_tooltip": "1行に1つずつ秘密鍵を入力してください。\nzアドレスとtアドレスの鍵の両方に対応しています。\n#で始まる行はコメントとして扱われます。",
|
||||
"import_key_warning": "警告:秘密鍵を決して共有しないでください!信頼できないソースからの鍵のインポートはウォレットを危険にさらす可能性があります。",
|
||||
"import_key_z_format": "Zアドレス支出鍵 (secret-extended-key-...)",
|
||||
"import_private_key": "秘密鍵をインポート...",
|
||||
"invalid_address": "無効なアドレス形式",
|
||||
"ip_address": "IPアドレス",
|
||||
"keep": "保持",
|
||||
"keep_daemon": "デーモンを実行し続ける",
|
||||
"key_export_fetching": "ウォレットから鍵を取得中...",
|
||||
"key_export_private_key": "秘密鍵:",
|
||||
"key_export_private_warning": "この鍵は秘密にしてください!この鍵を持つ人は誰でもあなたの資金を使えます。オンラインや信頼できない相手と共有しないでください。",
|
||||
"key_export_reveal": "鍵を表示",
|
||||
"key_export_viewing_key": "閲覧鍵:",
|
||||
"key_export_viewing_warning": "この閲覧鍵を使うと、他者があなたの受信取引と残高を見ることができますが、資金を使うことはできません。信頼できる相手とのみ共有してください。",
|
||||
"label": "ラベル:",
|
||||
"language": "言語",
|
||||
"light": "ライト",
|
||||
"loading": "読み込み中...",
|
||||
"loading_addresses": "アドレスを読み込み中...",
|
||||
"local_hashrate": "ローカルハッシュレート",
|
||||
"low_spec_mode": "省電力モード",
|
||||
"market": "市場",
|
||||
"market_12h": "12時間",
|
||||
"market_18h": "18時間",
|
||||
"market_24h": "24時間",
|
||||
"market_24h_volume": "24時間出来高",
|
||||
"market_6h": "6時間",
|
||||
"market_attribution": "価格データ:NonKYC提供",
|
||||
"market_btc_price": "BTC価格",
|
||||
"market_cap": "時価総額",
|
||||
"market_no_history": "価格履歴がありません",
|
||||
"market_no_price": "価格データなし",
|
||||
"market_now": "現在",
|
||||
"market_pct_shielded": "%.0f%% シールド済み",
|
||||
"market_portfolio": "ポートフォリオ",
|
||||
"market_price_unavailable": "価格データが利用できません",
|
||||
"market_refresh_price": "価格データを更新",
|
||||
"market_trade_on": "%s で取引",
|
||||
"mature": "成熟済み",
|
||||
"max": "最大",
|
||||
"memo": "メモ(任意、暗号化)",
|
||||
"memo_label": "メモ:",
|
||||
"memo_optional": "メモ(任意)",
|
||||
"memo_upper": "メモ",
|
||||
"memo_z_only": "注:メモはシールド (z) アドレスへの送金時のみ利用可能です",
|
||||
"merge_description": "複数のUTXOを単一のシールドアドレスに統合します。ウォレットサイズの縮小とプライバシーの向上に役立ちます。",
|
||||
"merge_funds": "資金を統合",
|
||||
"merge_started": "統合操作を開始しました",
|
||||
"merge_title": "アドレスに統合",
|
||||
"mine_when_idle": "アイドル時にマイニング",
|
||||
"mined": "採掘済み",
|
||||
"mined_filter": "採掘済み",
|
||||
"mined_type": "採掘済み",
|
||||
"mined_upper": "採掘済み",
|
||||
"miner_fee": "マイナー手数料",
|
||||
"mining": "マイニング",
|
||||
"mining_active": "アクティブ",
|
||||
"mining_address_copied": "マイニングアドレスをコピーしました",
|
||||
"mining_all_time": "全期間",
|
||||
"mining_already_saved": "プールURLは既に保存済みです",
|
||||
"mining_block_copied": "ブロックハッシュをコピーしました",
|
||||
"mining_chart_1m_ago": "1分前",
|
||||
"mining_chart_5m_ago": "5分前",
|
||||
"mining_chart_now": "現在",
|
||||
"mining_chart_start": "開始",
|
||||
"mining_click": "クリック",
|
||||
"mining_click_copy_address": "クリックしてアドレスをコピー",
|
||||
"mining_click_copy_block": "クリックしてブロックハッシュをコピー",
|
||||
"mining_click_copy_difficulty": "クリックして難易度をコピー",
|
||||
"mining_connected": "接続済み",
|
||||
"mining_connecting": "接続中...",
|
||||
"mining_control": "マイニング制御",
|
||||
"mining_difficulty_copied": "難易度をコピーしました",
|
||||
"mining_est_block": "予測ブロック",
|
||||
"mining_est_daily": "予測日収",
|
||||
"mining_filter_all": "すべて",
|
||||
"mining_filter_tip_all": "すべての収益を表示",
|
||||
"mining_filter_tip_pool": "プール収益のみ表示",
|
||||
"mining_filter_tip_solo": "ソロ収益のみ表示",
|
||||
"mining_idle_off_tooltip": "アイドルマイニングを有効にする",
|
||||
"mining_idle_on_tooltip": "アイドルマイニングを無効にする",
|
||||
"mining_local_hashrate": "ローカルハッシュレート",
|
||||
"mining_mine": "マイニング",
|
||||
"mining_mining_addr": "マイニングアドレス",
|
||||
"mining_network": "ネットワーク",
|
||||
"mining_no_blocks_yet": "まだブロックが見つかっていません",
|
||||
"mining_no_payouts_yet": "まだプール支払いがありません",
|
||||
"mining_no_saved_addresses": "保存されたアドレスがありません",
|
||||
"mining_no_saved_pools": "保存されたプールがありません",
|
||||
"mining_off": "マイニングはオフです",
|
||||
"mining_on": "マイニングはオンです",
|
||||
"mining_open_in_explorer": "エクスプローラーで開く",
|
||||
"mining_payout_address": "支払いアドレス",
|
||||
"mining_payout_tooltip": "マイニング報酬の受取アドレス",
|
||||
"mining_pool": "プール",
|
||||
"mining_pool_hashrate": "プールハッシュレート",
|
||||
"mining_pool_url": "プールURL",
|
||||
"mining_recent_blocks": "最近のブロック",
|
||||
"mining_recent_payouts": "最近のプール支払い",
|
||||
"mining_remove": "削除",
|
||||
"mining_reset_defaults": "デフォルトにリセット",
|
||||
"mining_save_payout_address": "支払いアドレスを保存",
|
||||
"mining_save_pool_url": "プールURLを保存",
|
||||
"mining_saved_addresses": "保存済みアドレス:",
|
||||
"mining_saved_pools": "保存済みプール:",
|
||||
"mining_shares": "シェア",
|
||||
"mining_show_chart": "チャート",
|
||||
"mining_show_log": "ログ",
|
||||
"mining_solo": "ソロ",
|
||||
"mining_starting": "起動中...",
|
||||
"mining_starting_tooltip": "マイナーを起動中...",
|
||||
"mining_statistics": "マイニング統計",
|
||||
"mining_stop": "停止",
|
||||
"mining_stop_solo_for_pool": "プールマイニングを開始する前にソロマイニングを停止してください",
|
||||
"mining_stop_solo_for_pool_settings": "プール設定を変更するにはソロマイニングを停止してください",
|
||||
"mining_stopping": "停止中...",
|
||||
"mining_stopping_tooltip": "マイナーを停止中...",
|
||||
"mining_syncing_tooltip": "ブロックチェーン同期中...",
|
||||
"mining_threads": "マイニングスレッド",
|
||||
"mining_to_save": "保存する",
|
||||
"mining_today": "今日",
|
||||
"mining_uptime": "稼働時間",
|
||||
"mining_yesterday": "昨日",
|
||||
"network": "ネットワーク",
|
||||
"network_fee": "ネットワーク手数料",
|
||||
"network_hashrate": "ネットワークハッシュレート",
|
||||
"new": "+ 新規",
|
||||
"new_shielded_created": "新しいシールドアドレスを作成しました",
|
||||
"new_t_address": "新しいTアドレス",
|
||||
"new_t_transparent": "新しいtアドレス(透明)",
|
||||
"new_transparent_created": "新しい透明アドレスを作成しました",
|
||||
"new_z_address": "新しいZアドレス",
|
||||
"new_z_shielded": "新しいzアドレス(シールド)",
|
||||
"no_addresses": "アドレスが見つかりません。上のボタンを使用して作成してください。",
|
||||
"no_addresses_available": "利用可能なアドレスがありません",
|
||||
"no_addresses_match": "フィルタに一致するアドレスがありません",
|
||||
"no_addresses_with_balance": "残高のあるアドレスがありません",
|
||||
"no_matching": "一致する取引がありません",
|
||||
"no_recent_receives": "最近の受信がありません",
|
||||
"no_recent_sends": "最近の送信がありません",
|
||||
"no_transactions": "取引が見つかりません",
|
||||
"node": "ノード",
|
||||
"node_security": "ノードとセキュリティ",
|
||||
"noise": "ノイズ",
|
||||
"not_connected": "デーモンに未接続...",
|
||||
"not_connected_to_daemon": "デーモンに未接続",
|
||||
"notes": "メモ",
|
||||
"notes_optional": "メモ(任意):",
|
||||
"output_filename": "出力ファイル名:",
|
||||
"overview": "概要",
|
||||
"paste": "貼り付け",
|
||||
"paste_from_clipboard": "クリップボードから貼り付け",
|
||||
"pay_from": "支払い元",
|
||||
"payment_request": "支払い請求",
|
||||
"payment_request_copied": "支払い請求をコピーしました",
|
||||
"payment_uri_copied": "支払いURIをコピーしました",
|
||||
"peers": "ピア",
|
||||
"peers_avg_ping": "平均Ping",
|
||||
"peers_ban_24h": "ピアを24時間ブロック",
|
||||
"peers_ban_score": "ブロックスコア:%d",
|
||||
"peers_banned": "ブロック済み",
|
||||
"peers_banned_count": "ブロック済み:%d",
|
||||
"peers_best_block": "最良ブロック",
|
||||
"peers_blockchain": "ブロックチェーン",
|
||||
"peers_blocks": "ブロック",
|
||||
"peers_blocks_left": "残り %d ブロック",
|
||||
"peers_clear_all_bans": "すべてのブロックを解除",
|
||||
"peers_click_copy": "クリックしてコピー",
|
||||
"peers_connected": "接続済み",
|
||||
"peers_connected_count": "接続済み:%d",
|
||||
"peers_copy_ip": "IPをコピー",
|
||||
"peers_dir_in": "入",
|
||||
"peers_dir_out": "出",
|
||||
"peers_hash_copied": "ハッシュをコピーしました",
|
||||
"peers_hashrate": "ハッシュレート",
|
||||
"peers_in_out": "入/出",
|
||||
"peers_longest": "最長",
|
||||
"peers_longest_chain": "最長チェーン",
|
||||
"peers_memory": "メモリ",
|
||||
"peers_no_banned": "ブロック済みピアなし",
|
||||
"peers_no_connected": "接続済みピアなし",
|
||||
"peers_no_tls": "TLSなし",
|
||||
"peers_notarized": "公証済み",
|
||||
"peers_p2p_port": "P2Pポート",
|
||||
"peers_protocol": "プロトコル",
|
||||
"peers_received": "受信",
|
||||
"peers_refresh": "更新",
|
||||
"peers_refresh_tooltip": "ピアリストを更新",
|
||||
"peers_refreshing": "更新中...",
|
||||
"peers_sent": "送信",
|
||||
"peers_tt_id": "ID:%d",
|
||||
"peers_tt_received": "受信:%s",
|
||||
"peers_tt_sent": "送信:%s",
|
||||
"peers_tt_services": "サービス:%s",
|
||||
"peers_tt_start_height": "開始高:%d",
|
||||
"peers_tt_synced": "同期済み H/B:%d/%d",
|
||||
"peers_tt_tls_cipher": "TLS:%s",
|
||||
"peers_unban": "ブロック解除",
|
||||
"peers_upper": "ピア",
|
||||
"peers_version": "バージョン",
|
||||
"pending": "保留中",
|
||||
"ping": "Ping",
|
||||
"price_chart": "価格チャート",
|
||||
"qr_code": "QRコード",
|
||||
"qr_failed": "QRコードの生成に失敗しました",
|
||||
"qr_title": "QRコード",
|
||||
"qr_unavailable": "QR利用不可",
|
||||
"receive": "受信",
|
||||
"received": "受信済み",
|
||||
"received_filter": "受信済み",
|
||||
"received_label": "受信済み",
|
||||
"received_upper": "受信済み",
|
||||
"receiving_addresses": "あなたの受信アドレス",
|
||||
"recent_received": "最近の受信",
|
||||
"recent_sends": "最近の送信",
|
||||
"recipient": "受取人",
|
||||
"recv_type": "受信",
|
||||
"refresh": "更新",
|
||||
"refresh_now": "今すぐ更新",
|
||||
"report_bug": "バグを報告",
|
||||
"request_amount": "金額(任意):",
|
||||
"request_copy_uri": "URIをコピー",
|
||||
"request_description": "他の人がスキャンまたはコピーできる支払い請求を生成します。QRコードにはアドレスとオプションの金額/メモが含まれます。",
|
||||
"request_label": "ラベル(任意):",
|
||||
"request_memo": "メモ(任意):",
|
||||
"request_payment": "支払いを請求",
|
||||
"request_payment_uri": "支払いURI:",
|
||||
"request_receive_address": "受信アドレス:",
|
||||
"request_select_address": "アドレスを選択...",
|
||||
"request_shielded_addrs": "-- シールドアドレス --",
|
||||
"request_title": "支払いを請求",
|
||||
"request_transparent_addrs": "-- 透明アドレス --",
|
||||
"request_uri_copied": "支払いURIをクリップボードにコピーしました",
|
||||
"rescan": "再スキャン",
|
||||
"reset_to_defaults": "デフォルトにリセット",
|
||||
"review_send": "送金を確認",
|
||||
"rpc_host": "RPCホスト",
|
||||
"rpc_pass": "パスワード",
|
||||
"rpc_port": "ポート",
|
||||
"rpc_user": "ユーザー名",
|
||||
"save": "保存",
|
||||
"save_settings": "設定を保存",
|
||||
"save_z_transactions": "Z取引を取引リストに保存",
|
||||
"search_placeholder": "検索...",
|
||||
"security": "セキュリティ",
|
||||
"select_address": "アドレスを選択...",
|
||||
"select_receiving_address": "受信アドレスを選択...",
|
||||
"select_source_address": "送信元アドレスを選択...",
|
||||
"send": "送金",
|
||||
"send_amount": "金額",
|
||||
"send_amount_details": "金額の詳細",
|
||||
"send_amount_upper": "金額",
|
||||
"send_clear_fields": "すべてのフォームフィールドをクリアしますか?",
|
||||
"send_copy_error": "エラーをコピー",
|
||||
"send_dismiss": "閉じる",
|
||||
"send_error_copied": "エラーをクリップボードにコピーしました",
|
||||
"send_error_prefix": "エラー:%s",
|
||||
"send_exceeds_available": "利用可能額を超過 (%.8f)",
|
||||
"send_fee": "手数料",
|
||||
"send_fee_high": "高い",
|
||||
"send_fee_low": "低い",
|
||||
"send_fee_normal": "通常",
|
||||
"send_form_restored": "フォームが復元されました",
|
||||
"send_from_this_address": "このアドレスから送金",
|
||||
"send_go_to_receive": "受信へ移動",
|
||||
"send_keep": "保持",
|
||||
"send_network_fee": "ネットワーク手数料",
|
||||
"send_no_balance": "残高なし",
|
||||
"send_no_recent": "最近の送信なし",
|
||||
"send_recent_sends": "最近の送信",
|
||||
"send_recipient": "受取人",
|
||||
"send_select_source": "送信元アドレスを選択...",
|
||||
"send_sending_from": "送信元",
|
||||
"send_submitting": "取引を送信中...",
|
||||
"send_switch_to_receive": "受信に切り替えてアドレスを取得し、資金の受け取りを開始してください。",
|
||||
"send_to": "送金先",
|
||||
"send_tooltip_enter_amount": "送金額を入力してください",
|
||||
"send_tooltip_exceeds_balance": "金額が利用可能残高を超えています",
|
||||
"send_tooltip_in_progress": "取引は既に進行中です",
|
||||
"send_tooltip_invalid_address": "有効な受取人アドレスを入力してください",
|
||||
"send_tooltip_not_connected": "デーモンに未接続",
|
||||
"send_tooltip_select_source": "まず送信元アドレスを選択してください",
|
||||
"send_tooltip_syncing": "ブロックチェーンの同期をお待ちください",
|
||||
"send_total": "合計",
|
||||
"send_transaction": "取引を送信",
|
||||
"send_tx_failed": "取引に失敗しました",
|
||||
"send_tx_sent": "取引を送信しました!",
|
||||
"send_tx_success": "取引の送信に成功しました!",
|
||||
"send_txid_copied": "TxIDをクリップボードにコピーしました",
|
||||
"send_txid_label": "TxID:%s",
|
||||
"send_valid_shielded": "有効なシールドアドレス",
|
||||
"send_valid_transparent": "有効な透明アドレス",
|
||||
"send_wallet_empty": "ウォレットは空です",
|
||||
"send_yes_clear": "はい、クリア",
|
||||
"sending": "取引を送信中",
|
||||
"sending_from": "送信元",
|
||||
"sent": "送信済み",
|
||||
"sent_filter": "送信済み",
|
||||
"sent_type": "送信済み",
|
||||
"sent_upper": "送信済み",
|
||||
"settings": "設定",
|
||||
"setup_wizard": "セットアップウィザード",
|
||||
"share": "共有",
|
||||
"shield_check_status": "ステータスを確認",
|
||||
"shield_completed": "操作が正常に完了しました!",
|
||||
"shield_description": "透明アドレスのcoinbase出力をシールドアドレスに送信して、マイニング報酬をシールドします。マイニング収入を隠すことでプライバシーが向上します。",
|
||||
"shield_from_address": "送信元アドレス:",
|
||||
"shield_funds": "資金をシールド",
|
||||
"shield_in_progress": "操作進行中...",
|
||||
"shield_max_utxos": "1回の操作あたりの最大UTXO数",
|
||||
"shield_merge_done": "シールド/統合が完了しました!",
|
||||
"shield_select_z": "zアドレスを選択...",
|
||||
"shield_started": "シールド操作を開始しました",
|
||||
"shield_title": "Coinbase報酬をシールド",
|
||||
"shield_to_address": "送信先アドレス(シールド):",
|
||||
"shield_utxo_limit": "UTXO制限:",
|
||||
"shield_wildcard_hint": "'*' を使用してすべての透明アドレスからシールド",
|
||||
"shielded": "シールド",
|
||||
"shielded_to": "シールド先",
|
||||
"shielded_type": "シールド",
|
||||
"show": "表示",
|
||||
"show_qr_code": "QRコードを表示",
|
||||
"showing_transactions": "%d\xe2\x80\x93%d / %d 件の取引を表示中(合計:%zu)",
|
||||
"simple_background": "シンプル背景",
|
||||
"start_mining": "マイニング開始",
|
||||
"status": "ステータス",
|
||||
"stop_external": "外部デーモンを停止",
|
||||
"stop_mining": "マイニング停止",
|
||||
"submitting_transaction": "取引を送信中...",
|
||||
"success": "成功",
|
||||
"summary": "概要",
|
||||
"syncing": "同期中...",
|
||||
"t_addresses": "Tアドレス",
|
||||
"test_connection": "テスト",
|
||||
"theme": "テーマ",
|
||||
"theme_effects": "テーマ効果",
|
||||
"time_days_ago": "%d日前",
|
||||
"time_hours_ago": "%d時間前",
|
||||
"time_minutes_ago": "%d分前",
|
||||
"time_seconds_ago": "%d秒前",
|
||||
"to": "宛先",
|
||||
"to_upper": "宛先",
|
||||
"tools": "ツール",
|
||||
"total": "合計",
|
||||
"transaction_id": "取引ID",
|
||||
"transaction_sent": "取引の送信に成功しました",
|
||||
"transaction_sent_msg": "取引を送信しました!",
|
||||
"transaction_url": "取引URL",
|
||||
"transactions": "取引",
|
||||
"transactions_upper": "取引",
|
||||
"transparent": "透明",
|
||||
"tx_confirmations": "%d 確認",
|
||||
"tx_details_title": "取引の詳細",
|
||||
"tx_from_address": "送信元アドレス:",
|
||||
"tx_id_label": "取引ID:",
|
||||
"tx_immature": "未成熟",
|
||||
"tx_mined": "採掘済み",
|
||||
"tx_received": "受信済み",
|
||||
"tx_sent": "送信済み",
|
||||
"tx_to_address": "送信先アドレス:",
|
||||
"tx_view_explorer": "エクスプローラーで表示",
|
||||
"txs_count": "%d 件",
|
||||
"type": "タイプ",
|
||||
"ui_opacity": "UI透明度",
|
||||
"unban": "ブロック解除",
|
||||
"unconfirmed": "未確認",
|
||||
"undo_clear": "クリアを元に戻す",
|
||||
"unknown": "不明",
|
||||
"use_embedded_daemon": "内蔵dragonxdを使用",
|
||||
"use_tor": "Torを使用",
|
||||
"validate_btn": "検証",
|
||||
"validate_description": "DragonXアドレスを入力して、有効かどうか、そしてこのウォレットに属しているかどうかを確認します。",
|
||||
"validate_invalid": "無効",
|
||||
"validate_is_mine": "このウォレットがこのアドレスを所有しています",
|
||||
"validate_not_mine": "このウォレットに属していません",
|
||||
"validate_ownership": "所有者:",
|
||||
"validate_results": "結果:",
|
||||
"validate_shielded_type": "シールド(zアドレス)",
|
||||
"validate_status": "ステータス:",
|
||||
"validate_title": "アドレスを検証",
|
||||
"validate_transparent_type": "透明(tアドレス)",
|
||||
"validate_type": "タイプ:",
|
||||
"validate_valid": "有効",
|
||||
"validating": "検証中...",
|
||||
"verbose_logging": "詳細ログ",
|
||||
"version": "バージョン",
|
||||
"view": "表示",
|
||||
"view_details": "詳細を表示",
|
||||
"view_on_explorer": "エクスプローラーで表示",
|
||||
"waiting_for_daemon": "デーモン接続を待機中...",
|
||||
"wallet": "ウォレット",
|
||||
"wallet_empty": "ウォレットは空です",
|
||||
"wallet_empty_hint": "受信に切り替えてアドレスを取得し、資金の受け取りを開始してください。",
|
||||
"warning": "警告",
|
||||
"warning_upper": "警告!",
|
||||
"website": "ウェブサイト",
|
||||
"window_opacity": "ウィンドウ透明度",
|
||||
"yes_clear": "はい、クリア",
|
||||
"your_addresses": "あなたのアドレス",
|
||||
"z_addresses": "Zアドレス",
|
||||
}
|
||||
|
||||
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "ja.json")
|
||||
with open(out, "w", encoding="utf-8") as f:
|
||||
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
|
||||
print(f"Wrote {len(translations)} Japanese translations to {os.path.abspath(out)}")
|
||||
@@ -0,0 +1,646 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate Korean (ko) translations for ObsidianDragon wallet."""
|
||||
import json, os
|
||||
|
||||
translations = {
|
||||
"24h_change": "24시간 변동",
|
||||
"24h_volume": "24시간 거래량",
|
||||
"about": "정보",
|
||||
"about_block_explorer": "블록 탐색기",
|
||||
"about_block_height": "블록 높이:",
|
||||
"about_build_date": "빌드 날짜:",
|
||||
"about_build_type": "빌드 유형:",
|
||||
"about_chain": "체인:",
|
||||
"about_connections": "연결:",
|
||||
"about_credits": "크레딧",
|
||||
"about_daemon": "데몬:",
|
||||
"about_debug": "디버그",
|
||||
"about_dragonx": "ObsidianDragon 정보",
|
||||
"about_edition": "ImGui 에디션",
|
||||
"about_github": "GitHub",
|
||||
"about_imgui": "ImGui:",
|
||||
"about_license": "라이선스",
|
||||
"about_license_text": "본 소프트웨어는 GNU General Public License v3 (GPLv3) 하에 배포됩니다. 라이선스 조건에 따라 자유롭게 사용, 수정 및 배포할 수 있습니다.",
|
||||
"about_peers_count": "%zu 피어",
|
||||
"about_release": "릴리스",
|
||||
"about_title": "ObsidianDragon 정보",
|
||||
"about_version": "버전:",
|
||||
"about_website": "웹사이트",
|
||||
"acrylic": "아크릴",
|
||||
"add": "추가",
|
||||
"address": "주소",
|
||||
"address_book_add": "주소 추가",
|
||||
"address_book_add_new": "새로 추가",
|
||||
"address_book_added": "주소록에 주소를 추가했습니다",
|
||||
"address_book_count": "저장된 주소 %zu개",
|
||||
"address_book_deleted": "항목이 삭제되었습니다",
|
||||
"address_book_edit": "주소 편집",
|
||||
"address_book_empty": "저장된 주소가 없습니다. '새로 추가'를 클릭하여 추가하세요.",
|
||||
"address_book_exists": "주소가 이미 주소록에 있습니다",
|
||||
"address_book_title": "주소록",
|
||||
"address_book_update_failed": "업데이트 실패 — 주소가 중복될 수 있습니다",
|
||||
"address_book_updated": "주소가 업데이트되었습니다",
|
||||
"address_copied": "주소가 클립보드에 복사되었습니다",
|
||||
"address_details": "주소 상세",
|
||||
"address_label": "주소:",
|
||||
"address_upper": "주소",
|
||||
"address_url": "주소 URL",
|
||||
"addresses_appear_here": "연결 후 수신 주소가 여기에 표시됩니다.",
|
||||
"advanced": "고급 설정",
|
||||
"all_filter": "전체",
|
||||
"allow_custom_fees": "사용자 정의 수수료 허용",
|
||||
"amount": "금액",
|
||||
"amount_details": "금액 상세",
|
||||
"amount_exceeds_balance": "금액이 잔액을 초과합니다",
|
||||
"amount_label": "금액:",
|
||||
"appearance": "외관",
|
||||
"auto_shield": "채굴 자동 차폐",
|
||||
"available": "사용 가능",
|
||||
"backup_backing_up": "백업 중...",
|
||||
"backup_create": "백업 생성",
|
||||
"backup_created": "지갑 백업이 생성되었습니다",
|
||||
"backup_data": "백업 및 데이터",
|
||||
"backup_description": "wallet.dat 파일의 백업을 생성합니다. 이 파일에는 모든 개인 키와 거래 내역이 포함되어 있습니다. 백업을 안전한 곳에 보관하세요.",
|
||||
"backup_destination": "백업 위치:",
|
||||
"backup_tip_external": "외장 드라이브 또는 클라우드 스토리지에 백업 저장",
|
||||
"backup_tip_multiple": "서로 다른 위치에 여러 백업 생성",
|
||||
"backup_tip_test": "정기적으로 백업 복원 테스트",
|
||||
"backup_tips": "팁:",
|
||||
"backup_title": "지갑 백업",
|
||||
"backup_wallet": "지갑 백업...",
|
||||
"backup_wallet_not_found": "경고: 예상 위치에서 wallet.dat를 찾을 수 없습니다",
|
||||
"balance": "잔액",
|
||||
"balance_layout": "잔액 레이아웃",
|
||||
"ban": "차단",
|
||||
"banned_peers": "차단된 피어",
|
||||
"block": "블록",
|
||||
"block_bits": "비트:",
|
||||
"block_click_next": "클릭하여 다음 블록 보기",
|
||||
"block_click_prev": "클릭하여 이전 블록 보기",
|
||||
"block_explorer": "블록 탐색기",
|
||||
"block_get_info": "블록 정보 조회",
|
||||
"block_hash": "블록 해시:",
|
||||
"block_height": "블록 높이:",
|
||||
"block_info_title": "블록 정보",
|
||||
"block_merkle_root": "머클 루트:",
|
||||
"block_nav_next": "다음 >>",
|
||||
"block_nav_prev": "<< 이전",
|
||||
"block_next": "다음 블록:",
|
||||
"block_previous": "이전 블록:",
|
||||
"block_size": "크기:",
|
||||
"block_timestamp": "타임스탬프:",
|
||||
"block_transactions": "트랜잭션:",
|
||||
"blockchain_syncing": "블록체인 동기화 중 (%.1f%%)... 잔액이 정확하지 않을 수 있습니다.",
|
||||
"cancel": "취소",
|
||||
"characters": "문자",
|
||||
"clear": "지우기",
|
||||
"clear_all_bans": "모든 차단 해제",
|
||||
"clear_form_confirm": "모든 양식 필드를 지우시겠습니까?",
|
||||
"clear_request": "요청 지우기",
|
||||
"click_copy_address": "클릭하여 주소 복사",
|
||||
"click_copy_uri": "클릭하여 URI 복사",
|
||||
"close": "닫기",
|
||||
"conf_count": "%d 확인",
|
||||
"confirm_and_send": "확인 후 전송",
|
||||
"confirm_send": "전송 확인",
|
||||
"confirm_transaction": "거래 확인",
|
||||
"confirmations": "확인 수",
|
||||
"confirmations_display": "%d 확인 | %s",
|
||||
"confirmed": "확인됨",
|
||||
"connected": "연결됨",
|
||||
"connected_peers": "연결된 피어",
|
||||
"connecting": "연결 중...",
|
||||
"console": "콘솔",
|
||||
"console_auto_scroll": "자동 스크롤",
|
||||
"console_available_commands": "사용 가능한 명령어:",
|
||||
"console_capturing_output": "데몬 출력 캡처 중...",
|
||||
"console_clear": "지우기",
|
||||
"console_clear_console": "콘솔 지우기",
|
||||
"console_cleared": "콘솔이 지워졌습니다",
|
||||
"console_click_commands": "위의 명령어를 클릭하여 삽입",
|
||||
"console_click_insert": "클릭하여 삽입",
|
||||
"console_click_insert_params": "클릭하여 매개변수와 함께 삽입",
|
||||
"console_close": "닫기",
|
||||
"console_commands": "명령어",
|
||||
"console_common_rpc": "일반 RPC 명령어:",
|
||||
"console_completions": "자동 완성:",
|
||||
"console_connected": "데몬에 연결됨",
|
||||
"console_copy_all": "모두 복사",
|
||||
"console_copy_selected": "복사",
|
||||
"console_daemon": "데몬",
|
||||
"console_daemon_error": "데몬 오류!",
|
||||
"console_daemon_started": "데몬이 시작되었습니다",
|
||||
"console_daemon_stopped": "데몬이 중지되었습니다",
|
||||
"console_disconnected": "데몬 연결이 끊어졌습니다",
|
||||
"console_errors": "오류",
|
||||
"console_filter_hint": "출력 필터...",
|
||||
"console_help_clear": " clear - 콘솔 지우기",
|
||||
"console_help_getbalance": " getbalance - 투명 잔액 표시",
|
||||
"console_help_getblockcount": " getblockcount - 현재 블록 높이 표시",
|
||||
"console_help_getinfo": " getinfo - 노드 정보 표시",
|
||||
"console_help_getmininginfo": " getmininginfo - 채굴 상태 표시",
|
||||
"console_help_getpeerinfo": " getpeerinfo - 연결된 피어 표시",
|
||||
"console_help_gettotalbalance": " gettotalbalance - 총 잔액 표시",
|
||||
"console_help_help": " help - 도움말 표시",
|
||||
"console_help_setgenerate": " setgenerate - 채굴 제어",
|
||||
"console_help_stop": " stop - 데몬 중지",
|
||||
"console_line_count": "%zu줄",
|
||||
"console_new_lines": "%d 새 줄",
|
||||
"console_no_daemon": "데몬 없음",
|
||||
"console_not_connected": "오류: 데몬에 연결되지 않았습니다",
|
||||
"console_rpc_reference": "RPC 명령어 참조",
|
||||
"console_scanline": "콘솔 스캔라인",
|
||||
"console_search_commands": "명령어 검색...",
|
||||
"console_select_all": "모두 선택",
|
||||
"console_show_daemon_output": "데몬 출력 표시",
|
||||
"console_show_errors_only": "오류만 표시",
|
||||
"console_show_rpc_ref": "RPC 명령어 참조 표시",
|
||||
"console_showing_lines": "%zu / %zu줄 표시 중",
|
||||
"console_starting_node": "노드 시작 중...",
|
||||
"console_status_error": "오류",
|
||||
"console_status_running": "실행 중",
|
||||
"console_status_starting": "시작 중",
|
||||
"console_status_stopped": "중지됨",
|
||||
"console_status_stopping": "중지 중",
|
||||
"console_status_unknown": "알 수 없음",
|
||||
"console_tab_completion": "Tab으로 자동 완성",
|
||||
"console_type_help": "'help'를 입력하여 사용 가능한 명령어 보기",
|
||||
"console_welcome": "ObsidianDragon 콘솔에 오신 것을 환영합니다",
|
||||
"console_zoom_in": "확대",
|
||||
"console_zoom_out": "축소",
|
||||
"copy": "복사",
|
||||
"copy_address": "전체 주소 복사",
|
||||
"copy_error": "오류 복사",
|
||||
"copy_to_clipboard": "클립보드에 복사",
|
||||
"copy_txid": "TxID 복사",
|
||||
"copy_uri": "URI 복사",
|
||||
"current_price": "현재 가격",
|
||||
"custom_fees": "사용자 정의 수수료",
|
||||
"dark": "다크",
|
||||
"date": "날짜",
|
||||
"date_label": "날짜:",
|
||||
"delete": "삭제",
|
||||
"difficulty": "난이도",
|
||||
"disconnected": "연결 끊김",
|
||||
"dismiss": "닫기",
|
||||
"display": "디스플레이",
|
||||
"dragonx_green": "DragonX(그린)",
|
||||
"edit": "편집",
|
||||
"error": "오류",
|
||||
"est_time_to_block": "예상 블록 시간",
|
||||
"exit": "종료",
|
||||
"explorer": "탐색기",
|
||||
"export": "내보내기",
|
||||
"export_csv": "CSV 내보내기",
|
||||
"export_keys_btn": "키 내보내기",
|
||||
"export_keys_danger": "위험: 지갑의 모든 개인 키가 내보내집니다! 이 파일에 접근할 수 있는 사람은 누구나 자금을 훔칠 수 있습니다. 안전하게 보관하고 사용 후 삭제하세요.",
|
||||
"export_keys_include_t": "T 주소 포함 (투명)",
|
||||
"export_keys_include_z": "Z 주소 포함 (차폐)",
|
||||
"export_keys_options": "내보내기 옵션:",
|
||||
"export_keys_success": "키 내보내기 성공",
|
||||
"export_keys_title": "모든 개인 키 내보내기",
|
||||
"export_private_key": "개인 키 내보내기",
|
||||
"export_tx_count": "%zu건의 거래를 CSV 파일로 내보냈습니다.",
|
||||
"export_tx_file_fail": "CSV 파일 생성 실패",
|
||||
"export_tx_none": "내보낼 거래가 없습니다",
|
||||
"export_tx_success": "거래 내보내기 성공",
|
||||
"export_tx_title": "거래를 CSV로 내보내기",
|
||||
"export_viewing_key": "조회 키 내보내기",
|
||||
"failed_create_shielded": "차폐 주소 생성 실패",
|
||||
"failed_create_transparent": "투명 주소 생성 실패",
|
||||
"fee": "수수료",
|
||||
"fee_high": "높음",
|
||||
"fee_label": "수수료:",
|
||||
"fee_low": "낮음",
|
||||
"fee_normal": "보통",
|
||||
"fetch_prices": "가격 조회",
|
||||
"file": "파일",
|
||||
"file_save_location": "파일 저장 위치: ~/.config/ObsidianDragon/",
|
||||
"font_scale": "글꼴 크기",
|
||||
"from": "보낸 곳",
|
||||
"from_upper": "보낸 곳",
|
||||
"full_details": "전체 세부 정보",
|
||||
"general": "일반",
|
||||
"go_to_receive": "수신으로 이동",
|
||||
"height": "높이",
|
||||
"help": "도움말",
|
||||
"hide": "숨기기",
|
||||
"history": "내역",
|
||||
"immature_type": "미성숙",
|
||||
"import": "가져오기",
|
||||
"import_key_btn": "키 가져오기",
|
||||
"import_key_formats": "지원되는 키 형식:",
|
||||
"import_key_full_rescan": "(0 = 전체 재스캔)",
|
||||
"import_key_label": "개인 키:",
|
||||
"import_key_no_valid": "입력에서 유효한 키를 찾을 수 없습니다",
|
||||
"import_key_rescan": "가져오기 후 블록체인 재스캔",
|
||||
"import_key_start_height": "시작 높이:",
|
||||
"import_key_success": "키 가져오기 성공",
|
||||
"import_key_t_format": "T 주소 WIF 개인 키",
|
||||
"import_key_title": "개인 키 가져오기",
|
||||
"import_key_tooltip": "한 줄에 하나의 개인 키를 입력하세요.\nz 주소와 t 주소 키 모두 지원됩니다.\n#으로 시작하는 줄은 주석으로 처리됩니다.",
|
||||
"import_key_warning": "경고: 개인 키를 절대 공유하지 마세요! 신뢰할 수 없는 소스의 키를 가져오면 지갑이 위험해질 수 있습니다.",
|
||||
"import_key_z_format": "Z 주소 지출 키 (secret-extended-key-...)",
|
||||
"import_private_key": "개인 키 가져오기...",
|
||||
"invalid_address": "잘못된 주소 형식",
|
||||
"ip_address": "IP 주소",
|
||||
"keep": "유지",
|
||||
"keep_daemon": "데몬 계속 실행",
|
||||
"key_export_fetching": "지갑에서 키를 가져오는 중...",
|
||||
"key_export_private_key": "개인 키:",
|
||||
"key_export_private_warning": "이 키를 비밀로 유지하세요! 이 키를 가진 사람은 누구나 자금을 사용할 수 있습니다. 온라인이나 신뢰할 수 없는 사람과 공유하지 마세요.",
|
||||
"key_export_reveal": "키 표시",
|
||||
"key_export_viewing_key": "조회 키:",
|
||||
"key_export_viewing_warning": "이 조회 키를 사용하면 다른 사람이 수신 거래와 잔액을 볼 수 있지만 자금을 사용할 수는 없습니다. 신뢰할 수 있는 사람에게만 공유하세요.",
|
||||
"label": "라벨:",
|
||||
"language": "언어",
|
||||
"light": "라이트",
|
||||
"loading": "로딩 중...",
|
||||
"loading_addresses": "주소 로딩 중...",
|
||||
"local_hashrate": "로컬 해시레이트",
|
||||
"low_spec_mode": "저사양 모드",
|
||||
"market": "시장",
|
||||
"market_12h": "12시간",
|
||||
"market_18h": "18시간",
|
||||
"market_24h": "24시간",
|
||||
"market_24h_volume": "24시간 거래량",
|
||||
"market_6h": "6시간",
|
||||
"market_attribution": "가격 데이터: NonKYC 제공",
|
||||
"market_btc_price": "BTC 가격",
|
||||
"market_cap": "시가총액",
|
||||
"market_no_history": "가격 내역 없음",
|
||||
"market_no_price": "가격 데이터 없음",
|
||||
"market_now": "현재",
|
||||
"market_pct_shielded": "%.0f%% 차폐됨",
|
||||
"market_portfolio": "포트폴리오",
|
||||
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
|
||||
"market_refresh_price": "가격 데이터 새로고침",
|
||||
"market_trade_on": "%s에서 거래",
|
||||
"mature": "성숙됨",
|
||||
"max": "최대",
|
||||
"memo": "메모 (선택, 암호화)",
|
||||
"memo_label": "메모:",
|
||||
"memo_optional": "메모 (선택)",
|
||||
"memo_upper": "메모",
|
||||
"memo_z_only": "참고: 메모는 차폐 (z) 주소로 전송할 때만 사용할 수 있습니다",
|
||||
"merge_description": "여러 UTXO를 단일 차폐 주소로 통합합니다. 지갑 크기를 줄이고 프라이버시를 향상시킵니다.",
|
||||
"merge_funds": "자금 통합",
|
||||
"merge_started": "통합 작업이 시작되었습니다",
|
||||
"merge_title": "주소로 통합",
|
||||
"mine_when_idle": "유휴 시 채굴",
|
||||
"mined": "채굴됨",
|
||||
"mined_filter": "채굴됨",
|
||||
"mined_type": "채굴됨",
|
||||
"mined_upper": "채굴됨",
|
||||
"miner_fee": "채굴 수수료",
|
||||
"mining": "채굴",
|
||||
"mining_active": "활성",
|
||||
"mining_address_copied": "채굴 주소가 복사되었습니다",
|
||||
"mining_all_time": "전체 기간",
|
||||
"mining_already_saved": "풀 URL이 이미 저장되어 있습니다",
|
||||
"mining_block_copied": "블록 해시가 복사되었습니다",
|
||||
"mining_chart_1m_ago": "1분 전",
|
||||
"mining_chart_5m_ago": "5분 전",
|
||||
"mining_chart_now": "현재",
|
||||
"mining_chart_start": "시작",
|
||||
"mining_click": "클릭",
|
||||
"mining_click_copy_address": "클릭하여 주소 복사",
|
||||
"mining_click_copy_block": "클릭하여 블록 해시 복사",
|
||||
"mining_click_copy_difficulty": "클릭하여 난이도 복사",
|
||||
"mining_connected": "연결됨",
|
||||
"mining_connecting": "연결 중...",
|
||||
"mining_control": "채굴 제어",
|
||||
"mining_difficulty_copied": "난이도가 복사되었습니다",
|
||||
"mining_est_block": "예상 블록",
|
||||
"mining_est_daily": "예상 일일 수익",
|
||||
"mining_filter_all": "전체",
|
||||
"mining_filter_tip_all": "모든 수익 표시",
|
||||
"mining_filter_tip_pool": "풀 수익만 표시",
|
||||
"mining_filter_tip_solo": "솔로 수익만 표시",
|
||||
"mining_idle_off_tooltip": "유휴 채굴 활성화",
|
||||
"mining_idle_on_tooltip": "유휴 채굴 비활성화",
|
||||
"mining_local_hashrate": "로컬 해시레이트",
|
||||
"mining_mine": "채굴",
|
||||
"mining_mining_addr": "채굴 주소",
|
||||
"mining_network": "네트워크",
|
||||
"mining_no_blocks_yet": "아직 블록을 찾지 못했습니다",
|
||||
"mining_no_payouts_yet": "아직 풀 지급이 없습니다",
|
||||
"mining_no_saved_addresses": "저장된 주소 없음",
|
||||
"mining_no_saved_pools": "저장된 풀 없음",
|
||||
"mining_off": "채굴이 꺼져 있습니다",
|
||||
"mining_on": "채굴이 켜져 있습니다",
|
||||
"mining_open_in_explorer": "탐색기에서 열기",
|
||||
"mining_payout_address": "지급 주소",
|
||||
"mining_payout_tooltip": "채굴 보상 수신 주소",
|
||||
"mining_pool": "풀",
|
||||
"mining_pool_hashrate": "풀 해시레이트",
|
||||
"mining_pool_url": "풀 URL",
|
||||
"mining_recent_blocks": "최근 블록",
|
||||
"mining_recent_payouts": "최근 풀 지급",
|
||||
"mining_remove": "제거",
|
||||
"mining_reset_defaults": "기본값으로 재설정",
|
||||
"mining_save_payout_address": "지급 주소 저장",
|
||||
"mining_save_pool_url": "풀 URL 저장",
|
||||
"mining_saved_addresses": "저장된 주소:",
|
||||
"mining_saved_pools": "저장된 풀:",
|
||||
"mining_shares": "셰어",
|
||||
"mining_show_chart": "차트",
|
||||
"mining_show_log": "로그",
|
||||
"mining_solo": "솔로",
|
||||
"mining_starting": "시작 중...",
|
||||
"mining_starting_tooltip": "채굴기 시작 중...",
|
||||
"mining_statistics": "채굴 통계",
|
||||
"mining_stop": "중지",
|
||||
"mining_stop_solo_for_pool": "풀 채굴을 시작하려면 솔로 채굴을 먼저 중지하세요",
|
||||
"mining_stop_solo_for_pool_settings": "풀 설정을 변경하려면 솔로 채굴을 중지하세요",
|
||||
"mining_stopping": "중지 중...",
|
||||
"mining_stopping_tooltip": "채굴기 중지 중...",
|
||||
"mining_syncing_tooltip": "블록체인 동기화 중...",
|
||||
"mining_threads": "채굴 스레드",
|
||||
"mining_to_save": "저장하려면",
|
||||
"mining_today": "오늘",
|
||||
"mining_uptime": "가동 시간",
|
||||
"mining_yesterday": "어제",
|
||||
"network": "네트워크",
|
||||
"network_fee": "네트워크 수수료",
|
||||
"network_hashrate": "네트워크 해시레이트",
|
||||
"new": "+ 새로 만들기",
|
||||
"new_shielded_created": "새 차폐 주소가 생성되었습니다",
|
||||
"new_t_address": "새 T 주소",
|
||||
"new_t_transparent": "새 t 주소 (투명)",
|
||||
"new_transparent_created": "새 투명 주소가 생성되었습니다",
|
||||
"new_z_address": "새 Z 주소",
|
||||
"new_z_shielded": "새 z 주소 (차폐)",
|
||||
"no_addresses": "주소가 없습니다. 위의 버튼을 사용하여 생성하세요.",
|
||||
"no_addresses_available": "사용 가능한 주소 없음",
|
||||
"no_addresses_match": "필터와 일치하는 주소가 없습니다",
|
||||
"no_addresses_with_balance": "잔액이 있는 주소가 없습니다",
|
||||
"no_matching": "일치하는 거래가 없습니다",
|
||||
"no_recent_receives": "최근 수신 내역 없음",
|
||||
"no_recent_sends": "최근 전송 내역 없음",
|
||||
"no_transactions": "거래 내역이 없습니다",
|
||||
"node": "노드",
|
||||
"node_security": "노드 및 보안",
|
||||
"noise": "노이즈",
|
||||
"not_connected": "데몬에 연결되지 않음...",
|
||||
"not_connected_to_daemon": "데몬에 연결되지 않음",
|
||||
"notes": "메모",
|
||||
"notes_optional": "메모 (선택):",
|
||||
"output_filename": "출력 파일명:",
|
||||
"overview": "개요",
|
||||
"paste": "붙여넣기",
|
||||
"paste_from_clipboard": "클립보드에서 붙여넣기",
|
||||
"pay_from": "보낼 곳",
|
||||
"payment_request": "결제 요청",
|
||||
"payment_request_copied": "결제 요청이 복사되었습니다",
|
||||
"payment_uri_copied": "결제 URI가 복사되었습니다",
|
||||
"peers": "피어",
|
||||
"peers_avg_ping": "평균 Ping",
|
||||
"peers_ban_24h": "피어 24시간 차단",
|
||||
"peers_ban_score": "차단 점수: %d",
|
||||
"peers_banned": "차단됨",
|
||||
"peers_banned_count": "차단됨: %d",
|
||||
"peers_best_block": "최고 블록",
|
||||
"peers_blockchain": "블록체인",
|
||||
"peers_blocks": "블록",
|
||||
"peers_blocks_left": "남은 블록: %d",
|
||||
"peers_clear_all_bans": "모든 차단 해제",
|
||||
"peers_click_copy": "클릭하여 복사",
|
||||
"peers_connected": "연결됨",
|
||||
"peers_connected_count": "연결됨: %d",
|
||||
"peers_copy_ip": "IP 복사",
|
||||
"peers_dir_in": "수신",
|
||||
"peers_dir_out": "송신",
|
||||
"peers_hash_copied": "해시가 복사되었습니다",
|
||||
"peers_hashrate": "해시레이트",
|
||||
"peers_in_out": "수신/송신",
|
||||
"peers_longest": "최장",
|
||||
"peers_longest_chain": "최장 체인",
|
||||
"peers_memory": "메모리",
|
||||
"peers_no_banned": "차단된 피어 없음",
|
||||
"peers_no_connected": "연결된 피어 없음",
|
||||
"peers_no_tls": "TLS 없음",
|
||||
"peers_notarized": "공증됨",
|
||||
"peers_p2p_port": "P2P 포트",
|
||||
"peers_protocol": "프로토콜",
|
||||
"peers_received": "수신됨",
|
||||
"peers_refresh": "새로고침",
|
||||
"peers_refresh_tooltip": "피어 목록 새로고침",
|
||||
"peers_refreshing": "새로고침 중...",
|
||||
"peers_sent": "전송됨",
|
||||
"peers_tt_id": "ID: %d",
|
||||
"peers_tt_received": "수신: %s",
|
||||
"peers_tt_sent": "전송: %s",
|
||||
"peers_tt_services": "서비스: %s",
|
||||
"peers_tt_start_height": "시작 높이: %d",
|
||||
"peers_tt_synced": "동기화 H/B: %d/%d",
|
||||
"peers_tt_tls_cipher": "TLS: %s",
|
||||
"peers_unban": "차단 해제",
|
||||
"peers_upper": "피어",
|
||||
"peers_version": "버전",
|
||||
"pending": "대기 중",
|
||||
"ping": "Ping",
|
||||
"price_chart": "가격 차트",
|
||||
"qr_code": "QR 코드",
|
||||
"qr_failed": "QR 코드 생성 실패",
|
||||
"qr_title": "QR 코드",
|
||||
"qr_unavailable": "QR 사용 불가",
|
||||
"receive": "수신",
|
||||
"received": "수신됨",
|
||||
"received_filter": "수신됨",
|
||||
"received_label": "수신됨",
|
||||
"received_upper": "수신됨",
|
||||
"receiving_addresses": "수신 주소",
|
||||
"recent_received": "최근 수신",
|
||||
"recent_sends": "최근 전송",
|
||||
"recipient": "수신자",
|
||||
"recv_type": "수신",
|
||||
"refresh": "새로고침",
|
||||
"refresh_now": "지금 새로고침",
|
||||
"report_bug": "버그 신고",
|
||||
"request_amount": "금액 (선택):",
|
||||
"request_copy_uri": "URI 복사",
|
||||
"request_description": "다른 사람이 스캔하거나 복사할 수 있는 결제 요청을 생성합니다. QR 코드에는 주소와 선택적 금액/메모가 포함됩니다.",
|
||||
"request_label": "라벨 (선택):",
|
||||
"request_memo": "메모 (선택):",
|
||||
"request_payment": "결제 요청",
|
||||
"request_payment_uri": "결제 URI:",
|
||||
"request_receive_address": "수신 주소:",
|
||||
"request_select_address": "주소 선택...",
|
||||
"request_shielded_addrs": "-- 차폐 주소 --",
|
||||
"request_title": "결제 요청",
|
||||
"request_transparent_addrs": "-- 투명 주소 --",
|
||||
"request_uri_copied": "결제 URI가 클립보드에 복사되었습니다",
|
||||
"rescan": "재스캔",
|
||||
"reset_to_defaults": "기본값으로 재설정",
|
||||
"review_send": "전송 검토",
|
||||
"rpc_host": "RPC 호스트",
|
||||
"rpc_pass": "비밀번호",
|
||||
"rpc_port": "포트",
|
||||
"rpc_user": "사용자명",
|
||||
"save": "저장",
|
||||
"save_settings": "설정 저장",
|
||||
"save_z_transactions": "Z 거래를 거래 목록에 저장",
|
||||
"search_placeholder": "검색...",
|
||||
"security": "보안",
|
||||
"select_address": "주소 선택...",
|
||||
"select_receiving_address": "수신 주소 선택...",
|
||||
"select_source_address": "보낼 주소 선택...",
|
||||
"send": "전송",
|
||||
"send_amount": "금액",
|
||||
"send_amount_details": "금액 상세",
|
||||
"send_amount_upper": "금액",
|
||||
"send_clear_fields": "모든 양식 필드를 지우시겠습니까?",
|
||||
"send_copy_error": "오류 복사",
|
||||
"send_dismiss": "닫기",
|
||||
"send_error_copied": "오류가 클립보드에 복사되었습니다",
|
||||
"send_error_prefix": "오류: %s",
|
||||
"send_exceeds_available": "사용 가능 금액 초과 (%.8f)",
|
||||
"send_fee": "수수료",
|
||||
"send_fee_high": "높음",
|
||||
"send_fee_low": "낮음",
|
||||
"send_fee_normal": "보통",
|
||||
"send_form_restored": "양식이 복원되었습니다",
|
||||
"send_from_this_address": "이 주소에서 전송",
|
||||
"send_go_to_receive": "수신으로 이동",
|
||||
"send_keep": "유지",
|
||||
"send_network_fee": "네트워크 수수료",
|
||||
"send_no_balance": "잔액 없음",
|
||||
"send_no_recent": "최근 전송 없음",
|
||||
"send_recent_sends": "최근 전송",
|
||||
"send_recipient": "수신자",
|
||||
"send_select_source": "보낼 주소 선택...",
|
||||
"send_sending_from": "보내는 곳",
|
||||
"send_submitting": "거래 제출 중...",
|
||||
"send_switch_to_receive": "수신으로 전환하여 주소를 받고 자금 수신을 시작하세요.",
|
||||
"send_to": "받는 곳",
|
||||
"send_tooltip_enter_amount": "전송할 금액을 입력하세요",
|
||||
"send_tooltip_exceeds_balance": "금액이 사용 가능 잔액을 초과합니다",
|
||||
"send_tooltip_in_progress": "거래가 이미 진행 중입니다",
|
||||
"send_tooltip_invalid_address": "유효한 수신자 주소를 입력하세요",
|
||||
"send_tooltip_not_connected": "데몬에 연결되지 않음",
|
||||
"send_tooltip_select_source": "먼저 보낼 주소를 선택하세요",
|
||||
"send_tooltip_syncing": "블록체인 동기화를 기다려 주세요",
|
||||
"send_total": "합계",
|
||||
"send_transaction": "거래 전송",
|
||||
"send_tx_failed": "거래 실패",
|
||||
"send_tx_sent": "거래가 전송되었습니다!",
|
||||
"send_tx_success": "거래 전송 성공!",
|
||||
"send_txid_copied": "TxID가 클립보드에 복사되었습니다",
|
||||
"send_txid_label": "TxID: %s",
|
||||
"send_valid_shielded": "유효한 차폐 주소",
|
||||
"send_valid_transparent": "유효한 투명 주소",
|
||||
"send_wallet_empty": "지갑이 비어 있습니다",
|
||||
"send_yes_clear": "예, 지우기",
|
||||
"sending": "거래 전송 중",
|
||||
"sending_from": "보내는 곳",
|
||||
"sent": "전송됨",
|
||||
"sent_filter": "전송됨",
|
||||
"sent_type": "전송됨",
|
||||
"sent_upper": "전송됨",
|
||||
"settings": "설정",
|
||||
"setup_wizard": "설정 마법사",
|
||||
"share": "공유",
|
||||
"shield_check_status": "상태 확인",
|
||||
"shield_completed": "작업이 성공적으로 완료되었습니다!",
|
||||
"shield_description": "투명 주소의 코인베이스 출력을 차폐 주소로 전송하여 채굴 보상을 차폐합니다. 채굴 수입을 숨겨 프라이버시가 향상됩니다.",
|
||||
"shield_from_address": "보내는 주소:",
|
||||
"shield_funds": "자금 차폐",
|
||||
"shield_in_progress": "작업 진행 중...",
|
||||
"shield_max_utxos": "작업당 최대 UTXO 수",
|
||||
"shield_merge_done": "차폐/통합이 완료되었습니다!",
|
||||
"shield_select_z": "z 주소 선택...",
|
||||
"shield_started": "차폐 작업이 시작되었습니다",
|
||||
"shield_title": "코인베이스 보상 차폐",
|
||||
"shield_to_address": "받는 주소 (차폐):",
|
||||
"shield_utxo_limit": "UTXO 제한:",
|
||||
"shield_wildcard_hint": "'*'를 사용하여 모든 투명 주소에서 차폐",
|
||||
"shielded": "차폐",
|
||||
"shielded_to": "차폐 대상",
|
||||
"shielded_type": "차폐",
|
||||
"show": "표시",
|
||||
"show_qr_code": "QR 코드 표시",
|
||||
"showing_transactions": "%d\xe2\x80\x93%d / %d건의 거래 표시 중 (총: %zu)",
|
||||
"simple_background": "단순 배경",
|
||||
"start_mining": "채굴 시작",
|
||||
"status": "상태",
|
||||
"stop_external": "외부 데몬 중지",
|
||||
"stop_mining": "채굴 중지",
|
||||
"submitting_transaction": "거래 제출 중...",
|
||||
"success": "성공",
|
||||
"summary": "요약",
|
||||
"syncing": "동기화 중...",
|
||||
"t_addresses": "T 주소",
|
||||
"test_connection": "테스트",
|
||||
"theme": "테마",
|
||||
"theme_effects": "테마 효과",
|
||||
"time_days_ago": "%d일 전",
|
||||
"time_hours_ago": "%d시간 전",
|
||||
"time_minutes_ago": "%d분 전",
|
||||
"time_seconds_ago": "%d초 전",
|
||||
"to": "받는 곳",
|
||||
"to_upper": "받는 곳",
|
||||
"tools": "도구",
|
||||
"total": "합계",
|
||||
"transaction_id": "거래 ID",
|
||||
"transaction_sent": "거래 전송 성공",
|
||||
"transaction_sent_msg": "거래가 전송되었습니다!",
|
||||
"transaction_url": "거래 URL",
|
||||
"transactions": "거래",
|
||||
"transactions_upper": "거래",
|
||||
"transparent": "투명",
|
||||
"tx_confirmations": "%d 확인",
|
||||
"tx_details_title": "거래 상세",
|
||||
"tx_from_address": "보낸 주소:",
|
||||
"tx_id_label": "거래 ID:",
|
||||
"tx_immature": "미성숙",
|
||||
"tx_mined": "채굴됨",
|
||||
"tx_received": "수신됨",
|
||||
"tx_sent": "전송됨",
|
||||
"tx_to_address": "받는 주소:",
|
||||
"tx_view_explorer": "탐색기에서 보기",
|
||||
"txs_count": "%d건",
|
||||
"type": "유형",
|
||||
"ui_opacity": "UI 투명도",
|
||||
"unban": "차단 해제",
|
||||
"unconfirmed": "미확인",
|
||||
"undo_clear": "지우기 취소",
|
||||
"unknown": "알 수 없음",
|
||||
"use_embedded_daemon": "내장 dragonxd 사용",
|
||||
"use_tor": "Tor 사용",
|
||||
"validate_btn": "검증",
|
||||
"validate_description": "DragonX 주소를 입력하여 유효한지 그리고 이 지갑에 속하는지 확인합니다.",
|
||||
"validate_invalid": "유효하지 않음",
|
||||
"validate_is_mine": "이 지갑이 이 주소를 소유합니다",
|
||||
"validate_not_mine": "이 지갑에 속하지 않음",
|
||||
"validate_ownership": "소유자:",
|
||||
"validate_results": "결과:",
|
||||
"validate_shielded_type": "차폐 (z 주소)",
|
||||
"validate_status": "상태:",
|
||||
"validate_title": "주소 검증",
|
||||
"validate_transparent_type": "투명 (t 주소)",
|
||||
"validate_type": "유형:",
|
||||
"validate_valid": "유효함",
|
||||
"validating": "검증 중...",
|
||||
"verbose_logging": "상세 로깅",
|
||||
"version": "버전",
|
||||
"view": "보기",
|
||||
"view_details": "상세 보기",
|
||||
"view_on_explorer": "탐색기에서 보기",
|
||||
"waiting_for_daemon": "데몬 연결 대기 중...",
|
||||
"wallet": "지갑",
|
||||
"wallet_empty": "지갑이 비어 있습니다",
|
||||
"wallet_empty_hint": "수신으로 전환하여 주소를 받고 자금 수신을 시작하세요.",
|
||||
"warning": "경고",
|
||||
"warning_upper": "경고!",
|
||||
"website": "웹사이트",
|
||||
"window_opacity": "창 투명도",
|
||||
"yes_clear": "예, 지우기",
|
||||
"your_addresses": "내 주소",
|
||||
"z_addresses": "Z 주소",
|
||||
}
|
||||
|
||||
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "ko.json")
|
||||
with open(out, "w", encoding="utf-8") as f:
|
||||
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
|
||||
print(f"Wrote {len(translations)} Korean translations to {os.path.abspath(out)}")
|
||||
@@ -0,0 +1,646 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate Portuguese (pt) translations for ObsidianDragon wallet."""
|
||||
import json, os
|
||||
|
||||
translations = {
|
||||
"24h_change": "Variação 24h",
|
||||
"24h_volume": "Volume 24h",
|
||||
"about": "Sobre",
|
||||
"about_block_explorer": "Explorador de Blocos",
|
||||
"about_block_height": "Altura do Bloco:",
|
||||
"about_build_date": "Data de Compilação:",
|
||||
"about_build_type": "Tipo de Build:",
|
||||
"about_chain": "Chain:",
|
||||
"about_connections": "Conexões:",
|
||||
"about_credits": "Créditos",
|
||||
"about_daemon": "Daemon:",
|
||||
"about_debug": "Depuração",
|
||||
"about_dragonx": "Sobre o ObsidianDragon",
|
||||
"about_edition": "Edição ImGui",
|
||||
"about_github": "GitHub",
|
||||
"about_imgui": "ImGui:",
|
||||
"about_license": "Licença",
|
||||
"about_license_text": "Este software é disponibilizado sob a Licença Pública Geral GNU v3 (GPLv3). Você é livre para usar, modificar e distribuir este software sob os termos da licença.",
|
||||
"about_peers_count": "%zu pares",
|
||||
"about_release": "Versão",
|
||||
"about_title": "Sobre o ObsidianDragon",
|
||||
"about_version": "Versão:",
|
||||
"about_website": "Website",
|
||||
"acrylic": "Acrílico",
|
||||
"add": "Adicionar",
|
||||
"address": "Endereço",
|
||||
"address_book_add": "Adicionar Endereço",
|
||||
"address_book_add_new": "Adicionar Novo",
|
||||
"address_book_added": "Endereço adicionado ao livro",
|
||||
"address_book_count": "%zu endereços salvos",
|
||||
"address_book_deleted": "Entrada excluída",
|
||||
"address_book_edit": "Editar Endereço",
|
||||
"address_book_empty": "Nenhum endereço salvo. Clique em 'Adicionar Novo' para criar um.",
|
||||
"address_book_exists": "Endereço já existe no livro",
|
||||
"address_book_title": "Livro de Endereços",
|
||||
"address_book_update_failed": "Falha na atualização - endereço pode ser duplicado",
|
||||
"address_book_updated": "Endereço atualizado",
|
||||
"address_copied": "Endereço copiado para a área de transferência",
|
||||
"address_details": "Detalhes do Endereço",
|
||||
"address_label": "Endereço:",
|
||||
"address_upper": "ENDEREÇO",
|
||||
"address_url": "URL do Endereço",
|
||||
"addresses_appear_here": "Seus endereços de recebimento aparecerão aqui após a conexão.",
|
||||
"advanced": "AVANÇADO",
|
||||
"all_filter": "Todos",
|
||||
"allow_custom_fees": "Permitir taxas personalizadas",
|
||||
"amount": "Valor",
|
||||
"amount_details": "DETALHES DO VALOR",
|
||||
"amount_exceeds_balance": "Valor excede o saldo",
|
||||
"amount_label": "Valor:",
|
||||
"appearance": "APARÊNCIA",
|
||||
"auto_shield": "Auto-blindar mineração",
|
||||
"available": "Disponível",
|
||||
"backup_backing_up": "Fazendo backup...",
|
||||
"backup_create": "Criar Backup",
|
||||
"backup_created": "Backup da carteira criado",
|
||||
"backup_data": "BACKUP & DADOS",
|
||||
"backup_description": "Crie um backup do seu arquivo wallet.dat. Este arquivo contém todas as suas chaves privadas e histórico de transações. Guarde o backup em um local seguro.",
|
||||
"backup_destination": "Destino do backup:",
|
||||
"backup_tip_external": "Armazene backups em unidades externas ou armazenamento em nuvem",
|
||||
"backup_tip_multiple": "Crie múltiplos backups em diferentes locais",
|
||||
"backup_tip_test": "Teste a restauração do backup periodicamente",
|
||||
"backup_tips": "Dicas:",
|
||||
"backup_title": "Backup da Carteira",
|
||||
"backup_wallet": "Fazer Backup da Carteira...",
|
||||
"backup_wallet_not_found": "Aviso: wallet.dat não encontrado no local esperado",
|
||||
"balance": "Saldo",
|
||||
"balance_layout": "Layout do Saldo",
|
||||
"ban": "Banir",
|
||||
"banned_peers": "Pares Banidos",
|
||||
"block": "Bloco",
|
||||
"block_bits": "Bits:",
|
||||
"block_click_next": "Clique para ver o próximo bloco",
|
||||
"block_click_prev": "Clique para ver o bloco anterior",
|
||||
"block_explorer": "Explorador de Blocos",
|
||||
"block_get_info": "Obter Info do Bloco",
|
||||
"block_hash": "Hash do Bloco:",
|
||||
"block_height": "Altura do Bloco:",
|
||||
"block_info_title": "Informações do Bloco",
|
||||
"block_merkle_root": "Raiz Merkle:",
|
||||
"block_nav_next": "Próximo >>",
|
||||
"block_nav_prev": "<< Anterior",
|
||||
"block_next": "Próximo Bloco:",
|
||||
"block_previous": "Bloco Anterior:",
|
||||
"block_size": "Tamanho:",
|
||||
"block_timestamp": "Carimbo de Data:",
|
||||
"block_transactions": "Transações:",
|
||||
"blockchain_syncing": "Blockchain sincronizando (%.1f%%)... Os saldos podem ser imprecisos.",
|
||||
"cancel": "Cancelar",
|
||||
"characters": "caracteres",
|
||||
"clear": "Limpar",
|
||||
"clear_all_bans": "Remover Todos os Banimentos",
|
||||
"clear_form_confirm": "Limpar todos os campos do formulário?",
|
||||
"clear_request": "Limpar Solicitação",
|
||||
"click_copy_address": "Clique para copiar o endereço",
|
||||
"click_copy_uri": "Clique para copiar a URI",
|
||||
"close": "Fechar",
|
||||
"conf_count": "%d conf.",
|
||||
"confirm_and_send": "Confirmar & Enviar",
|
||||
"confirm_send": "Confirmar Envio",
|
||||
"confirm_transaction": "Confirmar Transação",
|
||||
"confirmations": "Confirmações",
|
||||
"confirmations_display": "%d confirmações | %s",
|
||||
"confirmed": "Confirmado",
|
||||
"connected": "Conectado",
|
||||
"connected_peers": "Pares Conectados",
|
||||
"connecting": "Conectando...",
|
||||
"console": "Console",
|
||||
"console_auto_scroll": "Rolagem automática",
|
||||
"console_available_commands": "Comandos disponíveis:",
|
||||
"console_capturing_output": "Capturando saída do daemon...",
|
||||
"console_clear": "Limpar",
|
||||
"console_clear_console": "Limpar Console",
|
||||
"console_cleared": "Console limpo",
|
||||
"console_click_commands": "Clique nos comandos acima para inseri-los",
|
||||
"console_click_insert": "Clique para inserir",
|
||||
"console_click_insert_params": "Clique para inserir com parâmetros",
|
||||
"console_close": "Fechar",
|
||||
"console_commands": "Comandos",
|
||||
"console_common_rpc": "Comandos RPC comuns:",
|
||||
"console_completions": "Completações:",
|
||||
"console_connected": "Conectado ao daemon",
|
||||
"console_copy_all": "Copiar Tudo",
|
||||
"console_copy_selected": "Copiar",
|
||||
"console_daemon": "Daemon",
|
||||
"console_daemon_error": "Erro do daemon!",
|
||||
"console_daemon_started": "Daemon iniciado",
|
||||
"console_daemon_stopped": "Daemon parado",
|
||||
"console_disconnected": "Desconectado do daemon",
|
||||
"console_errors": "Erros",
|
||||
"console_filter_hint": "Filtrar saída...",
|
||||
"console_help_clear": " clear - Limpar o console",
|
||||
"console_help_getbalance": " getbalance - Mostrar saldo transparente",
|
||||
"console_help_getblockcount": " getblockcount - Mostrar altura atual do bloco",
|
||||
"console_help_getinfo": " getinfo - Mostrar informações do nó",
|
||||
"console_help_getmininginfo": " getmininginfo - Mostrar status da mineração",
|
||||
"console_help_getpeerinfo": " getpeerinfo - Mostrar pares conectados",
|
||||
"console_help_gettotalbalance": " gettotalbalance - Mostrar saldo total",
|
||||
"console_help_help": " help - Mostrar esta mensagem de ajuda",
|
||||
"console_help_setgenerate": " setgenerate - Controlar mineração",
|
||||
"console_help_stop": " stop - Parar o daemon",
|
||||
"console_line_count": "%zu linhas",
|
||||
"console_new_lines": "%d novas linhas",
|
||||
"console_no_daemon": "Sem daemon",
|
||||
"console_not_connected": "Erro: Não conectado ao daemon",
|
||||
"console_rpc_reference": "Referência de Comandos RPC",
|
||||
"console_scanline": "Scanline do console",
|
||||
"console_search_commands": "Pesquisar comandos...",
|
||||
"console_select_all": "Selecionar Tudo",
|
||||
"console_show_daemon_output": "Mostrar saída do daemon",
|
||||
"console_show_errors_only": "Mostrar apenas erros",
|
||||
"console_show_rpc_ref": "Mostrar referência de comandos RPC",
|
||||
"console_showing_lines": "Mostrando %zu de %zu linhas",
|
||||
"console_starting_node": "Iniciando nó...",
|
||||
"console_status_error": "Erro",
|
||||
"console_status_running": "Em execução",
|
||||
"console_status_starting": "Iniciando",
|
||||
"console_status_stopped": "Parado",
|
||||
"console_status_stopping": "Parando",
|
||||
"console_status_unknown": "Desconhecido",
|
||||
"console_tab_completion": "Tab para completar",
|
||||
"console_type_help": "Digite 'help' para comandos disponíveis",
|
||||
"console_welcome": "Bem-vindo ao Console ObsidianDragon",
|
||||
"console_zoom_in": "Aumentar zoom",
|
||||
"console_zoom_out": "Diminuir zoom",
|
||||
"copy": "Copiar",
|
||||
"copy_address": "Copiar Endereço Completo",
|
||||
"copy_error": "Copiar Erro",
|
||||
"copy_to_clipboard": "Copiar para Área de Transferência",
|
||||
"copy_txid": "Copiar TxID",
|
||||
"copy_uri": "Copiar URI",
|
||||
"current_price": "Preço Atual",
|
||||
"custom_fees": "Taxas personalizadas",
|
||||
"dark": "Escuro",
|
||||
"date": "Data",
|
||||
"date_label": "Data:",
|
||||
"delete": "Excluir",
|
||||
"difficulty": "Dificuldade",
|
||||
"disconnected": "Desconectado",
|
||||
"dismiss": "Dispensar",
|
||||
"display": "Exibição",
|
||||
"dragonx_green": "DragonX (Verde)",
|
||||
"edit": "Editar",
|
||||
"error": "Erro",
|
||||
"est_time_to_block": "Tempo Est. por Bloco",
|
||||
"exit": "Sair",
|
||||
"explorer": "EXPLORADOR",
|
||||
"export": "Exportar",
|
||||
"export_csv": "Exportar CSV",
|
||||
"export_keys_btn": "Exportar Chaves",
|
||||
"export_keys_danger": "PERIGO: Isto exportará TODAS as chaves privadas da sua carteira! Qualquer pessoa com acesso a este arquivo pode roubar seus fundos. Guarde com segurança e exclua após o uso.",
|
||||
"export_keys_include_t": "Incluir endereços T (transparentes)",
|
||||
"export_keys_include_z": "Incluir endereços Z (blindados)",
|
||||
"export_keys_options": "Opções de exportação:",
|
||||
"export_keys_success": "Chaves exportadas com sucesso",
|
||||
"export_keys_title": "Exportar Todas as Chaves Privadas",
|
||||
"export_private_key": "Exportar Chave Privada",
|
||||
"export_tx_count": "Exportar %zu transações para arquivo CSV.",
|
||||
"export_tx_file_fail": "Falha ao criar arquivo CSV",
|
||||
"export_tx_none": "Nenhuma transação para exportar",
|
||||
"export_tx_success": "Transações exportadas com sucesso",
|
||||
"export_tx_title": "Exportar Transações para CSV",
|
||||
"export_viewing_key": "Exportar Chave de Visualização",
|
||||
"failed_create_shielded": "Falha ao criar endereço blindado",
|
||||
"failed_create_transparent": "Falha ao criar endereço transparente",
|
||||
"fee": "Taxa",
|
||||
"fee_high": "Alta",
|
||||
"fee_label": "Taxa:",
|
||||
"fee_low": "Baixa",
|
||||
"fee_normal": "Normal",
|
||||
"fetch_prices": "Buscar preços",
|
||||
"file": "Arquivo",
|
||||
"file_save_location": "O arquivo será salvo em: ~/.config/ObsidianDragon/",
|
||||
"font_scale": "Escala da Fonte",
|
||||
"from": "De",
|
||||
"from_upper": "DE",
|
||||
"full_details": "Detalhes Completos",
|
||||
"general": "Geral",
|
||||
"go_to_receive": "Ir para Receber",
|
||||
"height": "Altura",
|
||||
"help": "Ajuda",
|
||||
"hide": "Ocultar",
|
||||
"history": "Histórico",
|
||||
"immature_type": "Imaturo",
|
||||
"import": "Importar",
|
||||
"import_key_btn": "Importar Chave(s)",
|
||||
"import_key_formats": "Formatos de chave suportados:",
|
||||
"import_key_full_rescan": "(0 = rescan completo)",
|
||||
"import_key_label": "Chave(s) Privada(s):",
|
||||
"import_key_no_valid": "Nenhuma chave válida encontrada na entrada",
|
||||
"import_key_rescan": "Reescanear blockchain após importação",
|
||||
"import_key_start_height": "Altura inicial:",
|
||||
"import_key_success": "Chaves importadas com sucesso",
|
||||
"import_key_t_format": "Chaves privadas WIF de endereços T",
|
||||
"import_key_title": "Importar Chave Privada",
|
||||
"import_key_tooltip": "Digite uma ou mais chaves privadas, uma por linha.\nSuporta chaves de z-endereço e t-endereço.\nLinhas começando com # são tratadas como comentários.",
|
||||
"import_key_warning": "Aviso: Nunca compartilhe suas chaves privadas! Importar chaves de fontes não confiáveis pode comprometer sua carteira.",
|
||||
"import_key_z_format": "Chaves de gasto de z-endereço (secret-extended-key-...)",
|
||||
"import_private_key": "Importar Chave Privada...",
|
||||
"invalid_address": "Formato de endereço inválido",
|
||||
"ip_address": "Endereço IP",
|
||||
"keep": "Manter",
|
||||
"keep_daemon": "Manter daemon em execução",
|
||||
"key_export_fetching": "Buscando chave da carteira...",
|
||||
"key_export_private_key": "Chave Privada:",
|
||||
"key_export_private_warning": "Mantenha esta chave em SEGREDO! Qualquer pessoa com esta chave pode gastar seus fundos. Nunca a compartilhe online ou com terceiros não confiáveis.",
|
||||
"key_export_reveal": "Revelar Chave",
|
||||
"key_export_viewing_key": "Chave de Visualização:",
|
||||
"key_export_viewing_warning": "Esta chave de visualização permite que outros vejam suas transações recebidas e saldo, mas NÃO gastem seus fundos. Compartilhe apenas com partes confiáveis.",
|
||||
"label": "Rótulo:",
|
||||
"language": "Idioma",
|
||||
"light": "Claro",
|
||||
"loading": "Carregando...",
|
||||
"loading_addresses": "Carregando endereços...",
|
||||
"local_hashrate": "Hashrate Local",
|
||||
"low_spec_mode": "Modo econômico",
|
||||
"market": "Mercado",
|
||||
"market_12h": "12h",
|
||||
"market_18h": "18h",
|
||||
"market_24h": "24h",
|
||||
"market_24h_volume": "VOLUME 24H",
|
||||
"market_6h": "6h",
|
||||
"market_attribution": "Dados de preço do NonKYC",
|
||||
"market_btc_price": "PREÇO BTC",
|
||||
"market_cap": "Capitalização",
|
||||
"market_no_history": "Nenhum histórico de preços disponível",
|
||||
"market_no_price": "Sem dados de preço",
|
||||
"market_now": "Agora",
|
||||
"market_pct_shielded": "%.0f%% Blindado",
|
||||
"market_portfolio": "PORTFÓLIO",
|
||||
"market_price_unavailable": "Dados de preço indisponíveis",
|
||||
"market_refresh_price": "Atualizar dados de preço",
|
||||
"market_trade_on": "Negociar no %s",
|
||||
"mature": "Maduro",
|
||||
"max": "Máx",
|
||||
"memo": "Memo (opcional, criptografado)",
|
||||
"memo_label": "Memo:",
|
||||
"memo_optional": "MEMO (OPCIONAL)",
|
||||
"memo_upper": "MEMO",
|
||||
"memo_z_only": "Nota: Memos só estão disponíveis ao enviar para endereços blindados (z)",
|
||||
"merge_description": "Fundir múltiplos UTXOs em um único endereço blindado. Isso pode ajudar a reduzir o tamanho da carteira e melhorar a privacidade.",
|
||||
"merge_funds": "Fundir Fundos",
|
||||
"merge_started": "Operação de fusão iniciada",
|
||||
"merge_title": "Fundir para Endereço",
|
||||
"mine_when_idle": "Minerar quando ocioso",
|
||||
"mined": "minerado",
|
||||
"mined_filter": "Minerado",
|
||||
"mined_type": "Minerado",
|
||||
"mined_upper": "MINERADO",
|
||||
"miner_fee": "Taxa de Minerador",
|
||||
"mining": "Mineração",
|
||||
"mining_active": "Ativo",
|
||||
"mining_address_copied": "Endereço de mineração copiado",
|
||||
"mining_all_time": "Todo o Tempo",
|
||||
"mining_already_saved": "URL do pool já salva",
|
||||
"mining_block_copied": "Hash do bloco copiado",
|
||||
"mining_chart_1m_ago": "1m atrás",
|
||||
"mining_chart_5m_ago": "5m atrás",
|
||||
"mining_chart_now": "Agora",
|
||||
"mining_chart_start": "Início",
|
||||
"mining_click": "Clique",
|
||||
"mining_click_copy_address": "Clique para copiar o endereço",
|
||||
"mining_click_copy_block": "Clique para copiar o hash do bloco",
|
||||
"mining_click_copy_difficulty": "Clique para copiar a dificuldade",
|
||||
"mining_connected": "Conectado",
|
||||
"mining_connecting": "Conectando...",
|
||||
"mining_control": "Controle de Mineração",
|
||||
"mining_difficulty_copied": "Dificuldade copiada",
|
||||
"mining_est_block": "Bloco Est.",
|
||||
"mining_est_daily": "Est. Diário",
|
||||
"mining_filter_all": "Todos",
|
||||
"mining_filter_tip_all": "Mostrar todos os ganhos",
|
||||
"mining_filter_tip_pool": "Mostrar apenas ganhos do pool",
|
||||
"mining_filter_tip_solo": "Mostrar apenas ganhos solo",
|
||||
"mining_idle_off_tooltip": "Ativar mineração ociosa",
|
||||
"mining_idle_on_tooltip": "Desativar mineração ociosa",
|
||||
"mining_local_hashrate": "Hashrate Local",
|
||||
"mining_mine": "Minerar",
|
||||
"mining_mining_addr": "End. Mineração",
|
||||
"mining_network": "Rede",
|
||||
"mining_no_blocks_yet": "Nenhum bloco encontrado ainda",
|
||||
"mining_no_payouts_yet": "Nenhum pagamento de pool ainda",
|
||||
"mining_no_saved_addresses": "Nenhum endereço salvo",
|
||||
"mining_no_saved_pools": "Nenhum pool salvo",
|
||||
"mining_off": "Mineração está DESLIGADA",
|
||||
"mining_on": "Mineração está LIGADA",
|
||||
"mining_open_in_explorer": "Abrir no explorador",
|
||||
"mining_payout_address": "Endereço de Pagamento",
|
||||
"mining_payout_tooltip": "Endereço para receber recompensas de mineração",
|
||||
"mining_pool": "Pool",
|
||||
"mining_pool_hashrate": "Hashrate do Pool",
|
||||
"mining_pool_url": "URL do Pool",
|
||||
"mining_recent_blocks": "BLOCOS RECENTES",
|
||||
"mining_recent_payouts": "PAGAMENTOS DE POOL RECENTES",
|
||||
"mining_remove": "Remover",
|
||||
"mining_reset_defaults": "Redefinir Padrões",
|
||||
"mining_save_payout_address": "Salvar endereço de pagamento",
|
||||
"mining_save_pool_url": "Salvar URL do pool",
|
||||
"mining_saved_addresses": "Endereços Salvos:",
|
||||
"mining_saved_pools": "Pools Salvos:",
|
||||
"mining_shares": "Shares",
|
||||
"mining_show_chart": "Gráfico",
|
||||
"mining_show_log": "Log",
|
||||
"mining_solo": "Solo",
|
||||
"mining_starting": "Iniciando...",
|
||||
"mining_starting_tooltip": "Minerador está iniciando...",
|
||||
"mining_statistics": "Estatísticas de Mineração",
|
||||
"mining_stop": "Parar",
|
||||
"mining_stop_solo_for_pool": "Pare a mineração solo antes de iniciar a mineração em pool",
|
||||
"mining_stop_solo_for_pool_settings": "Pare a mineração solo para alterar as configurações do pool",
|
||||
"mining_stopping": "Parando...",
|
||||
"mining_stopping_tooltip": "Minerador está parando...",
|
||||
"mining_syncing_tooltip": "Blockchain está sincronizando...",
|
||||
"mining_threads": "Threads de Mineração",
|
||||
"mining_to_save": "para salvar",
|
||||
"mining_today": "Hoje",
|
||||
"mining_uptime": "Tempo Ativo",
|
||||
"mining_yesterday": "Ontem",
|
||||
"network": "Rede",
|
||||
"network_fee": "TAXA DA REDE",
|
||||
"network_hashrate": "Hashrate da Rede",
|
||||
"new": "+ Novo",
|
||||
"new_shielded_created": "Novo endereço blindado criado",
|
||||
"new_t_address": "Novo Endereço T",
|
||||
"new_t_transparent": "Novo endereço t (Transparente)",
|
||||
"new_transparent_created": "Novo endereço transparente criado",
|
||||
"new_z_address": "Novo Endereço Z",
|
||||
"new_z_shielded": "Novo endereço z (Blindado)",
|
||||
"no_addresses": "Nenhum endereço encontrado. Crie um usando os botões acima.",
|
||||
"no_addresses_available": "Nenhum endereço disponível",
|
||||
"no_addresses_match": "Nenhum endereço corresponde ao filtro",
|
||||
"no_addresses_with_balance": "Nenhum endereço com saldo",
|
||||
"no_matching": "Nenhuma transação correspondente",
|
||||
"no_recent_receives": "Nenhum recebimento recente",
|
||||
"no_recent_sends": "Nenhum envio recente",
|
||||
"no_transactions": "Nenhuma transação encontrada",
|
||||
"node": "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)}")
|
||||
@@ -0,0 +1,646 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate Russian (ru) translations for ObsidianDragon wallet."""
|
||||
import json, os
|
||||
|
||||
translations = {
|
||||
"24h_change": "Изменение за 24ч",
|
||||
"24h_volume": "Объём за 24ч",
|
||||
"about": "О программе",
|
||||
"about_block_explorer": "Обозреватель блоков",
|
||||
"about_block_height": "Высота блока:",
|
||||
"about_build_date": "Дата сборки:",
|
||||
"about_build_type": "Тип сборки:",
|
||||
"about_chain": "Цепочка:",
|
||||
"about_connections": "Подключения:",
|
||||
"about_credits": "Благодарности",
|
||||
"about_daemon": "Daemon:",
|
||||
"about_debug": "Отладка",
|
||||
"about_dragonx": "Об ObsidianDragon",
|
||||
"about_edition": "Редакция ImGui",
|
||||
"about_github": "GitHub",
|
||||
"about_imgui": "ImGui:",
|
||||
"about_license": "Лицензия",
|
||||
"about_license_text": "Это программное обеспечение выпущено под лицензией GNU General Public License v3 (GPLv3). Вы можете свободно использовать, изменять и распространять это ПО в соответствии с условиями лицензии.",
|
||||
"about_peers_count": "%zu узлов",
|
||||
"about_release": "Релиз",
|
||||
"about_title": "Об ObsidianDragon",
|
||||
"about_version": "Версия:",
|
||||
"about_website": "Веб-сайт",
|
||||
"acrylic": "Акрил",
|
||||
"add": "Добавить",
|
||||
"address": "Адрес",
|
||||
"address_book_add": "Добавить адрес",
|
||||
"address_book_add_new": "Добавить новый",
|
||||
"address_book_added": "Адрес добавлен в книгу",
|
||||
"address_book_count": "%zu адресов сохранено",
|
||||
"address_book_deleted": "Запись удалена",
|
||||
"address_book_edit": "Редактировать адрес",
|
||||
"address_book_empty": "Нет сохранённых адресов. Нажмите 'Добавить новый', чтобы создать.",
|
||||
"address_book_exists": "Адрес уже существует в книге",
|
||||
"address_book_title": "Адресная книга",
|
||||
"address_book_update_failed": "Не удалось обновить — адрес может быть дубликатом",
|
||||
"address_book_updated": "Адрес обновлён",
|
||||
"address_copied": "Адрес скопирован в буфер обмена",
|
||||
"address_details": "Детали адреса",
|
||||
"address_label": "Адрес:",
|
||||
"address_upper": "АДРЕС",
|
||||
"address_url": "URL адреса",
|
||||
"addresses_appear_here": "Ваши адреса для получения появятся здесь после подключения.",
|
||||
"advanced": "РАСШИРЕННЫЕ",
|
||||
"all_filter": "Все",
|
||||
"allow_custom_fees": "Разрешить пользовательские комиссии",
|
||||
"amount": "Сумма",
|
||||
"amount_details": "ДЕТАЛИ СУММЫ",
|
||||
"amount_exceeds_balance": "Сумма превышает баланс",
|
||||
"amount_label": "Сумма:",
|
||||
"appearance": "ВНЕШНИЙ ВИД",
|
||||
"auto_shield": "Авто-экранирование майнинга",
|
||||
"available": "Доступно",
|
||||
"backup_backing_up": "Создание резервной копии...",
|
||||
"backup_create": "Создать резервную копию",
|
||||
"backup_created": "Резервная копия кошелька создана",
|
||||
"backup_data": "РЕЗЕРВНОЕ КОПИРОВАНИЕ И ДАННЫЕ",
|
||||
"backup_description": "Создайте резервную копию файла wallet.dat. Этот файл содержит все ваши приватные ключи и историю транзакций. Храните копию в безопасном месте.",
|
||||
"backup_destination": "Место сохранения:",
|
||||
"backup_tip_external": "Храните резервные копии на внешних дисках или в облаке",
|
||||
"backup_tip_multiple": "Создавайте несколько копий в разных местах",
|
||||
"backup_tip_test": "Периодически проверяйте восстановление из резервной копии",
|
||||
"backup_tips": "Советы:",
|
||||
"backup_title": "Резервное копирование кошелька",
|
||||
"backup_wallet": "Создать резервную копию...",
|
||||
"backup_wallet_not_found": "Предупреждение: wallet.dat не найден в ожидаемом расположении",
|
||||
"balance": "Баланс",
|
||||
"balance_layout": "Макет баланса",
|
||||
"ban": "Заблокировать",
|
||||
"banned_peers": "Заблокированные узлы",
|
||||
"block": "Блок",
|
||||
"block_bits": "Биты:",
|
||||
"block_click_next": "Нажмите для следующего блока",
|
||||
"block_click_prev": "Нажмите для предыдущего блока",
|
||||
"block_explorer": "Обозреватель блоков",
|
||||
"block_get_info": "Получить информацию о блоке",
|
||||
"block_hash": "Хэш блока:",
|
||||
"block_height": "Высота блока:",
|
||||
"block_info_title": "Информация о блоке",
|
||||
"block_merkle_root": "Корень Меркла:",
|
||||
"block_nav_next": "Далее >>",
|
||||
"block_nav_prev": "<< Назад",
|
||||
"block_next": "Следующий блок:",
|
||||
"block_previous": "Предыдущий блок:",
|
||||
"block_size": "Размер:",
|
||||
"block_timestamp": "Временная метка:",
|
||||
"block_transactions": "Транзакции:",
|
||||
"blockchain_syncing": "Синхронизация блокчейна (%.1f%%)... Балансы могут быть неточными.",
|
||||
"cancel": "Отмена",
|
||||
"characters": "символов",
|
||||
"clear": "Очистить",
|
||||
"clear_all_bans": "Снять все блокировки",
|
||||
"clear_form_confirm": "Очистить все поля формы?",
|
||||
"clear_request": "Очистить запрос",
|
||||
"click_copy_address": "Нажмите, чтобы скопировать адрес",
|
||||
"click_copy_uri": "Нажмите, чтобы скопировать URI",
|
||||
"close": "Закрыть",
|
||||
"conf_count": "%d подтв.",
|
||||
"confirm_and_send": "Подтвердить и отправить",
|
||||
"confirm_send": "Подтвердить отправку",
|
||||
"confirm_transaction": "Подтвердить транзакцию",
|
||||
"confirmations": "Подтверждения",
|
||||
"confirmations_display": "%d подтверждений | %s",
|
||||
"confirmed": "Подтверждено",
|
||||
"connected": "Подключено",
|
||||
"connected_peers": "Подключённые узлы",
|
||||
"connecting": "Подключение...",
|
||||
"console": "Консоль",
|
||||
"console_auto_scroll": "Авто-прокрутка",
|
||||
"console_available_commands": "Доступные команды:",
|
||||
"console_capturing_output": "Захват вывода daemon...",
|
||||
"console_clear": "Очистить",
|
||||
"console_clear_console": "Очистить консоль",
|
||||
"console_cleared": "Консоль очищена",
|
||||
"console_click_commands": "Нажмите на команды выше, чтобы вставить их",
|
||||
"console_click_insert": "Нажмите для вставки",
|
||||
"console_click_insert_params": "Нажмите для вставки с параметрами",
|
||||
"console_close": "Закрыть",
|
||||
"console_commands": "Команды",
|
||||
"console_common_rpc": "Частые RPC-команды:",
|
||||
"console_completions": "Дополнения:",
|
||||
"console_connected": "Подключено к daemon",
|
||||
"console_copy_all": "Копировать всё",
|
||||
"console_copy_selected": "Копировать",
|
||||
"console_daemon": "Daemon",
|
||||
"console_daemon_error": "Ошибка daemon!",
|
||||
"console_daemon_started": "Daemon запущен",
|
||||
"console_daemon_stopped": "Daemon остановлен",
|
||||
"console_disconnected": "Отключено от daemon",
|
||||
"console_errors": "Ошибки",
|
||||
"console_filter_hint": "Фильтр вывода...",
|
||||
"console_help_clear": " clear - Очистить консоль",
|
||||
"console_help_getbalance": " getbalance - Показать прозрачный баланс",
|
||||
"console_help_getblockcount": " getblockcount - Показать текущую высоту блока",
|
||||
"console_help_getinfo": " getinfo - Показать информацию об узле",
|
||||
"console_help_getmininginfo": " getmininginfo - Показать статус майнинга",
|
||||
"console_help_getpeerinfo": " getpeerinfo - Показать подключённые узлы",
|
||||
"console_help_gettotalbalance": " gettotalbalance - Показать общий баланс",
|
||||
"console_help_help": " help - Показать эту справку",
|
||||
"console_help_setgenerate": " setgenerate - Управление майнингом",
|
||||
"console_help_stop": " stop - Остановить daemon",
|
||||
"console_line_count": "%zu строк",
|
||||
"console_new_lines": "%d новых строк",
|
||||
"console_no_daemon": "Нет daemon",
|
||||
"console_not_connected": "Ошибка: Не подключено к daemon",
|
||||
"console_rpc_reference": "Справочник RPC-команд",
|
||||
"console_scanline": "Скан-линия консоли",
|
||||
"console_search_commands": "Поиск команд...",
|
||||
"console_select_all": "Выбрать всё",
|
||||
"console_show_daemon_output": "Показать вывод daemon",
|
||||
"console_show_errors_only": "Показать только ошибки",
|
||||
"console_show_rpc_ref": "Показать справочник RPC-команд",
|
||||
"console_showing_lines": "Показано %zu из %zu строк",
|
||||
"console_starting_node": "Запуск узла...",
|
||||
"console_status_error": "Ошибка",
|
||||
"console_status_running": "Работает",
|
||||
"console_status_starting": "Запуск",
|
||||
"console_status_stopped": "Остановлен",
|
||||
"console_status_stopping": "Остановка",
|
||||
"console_status_unknown": "Неизвестно",
|
||||
"console_tab_completion": "Tab для дополнения",
|
||||
"console_type_help": "Введите 'help' для списка команд",
|
||||
"console_welcome": "Добро пожаловать в консоль ObsidianDragon",
|
||||
"console_zoom_in": "Увеличить",
|
||||
"console_zoom_out": "Уменьшить",
|
||||
"copy": "Копировать",
|
||||
"copy_address": "Копировать полный адрес",
|
||||
"copy_error": "Копировать ошибку",
|
||||
"copy_to_clipboard": "Копировать в буфер обмена",
|
||||
"copy_txid": "Копировать TxID",
|
||||
"copy_uri": "Копировать URI",
|
||||
"current_price": "Текущая цена",
|
||||
"custom_fees": "Пользовательские комиссии",
|
||||
"dark": "Тёмная",
|
||||
"date": "Дата",
|
||||
"date_label": "Дата:",
|
||||
"delete": "Удалить",
|
||||
"difficulty": "Сложность",
|
||||
"disconnected": "Отключено",
|
||||
"dismiss": "Отклонить",
|
||||
"display": "Отображение",
|
||||
"dragonx_green": "DragonX (Зелёная)",
|
||||
"edit": "Редактировать",
|
||||
"error": "Ошибка",
|
||||
"est_time_to_block": "Расч. время до блока",
|
||||
"exit": "Выход",
|
||||
"explorer": "ОБОЗРЕВАТЕЛЬ",
|
||||
"export": "Экспорт",
|
||||
"export_csv": "Экспорт в CSV",
|
||||
"export_keys_btn": "Экспорт ключей",
|
||||
"export_keys_danger": "ОПАСНОСТЬ: Будут экспортированы ВСЕ приватные ключи из вашего кошелька! Любой, кто получит доступ к этому файлу, сможет украсть ваши средства. Храните его в безопасности и удалите после использования.",
|
||||
"export_keys_include_t": "Включить T-адреса (прозрачные)",
|
||||
"export_keys_include_z": "Включить Z-адреса (экранированные)",
|
||||
"export_keys_options": "Параметры экспорта:",
|
||||
"export_keys_success": "Ключи успешно экспортированы",
|
||||
"export_keys_title": "Экспорт всех приватных ключей",
|
||||
"export_private_key": "Экспорт приватного ключа",
|
||||
"export_tx_count": "Экспортировать %zu транзакций в файл CSV.",
|
||||
"export_tx_file_fail": "Не удалось создать файл CSV",
|
||||
"export_tx_none": "Нет транзакций для экспорта",
|
||||
"export_tx_success": "Транзакции успешно экспортированы",
|
||||
"export_tx_title": "Экспорт транзакций в CSV",
|
||||
"export_viewing_key": "Экспорт ключа просмотра",
|
||||
"failed_create_shielded": "Не удалось создать экранированный адрес",
|
||||
"failed_create_transparent": "Не удалось создать прозрачный адрес",
|
||||
"fee": "Комиссия",
|
||||
"fee_high": "Высокая",
|
||||
"fee_label": "Комиссия:",
|
||||
"fee_low": "Низкая",
|
||||
"fee_normal": "Обычная",
|
||||
"fetch_prices": "Получить цены",
|
||||
"file": "Файл",
|
||||
"file_save_location": "Файл будет сохранён в: ~/.config/ObsidianDragon/",
|
||||
"font_scale": "Масштаб шрифта",
|
||||
"from": "От",
|
||||
"from_upper": "ОТ",
|
||||
"full_details": "Полные детали",
|
||||
"general": "Общие",
|
||||
"go_to_receive": "Перейти к получению",
|
||||
"height": "Высота",
|
||||
"help": "Справка",
|
||||
"hide": "Скрыть",
|
||||
"history": "История",
|
||||
"immature_type": "Незрелая",
|
||||
"import": "Импорт",
|
||||
"import_key_btn": "Импорт ключей",
|
||||
"import_key_formats": "Поддерживаемые форматы ключей:",
|
||||
"import_key_full_rescan": "(0 = полное сканирование)",
|
||||
"import_key_label": "Приватный ключ(и):",
|
||||
"import_key_no_valid": "В введённых данных не найдено действительных ключей",
|
||||
"import_key_rescan": "Пересканировать блокчейн после импорта",
|
||||
"import_key_start_height": "Начальная высота:",
|
||||
"import_key_success": "Ключи успешно импортированы",
|
||||
"import_key_t_format": "Приватные ключи WIF для T-адресов",
|
||||
"import_key_title": "Импорт приватного ключа",
|
||||
"import_key_tooltip": "Введите один или несколько приватных ключей, по одному на строку.\nПоддерживаются ключи z-адресов и t-адресов.\nСтроки, начинающиеся с #, считаются комментариями.",
|
||||
"import_key_warning": "Предупреждение: Никогда не делитесь своими приватными ключами! Импорт ключей из ненадёжных источников может скомпрометировать ваш кошелёк.",
|
||||
"import_key_z_format": "Ключи расходования z-адресов (secret-extended-key-...)",
|
||||
"import_private_key": "Импорт приватного ключа...",
|
||||
"invalid_address": "Неверный формат адреса",
|
||||
"ip_address": "IP-адрес",
|
||||
"keep": "Сохранить",
|
||||
"keep_daemon": "Оставить daemon работающим",
|
||||
"key_export_fetching": "Получение ключа из кошелька...",
|
||||
"key_export_private_key": "Приватный ключ:",
|
||||
"key_export_private_warning": "Держите этот ключ в ТАЙНЕ! Любой, кто владеет этим ключом, может потратить ваши средства. Никогда не делитесь им в интернете или с ненадёжными лицами.",
|
||||
"key_export_reveal": "Показать ключ",
|
||||
"key_export_viewing_key": "Ключ просмотра:",
|
||||
"key_export_viewing_warning": "Этот ключ просмотра позволяет другим видеть входящие транзакции и баланс, но НЕ тратить ваши средства. Делитесь только с доверенными лицами.",
|
||||
"label": "Метка:",
|
||||
"language": "Язык",
|
||||
"light": "Светлая",
|
||||
"loading": "Загрузка...",
|
||||
"loading_addresses": "Загрузка адресов...",
|
||||
"local_hashrate": "Локальный хешрейт",
|
||||
"low_spec_mode": "Режим экономии",
|
||||
"market": "Рынок",
|
||||
"market_12h": "12ч",
|
||||
"market_18h": "18ч",
|
||||
"market_24h": "24ч",
|
||||
"market_24h_volume": "ОБЪЁМ 24Ч",
|
||||
"market_6h": "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)}")
|
||||
@@ -0,0 +1,646 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate Chinese Simplified (zh) translations for ObsidianDragon wallet."""
|
||||
import json, os
|
||||
|
||||
translations = {
|
||||
"24h_change": "24小时变化",
|
||||
"24h_volume": "24小时交易量",
|
||||
"about": "关于",
|
||||
"about_block_explorer": "区块浏览器",
|
||||
"about_block_height": "区块高度:",
|
||||
"about_build_date": "构建日期:",
|
||||
"about_build_type": "构建类型:",
|
||||
"about_chain": "链:",
|
||||
"about_connections": "连接数:",
|
||||
"about_credits": "致谢",
|
||||
"about_daemon": "守护进程:",
|
||||
"about_debug": "调试",
|
||||
"about_dragonx": "关于 ObsidianDragon",
|
||||
"about_edition": "ImGui 版本",
|
||||
"about_github": "GitHub",
|
||||
"about_imgui": "ImGui:",
|
||||
"about_license": "许可证",
|
||||
"about_license_text": "本软件根据 GNU 通用公共许可证 v3 (GPLv3) 发布。您可以根据许可证条款自由使用、修改和分发本软件。",
|
||||
"about_peers_count": "%zu 个节点",
|
||||
"about_release": "发布版",
|
||||
"about_title": "关于 ObsidianDragon",
|
||||
"about_version": "版本:",
|
||||
"about_website": "网站",
|
||||
"acrylic": "亚克力",
|
||||
"add": "添加",
|
||||
"address": "地址",
|
||||
"address_book_add": "添加地址",
|
||||
"address_book_add_new": "添加新地址",
|
||||
"address_book_added": "地址已添加到通讯录",
|
||||
"address_book_count": "已保存 %zu 个地址",
|
||||
"address_book_deleted": "条目已删除",
|
||||
"address_book_edit": "编辑地址",
|
||||
"address_book_empty": "没有保存的地址。点击'添加新地址'创建一个。",
|
||||
"address_book_exists": "地址已存在于通讯录中",
|
||||
"address_book_title": "地址簿",
|
||||
"address_book_update_failed": "更新失败——地址可能重复",
|
||||
"address_book_updated": "地址已更新",
|
||||
"address_copied": "地址已复制到剪贴板",
|
||||
"address_details": "地址详情",
|
||||
"address_label": "地址:",
|
||||
"address_upper": "地址",
|
||||
"address_url": "地址 URL",
|
||||
"addresses_appear_here": "连接后,您的接收地址将显示在此处。",
|
||||
"advanced": "高级",
|
||||
"all_filter": "全部",
|
||||
"allow_custom_fees": "允许自定义手续费",
|
||||
"amount": "金额",
|
||||
"amount_details": "金额详情",
|
||||
"amount_exceeds_balance": "金额超过余额",
|
||||
"amount_label": "金额:",
|
||||
"appearance": "外观",
|
||||
"auto_shield": "自动屏蔽挖矿",
|
||||
"available": "可用",
|
||||
"backup_backing_up": "正在备份...",
|
||||
"backup_create": "创建备份",
|
||||
"backup_created": "钱包备份已创建",
|
||||
"backup_data": "备份与数据",
|
||||
"backup_description": "创建 wallet.dat 文件的备份。此文件包含您所有的私钥和交易历史。请将备份存放在安全的地方。",
|
||||
"backup_destination": "备份目标:",
|
||||
"backup_tip_external": "将备份存储在外部驱动器或云存储中",
|
||||
"backup_tip_multiple": "在不同位置创建多个备份",
|
||||
"backup_tip_test": "定期测试从备份恢复",
|
||||
"backup_tips": "提示:",
|
||||
"backup_title": "备份钱包",
|
||||
"backup_wallet": "备份钱包...",
|
||||
"backup_wallet_not_found": "警告:在预期位置未找到 wallet.dat",
|
||||
"balance": "余额",
|
||||
"balance_layout": "余额布局",
|
||||
"ban": "封禁",
|
||||
"banned_peers": "已封禁节点",
|
||||
"block": "区块",
|
||||
"block_bits": "比特:",
|
||||
"block_click_next": "点击查看下一个区块",
|
||||
"block_click_prev": "点击查看上一个区块",
|
||||
"block_explorer": "区块浏览器",
|
||||
"block_get_info": "获取区块信息",
|
||||
"block_hash": "区块哈希:",
|
||||
"block_height": "区块高度:",
|
||||
"block_info_title": "区块信息",
|
||||
"block_merkle_root": "默克尔根:",
|
||||
"block_nav_next": "下一个 >>",
|
||||
"block_nav_prev": "<< 上一个",
|
||||
"block_next": "下一个区块:",
|
||||
"block_previous": "上一个区块:",
|
||||
"block_size": "大小:",
|
||||
"block_timestamp": "时间戳:",
|
||||
"block_transactions": "交易:",
|
||||
"blockchain_syncing": "区块链同步中 (%.1f%%)... 余额可能不准确。",
|
||||
"cancel": "取消",
|
||||
"characters": "字符",
|
||||
"clear": "清除",
|
||||
"clear_all_bans": "解除所有封禁",
|
||||
"clear_form_confirm": "清除所有表单字段?",
|
||||
"clear_request": "清除请求",
|
||||
"click_copy_address": "点击复制地址",
|
||||
"click_copy_uri": "点击复制 URI",
|
||||
"close": "关闭",
|
||||
"conf_count": "%d 确认",
|
||||
"confirm_and_send": "确认并发送",
|
||||
"confirm_send": "确认发送",
|
||||
"confirm_transaction": "确认交易",
|
||||
"confirmations": "确认数",
|
||||
"confirmations_display": "%d 次确认 | %s",
|
||||
"confirmed": "已确认",
|
||||
"connected": "已连接",
|
||||
"connected_peers": "已连接节点",
|
||||
"connecting": "连接中...",
|
||||
"console": "控制台",
|
||||
"console_auto_scroll": "自动滚动",
|
||||
"console_available_commands": "可用命令:",
|
||||
"console_capturing_output": "正在捕获守护进程输出...",
|
||||
"console_clear": "清除",
|
||||
"console_clear_console": "清除控制台",
|
||||
"console_cleared": "控制台已清除",
|
||||
"console_click_commands": "点击上方命令以插入",
|
||||
"console_click_insert": "点击插入",
|
||||
"console_click_insert_params": "点击插入(含参数)",
|
||||
"console_close": "关闭",
|
||||
"console_commands": "命令",
|
||||
"console_common_rpc": "常用 RPC 命令:",
|
||||
"console_completions": "补全:",
|
||||
"console_connected": "已连接到守护进程",
|
||||
"console_copy_all": "全部复制",
|
||||
"console_copy_selected": "复制",
|
||||
"console_daemon": "守护进程",
|
||||
"console_daemon_error": "守护进程错误!",
|
||||
"console_daemon_started": "守护进程已启动",
|
||||
"console_daemon_stopped": "守护进程已停止",
|
||||
"console_disconnected": "已断开与守护进程的连接",
|
||||
"console_errors": "错误",
|
||||
"console_filter_hint": "过滤输出...",
|
||||
"console_help_clear": " clear - 清除控制台",
|
||||
"console_help_getbalance": " getbalance - 显示透明余额",
|
||||
"console_help_getblockcount": " getblockcount - 显示当前区块高度",
|
||||
"console_help_getinfo": " getinfo - 显示节点信息",
|
||||
"console_help_getmininginfo": " getmininginfo - 显示挖矿状态",
|
||||
"console_help_getpeerinfo": " getpeerinfo - 显示已连接节点",
|
||||
"console_help_gettotalbalance": " gettotalbalance - 显示总余额",
|
||||
"console_help_help": " help - 显示此帮助信息",
|
||||
"console_help_setgenerate": " setgenerate - 控制挖矿",
|
||||
"console_help_stop": " stop - 停止守护进程",
|
||||
"console_line_count": "%zu 行",
|
||||
"console_new_lines": "%d 新行",
|
||||
"console_no_daemon": "无守护进程",
|
||||
"console_not_connected": "错误:未连接到守护进程",
|
||||
"console_rpc_reference": "RPC 命令参考",
|
||||
"console_scanline": "控制台扫描线",
|
||||
"console_search_commands": "搜索命令...",
|
||||
"console_select_all": "全选",
|
||||
"console_show_daemon_output": "显示守护进程输出",
|
||||
"console_show_errors_only": "仅显示错误",
|
||||
"console_show_rpc_ref": "显示 RPC 命令参考",
|
||||
"console_showing_lines": "显示 %zu / %zu 行",
|
||||
"console_starting_node": "正在启动节点...",
|
||||
"console_status_error": "错误",
|
||||
"console_status_running": "运行中",
|
||||
"console_status_starting": "启动中",
|
||||
"console_status_stopped": "已停止",
|
||||
"console_status_stopping": "停止中",
|
||||
"console_status_unknown": "未知",
|
||||
"console_tab_completion": "Tab 补全",
|
||||
"console_type_help": "输入 'help' 查看可用命令",
|
||||
"console_welcome": "欢迎使用 ObsidianDragon 控制台",
|
||||
"console_zoom_in": "放大",
|
||||
"console_zoom_out": "缩小",
|
||||
"copy": "复制",
|
||||
"copy_address": "复制完整地址",
|
||||
"copy_error": "复制错误",
|
||||
"copy_to_clipboard": "复制到剪贴板",
|
||||
"copy_txid": "复制交易ID",
|
||||
"copy_uri": "复制 URI",
|
||||
"current_price": "当前价格",
|
||||
"custom_fees": "自定义手续费",
|
||||
"dark": "深色",
|
||||
"date": "日期",
|
||||
"date_label": "日期:",
|
||||
"delete": "删除",
|
||||
"difficulty": "难度",
|
||||
"disconnected": "已断开",
|
||||
"dismiss": "关闭",
|
||||
"display": "显示",
|
||||
"dragonx_green": "DragonX(绿色)",
|
||||
"edit": "编辑",
|
||||
"error": "错误",
|
||||
"est_time_to_block": "预计出块时间",
|
||||
"exit": "退出",
|
||||
"explorer": "浏览器",
|
||||
"export": "导出",
|
||||
"export_csv": "导出 CSV",
|
||||
"export_keys_btn": "导出密钥",
|
||||
"export_keys_danger": "危险:这将导出您钱包中的所有私钥!任何获得此文件的人都可以窃取您的资金。请安全保管并在使用后删除。",
|
||||
"export_keys_include_t": "包含 T 地址(透明)",
|
||||
"export_keys_include_z": "包含 Z 地址(屏蔽)",
|
||||
"export_keys_options": "导出选项:",
|
||||
"export_keys_success": "密钥导出成功",
|
||||
"export_keys_title": "导出所有私钥",
|
||||
"export_private_key": "导出私钥",
|
||||
"export_tx_count": "导出 %zu 笔交易到 CSV 文件。",
|
||||
"export_tx_file_fail": "无法创建 CSV 文件",
|
||||
"export_tx_none": "没有交易可导出",
|
||||
"export_tx_success": "交易导出成功",
|
||||
"export_tx_title": "导出交易到 CSV",
|
||||
"export_viewing_key": "导出查看密钥",
|
||||
"failed_create_shielded": "无法创建屏蔽地址",
|
||||
"failed_create_transparent": "无法创建透明地址",
|
||||
"fee": "手续费",
|
||||
"fee_high": "高",
|
||||
"fee_label": "手续费:",
|
||||
"fee_low": "低",
|
||||
"fee_normal": "普通",
|
||||
"fetch_prices": "获取价格",
|
||||
"file": "文件",
|
||||
"file_save_location": "文件将保存至:~/.config/ObsidianDragon/",
|
||||
"font_scale": "字体大小",
|
||||
"from": "从",
|
||||
"from_upper": "从",
|
||||
"full_details": "完整详情",
|
||||
"general": "常规",
|
||||
"go_to_receive": "前往接收",
|
||||
"height": "高度",
|
||||
"help": "帮助",
|
||||
"hide": "隐藏",
|
||||
"history": "历史",
|
||||
"immature_type": "未成熟",
|
||||
"import": "导入",
|
||||
"import_key_btn": "导入密钥",
|
||||
"import_key_formats": "支持的密钥格式:",
|
||||
"import_key_full_rescan": "(0 = 完整重扫)",
|
||||
"import_key_label": "私钥:",
|
||||
"import_key_no_valid": "输入中未找到有效密钥",
|
||||
"import_key_rescan": "导入后重新扫描区块链",
|
||||
"import_key_start_height": "起始高度:",
|
||||
"import_key_success": "密钥导入成功",
|
||||
"import_key_t_format": "T 地址 WIF 私钥",
|
||||
"import_key_title": "导入私钥",
|
||||
"import_key_tooltip": "输入一个或多个私钥,每行一个。\n支持 z 地址和 t 地址密钥。\n以 # 开头的行视为注释。",
|
||||
"import_key_warning": "警告:切勿分享您的私钥!从不可信来源导入密钥可能会危及您的钱包安全。",
|
||||
"import_key_z_format": "Z 地址花费密钥 (secret-extended-key-...)",
|
||||
"import_private_key": "导入私钥...",
|
||||
"invalid_address": "无效的地址格式",
|
||||
"ip_address": "IP 地址",
|
||||
"keep": "保留",
|
||||
"keep_daemon": "保持守护进程运行",
|
||||
"key_export_fetching": "正在从钱包获取密钥...",
|
||||
"key_export_private_key": "私钥:",
|
||||
"key_export_private_warning": "请保密此密钥!任何拥有此密钥的人都可以花费您的资金。切勿在网上或与不可信的人分享。",
|
||||
"key_export_reveal": "显示密钥",
|
||||
"key_export_viewing_key": "查看密钥:",
|
||||
"key_export_viewing_warning": "此查看密钥允许他人查看您的入账交易和余额,但不能花费您的资金。仅与信任的人分享。",
|
||||
"label": "标签:",
|
||||
"language": "语言",
|
||||
"light": "浅色",
|
||||
"loading": "加载中...",
|
||||
"loading_addresses": "正在加载地址...",
|
||||
"local_hashrate": "本地算力",
|
||||
"low_spec_mode": "低配模式",
|
||||
"market": "市场",
|
||||
"market_12h": "12小时",
|
||||
"market_18h": "18小时",
|
||||
"market_24h": "24小时",
|
||||
"market_24h_volume": "24小时交易量",
|
||||
"market_6h": "6小时",
|
||||
"market_attribution": "价格数据来自 NonKYC",
|
||||
"market_btc_price": "BTC 价格",
|
||||
"market_cap": "市值",
|
||||
"market_no_history": "无价格历史",
|
||||
"market_no_price": "无价格数据",
|
||||
"market_now": "现在",
|
||||
"market_pct_shielded": "%.0f%% 屏蔽",
|
||||
"market_portfolio": "投资组合",
|
||||
"market_price_unavailable": "价格数据不可用",
|
||||
"market_refresh_price": "刷新价格数据",
|
||||
"market_trade_on": "在 %s 交易",
|
||||
"mature": "已成熟",
|
||||
"max": "最大",
|
||||
"memo": "备注(可选,加密)",
|
||||
"memo_label": "备注:",
|
||||
"memo_optional": "备注(可选)",
|
||||
"memo_upper": "备注",
|
||||
"memo_z_only": "注意:备注仅在发送到屏蔽 (z) 地址时可用",
|
||||
"merge_description": "将多个 UTXO 合并到一个屏蔽地址。这可以帮助减小钱包大小并提高隐私性。",
|
||||
"merge_funds": "合并资金",
|
||||
"merge_started": "合并操作已开始",
|
||||
"merge_title": "合并到地址",
|
||||
"mine_when_idle": "空闲时挖矿",
|
||||
"mined": "已挖得",
|
||||
"mined_filter": "已挖得",
|
||||
"mined_type": "已挖得",
|
||||
"mined_upper": "已挖得",
|
||||
"miner_fee": "矿工费",
|
||||
"mining": "挖矿",
|
||||
"mining_active": "活跃",
|
||||
"mining_address_copied": "挖矿地址已复制",
|
||||
"mining_all_time": "所有时间",
|
||||
"mining_already_saved": "矿池 URL 已保存",
|
||||
"mining_block_copied": "区块哈希已复制",
|
||||
"mining_chart_1m_ago": "1分钟前",
|
||||
"mining_chart_5m_ago": "5分钟前",
|
||||
"mining_chart_now": "现在",
|
||||
"mining_chart_start": "开始",
|
||||
"mining_click": "点击",
|
||||
"mining_click_copy_address": "点击复制地址",
|
||||
"mining_click_copy_block": "点击复制区块哈希",
|
||||
"mining_click_copy_difficulty": "点击复制难度",
|
||||
"mining_connected": "已连接",
|
||||
"mining_connecting": "连接中...",
|
||||
"mining_control": "挖矿控制",
|
||||
"mining_difficulty_copied": "难度已复制",
|
||||
"mining_est_block": "预计区块",
|
||||
"mining_est_daily": "预计日收益",
|
||||
"mining_filter_all": "全部",
|
||||
"mining_filter_tip_all": "显示所有收益",
|
||||
"mining_filter_tip_pool": "仅显示矿池收益",
|
||||
"mining_filter_tip_solo": "仅显示单人收益",
|
||||
"mining_idle_off_tooltip": "启用空闲挖矿",
|
||||
"mining_idle_on_tooltip": "禁用空闲挖矿",
|
||||
"mining_local_hashrate": "本地算力",
|
||||
"mining_mine": "挖矿",
|
||||
"mining_mining_addr": "挖矿地址",
|
||||
"mining_network": "网络",
|
||||
"mining_no_blocks_yet": "尚未找到区块",
|
||||
"mining_no_payouts_yet": "尚无矿池支付",
|
||||
"mining_no_saved_addresses": "没有保存的地址",
|
||||
"mining_no_saved_pools": "没有保存的矿池",
|
||||
"mining_off": "挖矿已关闭",
|
||||
"mining_on": "挖矿已开启",
|
||||
"mining_open_in_explorer": "在浏览器中打开",
|
||||
"mining_payout_address": "支付地址",
|
||||
"mining_payout_tooltip": "接收挖矿奖励的地址",
|
||||
"mining_pool": "矿池",
|
||||
"mining_pool_hashrate": "矿池算力",
|
||||
"mining_pool_url": "矿池 URL",
|
||||
"mining_recent_blocks": "最近区块",
|
||||
"mining_recent_payouts": "最近矿池支付",
|
||||
"mining_remove": "移除",
|
||||
"mining_reset_defaults": "重置默认值",
|
||||
"mining_save_payout_address": "保存支付地址",
|
||||
"mining_save_pool_url": "保存矿池 URL",
|
||||
"mining_saved_addresses": "已保存地址:",
|
||||
"mining_saved_pools": "已保存矿池:",
|
||||
"mining_shares": "份额",
|
||||
"mining_show_chart": "图表",
|
||||
"mining_show_log": "日志",
|
||||
"mining_solo": "单人",
|
||||
"mining_starting": "启动中...",
|
||||
"mining_starting_tooltip": "矿工正在启动...",
|
||||
"mining_statistics": "挖矿统计",
|
||||
"mining_stop": "停止",
|
||||
"mining_stop_solo_for_pool": "启动矿池挖矿前请先停止单人挖矿",
|
||||
"mining_stop_solo_for_pool_settings": "请停止单人挖矿以更改矿池设置",
|
||||
"mining_stopping": "停止中...",
|
||||
"mining_stopping_tooltip": "矿工正在停止...",
|
||||
"mining_syncing_tooltip": "区块链同步中...",
|
||||
"mining_threads": "挖矿线程",
|
||||
"mining_to_save": "保存",
|
||||
"mining_today": "今天",
|
||||
"mining_uptime": "运行时间",
|
||||
"mining_yesterday": "昨天",
|
||||
"network": "网络",
|
||||
"network_fee": "网络手续费",
|
||||
"network_hashrate": "全网算力",
|
||||
"new": "+ 新建",
|
||||
"new_shielded_created": "新屏蔽地址已创建",
|
||||
"new_t_address": "新 T 地址",
|
||||
"new_t_transparent": "新 t 地址(透明)",
|
||||
"new_transparent_created": "新透明地址已创建",
|
||||
"new_z_address": "新 Z 地址",
|
||||
"new_z_shielded": "新 z 地址(屏蔽)",
|
||||
"no_addresses": "未找到地址。请使用上方按钮创建一个。",
|
||||
"no_addresses_available": "无可用地址",
|
||||
"no_addresses_match": "没有匹配过滤器的地址",
|
||||
"no_addresses_with_balance": "没有有余额的地址",
|
||||
"no_matching": "没有匹配的交易",
|
||||
"no_recent_receives": "没有最近的接收",
|
||||
"no_recent_sends": "没有最近的发送",
|
||||
"no_transactions": "未找到交易",
|
||||
"node": "节点",
|
||||
"node_security": "节点与安全",
|
||||
"noise": "噪点",
|
||||
"not_connected": "未连接到守护进程...",
|
||||
"not_connected_to_daemon": "未连接到守护进程",
|
||||
"notes": "备注",
|
||||
"notes_optional": "备注(可选):",
|
||||
"output_filename": "输出文件名:",
|
||||
"overview": "概览",
|
||||
"paste": "粘贴",
|
||||
"paste_from_clipboard": "从剪贴板粘贴",
|
||||
"pay_from": "付款来源",
|
||||
"payment_request": "付款请求",
|
||||
"payment_request_copied": "付款请求已复制",
|
||||
"payment_uri_copied": "付款 URI 已复制",
|
||||
"peers": "节点",
|
||||
"peers_avg_ping": "平均延迟",
|
||||
"peers_ban_24h": "封禁节点 24 小时",
|
||||
"peers_ban_score": "封禁评分:%d",
|
||||
"peers_banned": "已封禁",
|
||||
"peers_banned_count": "已封禁:%d",
|
||||
"peers_best_block": "最佳区块",
|
||||
"peers_blockchain": "区块链",
|
||||
"peers_blocks": "区块",
|
||||
"peers_blocks_left": "剩余 %d 个区块",
|
||||
"peers_clear_all_bans": "解除所有封禁",
|
||||
"peers_click_copy": "点击复制",
|
||||
"peers_connected": "已连接",
|
||||
"peers_connected_count": "已连接:%d",
|
||||
"peers_copy_ip": "复制 IP",
|
||||
"peers_dir_in": "入",
|
||||
"peers_dir_out": "出",
|
||||
"peers_hash_copied": "哈希已复制",
|
||||
"peers_hashrate": "算力",
|
||||
"peers_in_out": "入/出",
|
||||
"peers_longest": "最长",
|
||||
"peers_longest_chain": "最长链",
|
||||
"peers_memory": "内存",
|
||||
"peers_no_banned": "无已封禁节点",
|
||||
"peers_no_connected": "无已连接节点",
|
||||
"peers_no_tls": "无 TLS",
|
||||
"peers_notarized": "已公证",
|
||||
"peers_p2p_port": "P2P 端口",
|
||||
"peers_protocol": "协议",
|
||||
"peers_received": "已接收",
|
||||
"peers_refresh": "刷新",
|
||||
"peers_refresh_tooltip": "刷新节点列表",
|
||||
"peers_refreshing": "刷新中...",
|
||||
"peers_sent": "已发送",
|
||||
"peers_tt_id": "ID:%d",
|
||||
"peers_tt_received": "已接收:%s",
|
||||
"peers_tt_sent": "已发送:%s",
|
||||
"peers_tt_services": "服务:%s",
|
||||
"peers_tt_start_height": "起始高度:%d",
|
||||
"peers_tt_synced": "已同步 H/B:%d/%d",
|
||||
"peers_tt_tls_cipher": "TLS:%s",
|
||||
"peers_unban": "解除封禁",
|
||||
"peers_upper": "节点",
|
||||
"peers_version": "版本",
|
||||
"pending": "待处理",
|
||||
"ping": "延迟",
|
||||
"price_chart": "价格图表",
|
||||
"qr_code": "二维码",
|
||||
"qr_failed": "无法生成二维码",
|
||||
"qr_title": "二维码",
|
||||
"qr_unavailable": "二维码不可用",
|
||||
"receive": "接收",
|
||||
"received": "已接收",
|
||||
"received_filter": "已接收",
|
||||
"received_label": "已接收",
|
||||
"received_upper": "已接收",
|
||||
"receiving_addresses": "您的接收地址",
|
||||
"recent_received": "最近接收",
|
||||
"recent_sends": "最近发送",
|
||||
"recipient": "收款方",
|
||||
"recv_type": "接收",
|
||||
"refresh": "刷新",
|
||||
"refresh_now": "立即刷新",
|
||||
"report_bug": "报告错误",
|
||||
"request_amount": "金额(可选):",
|
||||
"request_copy_uri": "复制 URI",
|
||||
"request_description": "生成一个付款请求,他人可以扫描或复制。二维码包含您的地址和可选的金额/备注。",
|
||||
"request_label": "标签(可选):",
|
||||
"request_memo": "备注(可选):",
|
||||
"request_payment": "请求付款",
|
||||
"request_payment_uri": "付款 URI:",
|
||||
"request_receive_address": "接收地址:",
|
||||
"request_select_address": "选择地址...",
|
||||
"request_shielded_addrs": "-- 屏蔽地址 --",
|
||||
"request_title": "请求付款",
|
||||
"request_transparent_addrs": "-- 透明地址 --",
|
||||
"request_uri_copied": "付款 URI 已复制到剪贴板",
|
||||
"rescan": "重新扫描",
|
||||
"reset_to_defaults": "重置为默认值",
|
||||
"review_send": "审核发送",
|
||||
"rpc_host": "RPC 主机",
|
||||
"rpc_pass": "密码",
|
||||
"rpc_port": "端口",
|
||||
"rpc_user": "用户名",
|
||||
"save": "保存",
|
||||
"save_settings": "保存设置",
|
||||
"save_z_transactions": "将 Z 交易保存到列表",
|
||||
"search_placeholder": "搜索...",
|
||||
"security": "安全",
|
||||
"select_address": "选择地址...",
|
||||
"select_receiving_address": "选择接收地址...",
|
||||
"select_source_address": "选择来源地址...",
|
||||
"send": "发送",
|
||||
"send_amount": "金额",
|
||||
"send_amount_details": "金额详情",
|
||||
"send_amount_upper": "金额",
|
||||
"send_clear_fields": "清除所有表单字段?",
|
||||
"send_copy_error": "复制错误",
|
||||
"send_dismiss": "关闭",
|
||||
"send_error_copied": "错误已复制到剪贴板",
|
||||
"send_error_prefix": "错误:%s",
|
||||
"send_exceeds_available": "超过可用额 (%.8f)",
|
||||
"send_fee": "手续费",
|
||||
"send_fee_high": "高",
|
||||
"send_fee_low": "低",
|
||||
"send_fee_normal": "普通",
|
||||
"send_form_restored": "表单已恢复",
|
||||
"send_from_this_address": "从此地址发送",
|
||||
"send_go_to_receive": "前往接收",
|
||||
"send_keep": "保留",
|
||||
"send_network_fee": "网络手续费",
|
||||
"send_no_balance": "无余额",
|
||||
"send_no_recent": "没有最近的发送",
|
||||
"send_recent_sends": "最近发送",
|
||||
"send_recipient": "收款方",
|
||||
"send_select_source": "选择来源地址...",
|
||||
"send_sending_from": "发送来源",
|
||||
"send_submitting": "正在提交交易...",
|
||||
"send_switch_to_receive": "切换到接收页面获取您的地址并开始接收资金。",
|
||||
"send_to": "发送至",
|
||||
"send_tooltip_enter_amount": "请输入发送金额",
|
||||
"send_tooltip_exceeds_balance": "金额超过可用余额",
|
||||
"send_tooltip_in_progress": "交易正在进行中",
|
||||
"send_tooltip_invalid_address": "请输入有效的收款地址",
|
||||
"send_tooltip_not_connected": "未连接到守护进程",
|
||||
"send_tooltip_select_source": "请先选择来源地址",
|
||||
"send_tooltip_syncing": "请等待区块链同步",
|
||||
"send_total": "合计",
|
||||
"send_transaction": "发送交易",
|
||||
"send_tx_failed": "交易失败",
|
||||
"send_tx_sent": "交易已发送!",
|
||||
"send_tx_success": "交易发送成功!",
|
||||
"send_txid_copied": "交易ID 已复制到剪贴板",
|
||||
"send_txid_label": "TxID:%s",
|
||||
"send_valid_shielded": "有效的屏蔽地址",
|
||||
"send_valid_transparent": "有效的透明地址",
|
||||
"send_wallet_empty": "您的钱包是空的",
|
||||
"send_yes_clear": "是,清除",
|
||||
"sending": "正在发送交易",
|
||||
"sending_from": "发送来源",
|
||||
"sent": "已发送",
|
||||
"sent_filter": "已发送",
|
||||
"sent_type": "已发送",
|
||||
"sent_upper": "已发送",
|
||||
"settings": "设置",
|
||||
"setup_wizard": "设置向导",
|
||||
"share": "分享",
|
||||
"shield_check_status": "检查状态",
|
||||
"shield_completed": "操作成功完成!",
|
||||
"shield_description": "通过将透明地址的 coinbase 输出发送到屏蔽地址来屏蔽您的挖矿奖励。这可以隐藏您的挖矿收入,提高隐私性。",
|
||||
"shield_from_address": "从地址:",
|
||||
"shield_funds": "屏蔽资金",
|
||||
"shield_in_progress": "操作进行中...",
|
||||
"shield_max_utxos": "每次操作最大 UTXO 数",
|
||||
"shield_merge_done": "屏蔽/合并完成!",
|
||||
"shield_select_z": "选择 z 地址...",
|
||||
"shield_started": "屏蔽操作已开始",
|
||||
"shield_title": "屏蔽 Coinbase 奖励",
|
||||
"shield_to_address": "至地址(屏蔽):",
|
||||
"shield_utxo_limit": "UTXO 限制:",
|
||||
"shield_wildcard_hint": "使用 '*' 从所有透明地址屏蔽",
|
||||
"shielded": "屏蔽",
|
||||
"shielded_to": "屏蔽至",
|
||||
"shielded_type": "屏蔽",
|
||||
"show": "显示",
|
||||
"show_qr_code": "显示二维码",
|
||||
"showing_transactions": "显示第 %d\xe2\x80\x93%d 笔,共 %d 笔交易(总计:%zu)",
|
||||
"simple_background": "简单背景",
|
||||
"start_mining": "开始挖矿",
|
||||
"status": "状态",
|
||||
"stop_external": "停止外部守护进程",
|
||||
"stop_mining": "停止挖矿",
|
||||
"submitting_transaction": "正在提交交易...",
|
||||
"success": "成功",
|
||||
"summary": "摘要",
|
||||
"syncing": "同步中...",
|
||||
"t_addresses": "T 地址",
|
||||
"test_connection": "测试",
|
||||
"theme": "主题",
|
||||
"theme_effects": "主题效果",
|
||||
"time_days_ago": "%d 天前",
|
||||
"time_hours_ago": "%d 小时前",
|
||||
"time_minutes_ago": "%d 分钟前",
|
||||
"time_seconds_ago": "%d 秒前",
|
||||
"to": "至",
|
||||
"to_upper": "至",
|
||||
"tools": "工具",
|
||||
"total": "合计",
|
||||
"transaction_id": "交易 ID",
|
||||
"transaction_sent": "交易发送成功",
|
||||
"transaction_sent_msg": "交易已发送!",
|
||||
"transaction_url": "交易 URL",
|
||||
"transactions": "交易",
|
||||
"transactions_upper": "交易",
|
||||
"transparent": "透明",
|
||||
"tx_confirmations": "%d 次确认",
|
||||
"tx_details_title": "交易详情",
|
||||
"tx_from_address": "发送地址:",
|
||||
"tx_id_label": "交易 ID:",
|
||||
"tx_immature": "未成熟",
|
||||
"tx_mined": "已挖得",
|
||||
"tx_received": "已接收",
|
||||
"tx_sent": "已发送",
|
||||
"tx_to_address": "接收地址:",
|
||||
"tx_view_explorer": "在浏览器中查看",
|
||||
"txs_count": "%d 笔交易",
|
||||
"type": "类型",
|
||||
"ui_opacity": "界面透明度",
|
||||
"unban": "解除封禁",
|
||||
"unconfirmed": "未确认",
|
||||
"undo_clear": "撤销清除",
|
||||
"unknown": "未知",
|
||||
"use_embedded_daemon": "使用内置 dragonxd",
|
||||
"use_tor": "使用 Tor",
|
||||
"validate_btn": "验证",
|
||||
"validate_description": "输入一个 DragonX 地址来检查它是否有效以及是否属于此钱包。",
|
||||
"validate_invalid": "无效",
|
||||
"validate_is_mine": "此钱包拥有该地址",
|
||||
"validate_not_mine": "不属于此钱包",
|
||||
"validate_ownership": "所有权:",
|
||||
"validate_results": "结果:",
|
||||
"validate_shielded_type": "屏蔽(z 地址)",
|
||||
"validate_status": "状态:",
|
||||
"validate_title": "验证地址",
|
||||
"validate_transparent_type": "透明(t 地址)",
|
||||
"validate_type": "类型:",
|
||||
"validate_valid": "有效",
|
||||
"validating": "验证中...",
|
||||
"verbose_logging": "详细日志",
|
||||
"version": "版本",
|
||||
"view": "查看",
|
||||
"view_details": "查看详情",
|
||||
"view_on_explorer": "在浏览器中查看",
|
||||
"waiting_for_daemon": "等待守护进程连接...",
|
||||
"wallet": "钱包",
|
||||
"wallet_empty": "您的钱包是空的",
|
||||
"wallet_empty_hint": "切换到接收页面获取您的地址并开始接收资金。",
|
||||
"warning": "警告",
|
||||
"warning_upper": "警告!",
|
||||
"website": "网站",
|
||||
"window_opacity": "窗口透明度",
|
||||
"yes_clear": "是,清除",
|
||||
"your_addresses": "您的地址",
|
||||
"z_addresses": "Z 地址",
|
||||
}
|
||||
|
||||
out = os.path.join(os.path.dirname(__file__), "..", "res", "lang", "zh.json")
|
||||
with open(out, "w", encoding="utf-8") as f:
|
||||
json.dump(translations, f, indent=4, ensure_ascii=False, sort_keys=True)
|
||||
print(f"Wrote {len(translations)} Chinese translations to {os.path.abspath(out)}")
|
||||
@@ -256,8 +256,8 @@ HEADER_START
|
||||
echo -e "${YELLOW}Note: Daemon binaries not found in prebuilt-binaries/dragonxd-win/ — wallet only${NC}"
|
||||
fi
|
||||
|
||||
# ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ────────────────
|
||||
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
||||
XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
if [ -f "$XMRIG_DIR/xmrig.exe" ]; then
|
||||
cp -f "$XMRIG_DIR/xmrig.exe" "$EMBED_RES_DIR/xmrig.exe"
|
||||
echo " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Package the prebuilt dragonx full-node binaries into per-platform release archives and sign them
|
||||
# for the wallet's in-app daemon updater (ed25519 over the EXACT archive bytes).
|
||||
#
|
||||
# The wallet verifies a detached ed25519 signature over the archive bytes against a public key
|
||||
# pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is MANDATORY
|
||||
# (kDaemonRequireSignature = true): an in-app update is refused unless a valid "<archive>.sig" is
|
||||
# published next to the archive. The wallet also checks each archive's SHA-256 against a markdown
|
||||
# checksum table in the release body, so `release` prints that table for you to paste in.
|
||||
#
|
||||
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl. OpenSSL's ed25519 is PureEdDSA (RFC 8032), the
|
||||
# same primitive libsodium's crypto_sign_verify_detached checks, so the signatures are compatible.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
||||
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||
# scripts/sign-daemon-release.sh sign <secret.key> <file>... # sign existing files -> <file>.sig
|
||||
# scripts/sign-daemon-release.sh release <secret.key> <version> [--src DIR] [--out DIR]
|
||||
# # zip prebuilt-binaries/dragonxd-{linux,mac,win}/ into dragonx-<version>-{linux-amd64,macos,
|
||||
# # win64}.zip, sign each, and print the SHA-256 checksum table. Platforms with no dragonxd
|
||||
# # binary staged are skipped.
|
||||
#
|
||||
# Keep the secret key (.ed25519.key) OFFLINE (mode 600). Paste the base64 public key into
|
||||
# kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h.
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
|
||||
|
||||
# Raw 32-byte ed25519 public key (base64) from a private key file. The DER SubjectPublicKeyInfo for
|
||||
# ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key.
|
||||
pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; }
|
||||
|
||||
sha256_of() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}';
|
||||
else shasum -a 256 "$1" | awk '{print $1}'; fi
|
||||
}
|
||||
|
||||
# Detached ed25519 signature over the raw file bytes -> <file>.sig (base64 of the 64-byte sig).
|
||||
sign_file() {
|
||||
local key="$1" f="$2" raw
|
||||
raw="$(mktemp)"
|
||||
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
|
||||
openssl base64 -A -in "$raw" > "$f.sig"
|
||||
printf '\n' >> "$f.sig"
|
||||
rm -f "$raw"
|
||||
}
|
||||
|
||||
# platform -> (staging dir under prebuilt-binaries, release token, expected daemon binary name)
|
||||
plat_dir() { case "$1" in linux) echo dragonxd-linux;; mac) echo dragonxd-mac;; win) echo dragonxd-win;; esac; }
|
||||
plat_token() { case "$1" in linux) echo linux-amd64;; mac) echo macos;; win) echo win64;; esac; }
|
||||
plat_daemon() { case "$1" in win) echo dragonxd.exe;; *) echo dragonxd;; esac; }
|
||||
|
||||
cmd="${1:-}"; shift || true
|
||||
case "$cmd" in
|
||||
keygen)
|
||||
prefix="${1:-dragonx-daemon}"
|
||||
[ -e "$prefix.ed25519.key" ] && die "$prefix.ed25519.key already exists — refusing to overwrite"
|
||||
openssl genpkey -algorithm ed25519 -out "$prefix.ed25519.key"
|
||||
chmod 600 "$prefix.ed25519.key"
|
||||
pub="$(pubkey_b64 "$prefix.ed25519.key")"
|
||||
printf '%s\n' "$pub" > "$prefix.ed25519.pub.b64"
|
||||
echo "secret key : $prefix.ed25519.key (KEEP OFFLINE, mode 600)"
|
||||
echo "public key : $prefix.ed25519.pub.b64"
|
||||
echo
|
||||
echo "Pin this in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64):"
|
||||
echo " $pub"
|
||||
;;
|
||||
|
||||
pubkey)
|
||||
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
|
||||
pubkey_b64 "$1"
|
||||
;;
|
||||
|
||||
sign)
|
||||
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
|
||||
key="$1"; shift
|
||||
[ -f "$key" ] || die "no such key: $key"
|
||||
for f in "$@"; do
|
||||
[ -f "$f" ] || die "no such file: $f"
|
||||
sign_file "$key" "$f"
|
||||
echo "signed: $f -> $f.sig"
|
||||
done
|
||||
echo "Upload each .sig as a release asset next to its archive."
|
||||
;;
|
||||
|
||||
release)
|
||||
[ $# -ge 2 ] || die "usage: release <secret.key> <version> [--src DIR] [--out DIR]"
|
||||
key="$1"; version="$2"; shift 2
|
||||
src="$PROJECT_ROOT/prebuilt-binaries"
|
||||
out="$PROJECT_ROOT/release/daemon"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--src) [ $# -ge 2 ] || die "--src needs a value"; src="$2"; shift 2 ;;
|
||||
--out) [ $# -ge 2 ] || die "--out needs a value"; out="$2"; shift 2 ;;
|
||||
*) die "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
[ -f "$key" ] || die "no such key: $key"
|
||||
[ -d "$src" ] || die "no such source dir: $src"
|
||||
command -v zip >/dev/null 2>&1 || die "zip not found (install 'zip')"
|
||||
mkdir -p "$out"
|
||||
|
||||
# Sanity: warn if this key does not match the public key pinned in the wallet (the wallet would
|
||||
# then reject every signature made with it — only expected when deliberately rotating the key).
|
||||
pinned="$(grep -oE '"[A-Za-z0-9+/]{43}="' "$PROJECT_ROOT/src/util/daemon_updater.h" 2>/dev/null | head -1 | tr -d '"')"
|
||||
mine="$(pubkey_b64 "$key")"
|
||||
if [ -n "$pinned" ] && [ "$pinned" != "$mine" ]; then
|
||||
echo "WARNING: this key's public key does not match the one pinned in daemon_updater.h:" >&2
|
||||
echo " signing key -> $mine" >&2
|
||||
echo " pinned key -> $pinned" >&2
|
||||
echo " The wallet will REJECT these signatures unless you are rotating the pinned key." >&2
|
||||
echo >&2
|
||||
fi
|
||||
|
||||
made=0
|
||||
table=""
|
||||
for plat in linux mac win; do
|
||||
d="$src/$(plat_dir "$plat")"
|
||||
daemon="$d/$(plat_daemon "$plat")"
|
||||
if [ ! -f "$daemon" ]; then
|
||||
echo "skip $plat: no $(plat_daemon "$plat") staged in $d" >&2
|
||||
continue
|
||||
fi
|
||||
archive="dragonx-$version-$(plat_token "$plat").zip"
|
||||
apath="$out/$archive"
|
||||
rm -f "$apath"
|
||||
# Zip the staged files at the archive root (binaries + sapling params + asmap), excluding
|
||||
# the .gitkeep placeholder. The updater flattens paths via baseName(), so a flat zip is fine.
|
||||
files=()
|
||||
while IFS= read -r fn; do files+=("$fn"); done < <(cd "$d" && ls -A | grep -vx '.gitkeep')
|
||||
[ "${#files[@]}" -gt 0 ] || { echo "skip $plat: nothing to package in $d" >&2; continue; }
|
||||
( cd "$d" && zip -q -X "$apath" "${files[@]}" )
|
||||
sign_file "$key" "$apath"
|
||||
sum="$(sha256_of "$apath")"
|
||||
table+="| $archive | \`$sum\` |"$'\n'
|
||||
echo "packaged + signed: $apath (+ .sig) sha256=$sum"
|
||||
made=$((made + 1))
|
||||
done
|
||||
[ "$made" -gt 0 ] || die "no platform had a staged daemon binary under $src/dragonxd-{linux,mac,win}/"
|
||||
|
||||
echo
|
||||
echo "Checksum table (paste into the release body so the wallet can verify SHA-256):"
|
||||
echo "| Archive | SHA-256 |"
|
||||
echo "|---|---|"
|
||||
printf '%s' "$table"
|
||||
echo
|
||||
echo "Upload each .zip AND its .zip.sig as release assets. Wallet enforces the ed25519 signature"
|
||||
echo "(kDaemonRequireSignature=true) and the SHA-256 from the table above."
|
||||
;;
|
||||
|
||||
*)
|
||||
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>... | release <secret.key> <version> [--src DIR] [--out DIR]}"
|
||||
;;
|
||||
esac
|
||||
@@ -1,79 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cross-compile the test suite (ObsidianDragonTests.exe) and the fake daemon (tools/fake_dragonxd)
|
||||
# for Windows with mingw-w64, then run the tests: directly under WSL (Windows interop), else with
|
||||
# wine if installed, else just build.
|
||||
#
|
||||
# scripts/test-windows.sh [--no-run] [--jobs N]
|
||||
#
|
||||
# Uses its own build/windows-tests tree (full-node config), so build/windows is left alone. Needs
|
||||
# the mingw toolchain file that ./build.sh --win-release writes to build/windows/. Dependency sources
|
||||
# already fetched into build/windows/_deps are reused, so nothing is downloaded twice.
|
||||
set -euo pipefail
|
||||
|
||||
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
bd="$repo/build/windows-tests"
|
||||
toolchain="$repo/build/windows/mingw-toolchain.cmake"
|
||||
deps="$repo/build/windows/_deps"
|
||||
run=true
|
||||
jobs=8 # each test TU is large; several parallel mingw builds can exhaust RAM
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-run) run=false; shift ;;
|
||||
--jobs) jobs="$2"; shift 2 ;;
|
||||
-h|--help) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "unknown option: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -f "$toolchain" ]]; then
|
||||
echo "missing $toolchain; run ./build.sh --win-release once to create the mingw toolchain" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
src_args=()
|
||||
for dep in sdl3 json tomlplusplus sqlite3 curl libwebp; do
|
||||
if [[ -d "$deps/$dep-src" ]]; then
|
||||
src_args+=("-DFETCHCONTENT_SOURCE_DIR_${dep^^}=$deps/$dep-src")
|
||||
fi
|
||||
done
|
||||
|
||||
cmake -S "$repo" -B "$bd" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$toolchain" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||
-DDRAGONX_BUILD_LITE=OFF \
|
||||
-DDRAGONX_ENABLE_LITE_BACKEND=OFF \
|
||||
-DBUILD_TESTING=ON \
|
||||
-DDRAGONX_BUILD_FAKE_DAEMON=ON \
|
||||
"${src_args[@]}"
|
||||
cmake --build "$bd" --target ObsidianDragonTests fake_dragonxd -j "$jobs"
|
||||
|
||||
exe="$bd/bin/ObsidianDragonTests.exe"
|
||||
echo "built: $exe"
|
||||
echo "built: $bd/fake_dragonxd/dragonxd.exe"
|
||||
$run || exit 0
|
||||
|
||||
# The fixture path compiled into the binary is a Linux path; hand the Windows process its own.
|
||||
# APPDATA/LOCALAPPDATA point at a sandbox so anything resolving them from the environment stays out
|
||||
# of the real profile. (SHGetFolderPath-based paths ignore the environment; the tests that would
|
||||
# write through those are compiled out on Windows.)
|
||||
export DRAGONX_TEST_FIXTURE_DIR="$repo/tests/fixtures"
|
||||
sandbox="$bd/test-appdata"
|
||||
mkdir -p "$sandbox"
|
||||
export APPDATA="$sandbox" LOCALAPPDATA="$sandbox"
|
||||
# The fake dragonxd.exe plays a "foreign" node (isolated datadir/port) for the never-kill-by-name test.
|
||||
export DRAGONX_TEST_FAKE_DAEMON="$bd/fake_dragonxd/dragonxd.exe"
|
||||
if grep -qi microsoft /proc/version 2>/dev/null && [[ -e /proc/sys/fs/binfmt_misc/WSLInterop ]]; then
|
||||
# /p: translate each to a Windows path for the Windows process
|
||||
export WSLENV="${WSLENV:+$WSLENV:}DRAGONX_TEST_FIXTURE_DIR/p:APPDATA/p:LOCALAPPDATA/p:DRAGONX_TEST_FAKE_DAEMON/p"
|
||||
cd "$bd/bin"
|
||||
exec "$exe"
|
||||
elif command -v wine >/dev/null 2>&1; then
|
||||
export DRAGONX_TEST_FIXTURE_DIR="$(winepath -w "$DRAGONX_TEST_FIXTURE_DIR")"
|
||||
export DRAGONX_TEST_FAKE_DAEMON="$(winepath -w "$DRAGONX_TEST_FAKE_DAEMON")"
|
||||
cd "$bd/bin"
|
||||
exec wine "$exe"
|
||||
else
|
||||
echo "not on WSL and no wine: copy $exe (and tests/fixtures) to Windows to run it" >&2
|
||||
fi
|
||||
@@ -133,7 +133,7 @@ pkgs_core_arch="base-devel cmake git pkg-config
|
||||
libxkbcommon wayland libsodium curl
|
||||
autoconf automake libtool wget python xxd"
|
||||
|
||||
pkgs_core_macos="bash cmake python xxd"
|
||||
pkgs_core_macos="cmake python xxd"
|
||||
|
||||
# Windows cross-compile (from Linux)
|
||||
pkgs_win_debian="mingw-w64 zip"
|
||||
@@ -284,14 +284,6 @@ fi
|
||||
header "Windows Cross-Compile"
|
||||
|
||||
if $SETUP_WIN; then
|
||||
# Only touch apt / update-alternatives (which need sudo) when the toolchain is missing. If it is
|
||||
# already installed, skip them so `./setup.sh --win` can run WITHOUT sudo — important because the
|
||||
# daemon cross-compile that follows should run as the invoking user. Running the whole setup under
|
||||
# sudo leaves root-owned build artifacts under external/dragonx, which then break `make clean` on
|
||||
# a later non-sudo build (stale objects get relinked -> the mingw link failure recurs).
|
||||
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
|
||||
ok "Windows cross-compile toolchain already present — skipping apt install"
|
||||
else
|
||||
win_pkgs="$(get_pkgs win)"
|
||||
if [[ -n "$win_pkgs" ]]; then
|
||||
install_pkgs "$win_pkgs" "Windows cross-compile"
|
||||
@@ -306,7 +298,6 @@ if $SETUP_WIN; then
|
||||
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fetch libsodium for Windows
|
||||
if [[ ! -f "$PROJECT_DIR/libs/libsodium-win/lib/libsodium.a" ]]; then
|
||||
@@ -400,26 +391,11 @@ elif $SETUP_SAPLING; then
|
||||
|
||||
SPEND_URL="https://z.cash/downloads/sapling-spend.params"
|
||||
OUTPUT_URL="https://z.cash/downloads/sapling-output.params"
|
||||
# Consensus-critical MPC parameters with fixed, well-known SHA-256 (identical across every
|
||||
# Zcash-family node; also pinned in scripts/build-lite-backend-artifact.sh). z.cash is
|
||||
# plain HTTPS with no signature, so verify the digest and refuse a tampered/corrupt file.
|
||||
SPEND_SHA256="8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"
|
||||
OUTPUT_SHA256="2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"
|
||||
|
||||
if curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" \
|
||||
&& echo "${SPEND_SHA256} $PARAMS_DIR/sapling-spend.params" | sha256sum -c --status; then
|
||||
ok "Downloaded + verified sapling-spend.params"
|
||||
else
|
||||
rm -f "$PARAMS_DIR/sapling-spend.params"
|
||||
err "sapling-spend.params download or SHA-256 verification failed — not installed"
|
||||
fi
|
||||
if curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" \
|
||||
&& echo "${OUTPUT_SHA256} $PARAMS_DIR/sapling-output.params" | sha256sum -c --status; then
|
||||
ok "Downloaded + verified sapling-output.params"
|
||||
else
|
||||
rm -f "$PARAMS_DIR/sapling-output.params"
|
||||
err "sapling-output.params download or SHA-256 verification failed — not installed"
|
||||
fi
|
||||
curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" && \
|
||||
ok "Downloaded sapling-spend.params"
|
||||
curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \
|
||||
ok "Downloaded sapling-output.params"
|
||||
fi
|
||||
else
|
||||
skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)"
|
||||
@@ -708,13 +684,11 @@ if [[ "$STALE_DAEMON" -eq 1 ]]; then
|
||||
warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac"
|
||||
fi
|
||||
|
||||
# ── 7. drg-xmrig (mining binary) ────────────────────────────────────────────
|
||||
header "drg-xmrig Mining Binary"
|
||||
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
|
||||
header "xmrig-hac Mining Binary"
|
||||
|
||||
XMRIG_SRC="$PROJECT_DIR/external/drg-xmrig"
|
||||
# Output dir bundled by build.sh (Linux zip, AppImage, Windows embed, mac .app)
|
||||
# and scripts/legacy/build-windows.sh — keep this path in sync with those.
|
||||
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/drg-xmrig"
|
||||
XMRIG_SRC="$PROJECT_DIR/external/xmrig-hac"
|
||||
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac"
|
||||
|
||||
# Clean previous prebuilt xmrig binaries so we always rebuild
|
||||
# Only clean the binary for the platform(s) we are actually building,
|
||||
@@ -726,14 +700,14 @@ if ! $CHECK_ONLY; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Helper: clone drg-xmrig if not present
|
||||
# Helper: clone xmrig-hac if not present
|
||||
clone_xmrig_if_needed() {
|
||||
if [[ ! -d "$XMRIG_SRC" ]]; then
|
||||
info "Cloning drg-xmrig..."
|
||||
git clone https://git.dragonx.is/DragonX/drg-xmrig.git "$XMRIG_SRC"
|
||||
info "Cloning xmrig-hac..."
|
||||
git clone https://git.dragonx.is/dragonx/xmrig-hac.git "$XMRIG_SRC"
|
||||
else
|
||||
ok "drg-xmrig source already present"
|
||||
info "Pulling latest drg-xmrig..."
|
||||
ok "xmrig-hac source already present"
|
||||
info "Pulling latest xmrig-hac..."
|
||||
(cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true)
|
||||
fi
|
||||
}
|
||||
@@ -754,15 +728,15 @@ else
|
||||
rm -rf "$XMRIG_SRC/build"
|
||||
|
||||
# Build dependencies (libuv, hwloc, openssl)
|
||||
info "Building drg-xmrig dependencies (libuv, hwloc, openssl)..."
|
||||
info "Building xmrig-hac dependencies (libuv, hwloc, openssl)..."
|
||||
(
|
||||
cd "$XMRIG_SRC/scripts"
|
||||
sh build_deps.sh
|
||||
)
|
||||
ok "drg-xmrig dependencies built"
|
||||
ok "xmrig-hac dependencies built"
|
||||
|
||||
# Build xmrig
|
||||
info "Building drg-xmrig (Linux)..."
|
||||
info "Building xmrig-hac (Linux)..."
|
||||
mkdir -p "$XMRIG_SRC/build"
|
||||
(
|
||||
cd "$XMRIG_SRC/build"
|
||||
@@ -779,7 +753,7 @@ else
|
||||
mkdir -p "$XMRIG_PREBUILT"
|
||||
if [[ -f "$XMRIG_SRC/build/xmrig" ]]; then
|
||||
cp "$XMRIG_SRC/build/xmrig" "$XMRIG_LINUX"
|
||||
ok "xmrig (Linux) built and installed to prebuilt-binaries/drg-xmrig/"
|
||||
ok "xmrig (Linux) built and installed to prebuilt-binaries/xmrig-hac/"
|
||||
else
|
||||
err "xmrig (Linux) build failed — binary not found"
|
||||
MISSING=$((MISSING + 1))
|
||||
@@ -803,7 +777,7 @@ else
|
||||
# Clean previous Windows build
|
||||
rm -rf "$XMRIG_SRC/build-windows"
|
||||
|
||||
info "Building drg-xmrig (Windows cross-compile)..."
|
||||
info "Building xmrig-hac (Windows cross-compile)..."
|
||||
(
|
||||
cd "$XMRIG_SRC/scripts"
|
||||
bash build_windows.sh
|
||||
@@ -813,7 +787,7 @@ else
|
||||
mkdir -p "$XMRIG_PREBUILT"
|
||||
if [[ -f "$XMRIG_SRC/build-windows/xmrig.exe" ]]; then
|
||||
cp "$XMRIG_SRC/build-windows/xmrig.exe" "$XMRIG_WIN"
|
||||
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/drg-xmrig/"
|
||||
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/xmrig-hac/"
|
||||
else
|
||||
err "xmrig.exe (Windows) build failed — binary not found"
|
||||
MISSING=$((MISSING + 1))
|
||||
@@ -823,7 +797,7 @@ fi
|
||||
# ── 8. Binary directories ───────────────────────────────────────────────────
|
||||
header "Binary Directories"
|
||||
|
||||
for platform in dragonxd-linux dragonxd-win dragonxd-mac drg-xmrig; do
|
||||
for platform in dragonxd-linux dragonxd-win dragonxd-mac xmrig; do
|
||||
dir="$PROJECT_DIR/prebuilt-binaries/$platform"
|
||||
if [[ -d "$dir" ]]; then
|
||||
# Count actual files (not .gitkeep)
|
||||
|
||||
+983
-5566
File diff suppressed because it is too large
Load Diff
+506
-6235
File diff suppressed because it is too large
Load Diff
+325
-875
File diff suppressed because it is too large
Load Diff
-1059
File diff suppressed because it is too large
Load Diff
+133
-294
@@ -73,11 +73,10 @@ WizardUiState s_wizardUi;
|
||||
void App::restartWizard()
|
||||
{
|
||||
if (!supportsFullNodeLifecycleActions()) {
|
||||
ui::Notifications::instance().warning(TR("toast_wizard_lite_unavailable"));
|
||||
ui::Notifications::instance().warning("Lite wallet lifecycle requests are available from Settings as dry-run readiness checks");
|
||||
return;
|
||||
}
|
||||
|
||||
if (refuseIfNodeBusy()) return; // the wizard stops the node
|
||||
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
|
||||
|
||||
// Reset crash counter for fresh wizard attempt
|
||||
@@ -87,7 +86,6 @@ void App::restartWizard()
|
||||
|
||||
// Disconnect RPC
|
||||
if (rpc_ && rpc_->isConnected()) {
|
||||
abortMaintenanceCalls(); // a long call holds the curl lock disconnect() needs
|
||||
rpc_->disconnect();
|
||||
}
|
||||
onDisconnected("Wizard restart");
|
||||
@@ -123,21 +121,16 @@ void App::renderFirstRunWizard() {
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0)); // reveal the skin backdrop behind
|
||||
ImGui::Begin("##FirstRunWizard", nullptr, flags);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImVec2 winPos = ImGui::GetWindowPos();
|
||||
ImVec2 winSize = ImGui::GetWindowSize();
|
||||
|
||||
// The app's skin backdrop (marble / gradient / acrylic) is already painted behind every window by
|
||||
// drawWindowBackdrop(); reveal it here instead of a flat Surface() slab, under a gentle theme-tinted
|
||||
// scrim so the wizard cards and text keep their contrast on busy skins.
|
||||
ImU32 bgCol = ui::material::Surface(); // still used by the completed / not-reached card overlays
|
||||
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y),
|
||||
ui::material::WithAlpha(ui::material::Background(), 120));
|
||||
// Background fill
|
||||
ImU32 bgCol = ui::material::Surface();
|
||||
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), bgCol);
|
||||
|
||||
// --- Determine which of the 3 masonry sections is focused ---
|
||||
// 0 = Appearance, 1 = Bootstrap, 2 = Encrypt + PIN
|
||||
@@ -179,36 +172,8 @@ void App::renderFirstRunWizard() {
|
||||
// DPI scale factor — multiply all pixel constants by dp
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
|
||||
// Vertical scroll: the wizard cards are hand-drawn at absolute Y offsets and grow ~1.5x with the
|
||||
// font-scale setting, so at high scale the focused card's primary button (Continue / Encrypt & Continue
|
||||
// / Skip) can fall below the fixed window. Offset the whole layout by a wheel-driven scroll, clamped to
|
||||
// last frame's measured content height, so every control stays reachable. The window keeps
|
||||
// NoScrollWithMouse, so ImGui doesn't consume the wheel — we read the raw delta and apply our own offset.
|
||||
static float s_wizScroll = 0.0f, s_wizContentH = 0.0f;
|
||||
if (ImGui::IsWindowAppearing()) s_wizScroll = 0.0f;
|
||||
const float wizMaxScroll = std::max(0.0f, s_wizContentH - winSize.y);
|
||||
// Don't steal the wheel from an open combo popup (e.g. the 9-item Language dropdown, which is a
|
||||
// scrollable popup): NoPopupHierarchy stops the popup counting as hovering the wizard, and the
|
||||
// IsPopupOpen guard ensures no wheel is consumed for the whole wizard while any popup is showing.
|
||||
const bool wizPopupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel);
|
||||
if (wizMaxScroll > 0.0f && !wizPopupOpen &&
|
||||
ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_NoPopupHierarchy)) {
|
||||
float wheel = ImGui::GetIO().MouseWheel;
|
||||
if (wheel != 0.0f) s_wizScroll -= wheel * 60.0f * dp;
|
||||
}
|
||||
s_wizScroll = std::max(0.0f, std::min(s_wizScroll, wizMaxScroll));
|
||||
const float scrollY = s_wizScroll;
|
||||
|
||||
// --- Header: Logo + Welcome ---
|
||||
// Vertically center the content when it fits (mirrors the horizontal centering below): on a tall
|
||||
// monitor top-anchoring leaves a large void under the cards. Using last frame's measured block
|
||||
// height, when the content fits inside the window (and we're NOT overflowing, so this doesn't
|
||||
// fight the scroll), push everything down by half the leftover space. No-op once content
|
||||
// fills/exceeds the window (s_wizContentH >= winSize.y ⇒ wizMaxScroll > 0 ⇒ vCenter skipped).
|
||||
float vCenter = 0.0f;
|
||||
if (wizMaxScroll == 0.0f && s_wizContentH > 0.0f && s_wizContentH < winSize.y)
|
||||
vCenter = std::max(0.0f, (winSize.y - s_wizContentH) * 0.5f);
|
||||
float headerCy = winPos.y - scrollY + 20.0f * dp + vCenter;
|
||||
float headerCy = winPos.y + 20.0f * dp;
|
||||
float logoSize = S.drawElement("screens.first-run", "logo").sizeOr(56.0f);
|
||||
if (logo_tex_ != 0) {
|
||||
float aspect = (logo_h_ > 0) ? (float)logo_w_ / (float)logo_h_ : 1.0f;
|
||||
@@ -219,18 +184,11 @@ void App::renderFirstRunWizard() {
|
||||
headerCy += logoSize + 8.0f * dp;
|
||||
|
||||
{
|
||||
const char* welcomeTitle = TR("wiz_welcome_title");
|
||||
const char* welcomeTitle = "Welcome to ObsidianDragon!";
|
||||
ImVec2 wts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, welcomeTitle);
|
||||
dl->AddText(titleFont, titleFont->LegacySize,
|
||||
ImVec2(winPos.x + (winSize.x - wts.x) * 0.5f, headerCy), textCol, welcomeTitle);
|
||||
headerCy += wts.y + 6.0f * dp;
|
||||
|
||||
// Warmer, less-sparse header: a one-line subtitle under the welcome (dimmed body).
|
||||
const char* welcomeSub = TR("wiz_welcome_sub");
|
||||
ImVec2 sts = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, 0, welcomeSub);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize,
|
||||
ImVec2(winPos.x + (winSize.x - sts.x) * 0.5f, headerCy), dimCol, welcomeSub);
|
||||
headerCy += sts.y + 16.0f * dp;
|
||||
headerCy += wts.y + 16.0f * dp;
|
||||
}
|
||||
|
||||
// --- Masonry: 2 columns ---
|
||||
@@ -265,8 +223,11 @@ void App::renderFirstRunWizard() {
|
||||
// Background (channel 0)
|
||||
dl->ChannelsSetCurrent(0);
|
||||
if (state == 1) {
|
||||
// Focused card lifts off the backdrop with the app's uniform card shadow (not a hard offset).
|
||||
ui::material::DrawCardDropShadow(dl, cMin, cMax, cardRound);
|
||||
// Focused card: subtle drop shadow
|
||||
float shadowOff = 3.0f * dp;
|
||||
dl->AddRectFilled(
|
||||
ImVec2(cMin.x + shadowOff, cMin.y + shadowOff), ImVec2(cMax.x + shadowOff, cMax.y + shadowOff),
|
||||
IM_COL32(0, 0, 0, 35), cardRound);
|
||||
}
|
||||
// Use DrawGlassPanel for proper acrylic/opacity/noise/theme effects
|
||||
ui::material::GlassPanelSpec glass;
|
||||
@@ -276,14 +237,14 @@ void App::renderFirstRunWizard() {
|
||||
// Overlays & borders (channel 2)
|
||||
dl->ChannelsSetCurrent(2);
|
||||
if (state == 1) {
|
||||
// Focused: soft accent ring
|
||||
dl->AddRect(cMin, cMax, ui::material::Primary(), cardRound, 0, 1.5f * dp);
|
||||
// Focused: accent border
|
||||
dl->AddRect(cMin, cMax, ui::material::Primary(), cardRound, 0, 2.0f * dp);
|
||||
} else if (state == 2) {
|
||||
// Completed: a light veil — reads as "done", not disabled.
|
||||
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 70), cardRound);
|
||||
// Completed: dim overlay (preserves color)
|
||||
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 110), cardRound);
|
||||
} else {
|
||||
// Upcoming: a gentle veil — reads as "waiting", not greyed-out.
|
||||
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 115), cardRound);
|
||||
// Not reached: heavy overlay (creates greyscale look)
|
||||
dl->AddRectFilled(cMin, cMax, (bgCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 165), cardRound);
|
||||
}
|
||||
|
||||
dl->ChannelsSetCurrent(1);
|
||||
@@ -296,34 +257,21 @@ void App::renderFirstRunWizard() {
|
||||
{
|
||||
int state = cardState(0);
|
||||
bool isFocused = (state == 1);
|
||||
bool isCollapsed = (state == 2); // Completed: minimize to a compact pill (mirrors Card 1)
|
||||
float cx = leftX + cardPad;
|
||||
float cy = card0Top + cardPad;
|
||||
float contentW = colW - 2 * cardPad;
|
||||
|
||||
// Step indicator + title (inline when collapsed)
|
||||
if (isCollapsed) {
|
||||
// Compact single-line: check icon + "Step 1" + "Appearance"
|
||||
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||
float labelX = cx + iconW + 4.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(labelX, cy), dimCol, TR("wiz_step1"));
|
||||
float step1W = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, TR("wiz_step1")).x;
|
||||
float titleX = labelX + step1W + 12.0f * dp;
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(titleX, cy), dimCol, TR("wiz_appearance"));
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
} else {
|
||||
// Step indicator
|
||||
{
|
||||
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step1"));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 1");
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
|
||||
// Title
|
||||
{
|
||||
const char* t = TR("wiz_appearance");
|
||||
const char* t = "Appearance";
|
||||
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
|
||||
cy += titleFont->LegacySize + 10.0f * dp;
|
||||
}
|
||||
@@ -332,7 +280,6 @@ void App::renderFirstRunWizard() {
|
||||
dl->AddLine(ImVec2(cx, cy), ImVec2(cx + contentW, cy),
|
||||
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
|
||||
cy += 14.0f * dp;
|
||||
}
|
||||
|
||||
float& wiz_blur_amount = wizardUi.blur_amount;
|
||||
bool& wiz_theme_effects = wizardUi.theme_effects;
|
||||
@@ -358,15 +305,16 @@ void App::renderFirstRunWizard() {
|
||||
if (lang.first == wiz_cur_lang) { wiz_language_index = idx; break; }
|
||||
idx++;
|
||||
}
|
||||
// Apply the loaded settings to the runtime (same low-spec-aware path as startup) so the
|
||||
// visuals match the slider values
|
||||
applyVisualSettings();
|
||||
// Apply loaded settings to runtime so visuals match slider values
|
||||
ui::effects::setLowSpecMode(wiz_low_spec);
|
||||
ui::effects::ImGuiAcrylic::ApplyBlurAmount(wiz_blur_amount);
|
||||
ui::effects::ImGuiAcrylic::SetUIOpacity(wiz_ui_opacity);
|
||||
ui::effects::ThemeEffects::instance().setEnabled(wiz_theme_effects);
|
||||
ui::effects::ThemeEffects::instance().setReducedTransparency(!wiz_theme_effects);
|
||||
ui::ConsoleTab::s_scanline_enabled = wiz_scanline;
|
||||
wiz_appearance_init = true;
|
||||
}
|
||||
|
||||
// Controls: rendered for the focused and upcoming states so content is visible under
|
||||
// the dim overlay; skipped entirely once completed so the card shrinks to a compact pill.
|
||||
if (!isCollapsed) {
|
||||
// Render controls always so content is visible under the dim
|
||||
// overlay when not focused; disable interaction when not active.
|
||||
ImGui::BeginDisabled(!isFocused);
|
||||
@@ -379,14 +327,14 @@ void App::renderFirstRunWizard() {
|
||||
for (const auto& skin : skins) {
|
||||
if (skin.id == skinMgr.activeSkinId()) { activePreview = skin.name; break; }
|
||||
}
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, TR("theme"));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Theme");
|
||||
float comboX = cx + 110.0f * dp;
|
||||
float comboW = contentW - 110.0f * dp;
|
||||
ImGui::SetCursorScreenPos(ImVec2(comboX, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
|
||||
ImGui::SetNextItemWidth(comboW);
|
||||
if (ImGui::BeginCombo("##wiz_theme", activePreview.c_str())) {
|
||||
ImGui::TextDisabled(TR("wiz_theme_builtin"));
|
||||
ImGui::TextDisabled("Built-in");
|
||||
ImGui::Separator();
|
||||
for (const auto& skin : skins) {
|
||||
if (!skin.bundled) continue;
|
||||
@@ -402,7 +350,7 @@ void App::renderFirstRunWizard() {
|
||||
for (const auto& skin : skins) { if (!skin.bundled) { hasCustom = true; break; } }
|
||||
if (hasCustom) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextDisabled(TR("wiz_theme_custom"));
|
||||
ImGui::TextDisabled("Custom");
|
||||
ImGui::Separator();
|
||||
for (const auto& skin : skins) {
|
||||
if (skin.bundled) continue;
|
||||
@@ -410,7 +358,7 @@ void App::renderFirstRunWizard() {
|
||||
if (!skin.valid) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1,0.3f,0.3f,1));
|
||||
ImGui::BeginDisabled(true);
|
||||
ImGui::Selectable((skin.name + TR("wiz_theme_invalid")).c_str(), false);
|
||||
ImGui::Selectable((skin.name + " (invalid)").c_str(), false);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleColor();
|
||||
} else {
|
||||
@@ -438,7 +386,7 @@ void App::renderFirstRunWizard() {
|
||||
for (const auto& l : layouts) {
|
||||
if (l.id == wiz_balance_layout) { balPreview = l.name; break; }
|
||||
}
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, TR("balance_layout"));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Balance Layout");
|
||||
float comboX = cx + 110.0f * dp;
|
||||
float comboW = contentW - 110.0f * dp;
|
||||
ImGui::SetCursorScreenPos(ImVec2(comboX, cy));
|
||||
@@ -469,7 +417,7 @@ void App::renderFirstRunWizard() {
|
||||
langNames.reserve(languages.size());
|
||||
for (const auto& lang : languages) langNames.push_back(lang.second.c_str());
|
||||
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, TR("language"));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy + 4.0f * dp), textCol, "Language");
|
||||
float comboX = cx + 110.0f * dp;
|
||||
float comboW = contentW - 110.0f * dp;
|
||||
ImGui::SetCursorScreenPos(ImVec2(comboX, cy));
|
||||
@@ -535,18 +483,15 @@ void App::renderFirstRunWizard() {
|
||||
settings_->setScanlineEnabled(wiz_scanline);
|
||||
settings_->setLowSpecMode(wiz_low_spec);
|
||||
settings_->save();
|
||||
// Make the runtime match what was just saved, both ways: unticking a low-spec that was
|
||||
// loaded rather than ticked here has no snapshot, so the branch above pushes nothing.
|
||||
applyVisualSettings();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::SameLine();
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol,
|
||||
TR("low_spec_mode"));
|
||||
"Low-spec mode");
|
||||
cy += bodyFont->LegacySize + 6.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx + 28.0f * dp, cy), dimCol, TR("wiz_lowspec_desc"));
|
||||
ImVec2(cx + 28.0f * dp, cy), dimCol, "Disable all heavy visual effects");
|
||||
cy += captionFont->LegacySize + 16.0f * dp;
|
||||
|
||||
ImGui::BeginDisabled(wiz_low_spec);
|
||||
@@ -554,15 +499,15 @@ void App::renderFirstRunWizard() {
|
||||
// Acrylic blur slider
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize,
|
||||
ImVec2(cx, cy + 2.0f * dp), textCol,
|
||||
TR("wiz_acrylic"));
|
||||
"Acrylic glass effects");
|
||||
cy += bodyFont->LegacySize + 4.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx, cy), dimCol, TR("wiz_acrylic_desc"));
|
||||
ImVec2(cx, cy), dimCol, "Translucent blur on panels (Off disables)");
|
||||
cy += captionFont->LegacySize + 10.0f * dp;
|
||||
|
||||
{
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx + 4.0f * dp, cy), textCol, TR("wiz_level"));
|
||||
ImVec2(cx + 4.0f * dp, cy), textCol, "Level:");
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx + 72.0f * dp, cy - 2.0f * dp));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
|
||||
float sliderW = contentW - 72.0f * dp;
|
||||
@@ -570,7 +515,7 @@ void App::renderFirstRunWizard() {
|
||||
{
|
||||
char blur_fmt[16];
|
||||
if (wiz_blur_amount < 0.01f)
|
||||
snprintf(blur_fmt, sizeof(blur_fmt), TR("wiz_off"));
|
||||
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
|
||||
else
|
||||
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", wiz_blur_amount * 25.0f);
|
||||
if (ImGui::SliderFloat("##wiz_blur", &wiz_blur_amount, 0.0f, 4.0f, blur_fmt,
|
||||
@@ -604,19 +549,19 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::SameLine();
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol,
|
||||
TR("wiz_theme_effects"));
|
||||
"Theme visual effects");
|
||||
cy += bodyFont->LegacySize + 6.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx + 28.0f * dp, cy), dimCol, TR("wiz_theme_effects_desc"));
|
||||
ImVec2(cx + 28.0f * dp, cy), dimCol, "Animated borders, color wash");
|
||||
cy += captionFont->LegacySize + 16.0f * dp;
|
||||
|
||||
// UI Opacity slider
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize,
|
||||
ImVec2(cx, cy + 2.0f * dp), textCol,
|
||||
TR("ui_opacity"));
|
||||
"UI Opacity");
|
||||
cy += bodyFont->LegacySize + 4.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx, cy), dimCol, TR("wiz_ui_opacity_desc"));
|
||||
ImVec2(cx, cy), dimCol, "Card & sidebar transparency (1.0 = solid)");
|
||||
cy += captionFont->LegacySize + 10.0f * dp;
|
||||
{
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy - 2.0f * dp));
|
||||
@@ -645,10 +590,10 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::SameLine();
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x, cy + 2.0f * dp), textCol,
|
||||
TR("console_scanline"));
|
||||
"Console scanline");
|
||||
cy += bodyFont->LegacySize + 6.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx + 28.0f * dp, cy), dimCol, TR("wiz_scanline_desc"));
|
||||
ImVec2(cx + 28.0f * dp, cy), dimCol, "CRT scanline effect in console");
|
||||
cy += captionFont->LegacySize + 24.0f * dp;
|
||||
|
||||
ImGui::EndDisabled(); // low-spec
|
||||
@@ -666,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 (ui::material::TactileButton(TR("wiz_continue"), ImVec2(btnW, btnH))) {
|
||||
if (ImGui::Button("Continue##app", ImVec2(btnW, btnH))) {
|
||||
// Save appearance choices, advance to Bootstrap
|
||||
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
|
||||
settings_->setAcrylicQuality(wiz_blur_amount > 0.001f
|
||||
@@ -686,22 +631,14 @@ void App::renderFirstRunWizard() {
|
||||
cy += btnH;
|
||||
}
|
||||
|
||||
} // if (!isCollapsed)
|
||||
|
||||
cy += cardPad;
|
||||
// Lock card height to the tallest content ever seen (but not when collapsed)
|
||||
// Lock card height to the tallest content ever seen
|
||||
float& card0MaxH = wizardUi.card0_max_h;
|
||||
if (isCollapsed) {
|
||||
// Completed: finalize immediately as a compact pill (do not stretch to the
|
||||
// right column height, and skip the deferred stretch below).
|
||||
card0Bot = card0Top + (cy - card0Top);
|
||||
finalizeCard(leftX, colW, card0Top, card0Bot, state);
|
||||
} else {
|
||||
card0MaxH = std::max(card0MaxH, cy - card0Top);
|
||||
card0Bot = card0Top + card0MaxH;
|
||||
|
||||
// Card 0 finalization deferred until after cards 1+2 are sized
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ======================= CARD 1: Bootstrap =======================
|
||||
@@ -721,23 +658,23 @@ void App::renderFirstRunWizard() {
|
||||
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||
float labelX = cx + iconW + 4.0f * dp;
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(labelX, cy), dimCol, TR("wiz_step2"));
|
||||
float step2W = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, TR("wiz_step2")).x;
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(labelX, cy), dimCol, "Step 2");
|
||||
float step2W = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, "Step 2").x;
|
||||
float titleX = labelX + step2W + 12.0f * dp;
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(titleX, cy), dimCol, TR("wiz_bootstrap"));
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(titleX, cy), dimCol, "Bootstrap");
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
} else {
|
||||
// Step indicator
|
||||
{
|
||||
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step2"));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 2");
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
}
|
||||
|
||||
// Title
|
||||
{
|
||||
const char* t = TR("wiz_bootstrap");
|
||||
const char* t = "Bootstrap";
|
||||
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
|
||||
cy += titleFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
@@ -760,11 +697,11 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
const char* statusTitle;
|
||||
if (prog.state == util::Bootstrap::State::Downloading)
|
||||
statusTitle = TR("bootstrap_downloading");
|
||||
statusTitle = "Downloading bootstrap...";
|
||||
else if (prog.state == util::Bootstrap::State::Verifying)
|
||||
statusTitle = TR("bootstrap_verifying");
|
||||
statusTitle = "Verifying checksums...";
|
||||
else
|
||||
statusTitle = TR("bootstrap_extracting");
|
||||
statusTitle = "Extracting blockchain data...";
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, statusTitle);
|
||||
cy += bodyFont->LegacySize + 12.0f * dp;
|
||||
|
||||
@@ -793,29 +730,28 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
if (prog.state == util::Bootstrap::State::Extracting) {
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||
dimCol, TR("bootstrap_wallet_protected"));
|
||||
dimCol, "(wallet.dat is protected)");
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
|
||||
// Daemon status indicator
|
||||
{
|
||||
bool daemonUp = isEmbeddedDaemonRunning();
|
||||
// Key the dot + label off the daemon state ENUM, not an English substring of the status
|
||||
// text — the substring match silently failed under every non-English language.
|
||||
const bool daemonStopping = daemon_controller_ &&
|
||||
daemon_controller_->state() == daemon::EmbeddedDaemon::State::Stopping;
|
||||
ImU32 dotCol = daemonStopping ? IM_COL32(255, 167, 38, 230) // orange
|
||||
: daemonUp ? IM_COL32(76, 175, 80, 230) // green
|
||||
: IM_COL32(120, 120, 120, 180); // gray
|
||||
const std::string& dStatus = getDaemonStatus();
|
||||
ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200) // green
|
||||
: IM_COL32(120, 120, 120, 160); // gray
|
||||
if (dStatus.find("Stopping") != std::string::npos)
|
||||
dotCol = IM_COL32(255, 167, 38, 200); // orange
|
||||
float dotR = 3.5f * dp;
|
||||
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
|
||||
dotR, dotCol);
|
||||
const char* label = daemonStopping ? TR("bootstrap_daemon_stopping")
|
||||
: daemonUp ? TR("bootstrap_daemon_running")
|
||||
: TR("bootstrap_daemon_stopped");
|
||||
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
|
||||
? "Daemon stopping..."
|
||||
: "Daemon running")
|
||||
: "Daemon stopped";
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
|
||||
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,220), label);
|
||||
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
|
||||
@@ -827,12 +763,9 @@ void App::renderFirstRunWizard() {
|
||||
float cancelBX = rightX + (colW - cancelW) * 0.5f;
|
||||
ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
// Disabled while the old chain data is being replaced (not cancellable).
|
||||
ImGui::BeginDisabled(!prog.cancellable);
|
||||
if (ui::material::TactileButton(TR("cancel"), ImVec2(cancelW, cancelH))) {
|
||||
bootstrap_->requestCancel(); // a no-op once the old chain data is being replaced
|
||||
if (ImGui::Button("Cancel##bs", ImVec2(cancelW, cancelH))) {
|
||||
bootstrap_->cancel();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleVar();
|
||||
cy += cancelH;
|
||||
|
||||
@@ -841,8 +774,6 @@ void App::renderFirstRunWizard() {
|
||||
auto finalProg = bootstrap_->getProgress();
|
||||
if (finalProg.state == util::Bootstrap::State::Completed) {
|
||||
bootstrap_.reset();
|
||||
// Reconcile the preserved wallet.dat against the new chain once the daemon is up.
|
||||
markPostBootstrapRescanPending();
|
||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||
} else {
|
||||
wizard_phase_ = WizardPhase::BootstrapFailed;
|
||||
@@ -857,25 +788,20 @@ void App::renderFirstRunWizard() {
|
||||
errMsg = bootstrap_->getProgress().error;
|
||||
bootstrap_.reset();
|
||||
}
|
||||
if (errMsg.empty()) errMsg = TR("wiz_bootstrap_failed");
|
||||
if (errMsg.empty()) errMsg = "Bootstrap failed";
|
||||
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy),
|
||||
ui::material::Error(), TR("wiz_download_failed"));
|
||||
ui::material::Error(), "Download Failed");
|
||||
cy += bodyFont->LegacySize + 8.0f * dp;
|
||||
|
||||
// Wrap the (often long) error within the card instead of letting it clip off the edge.
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol,
|
||||
errMsg.c_str(), nullptr, contentW);
|
||||
ImVec2 es = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, contentW, errMsg.c_str());
|
||||
cy += es.y + 16.0f * dp;
|
||||
errMsg.c_str());
|
||||
cy += captionFont->LegacySize + 16.0f * dp;
|
||||
|
||||
// Retry / (Try mirror) / Skip. Offer the mirror unless the last attempt already used it.
|
||||
const std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
|
||||
const bool showMirror = (wizard_bootstrap_url_ != mirrorUrl);
|
||||
// Retry / Skip
|
||||
float btnW2 = 120.0f * dp;
|
||||
float btnH2 = 40.0f * dp;
|
||||
const int nBtn = showMirror ? 3 : 2;
|
||||
float totalBW = btnW2 * nBtn + 12.0f * dp * (nBtn - 1);
|
||||
float totalBW = btnW2 * 2 + 12.0f * dp;
|
||||
float bx = rightX + (colW - totalBW) * 0.5f;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
|
||||
@@ -884,40 +810,21 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||
if (ui::material::TactileButton(TR("retry"), ImVec2(btnW2, btnH2)) && !refuseIfNodeBusy()) {
|
||||
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
|
||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||
stopDaemonForBootstrap();
|
||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||
if (wizard_bootstrap_url_.empty()) bootstrap_->start(dataDir);
|
||||
else bootstrap_->start(dataDir, wizard_bootstrap_url_); // keep the user's chosen source
|
||||
bootstrap_->start(dataDir);
|
||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
|
||||
float nextBx = bx + btnW2 + 12.0f * dp;
|
||||
if (showMirror) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(nextBx, cy));
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||
if (ui::material::TactileButton(TR("bootstrap_try_mirror"), ImVec2(btnW2, btnH2)) && !refuseIfNodeBusy()) {
|
||||
stopDaemonForBootstrap();
|
||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||
bootstrap_->start(util::Platform::getDragonXDataDir(), mirrorUrl);
|
||||
wizard_bootstrap_url_ = mirrorUrl;
|
||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered()) ui::material::Tooltip(TR("bootstrap_use_mirror_tooltip"));
|
||||
ImGui::PopStyleVar();
|
||||
nextBx += btnW2 + 12.0f * dp;
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(nextBx, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(btnW2, btnH2))) {
|
||||
if (ImGui::Button("Skip##bsfail", ImVec2(btnW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
@@ -960,20 +867,19 @@ void App::renderFirstRunWizard() {
|
||||
{
|
||||
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, TR("wiz_ext_daemon_running"));
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, "External daemon running");
|
||||
}
|
||||
cy += bodyFont->LegacySize + 4.0f * dp;
|
||||
{
|
||||
const char* warnBody = TR("wiz_ext_daemon_warning");
|
||||
const char* warnBody = "It must be stopped before downloading a bootstrap, otherwise chain data could be corrupted.";
|
||||
ImVec2 ws = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, contentW, warnBody);
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol, warnBody, nullptr, contentW);
|
||||
cy += ws.y + 12.0f * dp;
|
||||
}
|
||||
|
||||
if (wizard_stopping_external_) {
|
||||
const std::string ws = wizard_stop_status_.get();
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol,
|
||||
ws.c_str());
|
||||
wizard_stop_status_.c_str());
|
||||
cy += captionFont->LegacySize + 8.0f * dp;
|
||||
} else {
|
||||
float stopW = 150.0f * dp;
|
||||
@@ -988,9 +894,9 @@ void App::renderFirstRunWizard() {
|
||||
IM_COL32(220, 60, 60, 255)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 255));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton(TR("wiz_stop_daemon"), ImVec2(stopW, btnH2))) {
|
||||
if (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
|
||||
wizard_stopping_external_ = true;
|
||||
wizard_stop_status_ = TR("wiz_daemon_sending_stop");
|
||||
wizard_stop_status_ = "Sending stop command...";
|
||||
async_tasks_.submit("wizard-stop-external-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||
auto config = rpc::Connection::autoDetectConfig();
|
||||
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
|
||||
@@ -1002,20 +908,17 @@ void App::renderFirstRunWizard() {
|
||||
tmp_rpc->disconnect();
|
||||
}
|
||||
}
|
||||
wizard_stop_status_ = TR("wiz_daemon_waiting_stop");
|
||||
// Stopped means the PROCESS is gone, not just its RPC port: dragonxd unbinds RPC
|
||||
// early in its shutdown and holds the datadir lock until it exits.
|
||||
for (int i = 0; i < 120 && !token.cancelled(); i++) {
|
||||
wizard_stop_status_ = "Waiting for daemon to shut down...";
|
||||
for (int i = 0; i < 60 && !token.cancelled(); i++) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
if (!daemon::EmbeddedDaemon::isRpcPortInUse() &&
|
||||
!daemon::EmbeddedDaemon::isDaemonProcessRunning()) {
|
||||
wizard_stop_status_ = TR("wiz_daemon_stopped_ok");
|
||||
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
|
||||
wizard_stop_status_ = "Daemon stopped.";
|
||||
wizard_stopping_external_ = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (token.cancelled()) return;
|
||||
wizard_stop_status_ = TR("wiz_daemon_stop_failed");
|
||||
wizard_stop_status_ = "Daemon did not stop — try manually.";
|
||||
wizard_stopping_external_ = false;
|
||||
});
|
||||
}
|
||||
@@ -1024,7 +927,7 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx + stopW + 12.0f * dp, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(skipW2, btnH2))) {
|
||||
if (ImGui::Button("Skip##extd", ImVec2(skipW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
@@ -1033,21 +936,12 @@ void App::renderFirstRunWizard() {
|
||||
} else {
|
||||
// --- Normal bootstrap offer ---
|
||||
{
|
||||
const char* bsText = TR("wiz_bootstrap_desc");
|
||||
const char* bsText = "Download a blockchain bootstrap to dramatically speed up initial sync.\n\nYour existing wallet.dat will NOT be modified or replaced.";
|
||||
ImVec2 bsSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, bsText);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, bsText, nullptr, contentW);
|
||||
cy += bsSize.y + 8.0f * dp;
|
||||
}
|
||||
|
||||
// Honest size/time hint (no number is known until the download HEADs the remote file).
|
||||
{
|
||||
const char* hintText = TR("wiz_bootstrap_size_hint");
|
||||
ImVec2 hs = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, contentW, hintText);
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||
(dimCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 200), hintText, nullptr, contentW);
|
||||
cy += hs.y + 8.0f * dp;
|
||||
}
|
||||
|
||||
// Trust warning
|
||||
{
|
||||
float warnOpacity = S.drawElement("screens.first-run", "trust-warning").opacity;
|
||||
@@ -1055,7 +949,7 @@ void App::renderFirstRunWizard() {
|
||||
ImU32 warnCol = (textCol & 0x00FFFFFF) | ((ImU32)(255 * warnOpacity) << 24);
|
||||
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
|
||||
const char* twText = TR("bootstrap_trust_warning");
|
||||
const char* twText = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node.";
|
||||
float twWrap = contentW - iw - 4.0f * dp;
|
||||
ImVec2 twSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, twWrap, twText);
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, twText, nullptr, twWrap);
|
||||
@@ -1065,20 +959,21 @@ void App::renderFirstRunWizard() {
|
||||
// Daemon status indicator (subtle, before buttons)
|
||||
{
|
||||
bool daemonUp = isEmbeddedDaemonRunning();
|
||||
const bool daemonStopping = daemon_controller_ &&
|
||||
daemon_controller_->state() == daemon::EmbeddedDaemon::State::Stopping;
|
||||
ImU32 dotCol = daemonStopping ? IM_COL32(255, 167, 38, 230)
|
||||
: daemonUp ? IM_COL32(76, 175, 80, 230)
|
||||
: IM_COL32(120, 120, 120, 180);
|
||||
const std::string& dStatus = getDaemonStatus();
|
||||
ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200)
|
||||
: IM_COL32(120, 120, 120, 160);
|
||||
if (dStatus.find("Stopping") != std::string::npos)
|
||||
dotCol = IM_COL32(255, 167, 38, 200);
|
||||
float dotR = 3.5f * dp;
|
||||
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
|
||||
dotR, dotCol);
|
||||
const char* label = daemonStopping ? TR("bootstrap_daemon_stopping")
|
||||
: daemonUp ? TR("bootstrap_daemon_running")
|
||||
: TR("bootstrap_daemon_stopped");
|
||||
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
|
||||
? "Daemon stopping..."
|
||||
: "Daemon running")
|
||||
: "Daemon stopped";
|
||||
dl->AddText(captionFont, captionFont->LegacySize,
|
||||
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
|
||||
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,220), label);
|
||||
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
|
||||
cy += captionFont->LegacySize + 10.0f * dp;
|
||||
}
|
||||
|
||||
@@ -1098,13 +993,12 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||
if (ui::material::TactileButton(TR("download"), ImVec2(dlBtnW, btnH2)) && !refuseIfNodeBusy()) {
|
||||
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
|
||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||
stopDaemonForBootstrap();
|
||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||
bootstrap_->start(dataDir);
|
||||
wizard_bootstrap_url_.clear(); // default source; Retry won't force the mirror
|
||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
@@ -1118,18 +1012,17 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||
if (ui::material::TactileButton(TR("bootstrap_use_mirror"), ImVec2(mirrorW, btnH2)) && !refuseIfNodeBusy()) {
|
||||
if (ImGui::Button("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_bootstrap_url_ = mirrorUrl; // Retry on the failed card keeps this source
|
||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ui::material::Tooltip(TR("bootstrap_use_mirror_tooltip"));
|
||||
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
@@ -1137,7 +1030,7 @@ void App::renderFirstRunWizard() {
|
||||
// --- Skip button ---
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(skipW2, btnH2))) {
|
||||
if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
@@ -1189,14 +1082,14 @@ void App::renderFirstRunWizard() {
|
||||
{
|
||||
float iconW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, stepIcon(state)).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), dimCol, stepIcon(state));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, TR("wiz_step3"));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iconW + 4.0f * dp, cy), dimCol, "Step 3");
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
}
|
||||
|
||||
// Title (changes for PinSetup sub-state)
|
||||
{
|
||||
const char* t = (isFocused && wizard_phase_ == WizardPhase::PinSetup)
|
||||
? TR("wiz_pin_title") : TR("wiz_encryption");
|
||||
? "Quick-Unlock PIN" : "Encryption";
|
||||
dl->AddText(titleFont, titleFont->LegacySize, ImVec2(cx, cy), textCol, t);
|
||||
cy += titleFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
@@ -1213,11 +1106,11 @@ void App::renderFirstRunWizard() {
|
||||
ImU32 okCol = ui::material::Secondary();
|
||||
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_VERIFIED_USER).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), okCol, ICON_MD_VERIFIED_USER);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 6.0f * dp, cy), okCol, TR("wiz_already_encrypted"));
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 6.0f * dp, cy), okCol, "Wallet is already encrypted");
|
||||
cy += bodyFont->LegacySize + 12.0f * dp;
|
||||
}
|
||||
{
|
||||
const char* desc = TR("wiz_already_encrypted_desc");
|
||||
const char* desc = "Your wallet is protected with a passphrase. No further action is needed.";
|
||||
ImVec2 ds = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, desc);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, desc, nullptr, contentW);
|
||||
cy += ds.y + 20.0f * dp;
|
||||
@@ -1232,7 +1125,7 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton(TR("wiz_continue"), ImVec2(btnW2, btnH2))) {
|
||||
if (ImGui::Button("Continue##encok", ImVec2(btnW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::Done;
|
||||
settings_->setWizardCompleted(true);
|
||||
settings_->save();
|
||||
@@ -1244,7 +1137,7 @@ void App::renderFirstRunWizard() {
|
||||
} else if (isFocused) {
|
||||
// ---- Encryption offer + optional PIN (combined) ----
|
||||
{
|
||||
const char* encDesc = TR("wiz_encrypt_desc");
|
||||
const char* encDesc = "Encrypt your wallet to protect private keys with a passphrase.";
|
||||
ImVec2 edSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, encDesc);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, encDesc, nullptr, contentW);
|
||||
cy += edSize.y + 6.0f * dp;
|
||||
@@ -1253,7 +1146,7 @@ void App::renderFirstRunWizard() {
|
||||
ImU32 warnCol2 = ui::material::Warning();
|
||||
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
|
||||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol2, ICON_MD_WARNING);
|
||||
const char* warnLoss = TR("wiz_encrypt_warning");
|
||||
const char* warnLoss = "If you lose your passphrase, you lose access to your funds.";
|
||||
float wlWrap = contentW - iw - 4.0f * dp;
|
||||
ImVec2 wlSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, wlWrap, warnLoss);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol2, warnLoss, nullptr, wlWrap);
|
||||
@@ -1261,7 +1154,7 @@ void App::renderFirstRunWizard() {
|
||||
}
|
||||
|
||||
// Passphrase input
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, TR("wiz_passphrase"));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Passphrase:");
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||
@@ -1273,7 +1166,7 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PopItemWidth();
|
||||
cy += 36.0f * dp + 6.0f * dp;
|
||||
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, TR("wiz_confirm"));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Confirm:");
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||
@@ -1288,16 +1181,16 @@ void App::renderFirstRunWizard() {
|
||||
// Strength meter
|
||||
{
|
||||
size_t len = strlen(encrypt_pass_buf_);
|
||||
const char* strengthLabel = TR("wiz_strength_weak");
|
||||
const char* strengthLabel = "Weak";
|
||||
ImU32 strengthCol = ui::material::Error();
|
||||
float strengthPct = 0.25f;
|
||||
|
||||
if (len >= 16) {
|
||||
strengthLabel = TR("wiz_strength_strong"); strengthCol = ui::material::Secondary(); strengthPct = 1.0f;
|
||||
strengthLabel = "Strong"; strengthCol = ui::material::Secondary(); strengthPct = 1.0f;
|
||||
} else if (len >= 12) {
|
||||
strengthLabel = TR("wiz_strength_good"); strengthCol = ui::material::Secondary(); strengthPct = 0.75f;
|
||||
strengthLabel = "Good"; strengthCol = ui::material::Secondary(); strengthPct = 0.75f;
|
||||
} else if (len >= 8) {
|
||||
strengthLabel = TR("wiz_strength_fair"); strengthCol = ui::material::Warning(); strengthPct = 0.5f;
|
||||
strengthLabel = "Fair"; strengthCol = ui::material::Warning(); strengthPct = 0.5f;
|
||||
}
|
||||
|
||||
float sBarH = 4.0f * dp, sBarR = 2.0f * dp;
|
||||
@@ -1310,7 +1203,7 @@ void App::renderFirstRunWizard() {
|
||||
cy += sBarH + 4.0f * dp;
|
||||
|
||||
char slabel[64];
|
||||
snprintf(slabel, sizeof(slabel), TR("wiz_strength"), strengthLabel);
|
||||
snprintf(slabel, sizeof(slabel), "Strength: %s", strengthLabel);
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, slabel);
|
||||
cy += captionFont->LegacySize + 10.0f * dp;
|
||||
}
|
||||
@@ -1320,14 +1213,14 @@ void App::renderFirstRunWizard() {
|
||||
size_t pLen = strlen(encrypt_pass_buf_);
|
||||
if (pLen > 0 && pLen < 8) {
|
||||
char fb[80];
|
||||
snprintf(fb, sizeof(fb), TR("wiz_pass_too_short"), pLen);
|
||||
snprintf(fb, sizeof(fb), "Passphrase must be at least 8 characters (%zu/8)", pLen);
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||
ui::material::Error(), fb);
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
} else if (pLen >= 8 && strlen(encrypt_confirm_buf_) > 0 &&
|
||||
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) != 0) {
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||
ui::material::Error(), TR("wiz_pass_mismatch"));
|
||||
ui::material::Error(), "Passphrases do not match");
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
}
|
||||
@@ -1339,12 +1232,12 @@ void App::renderFirstRunWizard() {
|
||||
cy += 8.0f * dp;
|
||||
|
||||
{
|
||||
const char* pinTitle = TR("wiz_pin_optional");
|
||||
const char* pinTitle = "Quick-Unlock PIN (optional)";
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol, pinTitle);
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
}
|
||||
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, TR("wiz_pin_label"));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "PIN (4-8 digits):");
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||
@@ -1356,7 +1249,7 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PopItemWidth();
|
||||
cy += 36.0f * dp + 6.0f * dp;
|
||||
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, TR("wiz_pin_confirm"));
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), dimCol, "Confirm PIN:");
|
||||
cy += captionFont->LegacySize + 4.0f * dp;
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||
@@ -1373,12 +1266,12 @@ void App::renderFirstRunWizard() {
|
||||
std::string pinStr(wizard_pin_buf_);
|
||||
if (!pinStr.empty() && !util::SecureVault::isValidPin(pinStr)) {
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||
ui::material::Error(), TR("wiz_pin_invalid"));
|
||||
ui::material::Error(), "PIN must be 4-8 digits");
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
} else if (!pinStr.empty() && strlen(wizard_pin_confirm_buf_) > 0 &&
|
||||
pinStr != std::string(wizard_pin_confirm_buf_)) {
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||
ui::material::Error(), TR("wiz_pin_mismatch"));
|
||||
ui::material::Error(), "PINs do not match");
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
}
|
||||
@@ -1390,24 +1283,10 @@ void App::renderFirstRunWizard() {
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
|
||||
// Warn + block if the passphrase has leading/trailing whitespace. Silently trimming it would
|
||||
// change the passphrase the user believes they set and lock them out on the next unlock.
|
||||
bool passEdgeSpace = false;
|
||||
if (size_t pl = strlen(encrypt_pass_buf_)) {
|
||||
char a = encrypt_pass_buf_[0], b = encrypt_pass_buf_[pl - 1];
|
||||
passEdgeSpace = (a == ' ' || a == '\t' || b == ' ' || b == '\t');
|
||||
}
|
||||
if (passEdgeSpace) {
|
||||
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||
ui::material::Error(), TR("wiz_pass_spaces"));
|
||||
cy += captionFont->LegacySize + 6.0f * dp;
|
||||
}
|
||||
|
||||
// Buttons
|
||||
{
|
||||
bool passValid = strlen(encrypt_pass_buf_) >= 8 &&
|
||||
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0 &&
|
||||
!passEdgeSpace;
|
||||
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0;
|
||||
// PIN is optional: if entered, must be valid + confirmed
|
||||
std::string pinStr(wizard_pin_buf_);
|
||||
bool pinEntered = !pinStr.empty();
|
||||
@@ -1429,15 +1308,11 @@ void App::renderFirstRunWizard() {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
ImGui::BeginDisabled(!canEncrypt);
|
||||
if (ui::material::TactileButton(TR("wiz_encrypt_continue"), ImVec2(encBtnW, btnH2))) {
|
||||
if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
|
||||
// Save passphrase + optional PIN for background processing
|
||||
wallet_security_.beginDeferredEncryption(
|
||||
std::string(encrypt_pass_buf_),
|
||||
(pinEntered && pinOk) ? pinStr : std::string());
|
||||
// Persist that encryption was requested (never the passphrase) so a quit/crash or
|
||||
// failed daemon connect before it applies isn't silent — reconciled on the next
|
||||
// connect in App::reconcileEncryptionPending (W2-2). Saved with the wizard state below.
|
||||
settings_->setEncryptionPending(true);
|
||||
|
||||
// Clear sensitive buffers
|
||||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||
@@ -1456,8 +1331,7 @@ void App::renderFirstRunWizard() {
|
||||
wizard_phase_ = WizardPhase::Done;
|
||||
settings_->setWizardCompleted(true);
|
||||
settings_->save();
|
||||
// The handoff toast is now emitted once at the completion point in app.cpp
|
||||
// (wiz_handoff_encrypting), so don't also toast here.
|
||||
ui::Notifications::instance().info("Encryption will complete in the background");
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleVar();
|
||||
@@ -1465,21 +1339,7 @@ void App::renderFirstRunWizard() {
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(skipW2, btnH2))) {
|
||||
static bool s_skipEncConfirm = false;
|
||||
if (!s_skipEncConfirm) {
|
||||
// Skipping stores private keys UNENCRYPTED — require a confirming second click.
|
||||
s_skipEncConfirm = true;
|
||||
encrypt_status_ = TR("wiz_skip_confirm");
|
||||
} else {
|
||||
s_skipEncConfirm = false;
|
||||
// Skipping leaves the wallet UNENCRYPTED — wipe the passphrase/PIN the user may have
|
||||
// typed so it doesn't linger in these process-lifetime buffers (only the Encrypt
|
||||
// path cleared them before). (L-07)
|
||||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||
memset(encrypt_confirm_buf_, 0, sizeof(encrypt_confirm_buf_));
|
||||
memset(wizard_pin_buf_, 0, sizeof(wizard_pin_buf_));
|
||||
memset(wizard_pin_confirm_buf_, 0, sizeof(wizard_pin_confirm_buf_));
|
||||
if (ImGui::Button("Skip##enc", ImVec2(skipW2, btnH2))) {
|
||||
wizard_phase_ = WizardPhase::Done;
|
||||
settings_->setWizardCompleted(true);
|
||||
settings_->save();
|
||||
@@ -1491,13 +1351,12 @@ void App::renderFirstRunWizard() {
|
||||
}
|
||||
tryConnect();
|
||||
}
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
cy += btnH2;
|
||||
}
|
||||
} else {
|
||||
// ---- Not focused: show static description ----
|
||||
const char* encDesc = TR("wiz_encrypt_desc");
|
||||
const char* encDesc = "Encrypt your wallet to protect private keys with a passphrase.";
|
||||
ImVec2 edSize = bodyFont->CalcTextSizeA(bodyFont->LegacySize, FLT_MAX, contentW, encDesc);
|
||||
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), dimCol, encDesc, nullptr, contentW);
|
||||
cy += edSize.y + 6.0f * dp;
|
||||
@@ -1515,9 +1374,7 @@ void App::renderFirstRunWizard() {
|
||||
}
|
||||
|
||||
// --- Deferred Card 0 finalization: match right column total height ---
|
||||
// Only for the focused/upcoming Appearance card; a completed one was already finalized
|
||||
// above as a compact pill and must not be re-stretched.
|
||||
if (cardState(0) != 2) {
|
||||
{
|
||||
float rightColBot = card2Bot;
|
||||
if (rightColBot > card0Bot) card0Bot = rightColBot;
|
||||
finalizeCard(leftX, colW, card0Top, card0Bot, cardState(0));
|
||||
@@ -1526,24 +1383,6 @@ void App::renderFirstRunWizard() {
|
||||
// Merge channels: backgrounds → content → overlays
|
||||
dl->ChannelsMerge();
|
||||
|
||||
// Measure this frame's content height (feeds next frame's scroll clamp) and, when it overflows the
|
||||
// window, draw a slim scroll indicator so the off-screen content is discoverable.
|
||||
{
|
||||
float contentBottom = std::max(card0Bot, std::max(card1Bot, card2Bot));
|
||||
// Subtract vCenter back out: everything below the header was shifted down by it, so the raw
|
||||
// span includes it. We want s_wizContentH to be the true (un-centered) content height, or the
|
||||
// vertical-centering above would feed on itself and oscillate frame-to-frame.
|
||||
s_wizContentH = (contentBottom - winPos.y + scrollY - vCenter) + 24.0f * dp;
|
||||
if (wizMaxScroll > 0.0f && s_wizContentH > 0.0f) {
|
||||
float trackH = winSize.y - 8.0f * dp;
|
||||
float thumbH = std::min(trackH, std::max(32.0f * dp, trackH * (winSize.y / s_wizContentH)));
|
||||
float thumbY = winPos.y + 4.0f * dp + (trackH - thumbH) * (scrollY / wizMaxScroll);
|
||||
float barX = winPos.x + winSize.x - 6.0f * dp;
|
||||
dl->AddRectFilled(ImVec2(barX, thumbY), ImVec2(barX + 3.0f * dp, thumbY + thumbH),
|
||||
ui::material::WithAlpha(ui::material::OnSurface(), 55), 1.5f * dp);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
// DragonX Wallet - HushChat crypto primitives (implementation).
|
||||
|
||||
#include "chat_crypto.h"
|
||||
|
||||
#include <sodium.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
namespace {
|
||||
|
||||
// Local constants tied to the libsodium primitive. (The dev-only chat_fixture_tooling.h
|
||||
// declares equivalents, but that header is not linked into the app.)
|
||||
constexpr std::size_t kStreamHeaderBytes = 24; // crypto_secretstream_xchacha20poly1305_HEADERBYTES
|
||||
constexpr std::size_t kStreamABytes = 17; // crypto_secretstream_xchacha20poly1305_ABYTES
|
||||
|
||||
static_assert(kChatKeyBytes == crypto_kx_PUBLICKEYBYTES, "kx public key size mismatch");
|
||||
static_assert(kChatKeyBytes == crypto_kx_SECRETKEYBYTES, "kx secret key size mismatch");
|
||||
|
||||
// Decode exactly outLen bytes from a lowercase/uppercase hex string; reject any other length.
|
||||
bool hexToFixed(const std::string& hex, unsigned char* out, std::size_t outLen) {
|
||||
if (hex.size() != outLen * 2) return false;
|
||||
std::size_t binLen = 0;
|
||||
if (sodium_hex2bin(out, outLen, hex.data(), hex.size(), nullptr, &binLen, nullptr) != 0) {
|
||||
return false;
|
||||
}
|
||||
return binLen == outLen;
|
||||
}
|
||||
|
||||
// Decode a variable-length hex string into bytes.
|
||||
bool hexToBytes(const std::string& hex, std::vector<unsigned char>& out) {
|
||||
if (hex.empty() || (hex.size() % 2) != 0) return false;
|
||||
out.resize(hex.size() / 2);
|
||||
std::size_t binLen = 0;
|
||||
if (sodium_hex2bin(out.data(), out.size(), hex.data(), hex.size(), nullptr, &binLen, nullptr) != 0) {
|
||||
return false;
|
||||
}
|
||||
out.resize(binLen);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string bytesToHex(const unsigned char* bytes, std::size_t n) {
|
||||
std::string hex(n * 2 + 1, '\0');
|
||||
sodium_bin2hex(&hex[0], hex.size(), bytes, n);
|
||||
hex.resize(n * 2); // drop the NUL sodium_bin2hex appends
|
||||
return hex;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* chatCryptoStatusName(ChatCryptoStatus status) {
|
||||
switch (status) {
|
||||
case ChatCryptoStatus::Ok: return "Ok";
|
||||
case ChatCryptoStatus::SodiumInitFailed: return "SodiumInitFailed";
|
||||
case ChatCryptoStatus::BadPeerKey: return "BadPeerKey";
|
||||
case ChatCryptoStatus::BadHeaderHex: return "BadHeaderHex";
|
||||
case ChatCryptoStatus::BadCiphertextHex: return "BadCiphertextHex";
|
||||
case ChatCryptoStatus::CiphertextTooShort: return "CiphertextTooShort";
|
||||
case ChatCryptoStatus::SessionKeyFailed: return "SessionKeyFailed";
|
||||
case ChatCryptoStatus::EncryptFailed: return "EncryptFailed";
|
||||
case ChatCryptoStatus::DecryptFailed: return "DecryptFailed";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
void wipeChatKeyPair(ChatKeyPair& keys) {
|
||||
sodium_memzero(keys.public_key.data(), keys.public_key.size());
|
||||
sodium_memzero(keys.secret_key.data(), keys.secret_key.size());
|
||||
}
|
||||
|
||||
ChatCryptoStatus encryptOutgoing(const ChatKeyPair& mine,
|
||||
const std::string& peerPublicKeyHex,
|
||||
const std::string& plaintext,
|
||||
std::string& outStreamHeaderHex,
|
||||
std::string& outCiphertextHex) {
|
||||
static_assert(kStreamHeaderBytes == crypto_secretstream_xchacha20poly1305_HEADERBYTES, "");
|
||||
static_assert(kStreamABytes == crypto_secretstream_xchacha20poly1305_ABYTES, "");
|
||||
|
||||
if (sodium_init() < 0) return ChatCryptoStatus::SodiumInitFailed;
|
||||
|
||||
unsigned char peerPk[crypto_kx_PUBLICKEYBYTES];
|
||||
if (!hexToFixed(peerPublicKeyHex, peerPk, sizeof peerPk)) return ChatCryptoStatus::BadPeerKey;
|
||||
|
||||
unsigned char rx[crypto_kx_SESSIONKEYBYTES];
|
||||
unsigned char tx[crypto_kx_SESSIONKEYBYTES];
|
||||
if (crypto_kx_server_session_keys(rx, tx, mine.public_key.data(), mine.secret_key.data(), peerPk) != 0) {
|
||||
sodium_memzero(rx, sizeof rx);
|
||||
sodium_memzero(tx, sizeof tx);
|
||||
return ChatCryptoStatus::SessionKeyFailed;
|
||||
}
|
||||
|
||||
crypto_secretstream_xchacha20poly1305_state state;
|
||||
unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES];
|
||||
ChatCryptoStatus result = ChatCryptoStatus::EncryptFailed;
|
||||
|
||||
if (crypto_secretstream_xchacha20poly1305_init_push(&state, header, tx) == 0) {
|
||||
std::vector<unsigned char> ciphertext(plaintext.size() + crypto_secretstream_xchacha20poly1305_ABYTES);
|
||||
unsigned long long ctLen = 0;
|
||||
// Only report Ok if the push actually succeeded — otherwise ctLen stays 0 and we would
|
||||
// ship a valid header with an empty ciphertext.
|
||||
if (crypto_secretstream_xchacha20poly1305_push(
|
||||
&state, ciphertext.data(), &ctLen,
|
||||
reinterpret_cast<const unsigned char*>(plaintext.data()), plaintext.size(),
|
||||
nullptr, 0, crypto_secretstream_xchacha20poly1305_TAG_FINAL) == 0) {
|
||||
outStreamHeaderHex = bytesToHex(header, sizeof header);
|
||||
outCiphertextHex = bytesToHex(ciphertext.data(), static_cast<std::size_t>(ctLen));
|
||||
result = ChatCryptoStatus::Ok;
|
||||
}
|
||||
}
|
||||
|
||||
sodium_memzero(rx, sizeof rx);
|
||||
sodium_memzero(tx, sizeof tx);
|
||||
sodium_memzero(&state, sizeof state);
|
||||
return result;
|
||||
}
|
||||
|
||||
ChatCryptoStatus decryptIncoming(const ChatKeyPair& mine,
|
||||
const std::string& peerPublicKeyHex,
|
||||
const std::string& streamHeaderHex,
|
||||
const std::string& ciphertextHex,
|
||||
std::string& outPlaintext) {
|
||||
if (sodium_init() < 0) return ChatCryptoStatus::SodiumInitFailed;
|
||||
|
||||
unsigned char peerPk[crypto_kx_PUBLICKEYBYTES];
|
||||
if (!hexToFixed(peerPublicKeyHex, peerPk, sizeof peerPk)) return ChatCryptoStatus::BadPeerKey;
|
||||
|
||||
unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES];
|
||||
if (!hexToFixed(streamHeaderHex, header, sizeof header)) return ChatCryptoStatus::BadHeaderHex;
|
||||
|
||||
std::vector<unsigned char> ciphertext;
|
||||
if (!hexToBytes(ciphertextHex, ciphertext)) return ChatCryptoStatus::BadCiphertextHex;
|
||||
// Guard the size_t subtraction below (a ciphertext shorter than the auth tag can't be
|
||||
// authentic). Exactly ABYTES is the valid empty-plaintext case, so it round-trips
|
||||
// symmetrically with encryptOutgoing (message-content policy belongs to the caller).
|
||||
if (ciphertext.size() < crypto_secretstream_xchacha20poly1305_ABYTES) {
|
||||
return ChatCryptoStatus::CiphertextTooShort;
|
||||
}
|
||||
|
||||
unsigned char rx[crypto_kx_SESSIONKEYBYTES];
|
||||
unsigned char tx[crypto_kx_SESSIONKEYBYTES];
|
||||
if (crypto_kx_client_session_keys(rx, tx, mine.public_key.data(), mine.secret_key.data(), peerPk) != 0) {
|
||||
sodium_memzero(rx, sizeof rx);
|
||||
sodium_memzero(tx, sizeof tx);
|
||||
return ChatCryptoStatus::SessionKeyFailed;
|
||||
}
|
||||
|
||||
crypto_secretstream_xchacha20poly1305_state state;
|
||||
ChatCryptoStatus result = ChatCryptoStatus::DecryptFailed;
|
||||
|
||||
if (crypto_secretstream_xchacha20poly1305_init_pull(&state, header, rx) == 0) {
|
||||
std::vector<unsigned char> plain(ciphertext.size() - crypto_secretstream_xchacha20poly1305_ABYTES);
|
||||
unsigned long long plainLen = 0;
|
||||
unsigned char tag = 0;
|
||||
if (crypto_secretstream_xchacha20poly1305_pull(
|
||||
&state, plain.data(), &plainLen, &tag,
|
||||
ciphertext.data(), ciphertext.size(), nullptr, 0) == 0 &&
|
||||
tag == crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
|
||||
outPlaintext.assign(reinterpret_cast<const char*>(plain.data()),
|
||||
static_cast<std::size_t>(plainLen));
|
||||
result = ChatCryptoStatus::Ok;
|
||||
}
|
||||
sodium_memzero(plain.data(), plain.size()); // wipe the decrypted scratch
|
||||
}
|
||||
|
||||
sodium_memzero(rx, sizeof rx);
|
||||
sodium_memzero(tx, sizeof tx);
|
||||
sodium_memzero(&state, sizeof state);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace dragonx::chat
|
||||
@@ -1,65 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// DragonX Wallet - HushChat crypto primitives.
|
||||
//
|
||||
// crypto_kx (X25519) session-key agreement + crypto_secretstream_xchacha20poly1305
|
||||
// message encryption, byte-exact per the HushChat wire format so DragonX interoperates
|
||||
// with SilentDragonXLite. See docs/_archive/contacts-chat-phase1-detail-2026-07-05.md
|
||||
// (Appendix A.3/A.4). Pure crypto — no gating, no I/O; the feature gate lives at the
|
||||
// service layer. NEVER logs plaintext, ciphertext, keys, or session material.
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
// crypto_kx key sizes (== crypto_kx_PUBLICKEYBYTES / SECRETKEYBYTES == 32).
|
||||
constexpr std::size_t kChatKeyBytes = 32;
|
||||
|
||||
using ChatPublicKey = std::array<unsigned char, kChatKeyBytes>;
|
||||
using ChatSecretKey = std::array<unsigned char, kChatKeyBytes>;
|
||||
|
||||
struct ChatKeyPair {
|
||||
ChatPublicKey public_key{};
|
||||
ChatSecretKey secret_key{};
|
||||
};
|
||||
|
||||
enum class ChatCryptoStatus {
|
||||
Ok,
|
||||
SodiumInitFailed,
|
||||
BadPeerKey, // peer public-key hex missing / wrong length / not hex
|
||||
BadHeaderHex, // secretstream header hex missing / wrong length / not hex
|
||||
BadCiphertextHex, // ciphertext hex malformed
|
||||
CiphertextTooShort, // ciphertext shorter than the auth tag
|
||||
SessionKeyFailed, // crypto_kx_*_session_keys rejected the peer key
|
||||
EncryptFailed,
|
||||
DecryptFailed // init_pull / pull / tag mismatch — the single neutral auth failure
|
||||
};
|
||||
|
||||
const char* chatCryptoStatusName(ChatCryptoStatus status);
|
||||
|
||||
// Encrypt `plaintext` addressed to peer `peerPublicKeyHex` (64 lowercase hex chars).
|
||||
// Sender takes the crypto_kx "server" role (server_tx), matching SDXL's send path.
|
||||
// Outputs the secretstream header hex (the memo "e" field) and the ciphertext hex
|
||||
// (the payload memo). Returns Ok on success.
|
||||
ChatCryptoStatus encryptOutgoing(const ChatKeyPair& mine,
|
||||
const std::string& peerPublicKeyHex,
|
||||
const std::string& plaintext,
|
||||
std::string& outStreamHeaderHex,
|
||||
std::string& outCiphertextHex);
|
||||
|
||||
// Decrypt an incoming message addressed to us from peer `peerPublicKeyHex`.
|
||||
// Receiver takes the crypto_kx "client" role (client_rx), matching SDXL's receive path.
|
||||
// Requires the memo "e" (streamHeaderHex) and the payload ciphertext hex. Enforces the
|
||||
// Poly1305 auth tag AND that the stream tag is TAG_FINAL. Returns Ok + fills outPlaintext.
|
||||
ChatCryptoStatus decryptIncoming(const ChatKeyPair& mine,
|
||||
const std::string& peerPublicKeyHex,
|
||||
const std::string& streamHeaderHex,
|
||||
const std::string& ciphertextHex,
|
||||
std::string& outPlaintext);
|
||||
|
||||
// Zero both key arrays (call when discarding an identity's keys).
|
||||
void wipeChatKeyPair(ChatKeyPair& keys);
|
||||
|
||||
} // namespace dragonx::chat
|
||||
@@ -1,558 +0,0 @@
|
||||
// DragonX Wallet - HushChat persistent message store (implementation).
|
||||
|
||||
#include "chat_database.h"
|
||||
|
||||
#include "../util/logger.h"
|
||||
#include "../util/platform.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <sodium.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <utility>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
namespace {
|
||||
|
||||
// Domain-separated KDF contexts (used as the keyed-BLAKE2b key, like chat_identity). Both lengths
|
||||
// sit inside crypto_generichash's key-length bounds. Bumping a context rotates that derivation.
|
||||
constexpr char kStorageKeyContext[] = "DragonX-HushChat-Storage-v1";
|
||||
constexpr char kWalletTagContext[] = "DragonX-HushChat-WalletId-v1";
|
||||
constexpr std::size_t kStorageKeyContextLen = sizeof(kStorageKeyContext) - 1;
|
||||
constexpr std::size_t kWalletTagContextLen = sizeof(kWalletTagContext) - 1;
|
||||
|
||||
std::string toHex(const unsigned char* data, std::size_t len)
|
||||
{
|
||||
static const char* kHex = "0123456789abcdef";
|
||||
std::string out;
|
||||
out.reserve(len * 2);
|
||||
for (std::size_t i = 0; i < len; ++i) {
|
||||
out.push_back(kHex[data[i] >> 4]);
|
||||
out.push_back(kHex[data[i] & 0x0F]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// keyed-BLAKE2b: out = generichash(in=secret, key=context). Deterministic, so the same seed always
|
||||
// derives the same storage key + wallet tag across sessions.
|
||||
bool deriveKeyed(const std::string& secret, const char* context, std::size_t contextLen,
|
||||
unsigned char* out, std::size_t outLen)
|
||||
{
|
||||
return crypto_generichash(out, outLen,
|
||||
reinterpret_cast<const unsigned char*>(secret.data()), secret.size(),
|
||||
reinterpret_cast<const unsigned char*>(context), contextLen) == 0;
|
||||
}
|
||||
|
||||
std::string associatedData(const std::string& walletTag)
|
||||
{
|
||||
return std::string("obsidian-dragon-hushchat-v1:") + walletTag;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ChatDatabase::ChatDatabase() : database_path_(defaultDatabasePath()) {}
|
||||
ChatDatabase::ChatDatabase(std::string databasePath) : database_path_(std::move(databasePath)) {}
|
||||
|
||||
ChatDatabase::~ChatDatabase()
|
||||
{
|
||||
lock();
|
||||
close();
|
||||
}
|
||||
|
||||
std::string ChatDatabase::defaultDatabasePath()
|
||||
{
|
||||
return (fs::path(util::Platform::getConfigDir()) / "chat_messages.sqlite").string();
|
||||
}
|
||||
|
||||
bool ChatDatabase::unlockWithSecret(const std::string& secret)
|
||||
{
|
||||
if (sodium_init() < 0) return false;
|
||||
|
||||
if (!deriveKeyed(secret, kStorageKeyContext, kStorageKeyContextLen, key_.data(), key_.size()))
|
||||
return false;
|
||||
|
||||
unsigned char tag[32];
|
||||
if (!deriveKeyed(secret, kWalletTagContext, kWalletTagContextLen, tag, sizeof(tag))) {
|
||||
sodium_memzero(key_.data(), key_.size());
|
||||
return false;
|
||||
}
|
||||
wallet_tag_ = toHex(tag, sizeof(tag));
|
||||
sodium_memzero(tag, sizeof(tag));
|
||||
key_ready_ = true;
|
||||
|
||||
if (!ensureOpen()) {
|
||||
lock();
|
||||
return false;
|
||||
}
|
||||
loadTombstones();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ChatDatabase::lock()
|
||||
{
|
||||
sodium_memzero(key_.data(), key_.size());
|
||||
key_ready_ = false;
|
||||
lockSensitive(); // also drop the sensitive-conversation vault key
|
||||
wallet_tag_.clear();
|
||||
tombstones_.clear();
|
||||
}
|
||||
|
||||
bool ChatDatabase::append(const ChatMessage& message)
|
||||
{
|
||||
if (!key_ready_ || !ensureOpen()) return false;
|
||||
if (isTombstoned(message)) return false; // locally deleted — don't re-persist on a chain re-scan
|
||||
|
||||
bool skip = false;
|
||||
const unsigned char* wkey = writeKeyFor(message.conversation_id, skip);
|
||||
if (skip) return false; // sensitive conversation, vault locked — don't persist (re-imports when unlocked)
|
||||
|
||||
std::vector<unsigned char> nonce;
|
||||
std::vector<unsigned char> cipher;
|
||||
std::string plain;
|
||||
bool encrypted = false;
|
||||
try {
|
||||
// serialize() dumps strictly, so an invalid-UTF-8 (attacker-influenceable) body THROWS here;
|
||||
// skip the row instead of letting it escape the write path and crash (appendBatch already does).
|
||||
plain = serialize(message);
|
||||
encrypted = encrypt(wkey, plain, nonce, cipher);
|
||||
} catch (const std::exception& e) {
|
||||
if (!plain.empty()) sodium_memzero(&plain[0], plain.size());
|
||||
DEBUG_LOGF("[chat] append skipped a malformed message: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
int ChatDatabase::appendBatch(const std::vector<ChatMessage>& messages)
|
||||
{
|
||||
if (!key_ready_ || !ensureOpen()) return 0;
|
||||
if (messages.empty()) return 0;
|
||||
|
||||
struct Row { std::string dedup; std::vector<unsigned char> nonce, cipher; };
|
||||
std::vector<Row> rows;
|
||||
rows.reserve(messages.size());
|
||||
for (const auto& m : messages) {
|
||||
if (isTombstoned(m)) continue; // locally deleted — don't re-persist
|
||||
bool skip = false;
|
||||
const unsigned char* wkey = writeKeyFor(m.conversation_id, skip);
|
||||
if (skip) continue; // sensitive + vault locked — don't persist
|
||||
try {
|
||||
std::string plain = serialize(m); // strict json.dump() may throw on non-UTF-8 body
|
||||
std::vector<unsigned char> nonce, cipher;
|
||||
const bool ok = encrypt(wkey, plain, nonce, cipher);
|
||||
if (!plain.empty()) sodium_memzero(&plain[0], plain.size());
|
||||
if (!ok) continue;
|
||||
rows.push_back({ dedupHash(m.txid, m.payload_position), std::move(nonce), std::move(cipher) });
|
||||
} catch (const std::exception&) {
|
||||
continue; // skip only this message; never open a txn with bad data
|
||||
}
|
||||
}
|
||||
if (rows.empty()) return 0;
|
||||
|
||||
if (!exec("BEGIN")) return 0; // match deleteMessages() in-file style
|
||||
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) {
|
||||
exec("ROLLBACK");
|
||||
return 0;
|
||||
}
|
||||
int inserted = 0;
|
||||
bool ok = true;
|
||||
for (const auto& r : rows) {
|
||||
sqlite3_reset(stmt);
|
||||
sqlite3_clear_bindings(stmt);
|
||||
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, r.dedup.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_blob(stmt, 3, r.nonce.data(), static_cast<int>(r.nonce.size()), SQLITE_TRANSIENT);
|
||||
sqlite3_bind_blob(stmt, 4, r.cipher.data(), static_cast<int>(r.cipher.size()), SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(stmt) != SQLITE_DONE) { ok = false; break; }
|
||||
if (sqlite3_changes(db_) > 0) ++inserted;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
if (!exec(ok ? "COMMIT" : "ROLLBACK")) return 0;
|
||||
return ok ? inserted : 0;
|
||||
}
|
||||
|
||||
bool ChatDatabase::upsert(const ChatMessage& message)
|
||||
{
|
||||
if (!key_ready_ || !ensureOpen()) return false;
|
||||
|
||||
bool skip = false;
|
||||
const unsigned char* wkey = writeKeyFor(message.conversation_id, skip);
|
||||
if (skip) return false; // sensitive conversation, vault locked — don't persist the echo under the seed key
|
||||
|
||||
std::vector<unsigned char> nonce;
|
||||
std::vector<unsigned char> cipher;
|
||||
std::string plain;
|
||||
bool encrypted = false;
|
||||
try {
|
||||
plain = serialize(message); // strict dump → a malformed body throws; skip rather than crash
|
||||
encrypted = encrypt(wkey, plain, nonce, cipher);
|
||||
} catch (const std::exception& e) {
|
||||
if (!plain.empty()) sodium_memzero(&plain[0], plain.size());
|
||||
DEBUG_LOGF("[chat] upsert skipped a malformed message: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
if (!plain.empty()) sodium_memzero(&plain[0], plain.size());
|
||||
if (!encrypted) return false;
|
||||
|
||||
const std::string dedup = dedupHash(message.txid, message.payload_position);
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_,
|
||||
"INSERT INTO chat_messages (wallet_tag, dedup_hash, nonce, payload) VALUES (?, ?, ?, ?) "
|
||||
"ON CONFLICT(wallet_tag, dedup_hash) DO UPDATE SET nonce=excluded.nonce, payload=excluded.payload",
|
||||
-1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return false;
|
||||
}
|
||||
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, dedup.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_blob(stmt, 3, nonce.data(), static_cast<int>(nonce.size()), SQLITE_TRANSIENT);
|
||||
sqlite3_bind_blob(stmt, 4, cipher.data(), static_cast<int>(cipher.size()), SQLITE_TRANSIENT);
|
||||
const bool done = sqlite3_step(stmt) == SQLITE_DONE;
|
||||
sqlite3_finalize(stmt);
|
||||
return done;
|
||||
}
|
||||
|
||||
std::vector<ChatMessage> ChatDatabase::load()
|
||||
{
|
||||
std::vector<ChatMessage> out;
|
||||
if (!key_ready_ || !ensureOpen()) return out;
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_,
|
||||
"SELECT nonce, payload FROM chat_messages WHERE wallet_tag = ? ORDER BY rowid",
|
||||
-1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return out;
|
||||
}
|
||||
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const auto* noncePtr = static_cast<const unsigned char*>(sqlite3_column_blob(stmt, 0));
|
||||
const int nonceLen = sqlite3_column_bytes(stmt, 0);
|
||||
const auto* cipherPtr = static_cast<const unsigned char*>(sqlite3_column_blob(stmt, 1));
|
||||
const int cipherLen = sqlite3_column_bytes(stmt, 1);
|
||||
if (!noncePtr || !cipherPtr) continue;
|
||||
|
||||
std::vector<unsigned char> nonce(noncePtr, noncePtr + nonceLen);
|
||||
std::vector<unsigned char> cipher(cipherPtr, cipherPtr + cipherLen);
|
||||
std::string plain;
|
||||
// Try the seed key first; a sensitive row only decrypts under the vault key (and is silently
|
||||
// withheld when the vault is locked / on a fresh seed import that has no vault key). A wrong
|
||||
// wallet / tampered row fails both and is skipped.
|
||||
if (!decrypt(key_.data(), nonce, cipher, plain) &&
|
||||
!(sensitive_ready_ && decrypt(sensitive_key_.data(), nonce, cipher, plain))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
for (const char* sql : {"DELETE FROM chat_messages WHERE wallet_tag = ?",
|
||||
"DELETE FROM chat_deleted WHERE wallet_tag = ?"}) {
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) continue;
|
||||
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
tombstones_.clear();
|
||||
}
|
||||
|
||||
bool ChatDatabase::deleteMessages(const std::vector<ChatMessage>& messages, bool tombstone)
|
||||
{
|
||||
if (!key_ready_ || !ensureOpen()) return false;
|
||||
if (messages.empty()) return true;
|
||||
|
||||
if (!exec("BEGIN")) return false;
|
||||
bool ok = true;
|
||||
for (const auto& m : messages) {
|
||||
const std::string dedup = dedupHash(m.txid, m.payload_position);
|
||||
|
||||
sqlite3_stmt* del = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, "DELETE FROM chat_messages WHERE wallet_tag = ? AND dedup_hash = ?",
|
||||
-1, &del, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_text(del, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(del, 2, dedup.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(del) != SQLITE_DONE) ok = false;
|
||||
sqlite3_finalize(del);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (tombstone) {
|
||||
sqlite3_stmt* ins = nullptr;
|
||||
if (sqlite3_prepare_v2(db_,
|
||||
"INSERT OR IGNORE INTO chat_deleted (wallet_tag, dedup_hash) VALUES (?, ?)",
|
||||
-1, &ins, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_text(ins, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(ins, 2, dedup.c_str(), -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(ins) != SQLITE_DONE) ok = false;
|
||||
sqlite3_finalize(ins);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!exec(ok ? "COMMIT" : "ROLLBACK")) ok = false;
|
||||
// Only reflect the tombstones in the in-memory cache once they are durably committed.
|
||||
if (ok && tombstone)
|
||||
for (const auto& m : messages) tombstones_.insert(dedupHash(m.txid, m.payload_position));
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool ChatDatabase::isTombstoned(const ChatMessage& message) const
|
||||
{
|
||||
if (!key_ready_ || tombstones_.empty()) return false;
|
||||
return tombstones_.count(dedupHash(message.txid, message.payload_position)) > 0;
|
||||
}
|
||||
|
||||
void ChatDatabase::loadTombstones()
|
||||
{
|
||||
tombstones_.clear();
|
||||
if (!key_ready_ || !ensureOpen()) return;
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, "SELECT dedup_hash FROM chat_deleted WHERE wallet_tag = ?",
|
||||
-1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return;
|
||||
}
|
||||
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const auto* h = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
||||
if (h) tombstones_.insert(h);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
bool ChatDatabase::ensureOpen()
|
||||
{
|
||||
if (db_) return true;
|
||||
|
||||
try {
|
||||
fs::path path(database_path_);
|
||||
if (!path.parent_path().empty()) fs::create_directories(path.parent_path());
|
||||
} catch (const std::exception& exception) {
|
||||
DEBUG_LOGF("Failed to create chat database directory: %s\n", exception.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3* openedDb = nullptr;
|
||||
if (sqlite3_open(database_path_.c_str(), &openedDb) != SQLITE_OK) {
|
||||
DEBUG_LOGF("Failed to open chat database: %s\n",
|
||||
openedDb ? sqlite3_errmsg(openedDb) : "unknown error");
|
||||
if (openedDb) sqlite3_close(openedDb);
|
||||
return false;
|
||||
}
|
||||
|
||||
db_ = openedDb;
|
||||
sqlite3_busy_timeout(db_, 2000);
|
||||
exec("PRAGMA journal_mode=WAL");
|
||||
exec("PRAGMA synchronous=NORMAL");
|
||||
|
||||
// C3-1: restrict the chat DB and its WAL/SHM sidecars to owner-only. sqlite creates them with
|
||||
// umask-derived permissions (often world/group-readable); they hold per-row nonces + AEAD
|
||||
// ciphertext of the user's messages. Best-effort (errors swallowed; a no-op-ish on Windows).
|
||||
{
|
||||
std::error_code perr;
|
||||
const auto ownerOnly = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write;
|
||||
std::filesystem::permissions(database_path_, ownerOnly, std::filesystem::perm_options::replace, perr);
|
||||
std::filesystem::permissions(database_path_ + "-wal", ownerOnly, std::filesystem::perm_options::replace, perr);
|
||||
std::filesystem::permissions(database_path_ + "-shm", ownerOnly, std::filesystem::perm_options::replace, perr);
|
||||
}
|
||||
|
||||
if (!createSchema()) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChatDatabase::exec(const char* sql)
|
||||
{
|
||||
if (!db_) return false;
|
||||
char* error = nullptr;
|
||||
if (sqlite3_exec(db_, sql, nullptr, nullptr, &error) != SQLITE_OK) {
|
||||
DEBUG_LOGF("Chat database SQL error: %s\n", error ? error : sqlite3_errmsg(db_));
|
||||
if (error) sqlite3_free(error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChatDatabase::createSchema()
|
||||
{
|
||||
if (!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))"))
|
||||
return false;
|
||||
// Tombstones for locally-deleted messages (dedup_hash only — the same keyed, non-revealing hash the
|
||||
// message rows use). A chain re-scan checks this so a deleted message never re-imports.
|
||||
return exec("CREATE TABLE IF NOT EXISTS chat_deleted ("
|
||||
"wallet_tag TEXT NOT NULL, "
|
||||
"dedup_hash TEXT 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(); // strict: an invalid-UTF-8 body throws and is DROPPED by the callers (contract)
|
||||
}
|
||||
|
||||
bool ChatDatabase::deserialize(const std::string& json, ChatMessage& out) const
|
||||
{
|
||||
try {
|
||||
const auto parsed = nlohmann::json::parse(json);
|
||||
out.direction = static_cast<ChatDirection>(parsed.value("d", 0));
|
||||
out.kind = static_cast<ChatMessageKind>(parsed.value("k", 0));
|
||||
out.txid = parsed.value("txid", std::string());
|
||||
out.conversation_id = parsed.value("cid", std::string());
|
||||
out.peer_zaddr = parsed.value("z", std::string());
|
||||
out.peer_public_key_hex = parsed.value("p", std::string());
|
||||
out.body = parsed.value("b", std::string());
|
||||
out.timestamp = parsed.value("ts", static_cast<std::int64_t>(0));
|
||||
out.payload_position = static_cast<std::size_t>(parsed.value("pos", static_cast<std::uint64_t>(0)));
|
||||
out.delivery = static_cast<ChatDelivery>(parsed.value("dl", 0)); // old rows → Sent (0)
|
||||
// A persisted "Sending" means we crashed mid-broadcast; the outcome is unknown. Resolve it
|
||||
// optimistically to Sent on load so it can't show a stuck spinner forever.
|
||||
if (out.delivery == ChatDelivery::Sending) out.delivery = ChatDelivery::Sent;
|
||||
return true;
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void ChatDatabase::setSensitiveKey(const std::array<unsigned char, 32>& key)
|
||||
{
|
||||
sensitive_key_ = key; // copy; the caller still owns & wipes its own copy
|
||||
sensitive_ready_ = true;
|
||||
}
|
||||
|
||||
void ChatDatabase::lockSensitive()
|
||||
{
|
||||
sodium_memzero(sensitive_key_.data(), sensitive_key_.size());
|
||||
sensitive_ready_ = false;
|
||||
}
|
||||
|
||||
const unsigned char* ChatDatabase::writeKeyFor(const std::string& cid, bool& skip) const
|
||||
{
|
||||
skip = false;
|
||||
if (sensitive_pred_ && sensitive_pred_(cid)) {
|
||||
if (!sensitive_ready_) { skip = true; return nullptr; } // don't persist under the seed key
|
||||
return sensitive_key_.data();
|
||||
}
|
||||
return key_.data();
|
||||
}
|
||||
|
||||
bool ChatDatabase::encrypt(const unsigned char* key,
|
||||
const std::string& plain,
|
||||
std::vector<unsigned char>& nonce,
|
||||
std::vector<unsigned char>& cipher) const
|
||||
{
|
||||
if (!key) 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) != 0) {
|
||||
return false;
|
||||
}
|
||||
cipher.resize(static_cast<std::size_t>(cipherLen));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChatDatabase::decrypt(const unsigned char* key,
|
||||
const std::vector<unsigned char>& nonce,
|
||||
const std::vector<unsigned char>& cipher,
|
||||
std::string& plain) const
|
||||
{
|
||||
if (!key) 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) != 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
|
||||
@@ -1,120 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// DragonX Wallet - HushChat persistent message store (Phase 2).
|
||||
//
|
||||
// Sqlite-backed, encrypted at rest with a SEED-DERIVED key (no wallet passphrase). Every record —
|
||||
// message bodies, peer z-addresses, threading (conversation id), and timestamps — is AEAD-encrypted
|
||||
// under a key derived from the wallet's own seed secret (the same secret used for the chat
|
||||
// identity), and even the per-message dedup key is a KEYED hash of the txid — so the database
|
||||
// reveals nothing about your conversations to disk-level access without the seed. Rows are
|
||||
// partitioned by a seed-derived wallet tag so one file can hold several wallets, each readable only
|
||||
// with its own seed. Not thread-safe — drive from the main thread. Mirrors the lifecycle of
|
||||
// data::TransactionHistoryCache.
|
||||
|
||||
#include "chat_message.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#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_; }
|
||||
|
||||
// --- Sensitive conversations (per-conversation local-key encryption) ---
|
||||
// Provide the local vault key (from ChatSensitiveVault). Rows of conversations the sensitive
|
||||
// predicate marks sensitive are AEAD-encrypted under THIS key instead of the seed-derived key, so
|
||||
// they can't be read without it (even by the seed). The caller still owns and should wipe `key`.
|
||||
void setSensitiveKey(const std::array<unsigned char, 32>& key);
|
||||
void lockSensitive(); // wipe the sensitive key; sensitive rows then withheld
|
||||
bool hasSensitiveKey() const { return sensitive_ready_; }
|
||||
// Predicate: true for a conversation id that should be stored under the sensitive key. Consulted on
|
||||
// WRITE to pick the key; on READ both keys are tried, so a predicate isn't needed to decrypt.
|
||||
void setSensitivePredicate(std::function<bool(const std::string&)> pred) { sensitive_pred_ = std::move(pred); }
|
||||
|
||||
// Persist one message (INSERT OR IGNORE, deduped by a keyed hash of txid+payload_position).
|
||||
// Returns true if newly inserted; false on duplicate or while locked.
|
||||
bool append(const ChatMessage& message);
|
||||
|
||||
// Persist many messages in ONE transaction (INSERT OR IGNORE, same dedup key as append()).
|
||||
// Serializes+encrypts every message BEFORE opening the transaction, so a non-UTF-8 body is
|
||||
// skipped without leaving a transaction open. Preserves input order (load() relies on rowid).
|
||||
// Returns the number of rows actually inserted. No-op (returns 0) while locked or on empty input.
|
||||
int appendBatch(const std::vector<ChatMessage>& messages);
|
||||
|
||||
// Persist-or-overwrite one message by its (txid+position) dedup key. Unlike append(), this
|
||||
// updates an existing row's payload — used for outgoing echoes whose delivery status changes
|
||||
// (Sending → Sent/Failed). Returns true on success; false while locked / on error.
|
||||
bool upsert(const ChatMessage& message);
|
||||
|
||||
// Decrypt and return every stored message for the unlocked wallet, in insertion order. Empty
|
||||
// while locked or if none. Rows that fail to decrypt/parse are skipped.
|
||||
std::vector<ChatMessage> load();
|
||||
|
||||
void clearWallet(); // delete the unlocked wallet's rows
|
||||
|
||||
// Per-conversation local delete. Removes the given messages' rows; when `tombstone` is true it also
|
||||
// records their (txid,position) dedup keys so a chain re-scan never re-imports them — this backs the
|
||||
// "delete, but a NEW message revives the thread" path. With `tombstone` false the rows are simply
|
||||
// removed (used by "delete & block", where a settings-level cid block suppresses re-import until the
|
||||
// user unblocks, at which point the history re-imports from chain). Atomic; no-op while locked.
|
||||
bool deleteMessages(const std::vector<ChatMessage>& messages, bool tombstone);
|
||||
|
||||
// True if this message's (txid,position) was locally deleted with a tombstone. Checked against an
|
||||
// in-memory cache loaded on unlock — O(1), no SQL. False while locked.
|
||||
bool isTombstoned(const ChatMessage& message) const;
|
||||
|
||||
private:
|
||||
bool ensureOpen();
|
||||
void loadTombstones(); // populate tombstones_ from chat_deleted for the unlocked wallet
|
||||
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 unsigned char* key,
|
||||
const std::string& plain,
|
||||
std::vector<unsigned char>& nonce,
|
||||
std::vector<unsigned char>& cipher) const;
|
||||
bool decrypt(const unsigned char* key,
|
||||
const std::vector<unsigned char>& nonce,
|
||||
const std::vector<unsigned char>& cipher,
|
||||
std::string& plain) const;
|
||||
// The key a row for `cid` is written under. Returns nullptr and sets skip=true when the conversation
|
||||
// is sensitive but the vault is locked — the row must NOT be persisted (under the seed key it would be
|
||||
// readable without the vault, defeating the point); it re-imports from chain once the vault is unlocked.
|
||||
const unsigned char* writeKeyFor(const std::string& cid, bool& skip) 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;
|
||||
std::array<unsigned char, 32> sensitive_key_{}; // local vault key for sensitive conversations' rows
|
||||
bool sensitive_ready_ = false;
|
||||
std::function<bool(const std::string&)> sensitive_pred_; // true => store this cid under the sensitive key
|
||||
std::unordered_set<std::string> tombstones_; // dedup_hash cache of locally-deleted messages
|
||||
};
|
||||
|
||||
} // namespace dragonx::chat
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,505 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "chat_protocol.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// HushChat compatibility-fixture / capture-manifest / seed-projection tooling.
|
||||
//
|
||||
// Everything in this header is DEAD AT RUNTIME in the shipping app and test
|
||||
// binaries — its only caller is the standalone dev CLI
|
||||
// tools/hushchat_fixture_check.cpp. It is deliberately compiled ONLY into the
|
||||
// HushChatFixtureCheck target so this validation scaffolding (and its libsodium
|
||||
// seed-projection path) never reaches the wallet binary. Keep it that way: do
|
||||
// not add chat_fixture_tooling.cpp to APP_SOURCES or the test target.
|
||||
//
|
||||
// It builds on the runtime types declared in chat_protocol.h.
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
enum class HushChatDecryptPreflightError {
|
||||
None,
|
||||
FeatureDisabled,
|
||||
NonMessageHeader,
|
||||
InvalidHeaderNumber,
|
||||
UnsupportedVersion,
|
||||
MissingReplyAddress,
|
||||
MissingConversationId,
|
||||
InvalidSecretstreamHeader,
|
||||
InvalidPublicKey,
|
||||
EmptyCiphertext,
|
||||
OversizedCiphertext,
|
||||
OddLengthCiphertext,
|
||||
InvalidCiphertextHex,
|
||||
TruncatedCiphertext
|
||||
};
|
||||
|
||||
struct HushChatDecryptPreflightInput {
|
||||
HushChatHeader header;
|
||||
std::string ciphertext_hex;
|
||||
};
|
||||
|
||||
struct HushChatDecryptPreflightResult {
|
||||
bool ok = false;
|
||||
bool feature_enabled = false;
|
||||
HushChatDecryptPreflightError error = HushChatDecryptPreflightError::None;
|
||||
const char* error_name = "None";
|
||||
std::size_t ciphertext_size = 0;
|
||||
};
|
||||
|
||||
enum class HushChatHexDecodeError {
|
||||
None,
|
||||
Empty,
|
||||
OddLength,
|
||||
InvalidHex,
|
||||
UnexpectedByteLength
|
||||
};
|
||||
|
||||
struct HushChatHexDecodeResult {
|
||||
bool ok = false;
|
||||
HushChatHexDecodeError error = HushChatHexDecodeError::None;
|
||||
const char* error_name = "None";
|
||||
std::vector<unsigned char> bytes;
|
||||
};
|
||||
|
||||
enum class HushChatDecryptDirection {
|
||||
Incoming,
|
||||
Outgoing
|
||||
};
|
||||
|
||||
enum class HushChatSessionKeySelection {
|
||||
ClientRx,
|
||||
ServerTx
|
||||
};
|
||||
|
||||
enum class HushChatDecryptInputError {
|
||||
None,
|
||||
FeatureDisabled,
|
||||
InvalidStoredChatKey,
|
||||
DecryptPreflightFailed,
|
||||
InvalidPeerPublicKey,
|
||||
InvalidStreamHeader,
|
||||
InvalidCiphertext
|
||||
};
|
||||
|
||||
struct HushChatDecryptInputMaterial {
|
||||
std::string stored_chat_key_hex;
|
||||
HushChatHeader header;
|
||||
std::string ciphertext_hex;
|
||||
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
|
||||
std::string peer_public_key_hex;
|
||||
};
|
||||
|
||||
struct HushChatPreparedDecryptInput {
|
||||
std::vector<unsigned char> stored_chat_key_bytes;
|
||||
std::vector<unsigned char> seed_bytes;
|
||||
std::vector<unsigned char> peer_public_key_bytes;
|
||||
std::vector<unsigned char> stream_header_bytes;
|
||||
std::vector<unsigned char> ciphertext_bytes;
|
||||
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
|
||||
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
|
||||
std::size_t plaintext_capacity = 0;
|
||||
};
|
||||
|
||||
struct HushChatDecryptInputPreparationResult {
|
||||
bool ok = false;
|
||||
bool feature_enabled = false;
|
||||
HushChatDecryptInputError error = HushChatDecryptInputError::None;
|
||||
const char* error_name = "None";
|
||||
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
|
||||
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
|
||||
HushChatPreparedDecryptInput prepared;
|
||||
};
|
||||
|
||||
struct HushChatDecryptFixtureReadinessResult {
|
||||
bool ready = false;
|
||||
std::size_t stored_chat_key_size = 0;
|
||||
std::size_t seed_size = 0;
|
||||
std::size_t peer_public_key_size = 0;
|
||||
std::size_t stream_header_size = 0;
|
||||
std::size_t ciphertext_size = 0;
|
||||
std::size_t plaintext_capacity = 0;
|
||||
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
|
||||
};
|
||||
|
||||
enum class HushChatCompatibilityFixtureError {
|
||||
None,
|
||||
FeatureDisabled,
|
||||
MissingFixtureId,
|
||||
InvalidLocalPublicKey,
|
||||
InvalidPeerPublicKey,
|
||||
InvalidHeaderMemo,
|
||||
InvalidMemoPair,
|
||||
NonMemoHeader,
|
||||
HeaderPublicKeyMismatch,
|
||||
DecryptInputFailed,
|
||||
NotFixtureReady,
|
||||
ExpectedStoredChatKeyLengthMismatch,
|
||||
ExpectedSeedLengthMismatch,
|
||||
ExpectedLocalPublicKeyLengthMismatch,
|
||||
ExpectedPeerPublicKeyLengthMismatch,
|
||||
ExpectedStreamHeaderLengthMismatch,
|
||||
ExpectedCiphertextLengthMismatch,
|
||||
ExpectedPlaintextLengthMismatch,
|
||||
ExpectedRoleMismatch,
|
||||
InvalidPlaintextHash
|
||||
};
|
||||
|
||||
struct HushChatCompatibilityFixture {
|
||||
std::string fixture_id;
|
||||
std::string stored_chat_key_hex;
|
||||
std::string local_public_key_hex;
|
||||
std::string peer_public_key_hex;
|
||||
std::string header_memo;
|
||||
std::string ciphertext_memo;
|
||||
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
|
||||
HushChatSessionKeySelection expected_session_key_selection = HushChatSessionKeySelection::ClientRx;
|
||||
std::size_t expected_stored_chat_key_size = 32;
|
||||
std::size_t expected_seed_size = 32;
|
||||
std::size_t expected_local_public_key_size = 32;
|
||||
std::size_t expected_peer_public_key_size = 32;
|
||||
std::size_t expected_stream_header_size = 24;
|
||||
std::size_t expected_ciphertext_size = 0;
|
||||
std::size_t expected_plaintext_size = 0;
|
||||
std::string expected_plaintext_hash_hex;
|
||||
};
|
||||
|
||||
struct HushChatCompatibilityFixtureVerificationResult {
|
||||
bool ok = false;
|
||||
bool feature_enabled = false;
|
||||
HushChatCompatibilityFixtureError error = HushChatCompatibilityFixtureError::None;
|
||||
const char* error_name = "None";
|
||||
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
|
||||
HushChatDecryptInputError decrypt_input_error = HushChatDecryptInputError::None;
|
||||
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
|
||||
HushChatHeader header;
|
||||
HushChatDecryptInputPreparationResult preparation;
|
||||
HushChatDecryptFixtureReadinessResult readiness;
|
||||
std::size_t local_public_key_size = 0;
|
||||
std::size_t peer_public_key_size = 0;
|
||||
std::size_t plaintext_hash_size = 0;
|
||||
};
|
||||
|
||||
enum class HushChatCompatibilityFixtureKind {
|
||||
IncomingMemo,
|
||||
OutgoingMemo,
|
||||
SeedPublicKeyProjection,
|
||||
CorruptedAuthFailure,
|
||||
ContactExclusion
|
||||
};
|
||||
|
||||
enum class HushChatCompatibilityFixtureFileStatus {
|
||||
Pending,
|
||||
Ready
|
||||
};
|
||||
|
||||
enum class HushChatCompatibilityFixtureFileError {
|
||||
None,
|
||||
FeatureDisabled,
|
||||
InvalidJson,
|
||||
JsonNotObject,
|
||||
InvalidSchema,
|
||||
MissingKind,
|
||||
UnknownKind,
|
||||
MissingStatus,
|
||||
UnknownStatus,
|
||||
MissingFixtureId,
|
||||
MissingPendingReason,
|
||||
MissingFixtureObject,
|
||||
InvalidFixtureField,
|
||||
FixtureVerificationFailed,
|
||||
ContactFixtureNotExcluded,
|
||||
FileReadFailed
|
||||
};
|
||||
|
||||
struct HushChatCompatibilityFixtureFile {
|
||||
std::string schema;
|
||||
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||
HushChatCompatibilityFixtureFileStatus status = HushChatCompatibilityFixtureFileStatus::Pending;
|
||||
std::string fixture_id;
|
||||
std::string pending_reason;
|
||||
HushChatCompatibilityFixture fixture;
|
||||
};
|
||||
|
||||
struct HushChatCompatibilityFixtureFileParseResult {
|
||||
bool ok = false;
|
||||
bool feature_enabled = false;
|
||||
bool pending = false;
|
||||
bool verified = false;
|
||||
bool excluded_from_decrypt = false;
|
||||
HushChatCompatibilityFixtureFileError error = HushChatCompatibilityFixtureFileError::None;
|
||||
const char* error_name = "None";
|
||||
HushChatCompatibilityFixtureFile file;
|
||||
HushChatCompatibilityFixtureVerificationResult verification;
|
||||
};
|
||||
|
||||
enum class HushChatSeedPublicKeyProjectionError {
|
||||
None,
|
||||
FeatureDisabled,
|
||||
MissingFixtureId,
|
||||
InvalidStoredChatKey,
|
||||
InvalidLocalPublicKey,
|
||||
ExpectedStoredChatKeyLengthMismatch,
|
||||
ExpectedSeedLengthMismatch,
|
||||
ExpectedLocalPublicKeyLengthMismatch,
|
||||
SodiumInitializationFailed,
|
||||
KeypairProjectionFailed,
|
||||
ProjectedPublicKeyMismatch
|
||||
};
|
||||
|
||||
struct HushChatSeedPublicKeyProjectionResult {
|
||||
bool ok = false;
|
||||
bool feature_enabled = false;
|
||||
HushChatSeedPublicKeyProjectionError error = HushChatSeedPublicKeyProjectionError::None;
|
||||
const char* error_name = "None";
|
||||
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
|
||||
std::size_t stored_chat_key_size = 0;
|
||||
std::size_t seed_size = 0;
|
||||
std::size_t local_public_key_size = 0;
|
||||
std::size_t projected_public_key_size = 0;
|
||||
};
|
||||
|
||||
enum class HushChatCorruptedAuthFailureReadinessError {
|
||||
None,
|
||||
FeatureDisabled,
|
||||
FixturePending,
|
||||
WrongFixtureKind,
|
||||
FixtureNotVerified,
|
||||
SeedProjectionNotVerified
|
||||
};
|
||||
|
||||
struct HushChatCorruptedAuthFailureReadinessResult {
|
||||
bool ok = false;
|
||||
bool feature_enabled = false;
|
||||
bool structurally_ready_for_future_auth_check = false;
|
||||
bool requires_future_secretstream_auth_failure = false;
|
||||
bool decrypted = false;
|
||||
bool authenticated = false;
|
||||
HushChatCorruptedAuthFailureReadinessError error = HushChatCorruptedAuthFailureReadinessError::None;
|
||||
const char* error_name = "None";
|
||||
};
|
||||
|
||||
enum class HushChatCompatibilityFixtureImportError {
|
||||
None,
|
||||
FeatureDisabled,
|
||||
MissingRequiredKind,
|
||||
DuplicateKind,
|
||||
FixtureLoadFailed,
|
||||
FixtureKindMismatch,
|
||||
FixturePending,
|
||||
FixtureInvalid,
|
||||
FixtureNotVerified,
|
||||
SeedProjectionFailed,
|
||||
AuthFailureScaffoldFailed,
|
||||
ContactFixtureNotExcluded
|
||||
};
|
||||
|
||||
struct HushChatCompatibilityFixtureImportCandidate {
|
||||
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
struct HushChatCompatibilityFixtureImportItem {
|
||||
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||
std::string path;
|
||||
bool supplied = false;
|
||||
bool pending = false;
|
||||
bool replacement_eligible = false;
|
||||
bool seed_projection_verified = false;
|
||||
bool future_auth_failure_required = false;
|
||||
bool structurally_ready_for_future_auth_check = false;
|
||||
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
|
||||
const char* error_name = "None";
|
||||
HushChatCompatibilityFixtureFileParseResult parsed;
|
||||
HushChatSeedPublicKeyProjectionResult seed_projection;
|
||||
HushChatCorruptedAuthFailureReadinessResult auth_failure_readiness;
|
||||
};
|
||||
|
||||
struct HushChatCompatibilityFixtureImportChecklistResult {
|
||||
bool ok = false;
|
||||
bool feature_enabled = false;
|
||||
bool replacement_ready = false;
|
||||
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
|
||||
const char* error_name = "None";
|
||||
std::size_t required_count = 0;
|
||||
std::size_t supplied_count = 0;
|
||||
std::size_t missing_count = 0;
|
||||
std::size_t pending_count = 0;
|
||||
std::size_t verified_count = 0;
|
||||
std::size_t seed_projection_verified_count = 0;
|
||||
std::size_t future_auth_failure_required_count = 0;
|
||||
std::size_t auth_failure_structural_ready_count = 0;
|
||||
std::size_t excluded_count = 0;
|
||||
std::size_t rejected_count = 0;
|
||||
std::vector<HushChatCompatibilityFixtureImportItem> items;
|
||||
};
|
||||
|
||||
struct HushChatCompatibilityFixtureReplacementReportItem {
|
||||
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||
std::string path;
|
||||
bool supplied = false;
|
||||
bool pending = false;
|
||||
bool replacement_eligible = false;
|
||||
bool refused = true;
|
||||
bool seed_projection_verified = false;
|
||||
bool future_auth_failure_required = false;
|
||||
bool structurally_ready_for_future_auth_check = false;
|
||||
bool cont_excluded = false;
|
||||
bool decrypted = false;
|
||||
bool authenticated = false;
|
||||
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
|
||||
const char* error_name = "None";
|
||||
};
|
||||
|
||||
struct HushChatCompatibilityFixtureReplacementDryRunResult {
|
||||
bool ok = false;
|
||||
bool feature_enabled = false;
|
||||
bool dry_run_only = true;
|
||||
bool redacted_report = true;
|
||||
bool would_replace = false;
|
||||
bool replacement_refused = true;
|
||||
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
|
||||
const char* error_name = "None";
|
||||
std::size_t required_count = 0;
|
||||
std::size_t supplied_count = 0;
|
||||
std::size_t missing_count = 0;
|
||||
std::size_t pending_count = 0;
|
||||
std::size_t verified_count = 0;
|
||||
std::size_t seed_projection_verified_count = 0;
|
||||
std::size_t future_auth_failure_required_count = 0;
|
||||
std::size_t auth_failure_structural_ready_count = 0;
|
||||
std::size_t excluded_count = 0;
|
||||
std::size_t rejected_count = 0;
|
||||
std::vector<HushChatCompatibilityFixtureReplacementReportItem> report_items;
|
||||
};
|
||||
|
||||
enum class HushChatCaptureManifestError {
|
||||
None,
|
||||
FeatureDisabled,
|
||||
FileReadFailed,
|
||||
InvalidJson,
|
||||
JsonNotObject,
|
||||
InvalidSchema,
|
||||
MissingManifestId,
|
||||
MissingStatus,
|
||||
UnknownStatus,
|
||||
MissingFixtureDirectory,
|
||||
MissingDryRunCommand,
|
||||
InvalidDryRunCommand,
|
||||
MissingProvenance,
|
||||
MissingSourceClient,
|
||||
InvalidSourceClient,
|
||||
MissingSourceClientVersion,
|
||||
MissingCaptureDate,
|
||||
MissingNetwork,
|
||||
MissingCaptureMethod,
|
||||
MissingHandling,
|
||||
MissingHandlingFlag,
|
||||
HandlingFlagNotTrue,
|
||||
MissingCategories,
|
||||
InvalidCategoryEntry,
|
||||
UnknownCategory,
|
||||
DuplicateCategory,
|
||||
MissingRequiredCategory,
|
||||
ProhibitedFieldPresent
|
||||
};
|
||||
|
||||
enum class HushChatCaptureManifestStatus {
|
||||
Staged
|
||||
};
|
||||
|
||||
struct HushChatCaptureManifestCategoryReport {
|
||||
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||
std::string staged_filename;
|
||||
bool declared = false;
|
||||
};
|
||||
|
||||
struct HushChatCaptureManifestValidationResult {
|
||||
bool ok = false;
|
||||
bool feature_enabled = false;
|
||||
bool redacted_report = true;
|
||||
bool validates_provenance_only = true;
|
||||
bool no_sensitive_material_declared = false;
|
||||
bool has_dry_run_command = false;
|
||||
HushChatCaptureManifestError error = HushChatCaptureManifestError::None;
|
||||
const char* error_name = "None";
|
||||
HushChatCaptureManifestStatus status = HushChatCaptureManifestStatus::Staged;
|
||||
std::string manifest_path;
|
||||
std::string fixture_directory;
|
||||
std::size_t required_count = 0;
|
||||
std::size_t declared_count = 0;
|
||||
std::size_t missing_count = 0;
|
||||
std::size_t duplicate_count = 0;
|
||||
std::size_t prohibited_field_count = 0;
|
||||
std::size_t handling_flag_count = 0;
|
||||
std::vector<HushChatCaptureManifestCategoryReport> categories;
|
||||
};
|
||||
|
||||
constexpr std::size_t kHushChatSecretstreamABytes = 17;
|
||||
constexpr std::size_t kHushChatStoredChatKeyByteLength = 32;
|
||||
constexpr std::size_t kHushChatStoredChatKeyHexLength = kHushChatStoredChatKeyByteLength * 2;
|
||||
constexpr std::size_t kHushChatSeedByteLength = 32;
|
||||
constexpr std::size_t kHushChatPublicKeyByteLength = kHushChatPublicKeyHexLength / 2;
|
||||
constexpr std::size_t kHushChatSecretstreamHeaderByteLength = kHushChatSecretstreamHeaderHexLength / 2;
|
||||
constexpr const char* kHushChatCompatibilityFixtureSchema = "dragonx.hushchat.compat-fixture.v1";
|
||||
constexpr const char* kHushChatCaptureManifestSchema = "dragonx.hushchat.capture-manifest.v1";
|
||||
|
||||
HushChatDecryptPreflightResult validateHushChatMemoDecryptPreflight(
|
||||
const HushChatDecryptPreflightInput& input,
|
||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||
HushChatHexDecodeResult decodeHushChatHexBytes(const std::string& hex,
|
||||
std::size_t expectedByteLength);
|
||||
HushChatDecryptInputPreparationResult prepareHushChatDecryptInput(
|
||||
const HushChatDecryptInputMaterial& material,
|
||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||
HushChatDecryptFixtureReadinessResult inspectHushChatDecryptFixtureReadiness(
|
||||
const HushChatPreparedDecryptInput& prepared);
|
||||
HushChatSessionKeySelection hushChatSessionKeySelectionForDirection(HushChatDecryptDirection direction);
|
||||
HushChatCompatibilityFixtureVerificationResult verifyHushChatCompatibilityFixture(
|
||||
const HushChatCompatibilityFixture& fixture,
|
||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||
HushChatCompatibilityFixtureFileParseResult parseHushChatCompatibilityFixtureFile(
|
||||
const std::string& jsonText,
|
||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||
HushChatCompatibilityFixtureFileParseResult loadHushChatCompatibilityFixtureFile(
|
||||
const std::string& path,
|
||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||
HushChatSeedPublicKeyProjectionResult verifyHushChatSeedPublicKeyProjection(
|
||||
const HushChatCompatibilityFixture& fixture,
|
||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||
HushChatCorruptedAuthFailureReadinessResult inspectHushChatCorruptedAuthFailureReadiness(
|
||||
const HushChatCompatibilityFixtureFileParseResult& parsed,
|
||||
const HushChatSeedPublicKeyProjectionResult& seedProjection,
|
||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||
std::vector<HushChatCompatibilityFixtureKind> hushChatRequiredCompatibilityFixtureKinds();
|
||||
HushChatCompatibilityFixtureImportChecklistResult inspectHushChatCompatibilityFixtureImportChecklist(
|
||||
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
|
||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||
HushChatCompatibilityFixtureReplacementDryRunResult inspectHushChatCompatibilityFixtureReplacementDryRun(
|
||||
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
|
||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||
HushChatCaptureManifestValidationResult validateHushChatCaptureManifest(
|
||||
const std::string& jsonText,
|
||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||
HushChatCaptureManifestValidationResult loadHushChatCaptureManifestFile(
|
||||
const std::string& path,
|
||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||
const char* hushChatDecryptPreflightErrorName(HushChatDecryptPreflightError error);
|
||||
const char* hushChatHexDecodeErrorName(HushChatHexDecodeError error);
|
||||
const char* hushChatDecryptDirectionName(HushChatDecryptDirection direction);
|
||||
const char* hushChatSessionKeySelectionName(HushChatSessionKeySelection selection);
|
||||
const char* hushChatDecryptInputErrorName(HushChatDecryptInputError error);
|
||||
const char* hushChatCompatibilityFixtureErrorName(HushChatCompatibilityFixtureError error);
|
||||
const char* hushChatCompatibilityFixtureKindName(HushChatCompatibilityFixtureKind kind);
|
||||
const char* hushChatCompatibilityFixtureFileStatusName(HushChatCompatibilityFixtureFileStatus status);
|
||||
const char* hushChatCompatibilityFixtureFileErrorName(HushChatCompatibilityFixtureFileError error);
|
||||
const char* hushChatSeedPublicKeyProjectionErrorName(HushChatSeedPublicKeyProjectionError error);
|
||||
const char* hushChatCorruptedAuthFailureReadinessErrorName(HushChatCorruptedAuthFailureReadinessError error);
|
||||
const char* hushChatCompatibilityFixtureImportErrorName(HushChatCompatibilityFixtureImportError error);
|
||||
const char* hushChatCaptureManifestErrorName(HushChatCaptureManifestError error);
|
||||
|
||||
} // namespace dragonx::chat
|
||||
@@ -1,70 +0,0 @@
|
||||
// DragonX Wallet - HushChat identity derivation (implementation).
|
||||
|
||||
#include "chat_identity.h"
|
||||
|
||||
#include <sodium.h>
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
// The KDF context is used as the BLAKE2b key, so its length must sit within the primitive's
|
||||
// key bounds.
|
||||
static_assert(kChatIdentityKdfContextLen >= crypto_generichash_KEYBYTES_MIN,
|
||||
"chat identity KDF context is shorter than BLAKE2b's minimum key length");
|
||||
static_assert(kChatIdentityKdfContextLen <= crypto_generichash_KEYBYTES_MAX,
|
||||
"chat identity KDF context is longer than BLAKE2b's maximum key length");
|
||||
|
||||
const char* chatIdentityStatusName(ChatIdentityStatus status) {
|
||||
switch (status) {
|
||||
case ChatIdentityStatus::Ready: return "Ready";
|
||||
case ChatIdentityStatus::FeatureDisabled: return "FeatureDisabled";
|
||||
case ChatIdentityStatus::SecretUnavailable: return "SecretUnavailable";
|
||||
case ChatIdentityStatus::DerivationFailed: return "DerivationFailed";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
std::string chatIdentityPublicKeyHex(const ChatKeyPair& keys) {
|
||||
char hex[crypto_kx_PUBLICKEYBYTES * 2 + 1];
|
||||
sodium_bin2hex(hex, sizeof hex, keys.public_key.data(), keys.public_key.size());
|
||||
return std::string(hex);
|
||||
}
|
||||
|
||||
ChatIdentityResult deriveChatIdentityFromSecret(const std::string& stableSecret,
|
||||
ChatKeyPair& outKeys,
|
||||
bool featureEnabled) {
|
||||
ChatIdentityResult result;
|
||||
auto finish = [&result](ChatIdentityStatus status) -> ChatIdentityResult {
|
||||
result.status = status;
|
||||
result.error_name = chatIdentityStatusName(status);
|
||||
return result;
|
||||
};
|
||||
|
||||
if (!featureEnabled) return finish(ChatIdentityStatus::FeatureDisabled);
|
||||
if (stableSecret.empty()) return finish(ChatIdentityStatus::SecretUnavailable);
|
||||
if (sodium_init() < 0) return finish(ChatIdentityStatus::DerivationFailed);
|
||||
|
||||
unsigned char kxSeed[crypto_kx_SEEDBYTES];
|
||||
static_assert(sizeof(kxSeed) == kChatKeyBytes, "kx seed size mismatch");
|
||||
|
||||
const int hashStatus = crypto_generichash(
|
||||
kxSeed, sizeof kxSeed,
|
||||
reinterpret_cast<const unsigned char*>(stableSecret.data()), stableSecret.size(),
|
||||
reinterpret_cast<const unsigned char*>(kChatIdentityKdfContext), kChatIdentityKdfContextLen);
|
||||
if (hashStatus != 0) {
|
||||
sodium_memzero(kxSeed, sizeof kxSeed);
|
||||
return finish(ChatIdentityStatus::DerivationFailed);
|
||||
}
|
||||
|
||||
const int keypairStatus =
|
||||
crypto_kx_seed_keypair(outKeys.public_key.data(), outKeys.secret_key.data(), kxSeed);
|
||||
sodium_memzero(kxSeed, sizeof kxSeed); // wipe the seed immediately, success or failure
|
||||
if (keypairStatus != 0) {
|
||||
wipeChatKeyPair(outKeys);
|
||||
return finish(ChatIdentityStatus::DerivationFailed);
|
||||
}
|
||||
|
||||
result.public_key_hex = chatIdentityPublicKeyHex(outKeys);
|
||||
return finish(ChatIdentityStatus::Ready);
|
||||
}
|
||||
|
||||
} // namespace dragonx::chat
|
||||
@@ -1,57 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// DragonX Wallet - HushChat identity derivation.
|
||||
//
|
||||
// The DragonX-native chat identity is an X25519 (crypto_kx) keypair derived from a stable
|
||||
// per-wallet secret via a domain-separated keyed BLAKE2b KDF. This deliberately does NOT
|
||||
// use SDXL's UTF-8-hex-seed quirk (that quirk is only for the Phase-4 "import an existing
|
||||
// SDXL identity" path). Interop is unaffected: identity derivation is local — peers only
|
||||
// exchange public keys. See docs/_archive/contacts-chat-tab-plan-2026-07-05.md §5.6.
|
||||
|
||||
#include "chat_crypto.h" // ChatKeyPair
|
||||
#include "chat_protocol.h" // hushChatFeatureEnabledAtBuild()
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
// Domain-separation label — used as the BLAKE2b key so a different app/version cannot
|
||||
// derive the same identity from the same wallet secret. A char[] (not const char*) so its
|
||||
// length is a compile-time constant for the KEYBYTES-bounds static_assert.
|
||||
inline constexpr char kChatIdentityKdfContext[] = "DragonX-HushChat-Identity-v1";
|
||||
inline constexpr std::size_t kChatIdentityKdfContextLen = sizeof(kChatIdentityKdfContext) - 1;
|
||||
|
||||
enum class ChatIdentityStatus {
|
||||
Ready,
|
||||
FeatureDisabled, // DRAGONX_ENABLE_CHAT off (or caller passed featureEnabled=false)
|
||||
SecretUnavailable, // no stable secret (wallet locked / not open / empty)
|
||||
DerivationFailed // libsodium init or KDF/keypair failure
|
||||
};
|
||||
|
||||
const char* chatIdentityStatusName(ChatIdentityStatus status);
|
||||
|
||||
struct ChatIdentityResult {
|
||||
ChatIdentityStatus status = ChatIdentityStatus::FeatureDisabled;
|
||||
std::string public_key_hex; // 64 lowercase hex chars when Ready
|
||||
const char* error_name = "FeatureDisabled"; // == chatIdentityStatusName(status)
|
||||
};
|
||||
|
||||
// Pure, no-I/O derivation: hashes `stableSecret` (variable-length: a mnemonic on lite, a
|
||||
// spending key on full-node) with the KDF context as the BLAKE2b key into a clean 32-byte
|
||||
// crypto_kx seed, then crypto_kx_seed_keypair() into `outKeys`. Deterministic for a given
|
||||
// secret. On any non-Ready result `outKeys` is left wiped. featureEnabled defaults to the
|
||||
// build predicate but tests pass true to exercise the crypto in an OFF build.
|
||||
//
|
||||
// OWNERSHIP: `stableSecret` is BORROWED (const&) and is NOT wiped here — the caller owns it
|
||||
// and MUST sodium_memzero its backing buffer after this returns. The per-variant provider
|
||||
// that fetches the wallet secret (mnemonic / spending key) should hold it in a wipeable
|
||||
// buffer, not a plain std::string literal, in production.
|
||||
ChatIdentityResult deriveChatIdentityFromSecret(const std::string& stableSecret,
|
||||
ChatKeyPair& outKeys,
|
||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||
|
||||
// 64-char lowercase hex of the public key.
|
||||
std::string chatIdentityPublicKeyHex(const ChatKeyPair& keys);
|
||||
|
||||
} // namespace dragonx::chat
|
||||
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// DragonX Wallet - HushChat decrypted message model. Held in memory by ChatStore and persisted at
|
||||
// rest (encrypted under a seed-derived key) by ChatDatabase.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
enum class ChatDirection { Incoming, Outgoing };
|
||||
enum class ChatMessageKind { Message, ContactRequest };
|
||||
// Outgoing delivery status. Sending = broadcast in flight (async op not yet resolved); Sent = the
|
||||
// daemon accepted + broadcast the tx; Failed = it didn't (not connected, no funded address, rejected).
|
||||
// Always Sent for incoming. NB: the values are persisted (chat DB serializes the int), so Sent MUST
|
||||
// stay 0 and new states are APPENDED — never reordered.
|
||||
enum class ChatDelivery { Sent, Failed, Sending };
|
||||
|
||||
struct ChatMessage {
|
||||
ChatDirection direction = ChatDirection::Incoming;
|
||||
ChatMessageKind kind = ChatMessageKind::Message;
|
||||
std::string txid;
|
||||
std::string conversation_id; // cid — the conversation thread key
|
||||
std::string peer_zaddr; // header "z": peer's reply z-address
|
||||
std::string peer_public_key_hex; // header "p": peer's crypto_kx public key
|
||||
std::string body; // decrypted plaintext (Message) or request text (ContactRequest)
|
||||
std::int64_t timestamp = 0; // tx time in seconds; set by the ingesting caller
|
||||
std::size_t payload_position = 0; // together with txid, the dedup key
|
||||
ChatDelivery delivery = ChatDelivery::Sent; // outgoing only. Sent = broadcast to the network (not
|
||||
// necessarily confirmed/delivered yet).
|
||||
std::string fail_reason; // outgoing Failed only; in-memory, NOT persisted
|
||||
};
|
||||
|
||||
} // namespace dragonx::chat
|
||||
@@ -1,267 +0,0 @@
|
||||
// DragonX Wallet - HushChat outgoing memo construction (implementation).
|
||||
|
||||
#include "chat_outgoing.h"
|
||||
|
||||
#include "chat_protocol.h" // kHushChat* constants
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <sodium.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
namespace {
|
||||
|
||||
// Serialize the HushChat header. nlohmann emits object keys in sorted (alphabetical) order —
|
||||
// cid,e,h,p,t,v,z — which is exactly SilentDragonXLite's on-wire key order.
|
||||
std::string buildHeaderMemo(const std::string& replyZaddr,
|
||||
const std::string& conversationId,
|
||||
const char* type,
|
||||
const std::string& streamHeaderHex,
|
||||
const std::string& publicKeyHex,
|
||||
std::int64_t sentAt,
|
||||
const std::string& op = "",
|
||||
int parts = 1,
|
||||
const std::string& messageId = "")
|
||||
{
|
||||
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" / "Cont" / "Ctrl"
|
||||
header["e"] = streamHeaderHex; // 48-hex secretstream header (Memo) / "" (Cont, Ctrl)
|
||||
header["p"] = publicKeyHex; // my 64-hex crypto_kx public key
|
||||
if (!op.empty()) header["op"] = op; // Control only; alphabetically sorts to cid,e,h,op,p,t,v,z
|
||||
if (parts > 1) header["n"] = parts; // multi-part Message only; omitted (==1) keeps 1-part byte-identical
|
||||
if (!messageId.empty()) header["mid"] = messageId; // tier 3 only: ties this tx's slice to its siblings
|
||||
if (sentAt > 0) header["ts"] = sentAt; // optional sender compose time (Unix s) — receiver shows this
|
||||
return header.dump();
|
||||
}
|
||||
|
||||
// The N 2-hex index prefix for a multi-part ciphertext chunk (index 0..kHushChatMaxMessageParts-1).
|
||||
std::string partIndexPrefix(std::size_t i)
|
||||
{
|
||||
static const char* kHex = "0123456789abcdef";
|
||||
std::string p;
|
||||
p += kHex[(i >> 4) & 0xF];
|
||||
p += kHex[i & 0xF];
|
||||
return p;
|
||||
}
|
||||
|
||||
bool present(const std::string& value) { return !value.empty(); }
|
||||
|
||||
// A random 8-byte message id as 16 hex chars — ties a tier-3 message's sibling transactions together.
|
||||
std::string newMessageId()
|
||||
{
|
||||
unsigned char buf[8];
|
||||
randombytes_buf(buf, sizeof buf);
|
||||
static const char* kHex = "0123456789abcdef";
|
||||
std::string s;
|
||||
s.reserve(sizeof(buf) * 2);
|
||||
for (unsigned char b : buf) { s += kHex[b >> 4]; s += kHex[b & 0xF]; }
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<ChatSendOutput> chatSendOutputs(const OutgoingChatMemos& memos, bool utf8Prefix)
|
||||
{
|
||||
const std::string prefix = utf8Prefix ? "utf8:" : "";
|
||||
std::vector<ChatSendOutput> outs;
|
||||
outs.reserve(1 + memos.payloadMemos.size());
|
||||
outs.push_back({ memos.recipientZaddr, prefix + memos.headerMemo }); // header (position doesn't matter;
|
||||
for (const auto& p : memos.payloadMemos) // the receiver reassembles by index)
|
||||
outs.push_back({ memos.recipientZaddr, prefix + p });
|
||||
return outs;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Chunk the ciphertext hex across payload memos. A single memo → the classic 1-part message, byte-
|
||||
// identical on the wire (no "n", no index prefix). Longer → N index-prefixed chunks (tier 2), capped
|
||||
// at kHushChatMaxMessageParts; beyond that it is refused as TooLong (tier 3 would lift the ceiling).
|
||||
std::vector<std::string> payloads;
|
||||
int parts = 1;
|
||||
if (ciphertextHex.size() <= kHushChatMemoByteLimit) {
|
||||
payloads.push_back(ciphertextHex);
|
||||
} else {
|
||||
const std::size_t chunkHex = (kHushChatMemoByteLimit - kHushChatPartIndexHexLen) & ~std::size_t(1);
|
||||
const std::size_t n = (ciphertextHex.size() + chunkHex - 1) / chunkHex;
|
||||
if (static_cast<int>(n) > kHushChatMaxMessageParts) return ChatComposeStatus::TooLong;
|
||||
parts = static_cast<int>(n);
|
||||
payloads.reserve(n);
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
payloads.push_back(partIndexPrefix(i) + ciphertextHex.substr(i * chunkHex, chunkHex));
|
||||
}
|
||||
}
|
||||
|
||||
OutgoingChatMemos memos;
|
||||
memos.recipientZaddr = peerZaddr;
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex, myPublicKeyHex,
|
||||
static_cast<std::int64_t>(std::time(nullptr)), "", parts);
|
||||
memos.payloadMemos = std::move(payloads);
|
||||
if (memos.headerMemo.size() > kHushChatMemoByteLimit) return ChatComposeStatus::TooLong;
|
||||
for (const auto& p : memos.payloadMemos)
|
||||
if (p.size() > kHushChatMemoByteLimit) return ChatComposeStatus::TooLong;
|
||||
out = std::move(memos);
|
||||
return ChatComposeStatus::Ok;
|
||||
}
|
||||
|
||||
ChatComposeStatus splitOutgoingMessage(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,
|
||||
std::vector<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;
|
||||
|
||||
// Encrypt the whole message ONCE (one secretstream session). Chunking is transport-only, so every
|
||||
// transaction of a tier-3 message carries the same secretstream header "e" and its slice of the one
|
||||
// ciphertext — the crypto is byte-identical to the single-tx path.
|
||||
std::string streamHeaderHex;
|
||||
std::string ciphertextHex;
|
||||
if (encryptOutgoing(mine, peerPublicKeyHex, plaintext, streamHeaderHex, ciphertextHex)
|
||||
!= ChatCryptoStatus::Ok) {
|
||||
return ChatComposeStatus::EncryptFailed;
|
||||
}
|
||||
|
||||
const std::int64_t now = static_cast<std::int64_t>(std::time(nullptr));
|
||||
|
||||
// 1 chunk if the ciphertext fits one memo, else ceil(hex / chunkHex) chunks (chunkHex = the 512-byte
|
||||
// memo limit minus the 2-hex global index prefix, rounded to an even hex length).
|
||||
const std::size_t chunkHex = (kHushChatMemoByteLimit - kHushChatPartIndexHexLen) & ~std::size_t(1);
|
||||
const std::size_t nChunks = (ciphertextHex.size() <= kHushChatMemoByteLimit)
|
||||
? 1
|
||||
: (ciphertextHex.size() + chunkHex - 1) / chunkHex;
|
||||
if (static_cast<int>(nChunks) > kHushChatMaxMessageChunks) return ChatComposeStatus::TooLong;
|
||||
|
||||
std::vector<OutgoingChatMemos> txs;
|
||||
|
||||
if (nChunks <= 1) {
|
||||
// Tier 1: one classic payload — no index prefix, no "n", no "mid" (byte-identical on the wire).
|
||||
OutgoingChatMemos m;
|
||||
m.recipientZaddr = peerZaddr;
|
||||
m.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex,
|
||||
myPublicKeyHex, now, "", 1, "");
|
||||
m.payloadMemos = { ciphertextHex };
|
||||
txs.push_back(std::move(m));
|
||||
} else {
|
||||
// Tier 2/3: index each chunk with its GLOBAL index and pack up to kHushChatMaxMessageParts chunks
|
||||
// per transaction. One tx (≤ the per-tx cap) is tier 2 (no "mid"); several is tier 3 (shared "mid",
|
||||
// same "n" = total chunk count in every sibling header).
|
||||
const bool multiTx = static_cast<int>(nChunks) > kHushChatMaxMessageParts;
|
||||
const std::string mid = multiTx ? newMessageId() : std::string();
|
||||
const std::size_t perTx = static_cast<std::size_t>(kHushChatMaxMessageParts);
|
||||
for (std::size_t base = 0; base < nChunks; base += perTx) {
|
||||
OutgoingChatMemos m;
|
||||
m.recipientZaddr = peerZaddr;
|
||||
m.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex,
|
||||
myPublicKeyHex, now, "", static_cast<int>(nChunks), mid);
|
||||
const std::size_t end = std::min(nChunks, base + perTx);
|
||||
m.payloadMemos.reserve(end - base);
|
||||
for (std::size_t i = base; i < end; ++i)
|
||||
m.payloadMemos.push_back(partIndexPrefix(i) + ciphertextHex.substr(i * chunkHex, chunkHex));
|
||||
txs.push_back(std::move(m));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& m : txs) {
|
||||
if (m.headerMemo.size() > kHushChatMemoByteLimit) return ChatComposeStatus::TooLong;
|
||||
for (const auto& p : m.payloadMemos)
|
||||
if (p.size() > kHushChatMemoByteLimit) return ChatComposeStatus::TooLong;
|
||||
}
|
||||
out = std::move(txs);
|
||||
return ChatComposeStatus::Ok;
|
||||
}
|
||||
|
||||
ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
||||
const std::string& myReplyZaddr,
|
||||
const std::string& peerZaddr,
|
||||
const std::string& conversationId,
|
||||
const std::string& requestText,
|
||||
OutgoingChatMemos& out)
|
||||
{
|
||||
if (requestText.empty()) return ChatComposeStatus::EmptyBody;
|
||||
// The receive parser treats any memo starting with '{' as a header, so a request payload must
|
||||
// not start with one (see isContactPayloadCandidate in chat_protocol.cpp).
|
||||
if (requestText.front() == '{') return ChatComposeStatus::BadRequestText;
|
||||
if (!present(myPublicKeyHex) || !present(myReplyZaddr) || !present(peerZaddr) ||
|
||||
!present(conversationId)) {
|
||||
return ChatComposeStatus::MissingField;
|
||||
}
|
||||
|
||||
OutgoingChatMemos memos;
|
||||
memos.recipientZaddr = peerZaddr;
|
||||
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Cont", "", myPublicKeyHex,
|
||||
static_cast<std::int64_t>(std::time(nullptr)));
|
||||
memos.payloadMemos = { requestText };
|
||||
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||
memos.payloadMemos[0].size() > kHushChatMemoByteLimit) {
|
||||
return ChatComposeStatus::TooLong;
|
||||
}
|
||||
out = std::move(memos);
|
||||
return ChatComposeStatus::Ok;
|
||||
}
|
||||
|
||||
ChatComposeStatus buildOutgoingControl(const std::string& myPublicKeyHex,
|
||||
const std::string& selfZaddr,
|
||||
const std::string& conversationId,
|
||||
const std::string& op,
|
||||
const std::string& label,
|
||||
OutgoingChatMemos& out)
|
||||
{
|
||||
// A control signal is addressed to SELF: only this wallet (any device holding the seed) harvests
|
||||
// it, and it never reaches the peer. op carries the action; the label is a plaintext hint (peer
|
||||
// address / display name) so a fresh import can show the conversation it refers to.
|
||||
if (label.empty()) return ChatComposeStatus::EmptyBody;
|
||||
if (label.front() == '{') return ChatComposeStatus::BadRequestText; // would parse as a header
|
||||
if (!present(op) || !present(myPublicKeyHex) || !present(selfZaddr) || !present(conversationId)) {
|
||||
return ChatComposeStatus::MissingField;
|
||||
}
|
||||
|
||||
OutgoingChatMemos memos;
|
||||
memos.recipientZaddr = selfZaddr;
|
||||
memos.headerMemo = buildHeaderMemo(selfZaddr, conversationId, "Ctrl", "", myPublicKeyHex,
|
||||
static_cast<std::int64_t>(std::time(nullptr)), op);
|
||||
memos.payloadMemos = { label };
|
||||
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||
memos.payloadMemos[0].size() > kHushChatMemoByteLimit) {
|
||||
return ChatComposeStatus::TooLong;
|
||||
}
|
||||
out = std::move(memos);
|
||||
return ChatComposeStatus::Ok;
|
||||
}
|
||||
|
||||
} // namespace dragonx::chat
|
||||
@@ -1,96 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// DragonX Wallet - HushChat outgoing memo construction (the inverse of the receive parser).
|
||||
//
|
||||
// Given the sender's identity and the peer, produce the header memo JSON + payload memo that,
|
||||
// sent as two 0-value memo outputs to the peer's z-address (header at the LOWER memo position),
|
||||
// another HushChat client parses and decrypts. The byte format matches SilentDragonXLite: the
|
||||
// header keys serialize alphabetically (nlohmann default) to cid,e,h,p,t,v,z. Pure — no I/O, no
|
||||
// network; broadcasting the memos is the caller's job (the transport lands in a later phase).
|
||||
|
||||
#include "chat_crypto.h" // ChatKeyPair
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
struct OutgoingChatMemos {
|
||||
std::string recipientZaddr; // the peer's z-address (recipient of every memo output)
|
||||
std::string headerMemo; // JSON header
|
||||
// One payload (ciphertext hex for a Message, plaintext for a ContactRequest/Control), OR — for a
|
||||
// multi-part Message (header "n" = N > 1) — N index-prefixed ciphertext chunks (a 2-hex part index
|
||||
// then that chunk's ciphertext hex), reassembled by the receiver regardless of on-chain order.
|
||||
std::vector<std::string> payloadMemos;
|
||||
};
|
||||
|
||||
// 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::vector<ChatSendOutput> 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 an ENCRYPTED message, splitting it across as many transactions as its length needs (larger-
|
||||
// messages tier 3). Returns one OutgoingChatMemos per transaction to broadcast (in order): a short
|
||||
// message yields a single element identical to buildOutgoingMessage (tier 1/2); a long one yields
|
||||
// several, each a self-contained chat send carrying a slice of the one ciphertext under a shared
|
||||
// message id. The receiver reassembles across the siblings. Refused as TooLong beyond
|
||||
// kHushChatMaxMessageChunks total chunks. The caller broadcasts each element as its own chat tx.
|
||||
ChatComposeStatus splitOutgoingMessage(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,
|
||||
std::vector<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);
|
||||
|
||||
// Build a self-addressed control signal (delete / restore / mark-sensitive). It is sent to your OWN
|
||||
// z-address, so only this wallet's seed can harvest it and it never reaches the peer. `op` is the
|
||||
// action ("del"/"res"/...); `label` is a plaintext hint (peer address or name) carried as the payload
|
||||
// so a fresh seed import can name the conversation the signal refers to.
|
||||
ChatComposeStatus buildOutgoingControl(const std::string& myPublicKeyHex,
|
||||
const std::string& selfZaddr,
|
||||
const std::string& conversationId,
|
||||
const std::string& op,
|
||||
const std::string& label,
|
||||
OutgoingChatMemos& out);
|
||||
|
||||
} // namespace dragonx::chat
|
||||
+1588
-180
File diff suppressed because it is too large
Load Diff
+482
-49
@@ -1,9 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#ifndef DRAGONX_ENABLE_CHAT
|
||||
@@ -14,8 +12,7 @@ namespace dragonx::chat {
|
||||
|
||||
enum class HushChatHeaderType {
|
||||
Message,
|
||||
ContactRequest,
|
||||
Control // a self-addressed control signal (delete / restore / mark sensitive); carries an `op`
|
||||
ContactRequest
|
||||
};
|
||||
|
||||
struct HushChatHeader {
|
||||
@@ -24,21 +21,8 @@ struct HushChatHeader {
|
||||
std::string reply_zaddr;
|
||||
std::string conversation_id;
|
||||
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||
std::string op; // Control only ("del" / "res" / "sensitive" / "unsensitive" / future ops); empty otherwise
|
||||
std::string secretstream_header_hex;
|
||||
std::string public_key_hex;
|
||||
// Message only: how many payload outputs this message's ciphertext is split across (header "n").
|
||||
// 1 = the classic single-payload message (field omitted on the wire). >1 means the payloads are
|
||||
// index-prefixed ciphertext chunks reassembled in order. For tier 2 all N chunks ride ONE tx; for
|
||||
// tier 3 (message_id set) "n" is the TOTAL chunk count spread across several sibling transactions.
|
||||
int parts = 1;
|
||||
// Message only, larger-messages tier 3: when a message needs more chunks than one tx can carry, it
|
||||
// is split across several transactions that all share this random id (header "mid"); each tx's
|
||||
// payloads carry their GLOBAL chunk index (0..parts-1). Empty = a single-tx message (tier 1/2).
|
||||
std::string message_id;
|
||||
// Optional sender-stamped compose time (header "ts", Unix seconds). 0 = absent (older sender) → the
|
||||
// receiver falls back to the tx/receive time. Lets both sides show the SAME (send) time.
|
||||
std::int64_t sent_at = 0;
|
||||
};
|
||||
|
||||
struct HushChatHeaderParseResult {
|
||||
@@ -57,12 +41,6 @@ struct HushChatMemoPair {
|
||||
std::size_t header_position = 0;
|
||||
std::size_t payload_position = 0;
|
||||
std::string payload_memo;
|
||||
// Larger-messages tier 3: a single transaction that carries only PART of a bigger message (the rest
|
||||
// ride sibling txs sharing message_id). payload_memo is then empty; partial_chunks holds this tx's
|
||||
// (global index, ciphertext-hex chunk) pairs and total_parts is the whole message's chunk count.
|
||||
std::string message_id;
|
||||
int total_parts = 0;
|
||||
std::vector<std::pair<int, std::string>> partial_chunks;
|
||||
};
|
||||
|
||||
enum class HushChatMemoGroupingIssue {
|
||||
@@ -92,25 +70,11 @@ struct HushChatTransactionInput {
|
||||
struct HushChatTransactionMetadata {
|
||||
std::string txid;
|
||||
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||
std::string op; // Control only: the control op ("del"/"res"/...); empty otherwise
|
||||
std::string conversation_id;
|
||||
std::string reply_zaddr;
|
||||
std::size_t header_position = 0;
|
||||
std::size_t payload_position = 0;
|
||||
std::size_t payload_size = 0;
|
||||
// Decrypt inputs carried through from the paired header + payload memos so the chat
|
||||
// service can actually decrypt (a Message) or read the request (a ContactRequest).
|
||||
std::string sender_public_key_hex; // header "p": peer crypto_kx public key (hex)
|
||||
std::string secretstream_header_hex; // header "e": secretstream header (hex; empty for ContactRequest)
|
||||
std::string payload_memo; // ciphertext hex (Message) or plaintext request text (ContactRequest)
|
||||
std::int64_t sent_at = 0; // header "ts": sender compose time (Unix s); 0 = absent → use tx time
|
||||
// Larger-messages tier 3 (a multi-transaction Message): message_id ties this tx's slice to its
|
||||
// siblings, total_parts is the whole message's chunk count, and partial_chunks are this tx's
|
||||
// (global index, ciphertext-hex) chunks. Empty message_id = an ordinary single-tx message (payload
|
||||
// in payload_memo). The chat service accumulates partials by message_id and reassembles when whole.
|
||||
std::string message_id;
|
||||
int total_parts = 0;
|
||||
std::vector<std::pair<int, std::string>> partial_chunks;
|
||||
};
|
||||
|
||||
struct HushChatTransactionExtractionResult {
|
||||
@@ -120,22 +84,438 @@ struct HushChatTransactionExtractionResult {
|
||||
std::size_t ignored_memo_count = 0;
|
||||
};
|
||||
|
||||
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 int kHushChatSupportedVersion = 0;
|
||||
constexpr std::size_t kHushChatMemoByteLimit = 512;
|
||||
// Larger-messages tier 2: the most payload outputs a single transaction may carry (one tx). Keeps the
|
||||
// chat tx well under the shielded max_tx_size after Sietch decoy padding + proving cost. A message
|
||||
// needing more chunks than this spills into sibling transactions (tier 3), up to kHushChatMaxMessageChunks.
|
||||
constexpr int kHushChatMaxMessageParts = 16;
|
||||
// Larger-messages tier 3: the most chunks a single message may span IN TOTAL, across all its sibling
|
||||
// transactions (ceil(chunks / kHushChatMaxMessageParts) txs, each a separate 0-value chat send + fee).
|
||||
// Bounds the on-chain cost / broadcast time of one message; beyond this compose returns TooLong. The
|
||||
// 2-hex global part index (below) tops out at 256, so this must stay ≤ 256.
|
||||
constexpr int kHushChatMaxMessageChunks = 64;
|
||||
// Each multi-part payload chunk is prefixed with a 2-hex-char (1-byte) part index so the receiver can
|
||||
// reorder chunks after the daemon shuffles a tx's outputs. 2 hex covers indices 0..255 (>> the cap).
|
||||
constexpr std::size_t kHushChatPartIndexHexLen = 2;
|
||||
constexpr std::size_t kHushChatPublicKeyHexLength = 64;
|
||||
constexpr std::size_t kHushChatSecretstreamHeaderHexLength = 48;
|
||||
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";
|
||||
|
||||
constexpr bool hushChatFeatureEnabledAtBuild()
|
||||
{
|
||||
@@ -147,7 +527,60 @@ HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMe
|
||||
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
|
||||
const HushChatTransactionInput& transaction,
|
||||
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||
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* hushChatHeaderTypeName(HushChatHeaderType type);
|
||||
const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue);
|
||||
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
|
||||
@@ -1,147 +0,0 @@
|
||||
// DragonX Wallet - HushChat sensitive-conversation local key vault (implementation).
|
||||
|
||||
#include "chat_sensitive_vault.h"
|
||||
|
||||
#include <sodium.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <vector>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
namespace {
|
||||
constexpr unsigned char kVersion = 0x01;
|
||||
constexpr std::size_t kSaltLen = 16; // crypto_pwhash_SALTBYTES
|
||||
constexpr std::size_t kNonceLen = 24; // crypto_secretbox_NONCEBYTES
|
||||
constexpr std::size_t kKeyLen = 32; // the vault key / crypto_secretbox_KEYBYTES
|
||||
constexpr std::size_t kMacLen = 16; // crypto_secretbox_MACBYTES
|
||||
constexpr std::size_t kCipherLen = kKeyLen + kMacLen; // 48
|
||||
constexpr std::size_t kFileLen = 1 + kSaltLen + kNonceLen + kCipherLen; // 89
|
||||
} // namespace
|
||||
|
||||
ChatSensitiveVault::ChatSensitiveVault(std::string vaultPath) : path_(std::move(vaultPath)) {}
|
||||
|
||||
void ChatSensitiveVault::setPath(std::string vaultPath) { path_ = std::move(vaultPath); }
|
||||
|
||||
bool ChatSensitiveVault::deriveKek(const std::string& passphrase, const unsigned char* salt,
|
||||
unsigned char* kek) const {
|
||||
// Fixed MODERATE limits (≈0.7s / 256 MiB) resist brute-forcing a weak passphrase; because they are
|
||||
// fixed, the file carries no KDF parameters and unlock derives with exactly the same cost.
|
||||
return crypto_pwhash(kek, kKeyLen, passphrase.c_str(), passphrase.size(), salt,
|
||||
crypto_pwhash_OPSLIMIT_MODERATE, crypto_pwhash_MEMLIMIT_MODERATE,
|
||||
crypto_pwhash_ALG_ARGON2ID13) == 0;
|
||||
}
|
||||
|
||||
bool ChatSensitiveVault::writeVault(const unsigned char* salt, const unsigned char* nonce,
|
||||
const unsigned char* cipher, std::size_t cipherLen) const {
|
||||
if (path_.empty()) return false;
|
||||
const std::string tmp = path_ + ".tmp";
|
||||
{
|
||||
std::ofstream f(tmp, std::ios::binary | std::ios::trunc);
|
||||
if (!f) return false;
|
||||
f.put(static_cast<char>(kVersion));
|
||||
f.write(reinterpret_cast<const char*>(salt), kSaltLen);
|
||||
f.write(reinterpret_cast<const char*>(nonce), kNonceLen);
|
||||
f.write(reinterpret_cast<const char*>(cipher), static_cast<std::streamsize>(cipherLen));
|
||||
if (!f) { f.close(); std::remove(tmp.c_str()); return false; }
|
||||
}
|
||||
#if !defined(_WIN32)
|
||||
::chmod(tmp.c_str(), 0600); // best-effort: the key is passphrase-wrapped, so this is defense-in-depth
|
||||
#endif
|
||||
if (std::rename(tmp.c_str(), path_.c_str()) != 0) { std::remove(tmp.c_str()); return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChatSensitiveVault::exists() const {
|
||||
if (path_.empty()) return false;
|
||||
std::ifstream f(path_, std::ios::binary);
|
||||
if (!f) return false;
|
||||
char ver = 0;
|
||||
if (!f.get(ver) || static_cast<unsigned char>(ver) != kVersion) return false;
|
||||
f.seekg(0, std::ios::end);
|
||||
return f.good() && static_cast<std::size_t>(f.tellg()) == kFileLen;
|
||||
}
|
||||
|
||||
bool ChatSensitiveVault::create(const std::string& passphrase, std::array<unsigned char, 32>& keyOut) {
|
||||
keyOut.fill(0);
|
||||
if (path_.empty() || passphrase.empty()) return false;
|
||||
if (exists()) return false; // never clobber an existing vault
|
||||
if (sodium_init() < 0) return false;
|
||||
|
||||
unsigned char salt[kSaltLen], nonce[kNonceLen], kek[kKeyLen];
|
||||
unsigned char cipher[kCipherLen];
|
||||
randombytes_buf(salt, kSaltLen);
|
||||
randombytes_buf(nonce, kNonceLen);
|
||||
randombytes_buf(keyOut.data(), kKeyLen); // the random vault key
|
||||
|
||||
bool ok = deriveKek(passphrase, salt, kek);
|
||||
if (ok) {
|
||||
crypto_secretbox_easy(cipher, keyOut.data(), kKeyLen, nonce, kek);
|
||||
ok = writeVault(salt, nonce, cipher, kCipherLen);
|
||||
}
|
||||
sodium_memzero(kek, sizeof kek);
|
||||
sodium_memzero(cipher, sizeof cipher);
|
||||
if (!ok) keyOut.fill(0);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool ChatSensitiveVault::unlock(const std::string& passphrase, std::array<unsigned char, 32>& keyOut) const {
|
||||
keyOut.fill(0);
|
||||
if (path_.empty() || passphrase.empty()) return false;
|
||||
if (sodium_init() < 0) return false;
|
||||
|
||||
std::ifstream f(path_, std::ios::binary);
|
||||
if (!f) return false;
|
||||
std::vector<unsigned char> buf((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
if (buf.size() != kFileLen || buf[0] != kVersion) return false;
|
||||
|
||||
const unsigned char* salt = buf.data() + 1;
|
||||
const unsigned char* nonce = salt + kSaltLen;
|
||||
const unsigned char* cipher = nonce + kNonceLen;
|
||||
|
||||
unsigned char kek[kKeyLen];
|
||||
if (!deriveKek(passphrase, salt, kek)) { sodium_memzero(kek, sizeof kek); return false; }
|
||||
int rc = crypto_secretbox_open_easy(keyOut.data(), cipher, kCipherLen, nonce, kek);
|
||||
sodium_memzero(kek, sizeof kek);
|
||||
if (rc != 0) { keyOut.fill(0); return false; } // wrong passphrase or corrupt file
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChatSensitiveVault::changePassphrase(const std::string& oldPassphrase, const std::string& newPassphrase) {
|
||||
if (newPassphrase.empty()) return false;
|
||||
std::array<unsigned char, 32> key{};
|
||||
if (!unlock(oldPassphrase, key)) return false; // wrong old passphrase
|
||||
|
||||
unsigned char salt[kSaltLen], nonce[kNonceLen], kek[kKeyLen], cipher[kCipherLen];
|
||||
randombytes_buf(salt, kSaltLen);
|
||||
randombytes_buf(nonce, kNonceLen);
|
||||
bool ok = deriveKek(newPassphrase, salt, kek);
|
||||
if (ok) {
|
||||
crypto_secretbox_easy(cipher, key.data(), kKeyLen, nonce, kek);
|
||||
ok = writeVault(salt, nonce, cipher, kCipherLen);
|
||||
}
|
||||
sodium_memzero(kek, sizeof kek);
|
||||
sodium_memzero(cipher, sizeof cipher);
|
||||
sodium_memzero(key.data(), key.size());
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool ChatSensitiveVault::remove() {
|
||||
if (path_.empty()) return true;
|
||||
std::error_code ec;
|
||||
(void)ec;
|
||||
if (std::remove(path_.c_str()) != 0) {
|
||||
// Treat "already absent" as success; anything else is a real failure only if the file is still there.
|
||||
std::ifstream f(path_, std::ios::binary);
|
||||
return !f.good();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace dragonx::chat
|
||||
@@ -1,59 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// DragonX Wallet - HushChat "sensitive conversation" local key vault.
|
||||
//
|
||||
// A per-wallet random 32-byte key, wrapped under a USER PASSPHRASE (Argon2id-derived KEK +
|
||||
// XSalsa20-Poly1305), stored in a small local file. It encrypts the at-rest rows of conversations the
|
||||
// user marks "sensitive" in ChatDatabase, INDEPENDENTLY of the seed-derived chat key — so those rows
|
||||
// cannot be read by someone who has the wallet seed but not this passphrase, and a wallet freshly
|
||||
// restored from the seed alone cannot read them at all. Losing the passphrase (or the file) makes the
|
||||
// sensitive messages unrecoverable BY DESIGN. Not thread-safe; drive from the main thread.
|
||||
//
|
||||
// File format (version 1), 89 bytes:
|
||||
// [1] version (0x01)
|
||||
// [16] Argon2id salt
|
||||
// [24] XSalsa20-Poly1305 nonce
|
||||
// [48] ciphertext = 32-byte vault key + 16-byte Poly1305 tag
|
||||
// Argon2id uses MODERATE ops/mem limits (fixed), so the format needs to carry no KDF parameters.
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
class ChatSensitiveVault {
|
||||
public:
|
||||
ChatSensitiveVault() = default;
|
||||
explicit ChatSensitiveVault(std::string vaultPath);
|
||||
|
||||
void setPath(std::string vaultPath);
|
||||
const std::string& path() const { return path_; }
|
||||
|
||||
// True if a well-formed vault file exists (version byte + exact size). No passphrase / Argon2.
|
||||
bool exists() const;
|
||||
|
||||
// Create a NEW vault: generate a random 32-byte key, wrap it under `passphrase`, write the file, and
|
||||
// return the key in keyOut. Refuses (returns false) if a vault already exists, the passphrase is
|
||||
// empty, or on IO/crypto error. keyOut is zeroed on any failure.
|
||||
bool create(const std::string& passphrase, std::array<unsigned char, 32>& keyOut);
|
||||
|
||||
// Unlock: derive the KEK from `passphrase`, decrypt the stored key into keyOut. Returns false on a
|
||||
// wrong passphrase, a missing/corrupt file, or crypto failure; keyOut is zeroed unless it succeeds.
|
||||
bool unlock(const std::string& passphrase, std::array<unsigned char, 32>& keyOut) const;
|
||||
|
||||
// Re-wrap the existing key under a new passphrase (the key is unchanged). False if old is wrong.
|
||||
bool changePassphrase(const std::string& oldPassphrase, const std::string& newPassphrase);
|
||||
|
||||
// Delete the vault file. Returns true if removed or already absent.
|
||||
bool remove();
|
||||
|
||||
private:
|
||||
bool deriveKek(const std::string& passphrase, const unsigned char* salt, unsigned char* kek) const;
|
||||
bool writeVault(const unsigned char* salt, const unsigned char* nonce,
|
||||
const unsigned char* cipher, std::size_t cipherLen) const;
|
||||
|
||||
std::string path_;
|
||||
};
|
||||
|
||||
} // namespace dragonx::chat
|
||||
@@ -1,362 +0,0 @@
|
||||
// DragonX Wallet - HushChat service (implementation).
|
||||
|
||||
#include "chat_service.h"
|
||||
|
||||
#include "chat_database.h"
|
||||
#include "chat_identity.h" // chatIdentityPublicKeyHex
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
ChatService::~ChatService() {
|
||||
clearIdentity();
|
||||
}
|
||||
|
||||
void ChatService::setIdentity(const ChatKeyPair& keys) {
|
||||
identity_ = keys;
|
||||
has_identity_ = true;
|
||||
undecryptable_.clear(); // another identity may decrypt them
|
||||
}
|
||||
|
||||
void ChatService::clearIdentity() {
|
||||
wipeChatKeyPair(identity_);
|
||||
has_identity_ = false;
|
||||
undecryptable_.clear();
|
||||
}
|
||||
|
||||
int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
||||
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
|
||||
std::int64_t fallbackTimestamp,
|
||||
std::vector<std::string>* newIncomingCids) {
|
||||
if (!has_identity_) return 0;
|
||||
// Persistence is attached but not unlocked (e.g. the DB failed to open with the seed): we can't
|
||||
// consult tombstones, so ingesting now would resurface locally-deleted messages into the store.
|
||||
// Skip until the DB is usable — chat is degraded anyway without its store.
|
||||
if (db_ && !db_->hasKey()) return 0;
|
||||
|
||||
const std::string myPubKey = chatIdentityPublicKeyHex(identity_);
|
||||
|
||||
int added = 0;
|
||||
std::vector<ChatMessage> fresh; // persisted once after the loop (one transaction)
|
||||
|
||||
// Build, decrypt (Message) and store one piece of message metadata — a single-transaction message,
|
||||
// or a tier-3 message reassembled from its slices below. Shared so both apply the same guards exactly.
|
||||
auto processMessage = [&](const HushChatTransactionMetadata& meta) {
|
||||
// Already stored: append would refuse it. Every scan re-sends the whole history, so this skips the
|
||||
// decrypt for all but the new messages. For a reassembled tier-3 message this keys off its stable
|
||||
// canonical txid, deduping it across scans.
|
||||
if (store_.contains(meta.txid, meta.payload_position)) return;
|
||||
|
||||
ChatMessage message;
|
||||
message.direction = ChatDirection::Incoming;
|
||||
message.txid = meta.txid;
|
||||
message.conversation_id = meta.conversation_id;
|
||||
message.peer_zaddr = meta.reply_zaddr;
|
||||
message.peer_public_key_hex = meta.sender_public_key_hex;
|
||||
// Reference time: the tx/receive time (block time if confirmed, else the receiver's wall clock for
|
||||
// a mempool receive).
|
||||
const auto timeIt = txTimestamps.find(meta.txid);
|
||||
const std::int64_t refTime = timeIt != txTimestamps.end() ? timeIt->second : fallbackTimestamp;
|
||||
// Prefer the sender's stamped compose time (header "ts") — the true send time, shown identically on
|
||||
// both ends. But REJECT a value implausibly in the FUTURE vs the reference: a wrong/ahead peer clock
|
||||
// would otherwise pin their messages to the bottom of the thread forever. A compose time in the
|
||||
// PAST is fine — the note buffer can broadcast a queued message long after it was composed, and a
|
||||
// confirmed tx's block time is always >= the compose time.
|
||||
constexpr std::int64_t kSenderTsFutureToleranceSec = 3600; // 1 hour of clock skew tolerated
|
||||
if (meta.sent_at > 0 && (refTime <= 0 || meta.sent_at <= refTime + kSenderTsFutureToleranceSec)) {
|
||||
message.timestamp = meta.sent_at;
|
||||
} else {
|
||||
message.timestamp = refTime;
|
||||
}
|
||||
message.payload_position = meta.payload_position;
|
||||
|
||||
// Suppress locally-removed conversations before the (relatively costly) decrypt. A blocked cid
|
||||
// is dropped outright (old + future messages) until unblocked; a tombstoned (txid,position) was
|
||||
// deleted with "revive on new message", so only that exact message is skipped — a new message
|
||||
// in the same conversation has a different txid and flows through normally.
|
||||
if (blocked_pred_ && blocked_pred_(message.conversation_id)) return;
|
||||
if (conversationHiddenOnChain(message.conversation_id)) return; // hidden by an on-chain delete
|
||||
// Withhold a sensitive conversation's messages while its local vault key is absent (locked vault or
|
||||
// a fresh seed import). Fail-closed: if we can't store it under the vault key we don't surface it.
|
||||
// They re-import and persist under the vault key once it is unlocked and a scan runs.
|
||||
if (isConversationSensitive(message.conversation_id) && !(db_ && db_->hasSensitiveKey())) return;
|
||||
if (db_ && db_->isTombstoned(message)) return;
|
||||
|
||||
if (meta.type == HushChatHeaderType::ContactRequest) {
|
||||
message.kind = ChatMessageKind::ContactRequest;
|
||||
message.body = meta.payload_memo; // plaintext request text
|
||||
} else {
|
||||
message.kind = ChatMessageKind::Message;
|
||||
// Keyed by the decrypt's inputs, not only (txid, position): another scan path that pairs a
|
||||
// different header with this payload gets its own try.
|
||||
const std::string failKey = ChatStore::messageKey(meta.txid, meta.payload_position) + "|" +
|
||||
meta.sender_public_key_hex + "|" + meta.secretstream_header_hex;
|
||||
if (undecryptable_.count(failKey)) return;
|
||||
++decrypt_attempts_;
|
||||
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) { // drop undecryptable silently, and don't try it again
|
||||
if (status != ChatCryptoStatus::SodiumInitFailed) { // not a property of the message
|
||||
if (undecryptable_.size() >= 4096) undecryptable_.clear();
|
||||
undecryptable_.insert(failKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
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)) {
|
||||
fresh.push_back(message);
|
||||
++added;
|
||||
// Every ingested message is incoming — report its cid so the caller can notify without
|
||||
// relying on a seen-watermark delta (which block-time vs wall-clock skew can swallow).
|
||||
if (newIncomingCids) newIncomingCids->push_back(message.conversation_id);
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto& meta : metadata) {
|
||||
// A self-addressed control signal (delete / restore / ...). Reduce it and move on: it is never a
|
||||
// message, and it must be processed even though WE are its sender — unlike ordinary self-sent memos,
|
||||
// which the guard below skips. The label (peer hint) rides the payload.
|
||||
if (meta.type == HushChatHeaderType::Control) {
|
||||
const auto ctlTimeIt = txTimestamps.find(meta.txid);
|
||||
const std::int64_t ctlRef = ctlTimeIt != txTimestamps.end() ? ctlTimeIt->second : fallbackTimestamp;
|
||||
applyControl(meta.conversation_id, meta.op, meta.payload_memo,
|
||||
meta.sent_at > 0 ? meta.sent_at : ctlRef, meta.txid);
|
||||
continue;
|
||||
}
|
||||
// A memo whose sender is our OWN identity is something we sent (only we hold our key). The local
|
||||
// echo already records it as outgoing — ingesting it as incoming would duplicate it as a phantom
|
||||
// "from peer" message (this also collapses same-seed self-chat). Skips our own tier-3 slices too.
|
||||
if (!myPubKey.empty() && meta.sender_public_key_hex == myPubKey) continue;
|
||||
|
||||
// Larger-messages tier 3: a slice of a multi-transaction message. Accumulate it by message id;
|
||||
// the whole message is reassembled below once every chunk has arrived (across txs and scans).
|
||||
if (!meta.message_id.empty()) {
|
||||
if (meta.total_parts <= 0 || meta.total_parts > kHushChatMaxMessageChunks) continue; // sanity
|
||||
// Bound the buffer against a flood of never-completing partials (re-fed from chain, so a clear
|
||||
// only delays, never loses, a genuine message).
|
||||
if (tier3_pending_.size() > 256 && !tier3_pending_.count(meta.message_id)) tier3_pending_.clear();
|
||||
Tier3Pending& g = tier3_pending_[meta.message_id];
|
||||
if (g.chunks.empty()) { // first slice seen: capture the shared header fields
|
||||
g.total_parts = meta.total_parts;
|
||||
g.secretstream_header_hex = meta.secretstream_header_hex;
|
||||
g.sender_public_key_hex = meta.sender_public_key_hex;
|
||||
g.conversation_id = meta.conversation_id;
|
||||
g.reply_zaddr = meta.reply_zaddr;
|
||||
g.sent_at = meta.sent_at;
|
||||
g.canonical_txid = meta.txid;
|
||||
}
|
||||
if (meta.txid < g.canonical_txid) g.canonical_txid = meta.txid; // deterministic stable key
|
||||
for (const auto& c : meta.partial_chunks)
|
||||
if (c.first >= 0 && c.first < g.total_parts) g.chunks[c.first] = c.second;
|
||||
continue;
|
||||
}
|
||||
|
||||
processMessage(meta);
|
||||
}
|
||||
|
||||
// Finalize every tier-3 message whose chunks have all arrived: reassemble the one ciphertext and run
|
||||
// it through the shared message path (decrypt + the block/hide/sensitive guards). Retire the group
|
||||
// afterwards unless it was withheld only because the sensitive vault is still locked.
|
||||
{
|
||||
std::vector<std::string> retire;
|
||||
for (auto& entry : tier3_pending_) {
|
||||
Tier3Pending& g = entry.second;
|
||||
if (g.total_parts <= 0 || static_cast<int>(g.chunks.size()) != g.total_parts) continue;
|
||||
bool complete = true;
|
||||
for (int i = 0; i < g.total_parts; ++i) if (!g.chunks.count(i)) { complete = false; break; }
|
||||
if (!complete) continue;
|
||||
if (store_.contains(g.canonical_txid, 0)) { retire.push_back(entry.first); continue; } // already have it
|
||||
|
||||
HushChatTransactionMetadata synth;
|
||||
synth.type = HushChatHeaderType::Message;
|
||||
synth.txid = g.canonical_txid;
|
||||
synth.payload_position = 0;
|
||||
synth.conversation_id = g.conversation_id;
|
||||
synth.reply_zaddr = g.reply_zaddr;
|
||||
synth.sender_public_key_hex = g.sender_public_key_hex;
|
||||
synth.secretstream_header_hex = g.secretstream_header_hex;
|
||||
synth.sent_at = g.sent_at;
|
||||
std::string full;
|
||||
for (int i = 0; i < g.total_parts; ++i) full += g.chunks[i];
|
||||
synth.payload_memo = std::move(full);
|
||||
processMessage(synth);
|
||||
|
||||
const bool withheldSensitive =
|
||||
isConversationSensitive(g.conversation_id) && !(db_ && db_->hasSensitiveKey());
|
||||
if (!withheldSensitive) retire.push_back(entry.first);
|
||||
}
|
||||
for (const auto& mid : retire) tier3_pending_.erase(mid);
|
||||
}
|
||||
|
||||
if (db_ && !fresh.empty()) db_->appendBatch(fresh);
|
||||
// Enforce on-chain deletions against the store view: erase any conversation whose latest control op is
|
||||
// "del". This catches rows appended earlier in this very batch (before their delete signal was seen in
|
||||
// the metadata order) and rows rehydrated from the DB at startup before any control was reduced. The DB
|
||||
// rows are left intact so a later "res" — or a rescan — can restore the thread.
|
||||
for (const auto& entry : control_) {
|
||||
if (entry.second.op == "del") store_.eraseConversation(entry.first);
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
void ChatService::loadFromDatabase() {
|
||||
if (!db_) return;
|
||||
auto rows = db_->load();
|
||||
store_.reserve(store_.size() + rows.size());
|
||||
for (auto& message : rows) {
|
||||
// Never surface a blocked conversation, even if a prior "delete & block" failed to remove its
|
||||
// rows (defense-in-depth): the ingest guard already drops live scans, this covers the reload path.
|
||||
if (blocked_pred_ && blocked_pred_(message.conversation_id)) continue; // evaluate before the move
|
||||
store_.append(std::move(message));
|
||||
}
|
||||
}
|
||||
|
||||
bool ChatService::deleteConversation(const std::string& conversationId, bool block) {
|
||||
// Delete the persisted rows FIRST and only mutate the in-memory view if that succeeds. Doing it the
|
||||
// other way round means a failed DB write (disk full / locked) would empty the store while the rows
|
||||
// survive — and on the next reload the conversation silently reappears with no tombstone.
|
||||
// Revive-on-new-message => tombstone the removed rows so a re-scan won't re-import them.
|
||||
// Block => remove the rows without a tombstone; the caller's blocked predicate suppresses re-import
|
||||
// until unblocked, at which point the conversation re-imports from chain.
|
||||
if (db_) {
|
||||
std::vector<ChatMessage> msgs = store_.conversation(conversationId); // snapshot (copy)
|
||||
if (!db_->deleteMessages(msgs, /*tombstone=*/!block)) return false;
|
||||
}
|
||||
store_.eraseConversation(conversationId);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ChatService::applyControl(const std::string& cid, const std::string& op, const std::string& label,
|
||||
std::int64_t timestamp, const std::string& txid) {
|
||||
// Route the op to its axis; ignore unknown future ops so they never pollute either reduction.
|
||||
std::unordered_map<std::string, ControlState>* target = nullptr;
|
||||
if (op == "del" || op == "res") target = &control_;
|
||||
else if (op == "sensitive" || op == "unsensitive") target = &sensitive_control_;
|
||||
else return;
|
||||
|
||||
auto it = target->find(cid);
|
||||
if (it != target->end()) {
|
||||
const ControlState& cur = it->second;
|
||||
// A later signal wins; equal timestamps break by txid so the reduction is deterministic and a
|
||||
// replay of an older op can never flip the resolved state.
|
||||
if (timestamp < cur.timestamp || (timestamp == cur.timestamp && txid <= cur.txid)) return;
|
||||
it->second = ControlState{op, label, timestamp, txid};
|
||||
} else {
|
||||
target->emplace(cid, ControlState{op, label, timestamp, txid});
|
||||
}
|
||||
}
|
||||
|
||||
bool ChatService::conversationHiddenOnChain(const std::string& conversationId) const {
|
||||
auto it = control_.find(conversationId);
|
||||
return it != control_.end() && it->second.op == "del";
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> ChatService::onChainDeletedConversations() const {
|
||||
std::vector<std::pair<std::string, std::string>> out;
|
||||
for (const auto& entry : control_) {
|
||||
if (entry.second.op == "del") out.emplace_back(entry.first, entry.second.label);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
bool ChatService::conversationSensitiveOnChain(const std::string& conversationId) const {
|
||||
auto it = sensitive_control_.find(conversationId);
|
||||
return it != sensitive_control_.end() && it->second.op == "sensitive";
|
||||
}
|
||||
bool ChatService::isConversationSensitive(const std::string& conversationId) const {
|
||||
if (conversationSensitiveOnChain(conversationId)) return true;
|
||||
return local_sensitive_pred_ && local_sensitive_pred_(conversationId);
|
||||
}
|
||||
std::vector<ChatService::Tier3Progress> ChatService::pendingLargeMessages() const {
|
||||
std::vector<Tier3Progress> out;
|
||||
for (const auto& entry : tier3_pending_) {
|
||||
const Tier3Pending& g = entry.second;
|
||||
const int received = static_cast<int>(g.chunks.size());
|
||||
// Report only the genuinely in-progress ones. A complete group waiting on the sensitive vault key
|
||||
// (received == total but withheld) is done receiving, so it isn't shown as "incoming".
|
||||
if (g.total_parts > 0 && received < g.total_parts)
|
||||
out.push_back(Tier3Progress{ g.conversation_id, received, g.total_parts });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
void ChatService::setPersistence(ChatDatabase* db) {
|
||||
db_ = db;
|
||||
// The DB picks the per-row key by this predicate: a sensitive conversation's rows are stored under the
|
||||
// local vault key. Combines the on-chain reduction with the local (instant) mark.
|
||||
if (db_) db_->setSensitivePredicate([this](const std::string& cid) { return isConversationSensitive(cid); });
|
||||
}
|
||||
std::vector<std::pair<std::string, std::string>> ChatService::onChainSensitiveConversations() const {
|
||||
std::vector<std::pair<std::string, std::string>> out;
|
||||
for (const auto& entry : sensitive_control_) {
|
||||
if (entry.second.op == "sensitive") out.emplace_back(entry.first, entry.second.label);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
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::composeMessageParts(const std::string& myReplyZaddr,
|
||||
const std::string& peerPublicKeyHex,
|
||||
const std::string& peerZaddr,
|
||||
const std::string& conversationId,
|
||||
const std::string& plaintext,
|
||||
std::vector<OutgoingChatMemos>& out) const {
|
||||
if (!has_identity_) return ChatComposeStatus::MissingField;
|
||||
return splitOutgoingMessage(identity_, chatIdentityPublicKeyHex(identity_), myReplyZaddr,
|
||||
peerPublicKeyHex, peerZaddr, conversationId, plaintext, out);
|
||||
}
|
||||
|
||||
ChatComposeStatus ChatService::composeContactRequest(const std::string& myReplyZaddr,
|
||||
const std::string& peerZaddr,
|
||||
const std::string& conversationId,
|
||||
const std::string& requestText,
|
||||
OutgoingChatMemos& out) const {
|
||||
if (!has_identity_) return ChatComposeStatus::MissingField;
|
||||
return buildOutgoingContactRequest(chatIdentityPublicKeyHex(identity_), myReplyZaddr,
|
||||
peerZaddr, conversationId, requestText, out);
|
||||
}
|
||||
|
||||
bool ChatService::recordOutgoing(const ChatMessage& message) {
|
||||
if (store_.append(message)) {
|
||||
if (db_) db_->append(message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ChatService::recordOutgoingPending(const ChatMessage& message) {
|
||||
// Persist immediately (as Sending) so a send survives an app quit before the broadcast resolves;
|
||||
// resolveOutgoing() then UPSERTS the row to the final status. A stray persisted Sending (crash mid-
|
||||
// broadcast) loads as Sent (see ChatDatabase::deserialize).
|
||||
const bool appended = store_.append(message);
|
||||
if (appended && db_) db_->upsert(message);
|
||||
return appended;
|
||||
}
|
||||
|
||||
void ChatService::resolveOutgoing(const std::string& txid, ChatDelivery delivery, const std::string& reason) {
|
||||
const ChatMessage* updated = store_.updateDelivery(txid, delivery, reason);
|
||||
if (updated && db_) db_->upsert(*updated); // overwrite the Sending row with the final status
|
||||
}
|
||||
|
||||
} // namespace dragonx::chat
|
||||
@@ -1,197 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// DragonX Wallet - HushChat service: turns harvested memo metadata into decrypted, threaded
|
||||
// messages. Owns the long-lived chat identity keypair (a secret) + the in-memory store.
|
||||
// Move-disabled (the secret stays pinned); wipes the secret on destruction/clear.
|
||||
// Not thread-safe — drive from the main thread (where refresh results are applied).
|
||||
|
||||
#include "chat_crypto.h" // ChatKeyPair
|
||||
#include "chat_protocol.h" // HushChatTransactionMetadata
|
||||
#include "chat_outgoing.h" // OutgoingChatMemos, ChatComposeStatus
|
||||
#include "chat_store.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
class ChatDatabase; // optional persistent backing (Phase 2); set via setPersistence
|
||||
|
||||
class ChatService {
|
||||
public:
|
||||
ChatService() = default;
|
||||
~ChatService();
|
||||
ChatService(const ChatService&) = delete;
|
||||
ChatService& operator=(const ChatService&) = delete;
|
||||
ChatService(ChatService&&) = delete;
|
||||
ChatService& operator=(ChatService&&) = delete;
|
||||
|
||||
// Provision (or replace) the chat identity. Copies the keypair — the caller should wipe
|
||||
// its own copy afterwards (see chat_identity.h ownership note).
|
||||
void setIdentity(const ChatKeyPair& keys);
|
||||
bool hasIdentity() const { return has_identity_; }
|
||||
void clearIdentity(); // wipes the held secret key
|
||||
|
||||
// Decrypt/record each metadata entry (a Message is decrypted; a ContactRequest carries its
|
||||
// plaintext through) and thread it into the store. Each message is stamped with its own
|
||||
// transaction time via `txTimestamps` (keyed by txid), falling back to `fallbackTimestamp`
|
||||
// when the txid isn't present. Newly-added messages are also persisted (if a database is
|
||||
// attached). Returns the number of NEW messages added; 0 with no identity. Undecryptable
|
||||
// Messages are dropped silently (no logging of memo/plaintext).
|
||||
// When `newIncomingCids` is non-null it is filled with the conversation ids of the genuinely-new
|
||||
// incoming (non-request) messages appended this call — a reliable "a new message arrived here"
|
||||
// signal for notifications that doesn't depend on any timestamp/seen-watermark comparison.
|
||||
int ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
||||
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
|
||||
std::int64_t fallbackTimestamp = 0,
|
||||
std::vector<std::string>* newIncomingCids = nullptr);
|
||||
|
||||
// Attach a persistent backing store (Phase 2). Not owned. Pass nullptr to detach. New messages
|
||||
// from ingest() are written through; loadFromDatabase() rehydrates the in-memory store from it.
|
||||
// Also wires the DB's per-conversation sensitivity predicate to isConversationSensitive().
|
||||
void setPersistence(ChatDatabase* 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();
|
||||
|
||||
// Set a predicate returning true for a BLOCKED conversation id. ingest() drops those messages
|
||||
// entirely (they are never stored or persisted) until the predicate stops returning true — this is
|
||||
// how "delete & block" suppresses old and future messages. Typically wired to Settings::isChatBlocked.
|
||||
void setBlockedPredicate(std::function<bool(const std::string&)> pred) { blocked_pred_ = std::move(pred); }
|
||||
|
||||
// Locally delete a conversation: remove its messages from the store and the database. When `block`
|
||||
// is false, the removed messages are tombstoned so a chain re-scan won't re-import them, but a
|
||||
// genuinely NEW message (new txid) revives the thread. When `block` is true, the rows are removed
|
||||
// WITHOUT a tombstone (the caller also records the cid as blocked via the predicate above); unblocking
|
||||
// later lets the conversation re-import from chain. Returns false (leaving the store untouched) if the
|
||||
// persisted rows couldn't be removed, so the caller can avoid a store/DB divergence.
|
||||
bool deleteConversation(const std::string& conversationId, bool block);
|
||||
|
||||
// On-chain control signals (self-addressed Control memos) reduced per conversation: the latest op,
|
||||
// by sender compose time with a txid tiebreak, wins. A conversation whose latest op is "del" is hidden
|
||||
// on-chain — ingest() drops its messages and erases it from the store view, on every device and on a
|
||||
// fresh seed import — until a later "res" restores it. The state is re-derived from chain each session
|
||||
// (the control memos re-harvest on every scan), so it needs no local persistence.
|
||||
bool conversationHiddenOnChain(const std::string& conversationId) const;
|
||||
// Conversations whose latest on-chain control op is "del" (for the restore picker), each paired with
|
||||
// the label carried in its control memo (the peer address / display name at delete time).
|
||||
std::vector<std::pair<std::string, std::string>> onChainDeletedConversations() const;
|
||||
|
||||
// Sensitive axis (independent of hide/restore): a conversation whose latest on-chain control op is
|
||||
// "sensitive" is marked sensitive — its local rows are stored under the local vault key and withheld
|
||||
// on a device without that key (including a fresh seed import), until "unsensitive". Re-derived from
|
||||
// chain each session, like the hide axis.
|
||||
bool conversationSensitiveOnChain(const std::string& conversationId) const;
|
||||
// Conversations whose latest on-chain control op is "sensitive", each paired with its control label.
|
||||
std::vector<std::pair<std::string, std::string>> onChainSensitiveConversations() const;
|
||||
|
||||
// A local "sensitive" predicate (typically Settings::isChatSensitive) giving INSTANT effect the moment
|
||||
// the user marks a conversation, before the on-chain signal is harvested back. OR-combined with the
|
||||
// on-chain reduction by isConversationSensitive().
|
||||
void setLocalSensitivePredicate(std::function<bool(const std::string&)> pred) {
|
||||
local_sensitive_pred_ = std::move(pred);
|
||||
}
|
||||
// True if this conversation is sensitive (locally marked OR on-chain). Its rows are stored under the
|
||||
// local vault key; while that key is absent its messages are withheld from the store on ingest/load.
|
||||
bool isConversationSensitive(const std::string& conversationId) const;
|
||||
|
||||
// Larger-messages tier 3: a message still being received across transactions (some chunks arrived,
|
||||
// not yet all). Lets the UI show an "incoming large message — N of M parts" hint until it completes
|
||||
// and surfaces as an ordinary message. One entry per in-progress message; empty when none.
|
||||
struct Tier3Progress {
|
||||
std::string conversation_id;
|
||||
int received = 0; // chunks gathered so far
|
||||
int total = 0; // chunks the whole message needs
|
||||
};
|
||||
std::vector<Tier3Progress> pendingLargeMessages() const;
|
||||
|
||||
// --- 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 an ENCRYPTED message, splitting it across as many transactions as
|
||||
// its length needs (larger-messages tier 3). Returns one OutgoingChatMemos per transaction to send in
|
||||
// order; a short message yields a single element (identical to composeMessage).
|
||||
ChatComposeStatus composeMessageParts(const std::string& myReplyZaddr,
|
||||
const std::string& peerPublicKeyHex,
|
||||
const std::string& peerZaddr,
|
||||
const std::string& conversationId,
|
||||
const std::string& plaintext,
|
||||
std::vector<OutgoingChatMemos>& out) const;
|
||||
// Construct the outgoing memos for a plaintext contact request (no peer key needed yet).
|
||||
ChatComposeStatus composeContactRequest(const std::string& myReplyZaddr,
|
||||
const std::string& peerZaddr,
|
||||
const std::string& conversationId,
|
||||
const std::string& requestText,
|
||||
OutgoingChatMemos& out) const;
|
||||
// Echo a locally-composed outgoing message into the store (and DB). Returns true if new. (We
|
||||
// never harvest our own sent memos — they land on the peer's address — so this echo is the
|
||||
// only local record of what we sent.)
|
||||
bool recordOutgoing(const ChatMessage& message);
|
||||
|
||||
// Two-phase echo for delivery tracking: record + persist immediately as Sending (survives an app
|
||||
// quit), then resolveOutgoing() upserts the final status once the broadcast completes. A stray
|
||||
// persisted Sending (crash mid-broadcast) loads back as Sent.
|
||||
bool recordOutgoingPending(const ChatMessage& message);
|
||||
void resolveOutgoing(const std::string& txid, ChatDelivery delivery, const std::string& reason = {});
|
||||
|
||||
const ChatStore& store() const { return store_; }
|
||||
ChatStore& store() { return store_; }
|
||||
|
||||
std::uint64_t decryptAttempts() const { return decrypt_attempts_; } // tests / diagnostics
|
||||
|
||||
private:
|
||||
ChatKeyPair identity_{};
|
||||
bool has_identity_ = false;
|
||||
// Decrypt failures, keyed by what the decrypt reads (bounded). A rescan re-sends every message ever
|
||||
// received, so without it each poll re-ran the crypto for every undecryptable memo.
|
||||
std::unordered_set<std::string> undecryptable_;
|
||||
std::uint64_t decrypt_attempts_ = 0;
|
||||
ChatStore store_;
|
||||
ChatDatabase* db_ = nullptr; // optional; not owned
|
||||
std::function<bool(const std::string&)> blocked_pred_; // true => cid is blocked (drop its messages)
|
||||
std::function<bool(const std::string&)> local_sensitive_pred_; // true => cid locally marked sensitive
|
||||
|
||||
// Reduced on-chain control state, cid -> latest signal. Not persisted (re-derived from chain).
|
||||
struct ControlState {
|
||||
std::string op; // "del" / "res" / future ops
|
||||
std::string label; // the control memo's payload hint (peer address / display name)
|
||||
std::int64_t timestamp = 0;
|
||||
std::string txid;
|
||||
};
|
||||
std::unordered_map<std::string, ControlState> control_; // hide axis: del / res
|
||||
std::unordered_map<std::string, ControlState> sensitive_control_; // sensitive axis: sensitive / unsensitive
|
||||
|
||||
// Larger-messages tier 3: a big message split across several transactions arrives one slice at a
|
||||
// time (siblings share a message id). We accumulate the slices here until every chunk is present,
|
||||
// then reassemble + decrypt + emit the whole message. Not persisted — a rescan re-sends every slice
|
||||
// so the buffer re-derives from chain (like the control reducer); a completed group is retired.
|
||||
struct Tier3Pending {
|
||||
int total_parts = 0;
|
||||
std::string secretstream_header_hex; // "e", shared by every slice
|
||||
std::string sender_public_key_hex; // "p", shared
|
||||
std::string conversation_id;
|
||||
std::string reply_zaddr;
|
||||
std::int64_t sent_at = 0;
|
||||
std::string canonical_txid; // smallest sibling txid — the reassembled message's stable key
|
||||
std::unordered_map<int, std::string> chunks; // global index -> ciphertext-hex chunk
|
||||
};
|
||||
std::unordered_map<std::string, Tier3Pending> tier3_pending_; // message id -> accumulating slices
|
||||
// Monotonic reduce: route the op to its axis (hide vs sensitive), ignore unknown ops (forward-compat),
|
||||
// and within an axis a later (timestamp, txid) wins so a replay of an older op never flips the state.
|
||||
void applyControl(const std::string& cid, const std::string& op, const std::string& label,
|
||||
std::int64_t timestamp, const std::string& txid);
|
||||
};
|
||||
|
||||
} // namespace dragonx::chat
|
||||
@@ -1,126 +0,0 @@
|
||||
// DragonX Wallet - HushChat in-memory message store (implementation).
|
||||
|
||||
#include "chat_store.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sodium.h>
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
std::string ChatStore::messageKey(const std::string& txid, std::size_t payloadPosition) {
|
||||
return txid + ":" + std::to_string(payloadPosition);
|
||||
}
|
||||
|
||||
std::string ChatStore::dedupKey(const ChatMessage& message) {
|
||||
return messageKey(message.txid, message.payload_position);
|
||||
}
|
||||
|
||||
bool ChatStore::append(const ChatMessage& message) {
|
||||
if (!seen_.insert(dedupKey(message)).second) return false;
|
||||
messages_.push_back(message);
|
||||
++revision_;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChatStore::append(ChatMessage&& message) {
|
||||
if (!seen_.insert(dedupKey(message)).second) return false; // reads txid/payload_position before the move
|
||||
messages_.push_back(std::move(message));
|
||||
++revision_;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ChatStore::reserve(std::size_t n) {
|
||||
messages_.reserve(n);
|
||||
seen_.reserve(n);
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::int64_t> ChatStore::latestTimestampByConversation() const {
|
||||
std::unordered_map<std::string, std::int64_t> out;
|
||||
for (const auto& message : messages_) {
|
||||
auto it = out.find(message.conversation_id);
|
||||
if (it == out.end()) out.emplace(message.conversation_id, static_cast<std::int64_t>(message.timestamp));
|
||||
else if (static_cast<std::int64_t>(message.timestamp) > it->second) it->second = static_cast<std::int64_t>(message.timestamp);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<ChatMessage> ChatStore::conversation(const std::string& conversationId) const {
|
||||
std::vector<ChatMessage> out;
|
||||
for (const auto& message : messages_) {
|
||||
if (message.conversation_id == conversationId) out.push_back(message);
|
||||
}
|
||||
// Return chronological (oldest→newest). messages_ is in scan/insertion order, which is NOT
|
||||
// time-ordered (a full scan harvests txids in std::set/unordered_map order) — that would mis-order the
|
||||
// rendered thread AND let the reply-target pin (first-seen peer address) latch onto a non-establishing
|
||||
// message (B2). timestamp is the block/tx time (peer can't set it); tie-break txid + payload_position
|
||||
// for determinism.
|
||||
std::stable_sort(out.begin(), out.end(), [](const ChatMessage& a, const ChatMessage& b) {
|
||||
if (a.timestamp != b.timestamp) return a.timestamp < b.timestamp;
|
||||
if (a.txid != b.txid) return a.txid < b.txid;
|
||||
return a.payload_position < b.payload_position;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
const ChatMessage* ChatStore::updateDelivery(const std::string& txid, ChatDelivery delivery,
|
||||
const std::string& reason) {
|
||||
for (auto& message : messages_) {
|
||||
if (message.txid == txid) {
|
||||
message.delivery = delivery;
|
||||
message.fail_reason = (delivery == ChatDelivery::Failed) ? reason : std::string();
|
||||
++revision_;
|
||||
return &message;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int ChatStore::countUnread(const std::function<bool(const std::string&)>& excluded,
|
||||
const std::function<std::int64_t(const std::string&)>& seenFor) const {
|
||||
int unread = 0;
|
||||
for (const auto& m : messages_) {
|
||||
if (m.direction != ChatDirection::Incoming) continue;
|
||||
if (excluded && excluded(m.conversation_id)) continue;
|
||||
if (m.timestamp > seenFor(m.conversation_id)) ++unread;
|
||||
}
|
||||
return unread;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::vector<ChatMessage> ChatStore::eraseConversation(const std::string& conversationId) {
|
||||
std::vector<ChatMessage> removed;
|
||||
std::vector<ChatMessage> kept;
|
||||
kept.reserve(messages_.size());
|
||||
for (auto& m : messages_) {
|
||||
if (m.conversation_id == conversationId) {
|
||||
seen_.erase(dedupKey(m));
|
||||
removed.push_back(std::move(m));
|
||||
} else {
|
||||
kept.push_back(std::move(m));
|
||||
}
|
||||
}
|
||||
messages_.swap(kept);
|
||||
if (!removed.empty()) ++revision_;
|
||||
return removed;
|
||||
}
|
||||
|
||||
void ChatStore::clear() {
|
||||
// Zero decrypted plaintext (message bodies + peer address) before dropping them — clear() runs on
|
||||
// wallet lock / switch / teardown, and the freed std::strings would otherwise linger in the heap.
|
||||
auto wipe = [](std::string& s) { if (!s.empty()) sodium_memzero(&s[0], s.size()); s.clear(); };
|
||||
for (auto& m : messages_) { wipe(m.body); wipe(m.fail_reason); wipe(m.peer_zaddr); }
|
||||
messages_.clear();
|
||||
seen_.clear();
|
||||
txid_index_.clear(); // drop the History-badge txid copies too (revision_ below invalidates the memo)
|
||||
++revision_;
|
||||
}
|
||||
|
||||
} // namespace dragonx::chat
|
||||
@@ -1,108 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// DragonX Wallet - HushChat in-memory message store: the fast read model / dedup view. Durable
|
||||
// persistence lives in ChatDatabase; ChatService rehydrates this store from it on unlock.
|
||||
|
||||
#include "chat_message.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#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);
|
||||
// Same, but moves the message into the store (dedup key is computed before the move).
|
||||
bool append(ChatMessage&& message);
|
||||
// Pre-size the message vector + dedup set for a bulk load of n additional messages.
|
||||
void reserve(std::size_t n);
|
||||
// Max timestamp per conversation in ONE pass over messages_. Semantically equal to
|
||||
// conversation(cid).back().timestamp (conversation() sorts ascending) without the per-cid copy+sort.
|
||||
std::unordered_map<std::string, std::int64_t> latestTimestampByConversation() const;
|
||||
|
||||
// The dedup key of a message: "<txid>:<payload_position>".
|
||||
static std::string messageKey(const std::string& txid, std::size_t payloadPosition);
|
||||
// A message with this (txid, position) is in the store (append would refuse it).
|
||||
bool contains(const std::string& txid, std::size_t payloadPosition) const {
|
||||
return seen_.count(messageKey(txid, payloadPosition)) != 0;
|
||||
}
|
||||
|
||||
// Messages in a conversation, in insertion order.
|
||||
std::vector<ChatMessage> conversation(const std::string& conversationId) const;
|
||||
|
||||
// Update an outgoing echo's delivery status by its local txid. Returns a pointer to the updated
|
||||
// message (for the caller to persist), or nullptr if no message has that txid.
|
||||
const ChatMessage* updateDelivery(const std::string& txid, ChatDelivery delivery,
|
||||
const std::string& reason = {});
|
||||
|
||||
// Distinct conversation ids, in first-seen order.
|
||||
std::vector<std::string> conversationIds() const;
|
||||
|
||||
// The set of on-chain txids that carried a chat message (sent or received, messages + contact
|
||||
// requests). Builds a fresh COPY every call (O(messages) allocations) — prefer hasChatTxid() on hot paths.
|
||||
std::unordered_set<std::string> chatTxids() const {
|
||||
std::unordered_set<std::string> out;
|
||||
out.reserve(messages_.size());
|
||||
for (const auto& m : messages_)
|
||||
if (!m.txid.empty()) out.insert(m.txid);
|
||||
return out;
|
||||
}
|
||||
|
||||
// True if an on-chain txid carried a chat message (History tab "Message" badge / Chat filter). Backed by a
|
||||
// txid index rebuilt lazily when revision() moved (once per store mutation, O(messages)); O(1) on an empty
|
||||
// store and per lookup. One txid may back several messages; a from-scratch rebuild handles erase correctly.
|
||||
bool hasChatTxid(const std::string& txid) const {
|
||||
if (messages_.empty() || txid.empty()) return false;
|
||||
if (txid_index_rev_ != revision_) rebuildTxidIndex();
|
||||
return txid_index_.count(txid) != 0;
|
||||
}
|
||||
|
||||
// Remove every message in a conversation from the in-memory view and return the removed messages
|
||||
// (so the caller can delete/tombstone their persisted rows). Re-appends are prevented by the ingest
|
||||
// guard (blocked-cid predicate / DB tombstone), not here.
|
||||
std::vector<ChatMessage> eraseConversation(const std::string& conversationId);
|
||||
|
||||
std::size_t size() const { return messages_.size(); }
|
||||
bool empty() const { return messages_.empty(); }
|
||||
void clear();
|
||||
|
||||
// Monotonic counter bumped on every mutation (append / updateDelivery change / eraseConversation /
|
||||
// clear). Callers memoize expensive per-frame reads (conversation-list build, unread count) against it
|
||||
// so they only rebuild when the store actually changed.
|
||||
std::uint64_t revision() const { return revision_; }
|
||||
|
||||
// Count unread incoming messages in a SINGLE pass over the store: an incoming message counts when its
|
||||
// cid is not `excluded` and its timestamp is newer than `seenFor(cid)`. Avoids the per-conversation
|
||||
// copy+sort that conversation() does — the count doesn't need ordering.
|
||||
int countUnread(const std::function<bool(const std::string&)>& excluded,
|
||||
const std::function<std::int64_t(const std::string&)>& seenFor) const;
|
||||
|
||||
private:
|
||||
static std::string dedupKey(const ChatMessage& message);
|
||||
|
||||
void rebuildTxidIndex() const {
|
||||
txid_index_.clear();
|
||||
txid_index_.reserve(messages_.size());
|
||||
for (const auto& m : messages_)
|
||||
if (!m.txid.empty()) txid_index_.insert(m.txid);
|
||||
txid_index_rev_ = revision_;
|
||||
}
|
||||
|
||||
std::vector<ChatMessage> messages_;
|
||||
std::unordered_set<std::string> seen_;
|
||||
std::uint64_t revision_ = 0;
|
||||
// Lazy memo for hasChatTxid(): valid iff txid_index_rev_ == revision_. revision_ is monotonic and never reset,
|
||||
// so append/updateDelivery/eraseConversation/clear all invalidate it (~0 = never built). Main thread only.
|
||||
mutable std::unordered_set<std::string> txid_index_;
|
||||
mutable std::uint64_t txid_index_rev_ = ~std::uint64_t{0};
|
||||
};
|
||||
|
||||
} // namespace dragonx::chat
|
||||
+84
-392
@@ -12,10 +12,7 @@
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <ctime>
|
||||
#include <algorithm>
|
||||
#include <type_traits>
|
||||
|
||||
#include "../util/coalescing_writer.h"
|
||||
#include "../util/logger.h"
|
||||
#include "../util/platform.h"
|
||||
|
||||
@@ -60,76 +57,37 @@ const char* liteServerSelectionPreferenceModeName(
|
||||
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::vector<Settings::LiteServerPreference> Settings::defaultLiteServers()
|
||||
{
|
||||
return {
|
||||
{"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},
|
||||
{"https://lite6.dragonx.is", "DragonX Lite 6", true},
|
||||
{"https://lite7.dragonx.is", "DragonX Lite 7", true}
|
||||
};
|
||||
}
|
||||
|
||||
std::string Settings::getDefaultPath()
|
||||
{
|
||||
// Single per-platform, per-variant config dir (util::Platform::getConfigDir handles the
|
||||
// _WIN32 / __APPLE__ / XDG split and the DRAGONX_APP_NAME variant suffix in one place).
|
||||
const std::string dir = util::Platform::getConfigDir();
|
||||
#ifdef _WIN32
|
||||
char path[MAX_PATH];
|
||||
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
|
||||
std::string dir = std::string(path) + "\\" DRAGONX_APP_NAME;
|
||||
fs::create_directories(dir);
|
||||
return (fs::path(dir) / "settings.json").string();
|
||||
return dir + "\\settings.json";
|
||||
}
|
||||
return "settings.json";
|
||||
#elif defined(__APPLE__)
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
home = pw->pw_dir;
|
||||
}
|
||||
std::string dir = std::string(home) + "/Library/Application Support/" DRAGONX_APP_NAME;
|
||||
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/" DRAGONX_APP_NAME;
|
||||
fs::create_directories(dir);
|
||||
return dir + "/settings.json";
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Settings::load()
|
||||
@@ -150,71 +108,29 @@ bool Settings::load(const std::string& path)
|
||||
json j;
|
||||
file >> j;
|
||||
|
||||
loadScalar(j, "theme", theme_);
|
||||
loadScalar(j, "save_ztxs", save_ztxs_);
|
||||
loadScalar(j, "auto_shield", auto_shield_);
|
||||
loadScalar(j, "auto_consolidate", auto_consolidate_);
|
||||
loadScalar(j, "use_tor", use_tor_);
|
||||
loadScalar(j, "allow_custom_fees", allow_custom_fees_);
|
||||
loadScalar(j, "default_fee", default_fee_);
|
||||
loadScalar(j, "fetch_prices", fetch_prices_);
|
||||
loadScalar(j, "tx_explorer_url", tx_explorer_url_);
|
||||
loadScalar(j, "address_explorer_url", address_explorer_url_);
|
||||
loadScalar(j, "language", language_);
|
||||
loadScalar(j, "skin_id", skin_id_);
|
||||
loadScalar(j, "chat_reply_zaddr", chat_reply_zaddr_);
|
||||
if (j.contains("muted_chat_cids") && j["muted_chat_cids"].is_array()) {
|
||||
muted_chat_cids_.clear();
|
||||
for (const auto& c : j["muted_chat_cids"])
|
||||
if (c.is_string()) muted_chat_cids_.push_back(c.get<std::string>());
|
||||
}
|
||||
if (j.contains("hidden_chat_cids") && j["hidden_chat_cids"].is_array()) {
|
||||
hidden_chat_cids_.clear();
|
||||
for (const auto& c : j["hidden_chat_cids"])
|
||||
if (c.is_string()) hidden_chat_cids_.push_back(c.get<std::string>());
|
||||
}
|
||||
if (j.contains("blocked_chat_convs") && j["blocked_chat_convs"].is_array()) {
|
||||
blocked_chat_convs_.clear();
|
||||
for (const auto& c : j["blocked_chat_convs"])
|
||||
if (c.is_object() && c.contains("cid") && c["cid"].is_string())
|
||||
blocked_chat_convs_.push_back({c["cid"].get<std::string>(),
|
||||
(c.contains("name") && c["name"].is_string())
|
||||
? c["name"].get<std::string>() : std::string()});
|
||||
}
|
||||
if (j.contains("sensitive_chat_convs") && j["sensitive_chat_convs"].is_array()) {
|
||||
sensitive_chat_convs_.clear();
|
||||
for (const auto& c : j["sensitive_chat_convs"])
|
||||
if (c.is_object() && c.contains("cid") && c["cid"].is_string())
|
||||
sensitive_chat_convs_.push_back({c["cid"].get<std::string>(),
|
||||
(c.contains("name") && c["name"].is_string())
|
||||
? c["name"].get<std::string>() : std::string()});
|
||||
}
|
||||
// Chat-tab customization (re-clamped through the setters so hand-edited JSON stays in range).
|
||||
loadScalar(j, "chat_emoji_color", chat_emoji_color_);
|
||||
loadScalar(j, "chat_poll_rate_sec", chat_poll_rate_sec_); setChatPollRateSec(chat_poll_rate_sec_);
|
||||
loadScalar(j, "chat_bubble_style", chat_bubble_style_); setChatBubbleStyle(chat_bubble_style_);
|
||||
loadScalar(j, "chat_bubble_accent", chat_bubble_accent_); setChatBubbleAccent(chat_bubble_accent_);
|
||||
loadScalar(j, "chat_density", chat_density_); setChatDensity(chat_density_);
|
||||
loadScalar(j, "chat_font_scale", chat_font_scale_); setChatFontScale(chat_font_scale_);
|
||||
loadScalar(j, "chat_time_format", chat_time_format_); setChatTimeFormat(chat_time_format_);
|
||||
loadScalar(j, "chat_enter_sends", chat_enter_sends_);
|
||||
loadScalar(j, "time_format", time_format_); setTimeFormat(time_format_);
|
||||
loadScalar(j, "acrylic_enabled", acrylic_enabled_);
|
||||
loadScalar(j, "acrylic_quality", acrylic_quality_);
|
||||
loadScalar(j, "blur_multiplier", blur_multiplier_);
|
||||
loadScalar(j, "noise_opacity", noise_opacity_);
|
||||
loadScalar(j, "gradient_background", gradient_background_);
|
||||
// Migrate legacy reduced_transparency bool -> ui_opacity float (type-tolerant: a hand-edited or
|
||||
// inf->null re-saved value must not quarantine the whole settings file).
|
||||
if (j.contains("ui_opacity") && j["ui_opacity"].is_number()) {
|
||||
ui_opacity_ = std::clamp(j["ui_opacity"].get<float>(), 0.3f, 1.0f); // matches setUIOpacity()
|
||||
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].is_boolean()
|
||||
&& j["reduced_transparency"].get<bool>()) {
|
||||
if (j.contains("theme")) theme_ = j["theme"].get<std::string>();
|
||||
if (j.contains("save_ztxs")) save_ztxs_ = j["save_ztxs"].get<bool>();
|
||||
if (j.contains("auto_shield")) auto_shield_ = j["auto_shield"].get<bool>();
|
||||
if (j.contains("use_tor")) use_tor_ = j["use_tor"].get<bool>();
|
||||
if (j.contains("allow_custom_fees")) allow_custom_fees_ = j["allow_custom_fees"].get<bool>();
|
||||
if (j.contains("default_fee")) default_fee_ = j["default_fee"].get<double>();
|
||||
if (j.contains("fetch_prices")) fetch_prices_ = j["fetch_prices"].get<bool>();
|
||||
if (j.contains("tx_explorer_url")) tx_explorer_url_ = j["tx_explorer_url"].get<std::string>();
|
||||
if (j.contains("address_explorer_url")) address_explorer_url_ = j["address_explorer_url"].get<std::string>();
|
||||
if (j.contains("language")) language_ = j["language"].get<std::string>();
|
||||
if (j.contains("skin_id")) skin_id_ = j["skin_id"].get<std::string>();
|
||||
if (j.contains("acrylic_enabled")) acrylic_enabled_ = j["acrylic_enabled"].get<bool>();
|
||||
if (j.contains("acrylic_quality")) acrylic_quality_ = j["acrylic_quality"].get<int>();
|
||||
if (j.contains("blur_multiplier")) blur_multiplier_ = j["blur_multiplier"].get<float>();
|
||||
if (j.contains("noise_opacity")) noise_opacity_ = j["noise_opacity"].get<float>();
|
||||
if (j.contains("gradient_background")) gradient_background_ = j["gradient_background"].get<bool>();
|
||||
// Migrate legacy reduced_transparency bool -> ui_opacity float
|
||||
if (j.contains("ui_opacity")) {
|
||||
ui_opacity_ = j["ui_opacity"].get<float>();
|
||||
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) {
|
||||
ui_opacity_ = 1.0f; // legacy: reduced = fully opaque
|
||||
}
|
||||
loadScalar(j, "window_opacity", window_opacity_);
|
||||
loadScalar(j, "favorite_opacity", favorite_opacity_);
|
||||
favorite_opacity_ = std::clamp(favorite_opacity_, 0.15f, 1.0f); // matches setFavoriteOpacity()
|
||||
if (j.contains("window_opacity")) window_opacity_ = j["window_opacity"].get<float>();
|
||||
if (j.contains("balance_layout")) {
|
||||
if (j["balance_layout"].is_string())
|
||||
balance_layout_ = j["balance_layout"].get<std::string>();
|
||||
@@ -228,19 +144,7 @@ bool Settings::load(const std::string& path)
|
||||
if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
|
||||
}
|
||||
}
|
||||
loadScalar(j, "portfolio_style", portfolio_style_);
|
||||
if (portfolio_style_ < 0 || portfolio_style_ > 2) portfolio_style_ = 0;
|
||||
loadScalar(j, "contacts_view_mode", contacts_view_mode_);
|
||||
if (contacts_view_mode_ < 0 || contacts_view_mode_ > 2) contacts_view_mode_ = 0;
|
||||
loadScalar(j, "contacts_avatar_shape", contacts_avatar_shape_); setContactsAvatarShape(contacts_avatar_shape_);
|
||||
loadScalar(j, "contacts_list_scale", contacts_list_scale_); setContactsListScale(contacts_list_scale_);
|
||||
loadScalar(j, "animate_avatars", animate_avatars_);
|
||||
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
||||
loadScalar(j, "console_line_accents", console_line_accents_);
|
||||
loadScalar(j, "console_text_color", console_text_color_);
|
||||
loadScalar(j, "console_zoom", console_zoom_);
|
||||
if (!(console_zoom_ >= 0.25f && console_zoom_ <= 4.0f)) console_zoom_ = 1.0f; // guard bad/NaN
|
||||
loadScalar(j, "console_auto_focus", console_auto_focus_);
|
||||
if (j.contains("scanline_enabled")) scanline_enabled_ = j["scanline_enabled"].get<bool>();
|
||||
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
|
||||
hidden_addresses_.clear();
|
||||
for (const auto& a : j["hidden_addresses"])
|
||||
@@ -251,18 +155,6 @@ 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("earnings_addresses") && j["earnings_addresses"].is_array()) {
|
||||
earnings_addresses_.clear();
|
||||
for (const auto& a : j["earnings_addresses"])
|
||||
if (a.is_string()) earnings_addresses_.insert(a.get<std::string>());
|
||||
}
|
||||
loadScalar(j, "earnings_interval", earnings_interval_);
|
||||
earnings_interval_ = std::clamp(earnings_interval_, 0, 3);
|
||||
loadScalar(j, "earnings_mode", earnings_mode_);
|
||||
earnings_mode_ = std::clamp(earnings_mode_, 0, 1);
|
||||
loadScalar(j, "earnings_format", earnings_format_);
|
||||
earnings_format_ = std::clamp(earnings_format_, 0, 2);
|
||||
loadScalar(j, "earnings_show_all", earnings_show_all_);
|
||||
if (j.contains("address_meta") && j["address_meta"].is_object()) {
|
||||
address_meta_.clear();
|
||||
for (auto& [addr, meta] : j["address_meta"].items()) {
|
||||
@@ -278,35 +170,13 @@ bool Settings::load(const std::string& path)
|
||||
address_meta_[addr] = m;
|
||||
}
|
||||
}
|
||||
loadScalar(j, "wizard_completed", wizard_completed_);
|
||||
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
|
||||
loadScalar(j, "chat_intro_dismissed", chat_intro_dismissed_);
|
||||
loadScalar(j, "lock_failed_attempts", lock_failed_attempts_);
|
||||
loadScalar(j, "lockout_until_epoch", lockout_until_epoch_);
|
||||
loadScalar(j, "lock_state_wallet", lock_state_wallet_);
|
||||
loadScalar(j, "large_wallet_warned", large_wallet_warned_);
|
||||
if (j.contains("empty_wallet_warning_acked") && j["empty_wallet_warning_acked"].is_array()) {
|
||||
empty_wallet_warning_acked_.clear();
|
||||
for (const auto& w : j["empty_wallet_warning_acked"])
|
||||
if (w.is_string()) empty_wallet_warning_acked_.insert(w.get<std::string>());
|
||||
}
|
||||
loadScalar(j, "encryption_pending", encryption_pending_);
|
||||
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, "seed_migration_sweep_opid", seed_migration_sweep_opid_);
|
||||
loadScalar(j, "seed_migration_sweep_submitted", seed_migration_sweep_submitted_);
|
||||
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_);
|
||||
loadScalar(j, "stratum_host_enabled", stratum_host_enabled_);
|
||||
loadScalar(j, "stratum_allowip", stratum_allowip_);
|
||||
if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get<bool>();
|
||||
if (j.contains("auto_lock_timeout")) auto_lock_timeout_ = j["auto_lock_timeout"].get<int>();
|
||||
if (j.contains("unlock_duration")) unlock_duration_ = j["unlock_duration"].get<int>();
|
||||
if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get<bool>();
|
||||
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get<bool>();
|
||||
if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get<bool>();
|
||||
if (j.contains("max_connections")) max_connections_ = j["max_connections"].get<int>();
|
||||
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
|
||||
const auto& lite = j["lite_wallet"];
|
||||
if (lite.contains("server_selection_mode")) {
|
||||
@@ -353,16 +223,6 @@ bool Settings::load(const std::string& path)
|
||||
}
|
||||
lite_servers_.push_back(preference);
|
||||
}
|
||||
// Surface newly-shipped default servers (e.g. lite6/lite7) on an existing
|
||||
// install whose saved list predates them. Servers are hidden (see
|
||||
// lite_hidden_servers_), never deleted, so appending a missing default won't
|
||||
// resurrect one the user intentionally removed.
|
||||
for (const auto& def : defaultLiteServers()) {
|
||||
bool present = false;
|
||||
for (const auto& s : lite_servers_)
|
||||
if (s.url == def.url) { present = true; break; }
|
||||
if (!present) lite_servers_.push_back(def);
|
||||
}
|
||||
}
|
||||
if (lite.contains("rollout_override") && lite["rollout_override"].is_string()) {
|
||||
const auto v = lite["rollout_override"].get<std::string>();
|
||||
@@ -377,38 +237,33 @@ bool Settings::load(const std::string& path)
|
||||
if (u.is_string()) lite_hidden_servers_.insert(u.get<std::string>());
|
||||
}
|
||||
}
|
||||
loadScalar(j, "verbose_logging", verbose_logging_);
|
||||
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
|
||||
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
|
||||
debug_categories_.clear();
|
||||
for (const auto& c : j["debug_categories"])
|
||||
if (c.is_string()) debug_categories_.insert(c.get<std::string>());
|
||||
}
|
||||
loadScalar(j, "theme_effects_enabled", theme_effects_enabled_);
|
||||
loadScalar(j, "low_spec_mode", low_spec_mode_);
|
||||
loadScalar(j, "reduce_motion", reduce_motion_);
|
||||
loadScalar(j, "selected_exchange", selected_exchange_);
|
||||
loadScalar(j, "selected_pair", selected_pair_);
|
||||
loadScalar(j, "chart_interval", chart_interval_);
|
||||
loadScalar(j, "chart_style", chart_style_);
|
||||
loadScalar(j, "pool_url", pool_url_);
|
||||
if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get<bool>();
|
||||
if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get<bool>();
|
||||
if (j.contains("reduce_motion")) reduce_motion_ = j["reduce_motion"].get<bool>();
|
||||
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>();
|
||||
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
|
||||
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
|
||||
// Migrate old default pool URL that was missing the stratum port
|
||||
if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
|
||||
loadScalar(j, "pool_algo", pool_algo_);
|
||||
loadScalar(j, "pool_worker", pool_worker_);
|
||||
loadScalar(j, "pool_threads", pool_threads_);
|
||||
loadScalar(j, "pool_tls", pool_tls_);
|
||||
loadScalar(j, "pool_hugepages", pool_hugepages_);
|
||||
loadScalar(j, "pool_mode", pool_mode_);
|
||||
if (j.contains("pool_select_mode")) pool_select_mode_ = parsePoolSelectMode(j["pool_select_mode"]);
|
||||
loadScalar(j, "mine_when_idle", mine_when_idle_);
|
||||
loadScalar(j, "xmrig_version", xmrig_version_);
|
||||
// Lower-bounded only (min 30s), matching setMineIdleDelay; now type-guarded too.
|
||||
if (j.contains("mine_idle_delay") && j["mine_idle_delay"].is_number_integer())
|
||||
mine_idle_delay_ = std::max(30, j["mine_idle_delay"].get<int>());
|
||||
loadScalar(j, "idle_thread_scaling", idle_thread_scaling_);
|
||||
loadScalar(j, "idle_threads_active", idle_threads_active_);
|
||||
loadScalar(j, "idle_threads_idle", idle_threads_idle_);
|
||||
loadScalar(j, "idle_gpu_aware", idle_gpu_aware_);
|
||||
if (j.contains("pool_algo")) pool_algo_ = j["pool_algo"].get<std::string>();
|
||||
if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get<std::string>();
|
||||
if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get<int>();
|
||||
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
|
||||
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
|
||||
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
|
||||
if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>();
|
||||
if (j.contains("xmrig_version")) xmrig_version_ = j["xmrig_version"].get<std::string>();
|
||||
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>());
|
||||
if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get<bool>();
|
||||
if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get<int>();
|
||||
if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get<int>();
|
||||
if (j.contains("idle_gpu_aware")) idle_gpu_aware_ = j["idle_gpu_aware"].get<bool>();
|
||||
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
|
||||
saved_pool_urls_.clear();
|
||||
for (const auto& u : j["saved_pool_urls"])
|
||||
@@ -419,56 +274,15 @@ bool Settings::load(const std::string& path)
|
||||
for (const auto& w : j["saved_pool_workers"])
|
||||
if (w.is_string()) saved_pool_workers_.push_back(w.get<std::string>());
|
||||
}
|
||||
if (j.contains("portfolio_entries") && j["portfolio_entries"].is_array()) {
|
||||
portfolio_entries_.clear();
|
||||
for (const auto& e : j["portfolio_entries"]) {
|
||||
if (!e.is_object()) continue;
|
||||
PortfolioEntry entry;
|
||||
if (e.contains("label") && e["label"].is_string())
|
||||
entry.label = e["label"].get<std::string>();
|
||||
if (e.contains("addresses") && e["addresses"].is_array())
|
||||
for (const auto& a : e["addresses"])
|
||||
if (a.is_string()) entry.addresses.push_back(a.get<std::string>());
|
||||
if (e.contains("icon") && e["icon"].is_string())
|
||||
entry.icon = e["icon"].get<std::string>();
|
||||
if (e.contains("color") && e["color"].is_number())
|
||||
entry.color = e["color"].get<unsigned int>();
|
||||
if (e.contains("outline_opacity") && e["outline_opacity"].is_number_integer())
|
||||
entry.outlineOpacity = e["outline_opacity"].get<int>();
|
||||
if (e.contains("price_basis") && e["price_basis"].is_number_integer())
|
||||
entry.priceBasis = e["price_basis"].get<int>();
|
||||
if (e.contains("manual_price") && e["manual_price"].is_number())
|
||||
entry.manualPrice = e["manual_price"].get<double>();
|
||||
if (e.contains("manual_currency") && e["manual_currency"].is_string())
|
||||
entry.manualCurrency = e["manual_currency"].get<std::string>();
|
||||
if (e.contains("show_drgx") && e["show_drgx"].is_boolean())
|
||||
entry.showDrgx = e["show_drgx"].get<bool>();
|
||||
if (e.contains("show_value") && e["show_value"].is_boolean())
|
||||
entry.showValue = e["show_value"].get<bool>();
|
||||
if (e.contains("show_24h") && e["show_24h"].is_boolean())
|
||||
entry.show24h = e["show_24h"].get<bool>();
|
||||
if (e.contains("show_sparkline") && e["show_sparkline"].is_boolean())
|
||||
entry.showSparkline = e["show_sparkline"].get<bool>();
|
||||
if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
|
||||
entry.sparklineInterval = e["sparkline_interval"].get<int>();
|
||||
if (e.contains("scope") && e["scope"].is_string()) entry.scope = e["scope"].get<std::string>();
|
||||
if (e.contains("grid_col") && e["grid_col"].is_number_integer())
|
||||
entry.gridCol = e["grid_col"].get<int>();
|
||||
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
|
||||
entry.gridRow = e["grid_row"].get<int>();
|
||||
if (e.contains("grid_w") && e["grid_w"].is_number_integer())
|
||||
entry.gridW = e["grid_w"].get<int>();
|
||||
if (e.contains("grid_h") && e["grid_h"].is_number_integer())
|
||||
entry.gridH = e["grid_h"].get<int>();
|
||||
if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
loadClamped(j, "font_scale", font_scale_, 1.0f, 1.5f);
|
||||
loadScalar(j, "window_width", window_width_);
|
||||
loadScalar(j, "window_height", window_height_);
|
||||
if (j.contains("font_scale") && j["font_scale"].is_number())
|
||||
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>()));
|
||||
if (j.contains("window_width") && j["window_width"].is_number_integer())
|
||||
window_width_ = j["window_width"].get<int>();
|
||||
if (j.contains("window_height") && j["window_height"].is_number_integer())
|
||||
window_height_ = j["window_height"].get<int>();
|
||||
|
||||
// Version tracking — detect upgrades so we can re-save with new defaults
|
||||
loadScalar(j, "settings_version", settings_version_);
|
||||
if (j.contains("settings_version")) settings_version_ = j["settings_version"].get<std::string>();
|
||||
if (settings_version_ != DRAGONX_VERSION) {
|
||||
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
|
||||
settings_version_.empty() ? "(none)" : settings_version_.c_str(),
|
||||
@@ -494,26 +308,6 @@ bool Settings::load(const std::string& path)
|
||||
}
|
||||
}
|
||||
|
||||
int Settings::reattachLegacyPortfolioScopes(const std::string& scopeId, const std::string& identityHash,
|
||||
const std::set<std::string>& walletAddresses, bool singleWallet)
|
||||
{
|
||||
if (scopeId.empty()) return 0;
|
||||
int moved = 0;
|
||||
for (auto& e : portfolio_entries_) {
|
||||
if (!e.isLegacyScope()) continue; // global stays global; "w:" ids are already stable
|
||||
bool mine = singleWallet || (!identityHash.empty() && e.scope == identityHash);
|
||||
if (!mine) {
|
||||
for (const auto& a : e.addresses) {
|
||||
if (walletAddresses.count(a)) { mine = true; break; }
|
||||
}
|
||||
}
|
||||
if (!mine) continue; // ambiguous: leave legacy (fail-open visible)
|
||||
e.scope = scopeId;
|
||||
++moved;
|
||||
}
|
||||
return moved;
|
||||
}
|
||||
|
||||
bool Settings::save()
|
||||
{
|
||||
if (settings_path_.empty()) {
|
||||
@@ -524,14 +318,11 @@ bool Settings::save()
|
||||
|
||||
bool Settings::save(const std::string& path)
|
||||
{
|
||||
// UI sweep: nothing is written until sweepEnd(), which restores the real values and saves once.
|
||||
if (sweep_active_) { sweep_suppressed_save_ = true; return true; }
|
||||
json j;
|
||||
|
||||
j["theme"] = theme_;
|
||||
j["save_ztxs"] = save_ztxs_;
|
||||
j["auto_shield"] = auto_shield_;
|
||||
j["auto_consolidate"] = auto_consolidate_;
|
||||
j["use_tor"] = use_tor_;
|
||||
j["allow_custom_fees"] = allow_custom_fees_;
|
||||
j["default_fee"] = default_fee_;
|
||||
@@ -540,36 +331,6 @@ bool Settings::save(const std::string& path)
|
||||
j["address_explorer_url"] = address_explorer_url_;
|
||||
j["language"] = language_;
|
||||
j["skin_id"] = skin_id_;
|
||||
j["chat_reply_zaddr"] = chat_reply_zaddr_;
|
||||
j["muted_chat_cids"] = json::array();
|
||||
for (const auto& c : muted_chat_cids_)
|
||||
j["muted_chat_cids"].push_back(c);
|
||||
j["hidden_chat_cids"] = json::array();
|
||||
for (const auto& c : hidden_chat_cids_)
|
||||
j["hidden_chat_cids"].push_back(c);
|
||||
j["blocked_chat_convs"] = json::array();
|
||||
for (const auto& b : blocked_chat_convs_) {
|
||||
json o;
|
||||
o["cid"] = b.cid;
|
||||
o["name"] = b.name;
|
||||
j["blocked_chat_convs"].push_back(o);
|
||||
}
|
||||
j["sensitive_chat_convs"] = json::array();
|
||||
for (const auto& s : sensitive_chat_convs_) {
|
||||
json o;
|
||||
o["cid"] = s.cid;
|
||||
o["name"] = s.name;
|
||||
j["sensitive_chat_convs"].push_back(o);
|
||||
}
|
||||
j["chat_emoji_color"] = chat_emoji_color_;
|
||||
j["chat_poll_rate_sec"] = chat_poll_rate_sec_;
|
||||
j["chat_bubble_style"] = chat_bubble_style_;
|
||||
j["chat_bubble_accent"] = chat_bubble_accent_;
|
||||
j["chat_density"] = chat_density_;
|
||||
j["chat_font_scale"] = chat_font_scale_;
|
||||
j["chat_time_format"] = chat_time_format_;
|
||||
j["chat_enter_sends"] = chat_enter_sends_;
|
||||
j["time_format"] = time_format_;
|
||||
j["acrylic_enabled"] = acrylic_enabled_;
|
||||
j["acrylic_quality"] = acrylic_quality_;
|
||||
j["blur_multiplier"] = blur_multiplier_;
|
||||
@@ -577,31 +338,14 @@ bool Settings::save(const std::string& path)
|
||||
j["gradient_background"] = gradient_background_;
|
||||
j["ui_opacity"] = ui_opacity_;
|
||||
j["window_opacity"] = window_opacity_;
|
||||
j["favorite_opacity"] = favorite_opacity_;
|
||||
j["balance_layout"] = balance_layout_; // saved as string ID
|
||||
j["portfolio_style"] = portfolio_style_;
|
||||
j["contacts_view_mode"] = contacts_view_mode_;
|
||||
j["contacts_avatar_shape"] = contacts_avatar_shape_;
|
||||
j["contacts_list_scale"] = contacts_list_scale_;
|
||||
j["animate_avatars"] = animate_avatars_;
|
||||
j["scanline_enabled"] = scanline_enabled_;
|
||||
j["console_line_accents"] = console_line_accents_;
|
||||
j["console_text_color"] = console_text_color_;
|
||||
j["console_zoom"] = console_zoom_;
|
||||
j["console_auto_focus"] = console_auto_focus_;
|
||||
j["hidden_addresses"] = json::array();
|
||||
for (const auto& addr : hidden_addresses_)
|
||||
j["hidden_addresses"].push_back(addr);
|
||||
j["favorite_addresses"] = json::array();
|
||||
for (const auto& addr : favorite_addresses_)
|
||||
j["favorite_addresses"].push_back(addr);
|
||||
j["earnings_addresses"] = json::array();
|
||||
for (const auto& addr : earnings_addresses_)
|
||||
j["earnings_addresses"].push_back(addr);
|
||||
j["earnings_interval"] = earnings_interval_;
|
||||
j["earnings_mode"] = earnings_mode_;
|
||||
j["earnings_format"] = earnings_format_;
|
||||
j["earnings_show_all"] = earnings_show_all_;
|
||||
{
|
||||
json meta_obj = json::object();
|
||||
for (const auto& [addr, m] : address_meta_) {
|
||||
@@ -616,32 +360,12 @@ bool Settings::save(const std::string& path)
|
||||
j["address_meta"] = meta_obj;
|
||||
}
|
||||
j["wizard_completed"] = wizard_completed_;
|
||||
j["seed_backup_reminded"] = seed_backup_reminded_;
|
||||
j["chat_intro_dismissed"] = chat_intro_dismissed_;
|
||||
j["lock_failed_attempts"] = lock_failed_attempts_;
|
||||
j["lockout_until_epoch"] = lockout_until_epoch_;
|
||||
j["lock_state_wallet"] = lock_state_wallet_;
|
||||
j["large_wallet_warned"] = large_wallet_warned_;
|
||||
j["empty_wallet_warning_acked"] = json::array();
|
||||
for (const auto& w : empty_wallet_warning_acked_)
|
||||
j["empty_wallet_warning_acked"].push_back(w);
|
||||
j["encryption_pending"] = encryption_pending_;
|
||||
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["seed_migration_sweep_opid"] = seed_migration_sweep_opid_;
|
||||
j["seed_migration_sweep_submitted"] = seed_migration_sweep_submitted_;
|
||||
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_;
|
||||
j["stratum_host_enabled"] = stratum_host_enabled_;
|
||||
j["stratum_allowip"] = stratum_allowip_;
|
||||
{
|
||||
json lite = json::object();
|
||||
lite["server_selection_mode"] = liteServerSelectionPreferenceModeName(lite_server_selection_mode_);
|
||||
@@ -672,8 +396,6 @@ bool Settings::save(const std::string& path)
|
||||
j["reduce_motion"] = reduce_motion_;
|
||||
j["selected_exchange"] = selected_exchange_;
|
||||
j["selected_pair"] = selected_pair_;
|
||||
j["chart_interval"] = chart_interval_;
|
||||
j["chart_style"] = chart_style_;
|
||||
j["pool_url"] = pool_url_;
|
||||
j["pool_algo"] = pool_algo_;
|
||||
j["pool_worker"] = pool_worker_;
|
||||
@@ -681,7 +403,6 @@ 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_;
|
||||
@@ -695,30 +416,6 @@ bool Settings::save(const std::string& path)
|
||||
j["saved_pool_workers"] = json::array();
|
||||
for (const auto& w : saved_pool_workers_)
|
||||
j["saved_pool_workers"].push_back(w);
|
||||
j["portfolio_entries"] = json::array();
|
||||
for (const auto& e : portfolio_entries_) {
|
||||
json entry;
|
||||
entry["label"] = e.label;
|
||||
entry["addresses"] = json::array();
|
||||
for (const auto& a : e.addresses) entry["addresses"].push_back(a);
|
||||
entry["icon"] = e.icon;
|
||||
entry["color"] = e.color;
|
||||
entry["outline_opacity"] = e.outlineOpacity;
|
||||
entry["price_basis"] = e.priceBasis;
|
||||
entry["manual_price"] = e.manualPrice;
|
||||
entry["manual_currency"] = e.manualCurrency;
|
||||
entry["show_drgx"] = e.showDrgx;
|
||||
entry["show_value"] = e.showValue;
|
||||
entry["show_24h"] = e.show24h;
|
||||
entry["show_sparkline"] = e.showSparkline;
|
||||
entry["sparkline_interval"] = e.sparklineInterval;
|
||||
entry["scope"] = e.scope;
|
||||
entry["grid_col"] = e.gridCol;
|
||||
entry["grid_row"] = e.gridRow;
|
||||
entry["grid_w"] = e.gridW;
|
||||
entry["grid_h"] = e.gridH;
|
||||
j["portfolio_entries"].push_back(std::move(entry));
|
||||
}
|
||||
j["font_scale"] = font_scale_;
|
||||
j["settings_version"] = std::string(DRAGONX_VERSION);
|
||||
if (window_width_ > 0 && window_height_ > 0) {
|
||||
@@ -730,13 +427,8 @@ bool Settings::save(const std::string& path)
|
||||
// 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. With a writer attached the same atomic write
|
||||
// runs on its background thread (queued, latest-wins); after a critical setter
|
||||
// (durable_next_save_) this waits until the file is on disk, as the old save did.
|
||||
const bool ok = util::persistFileAtomically(writer_, durable_next_save_, path, j.dump(4),
|
||||
/*restrictPermissions=*/true);
|
||||
if (ok) durable_next_save_ = false;
|
||||
return ok;
|
||||
// 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;
|
||||
|
||||
+14
-407
@@ -12,7 +12,6 @@
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util { class CoalescingWriter; }
|
||||
namespace config {
|
||||
|
||||
/**
|
||||
@@ -40,16 +39,10 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Save settings to default location
|
||||
* With a file writer attached (the App's), the JSON snapshot is taken here, on the caller's thread,
|
||||
* and the atomic write runs on the writer's thread: returns true once queued. After a fund/security-
|
||||
* critical setter (see durable_next_save_) it waits until the file is on disk, like the old save().
|
||||
* @return true if saved (or queued) successfully
|
||||
* @return true if saved successfully
|
||||
*/
|
||||
bool save();
|
||||
|
||||
/** @brief Route saves through a background writer (not owned; must outlive this). Null = synchronous. */
|
||||
void setFileWriter(util::CoalescingWriter* writer) { writer_ = writer; }
|
||||
|
||||
/**
|
||||
* @brief Save settings to specific path
|
||||
* @param path Path to settings file
|
||||
@@ -67,62 +60,12 @@ public:
|
||||
Random
|
||||
};
|
||||
|
||||
// Pool selection mode for the mining tab: Manual (user picks the pool) or
|
||||
// AutoBalance (the wallet spreads miners across the official pools by hashrate).
|
||||
enum class PoolSelectMode {
|
||||
Manual,
|
||||
AutoBalance
|
||||
};
|
||||
|
||||
struct LiteServerPreference {
|
||||
std::string url;
|
||||
std::string label;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
// A user-defined portfolio entry: a custom label tied to a group of wallet addresses.
|
||||
// The Market tab's portfolio card sums these addresses' balances under the label.
|
||||
struct PortfolioEntry {
|
||||
std::string label;
|
||||
std::vector<std::string> addresses;
|
||||
std::string icon; // project_icons wallet-icon name; empty = no icon
|
||||
unsigned int color = 0; // packed IM_COL32 accent; 0 = theme default
|
||||
int outlineOpacity = 25; // accent-outline opacity, percent (0-100)
|
||||
// Per-group price data. priceBasis: 0 = live market (USD), 1 = live market (BTC),
|
||||
// 2 = DRGX only (no fiat value), 3 = manual price. Defaults preserve prior behavior
|
||||
// (show DRGX + USD value).
|
||||
int priceBasis = 0;
|
||||
double manualPrice = 0.0; // price per DRGX for the Manual basis
|
||||
std::string manualCurrency = "USD";
|
||||
bool showDrgx = true; // show the DRGX amount on the card
|
||||
bool showValue = true; // show the converted/fiat value on the card
|
||||
bool show24h = false; // show the 24h % change (live-market bases only)
|
||||
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
|
||||
int sparklineInterval = 4; // 0=minute 1=hour 2=day 3=week 4=month (default month: a real curve
|
||||
// from the daily series, vs the young in-session minute buffer)
|
||||
// Per-wallet visibility: "" (shown in every wallet - legacy/global) or the active wallet's STABLE scope
|
||||
// id ("w:"+hex, App::activeWalletScopeId(), persisted in wallets.json, never derived from the address
|
||||
// set). Older builds stored the address-set identity hash here, which drifted whenever an address was
|
||||
// added; such a legacy scope fails open (stays visible) until reattachLegacyPortfolioScopes() moves it
|
||||
// to the stable id. New entries are tagged with the current wallet so a portfolio built for wallet A
|
||||
// doesn't clutter wallet B.
|
||||
std::string scope;
|
||||
bool isGlobal() const { return scope.empty(); }
|
||||
bool isLegacyScope() const { return !scope.empty() && scope.rfind("w:", 0) != 0; }
|
||||
bool visibleInWallet(const std::string& scopeId) const {
|
||||
if (isGlobal()) return true;
|
||||
if (scopeId.empty()) return false; // wallet not resolved yet: never another wallet's groups
|
||||
if (isLegacyScope()) return true; // fail open until migrated
|
||||
return scope == scopeId;
|
||||
}
|
||||
// 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; }
|
||||
@@ -131,117 +74,15 @@ public:
|
||||
std::string getSkinId() const { return skin_id_; }
|
||||
void setSkinId(const std::string& id) { skin_id_ = id; }
|
||||
|
||||
// Stable z-address chosen for HushChat: the reply-to address in outgoing headers, and (for
|
||||
// legacy/non-mnemonic wallets) the source of the seed-derived chat identity. Persisted so the
|
||||
// identity + reply address don't shift when new addresses are generated.
|
||||
std::string getChatReplyZaddr() const { return chat_reply_zaddr_; }
|
||||
void setChatReplyZaddr(const std::string& z) { chat_reply_zaddr_ = z; }
|
||||
|
||||
// Muted chat conversations (by cid) — muted conversations don't badge or raise a toast (Q10).
|
||||
bool isChatMuted(const std::string& cid) const {
|
||||
return std::find(muted_chat_cids_.begin(), muted_chat_cids_.end(), cid) != muted_chat_cids_.end();
|
||||
}
|
||||
void setChatMuted(const std::string& cid, bool muted) {
|
||||
const bool already = isChatMuted(cid);
|
||||
if (muted && !already) muted_chat_cids_.push_back(cid);
|
||||
else if (!muted && already)
|
||||
muted_chat_cids_.erase(std::remove(muted_chat_cids_.begin(), muted_chat_cids_.end(), cid),
|
||||
muted_chat_cids_.end());
|
||||
}
|
||||
|
||||
// Hidden chat conversations (by cid) — hidden ones are filtered out of the list; a new incoming
|
||||
// message un-hides them (you can't un-receive) so nothing is silently lost.
|
||||
bool isChatHidden(const std::string& cid) const {
|
||||
return std::find(hidden_chat_cids_.begin(), hidden_chat_cids_.end(), cid) != hidden_chat_cids_.end();
|
||||
}
|
||||
void setChatHidden(const std::string& cid, bool hidden) {
|
||||
const bool already = isChatHidden(cid);
|
||||
if (hidden && !already) hidden_chat_cids_.push_back(cid);
|
||||
else if (!hidden && already)
|
||||
hidden_chat_cids_.erase(std::remove(hidden_chat_cids_.begin(), hidden_chat_cids_.end(), cid),
|
||||
hidden_chat_cids_.end());
|
||||
}
|
||||
|
||||
// Blocked chat conversations (by cid). Unlike hide (reversible, keeps messages), block DELETES the
|
||||
// local history AND suppresses every message for the cid — old and future — until you unblock, at
|
||||
// which point the conversation re-imports from the chain. The last-known peer name is kept so the
|
||||
// "Blocked" list can label the entry (its messages are gone from the local store).
|
||||
struct BlockedChatConv { std::string cid; std::string name; };
|
||||
bool isChatBlocked(const std::string& cid) const {
|
||||
for (const auto& b : blocked_chat_convs_) if (b.cid == cid) return true;
|
||||
return false;
|
||||
}
|
||||
void setChatBlocked(const std::string& cid, const std::string& name, bool blocked) {
|
||||
const bool already = isChatBlocked(cid);
|
||||
if (blocked && !already) blocked_chat_convs_.push_back({cid, name});
|
||||
else if (!blocked && already)
|
||||
blocked_chat_convs_.erase(
|
||||
std::remove_if(blocked_chat_convs_.begin(), blocked_chat_convs_.end(),
|
||||
[&](const BlockedChatConv& b) { return b.cid == cid; }),
|
||||
blocked_chat_convs_.end());
|
||||
}
|
||||
const std::vector<BlockedChatConv>& blockedChatConversations() const { return blocked_chat_convs_; }
|
||||
|
||||
// Sensitive chat conversations (by cid): their local rows are stored under the local chat vault key
|
||||
// (not the seed key) and are withheld without it. Kept locally for INSTANT effect the moment the user
|
||||
// marks one; the mark is also signaled on-chain so other devices / a fresh seed import gate it too.
|
||||
// The last-known peer name labels the sensitive-conversations list.
|
||||
struct SensitiveChatConv { std::string cid; std::string name; };
|
||||
bool isChatSensitive(const std::string& cid) const {
|
||||
for (const auto& s : sensitive_chat_convs_) if (s.cid == cid) return true;
|
||||
return false;
|
||||
}
|
||||
void setChatSensitive(const std::string& cid, const std::string& name, bool sensitive) {
|
||||
const bool already = isChatSensitive(cid);
|
||||
if (sensitive && !already) sensitive_chat_convs_.push_back({cid, name});
|
||||
else if (!sensitive && already)
|
||||
sensitive_chat_convs_.erase(
|
||||
std::remove_if(sensitive_chat_convs_.begin(), sensitive_chat_convs_.end(),
|
||||
[&](const SensitiveChatConv& s) { return s.cid == cid; }),
|
||||
sensitive_chat_convs_.end());
|
||||
}
|
||||
const std::vector<SensitiveChatConv>& sensitiveChatConversations() const { return sensitive_chat_convs_; }
|
||||
|
||||
// ── Chat-tab customization (chat settings modal + Settings → Chat & Contacts) ──────
|
||||
bool getChatEmojiColor() const { return chat_emoji_color_; }
|
||||
void setChatEmojiColor(bool v) { chat_emoji_color_ = v; }
|
||||
float getChatPollRateSec() const { return chat_poll_rate_sec_; }
|
||||
void setChatPollRateSec(float v) { chat_poll_rate_sec_ = std::max(0.5f, std::min(15.0f, v)); }
|
||||
int getChatBubbleStyle() const { return chat_bubble_style_; }
|
||||
void setChatBubbleStyle(int v) { chat_bubble_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
int getChatBubbleAccent() const { return chat_bubble_accent_; }
|
||||
void setChatBubbleAccent(int v) { chat_bubble_accent_ = (v < 0 || v > 5) ? 0 : v; }
|
||||
int getChatDensity() const { return chat_density_; }
|
||||
void setChatDensity(int v) { chat_density_ = (v < 0 || v > 1) ? 0 : v; }
|
||||
float getChatFontScale() const { return chat_font_scale_; }
|
||||
void setChatFontScale(float v) { chat_font_scale_ = std::max(0.8f, std::min(1.5f, v)); }
|
||||
int getChatTimeFormat() const { return chat_time_format_; } // 0=follow global, 1=24h, 2=12h
|
||||
void setChatTimeFormat(int v) { chat_time_format_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
bool getChatEnterSends() const { return chat_enter_sends_; }
|
||||
void setChatEnterSends(bool v) { chat_enter_sends_ = v; }
|
||||
// Global clock format (0=24h, 1=12h) — chat can override it for the Chat tab only.
|
||||
int getTimeFormat() const { return time_format_; }
|
||||
void setTimeFormat(int v) { time_format_ = (v < 0 || v > 1) ? 0 : v; }
|
||||
|
||||
// Privacy
|
||||
bool getSaveZtxs() const { return save_ztxs_; }
|
||||
void setSaveZtxs(bool save) { save_ztxs_ = save; }
|
||||
|
||||
bool getAutoShield() const { return auto_shield_; }
|
||||
void setAutoShield(bool shield) { auto_shield_ = shield; }
|
||||
// Automatic background note consolidation (full-node): when on, the wallet quietly merges small notes
|
||||
// in tiny batches while idle (see App::pumpAutoConsolidation). Each address's notes are merged back into
|
||||
// THAT SAME address (z_mergetoaddress [addr] -> addr), so funds never move between addresses. On by
|
||||
// default — a missing key in an older config therefore enables it on upgrade, which is intended.
|
||||
bool getAutoConsolidate() const { return auto_consolidate_; }
|
||||
void setAutoConsolidate(bool v) { auto_consolidate_ = v; }
|
||||
|
||||
bool getUseTor() const { return use_tor_; }
|
||||
void setUseTor(bool tor) { use_tor_ = tor; }
|
||||
// "Use Tor" was never implemented: no build routed any traffic through Tor. Clears a stored opt-in
|
||||
// and reports whether there was one, so App::init can tell that user once (the cleared key is the
|
||||
// "already told" flag).
|
||||
bool consumeLegacyUseTor() { const bool was = use_tor_; use_tor_ = false; return was; }
|
||||
|
||||
// Fees
|
||||
bool getAllowCustomFees() const { return allow_custom_fees_; }
|
||||
@@ -287,11 +128,6 @@ public:
|
||||
float getNoiseOpacity() const { return noise_opacity_; }
|
||||
void setNoiseOpacity(float v) { noise_opacity_ = v; }
|
||||
|
||||
// Opacity multiplier for the favorite gold visuals (row outline + star button) in the
|
||||
// Balance address list. 1.0 = theme default (unchanged); floor 0.15 so it can't vanish.
|
||||
float getFavoriteOpacity() const { return favorite_opacity_; }
|
||||
void setFavoriteOpacity(float v) { favorite_opacity_ = std::max(0.15f, std::min(1.0f, v)); }
|
||||
|
||||
// Gradient background mode (use gradient variant of theme background)
|
||||
bool getGradientBackground() const { return gradient_background_; }
|
||||
void setGradientBackground(bool v) { gradient_background_ = v; }
|
||||
@@ -300,39 +136,10 @@ public:
|
||||
std::string getBalanceLayout() const { return balance_layout_; }
|
||||
void setBalanceLayout(const std::string& v) { balance_layout_ = v; }
|
||||
|
||||
// Market-tab portfolio row style: 0 = Table (borderless grid), 1 = Cards (glass card + Z/T bar),
|
||||
// 2 = Spotlight (hero value). Cycled with Left/Right arrows or set in the Market settings modal.
|
||||
int getPortfolioStyle() const { return portfolio_style_; }
|
||||
void setPortfolioStyle(int v) { portfolio_style_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
// Contacts tab address-list view: 0 = cards, 1 = list, 2 = table.
|
||||
int getContactsViewMode() const { return contacts_view_mode_; }
|
||||
void setContactsViewMode(int v) { contacts_view_mode_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
// Contacts customization (gear modal): avatar shape + card/list row scale.
|
||||
// Avatar shape: 0 = circle, 1 = rounded square, 2 = full-row-height left tab (rounded-left, flat right).
|
||||
int getContactsAvatarShape() const { return contacts_avatar_shape_; }
|
||||
void setContactsAvatarShape(int v) { contacts_avatar_shape_ = (v < 0 || v > 2) ? 0 : v; }
|
||||
float getContactsListScale() const { return contacts_list_scale_; }
|
||||
void setContactsListScale(float v) { contacts_list_scale_ = std::max(0.8f, std::min(1.5f, v)); }
|
||||
// Play animated contact avatars (GIF/WebP). Off = show the first frame only.
|
||||
bool getAnimateAvatars() const { return animate_avatars_; }
|
||||
void setAnimateAvatars(bool v) { animate_avatars_ = v; }
|
||||
|
||||
// Console scanline effect
|
||||
bool getScanlineEnabled() const { return scanline_enabled_; }
|
||||
void setScanlineEnabled(bool v) { scanline_enabled_ = v; }
|
||||
|
||||
// Console output appearance: per-line left color accent bars, and per-channel text coloring.
|
||||
// (Defaults match the ConsoleTab statics so an upgrade re-save doesn't flip visible behavior.)
|
||||
bool getConsoleLineAccents() const { return console_line_accents_; }
|
||||
void setConsoleLineAccents(bool v) { console_line_accents_ = v; }
|
||||
bool getConsoleTextColor() const { return console_text_color_; }
|
||||
void setConsoleTextColor(bool v) { console_text_color_ = v; }
|
||||
float getConsoleZoom() const { return console_zoom_; }
|
||||
void setConsoleZoom(float v) { console_zoom_ = v; }
|
||||
// Auto-place the text cursor in the command box when the Console tab is opened.
|
||||
bool getConsoleAutoFocus() const { return console_auto_focus_; }
|
||||
void setConsoleAutoFocus(bool v) { console_auto_focus_ = v; }
|
||||
|
||||
// Hidden addresses (addresses hidden from the UI by the user)
|
||||
const std::set<std::string>& getHiddenAddresses() const { return hidden_addresses_; }
|
||||
bool isAddressHidden(const std::string& addr) const { return hidden_addresses_.count(addr) > 0; }
|
||||
@@ -347,22 +154,6 @@ public:
|
||||
void unfavoriteAddress(const std::string& addr) { favorite_addresses_.erase(addr); }
|
||||
int getFavoriteAddressCount() const { return (int)favorite_addresses_.size(); }
|
||||
|
||||
// Inline period-earnings (Balance tab). interval: 0=Hour 1=Day 2=Month 3=Year; mode: 0=Inflows
|
||||
// 1=Net; format: 0=DRGX 1=DRGX+fiat 2=Fiat. show-all toggles the per-address earnings column for
|
||||
// every address at once; earnings_addresses_ is the opt-in set shown when show-all is off.
|
||||
int getEarningsInterval() const { return earnings_interval_; }
|
||||
void setEarningsInterval(int v) { earnings_interval_ = (v < 0 ? 0 : (v > 3 ? 3 : v)); }
|
||||
int getEarningsMode() const { return earnings_mode_; }
|
||||
void setEarningsMode(int v) { earnings_mode_ = (v < 0 ? 0 : (v > 1 ? 1 : v)); }
|
||||
int getEarningsFormat() const { return earnings_format_; }
|
||||
void setEarningsFormat(int v) { earnings_format_ = (v < 0 ? 0 : (v > 2 ? 2 : v)); }
|
||||
bool getEarningsShowAll() const { return earnings_show_all_; }
|
||||
void setEarningsShowAll(bool v) { earnings_show_all_ = v; }
|
||||
const std::set<std::string>& getEarningsAddresses() const { return earnings_addresses_; }
|
||||
bool isAddressEarningsShown(const std::string& addr) const { return earnings_addresses_.count(addr) > 0; }
|
||||
void showAddressEarnings(const std::string& addr) { earnings_addresses_.insert(addr); }
|
||||
void hideAddressEarnings(const std::string& addr) { earnings_addresses_.erase(addr); }
|
||||
|
||||
// Address metadata (labels, icons, custom ordering)
|
||||
struct AddressMeta {
|
||||
std::string label;
|
||||
@@ -413,75 +204,7 @@ public:
|
||||
|
||||
// First-run wizard
|
||||
bool getWizardCompleted() const { return wizard_completed_; }
|
||||
void setWizardCompleted(bool v) { wizard_completed_ = v; durable_next_save_ = true; }
|
||||
|
||||
// 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; }
|
||||
bool getChatIntroDismissed() const { return chat_intro_dismissed_; }
|
||||
void setChatIntroDismissed(bool v) { chat_intro_dismissed_ = v; durable_next_save_ = true; }
|
||||
|
||||
// Unlock lockout state, persisted across restarts so the failed-attempt curve can't be reset by
|
||||
// killing the app. Keyed by the wallet file it applies to; the PIN/passphrase is never stored.
|
||||
int getLockFailedAttempts() const { return lock_failed_attempts_; }
|
||||
void setLockFailedAttempts(int v) { lock_failed_attempts_ = v; durable_next_save_ = true; }
|
||||
long long getLockoutUntilEpoch() const { return lockout_until_epoch_; }
|
||||
void setLockoutUntilEpoch(long long v) { lockout_until_epoch_ = v; durable_next_save_ = true; }
|
||||
std::string getLockStateWallet() const { return lock_state_wallet_; }
|
||||
void setLockStateWallet(const std::string& v) { lock_state_wallet_ = v; durable_next_save_ = true; }
|
||||
|
||||
// One-time nudge when wallet.dat grows past the bloat threshold (re-armed if it shrinks back).
|
||||
bool getLargeWalletWarned() const { return large_wallet_warned_; }
|
||||
void setLargeWalletWarned(bool v) { large_wallet_warned_ = v; }
|
||||
|
||||
// Wallet filenames for which the one-time "this wallet is empty but a sibling holds funds"
|
||||
// warning has been dismissed. Keyed per active wallet file so switching to a different empty
|
||||
// wallet can warn again (see App::maybeWarnEmptyWalletWithFundedSiblings).
|
||||
bool isEmptyWalletWarnAcked(const std::string& walletFile) const {
|
||||
return empty_wallet_warning_acked_.count(walletFile) > 0;
|
||||
}
|
||||
void ackEmptyWalletWarn(const std::string& walletFile) { empty_wallet_warning_acked_.insert(walletFile); }
|
||||
|
||||
// Persisted the moment deferred (wizard) encryption is requested; cleared only once the wallet is
|
||||
// observed to be actually encrypted. Lets a quit/crash/failed-connect before it applies be detected
|
||||
// and surfaced (W2-2). NEVER stores the passphrase — only the fact that encryption was requested.
|
||||
bool getEncryptionPending() const { return encryption_pending_; }
|
||||
void setEncryptionPending(bool v) { encryption_pending_ = v; durable_next_save_ = true; }
|
||||
|
||||
// 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; durable_next_save_ = true; }
|
||||
|
||||
// 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; durable_next_save_ = true; }
|
||||
std::string getSeedMigrationDest() const { return seed_migration_dest_; }
|
||||
void setSeedMigrationDest(const std::string& v) { seed_migration_dest_ = v; durable_next_save_ = true; }
|
||||
std::string getSeedMigrationTempDir() const { return seed_migration_temp_dir_; }
|
||||
void setSeedMigrationTempDir(const std::string& v) { seed_migration_temp_dir_ = v; durable_next_save_ = true; }
|
||||
// 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; durable_next_save_ = true; }
|
||||
// W3-3: the async sweep operation id, persisted while the sweep is in flight (before it resolves
|
||||
// to a txid). Lets a resume re-poll a mid-sweep interruption instead of dropping the txid. Cleared
|
||||
// in the same write that persists the txid, so the txid always outranks it (see [[decideSeedMigrationResume]]).
|
||||
std::string getSeedMigrationSweepOpid() const { return seed_migration_sweep_opid_; }
|
||||
void setSeedMigrationSweepOpid(const std::string& v) { seed_migration_sweep_opid_ = v; durable_next_save_ = true; }
|
||||
// Set TRUE synchronously when a sweep is broadcast; survives the stale-opid resume fallback so the
|
||||
// Sweep step knows a ~0 legacy balance may be an UNCONFIRMED in-flight sweep (funds not yet safe)
|
||||
// and must NOT short-circuit to adopt/swap wallet.dat. Cleared on confirmed-adopt / reset / a
|
||||
// failed submit. See [[decideSeedMigrationResume]] and App::renderSeedMigrationDialog (fund-safety).
|
||||
bool getSeedMigrationSweepSubmitted() const { return seed_migration_sweep_submitted_; }
|
||||
void setSeedMigrationSweepSubmitted(bool v) { seed_migration_sweep_submitted_ = v; durable_next_save_ = true; }
|
||||
void setWizardCompleted(bool v) { wizard_completed_ = v; }
|
||||
|
||||
// Security — auto-lock timeout (seconds; 0 = disabled)
|
||||
int getAutoLockTimeout() const { return auto_lock_timeout_; }
|
||||
@@ -493,7 +216,7 @@ public:
|
||||
|
||||
// Security — PIN unlock enabled
|
||||
bool getPinEnabled() const { return pin_enabled_; }
|
||||
void setPinEnabled(bool v) { pin_enabled_ = v; durable_next_save_ = true; }
|
||||
void setPinEnabled(bool v) { pin_enabled_ = v; }
|
||||
|
||||
// Daemon — keep running in background when closing the app
|
||||
bool getKeepDaemonRunning() const { return keep_daemon_running_; }
|
||||
@@ -506,11 +229,6 @@ public:
|
||||
// Daemon — maximum peer connections (0 = daemon default)
|
||||
int getMaxConnections() const { return max_connections_; }
|
||||
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
|
||||
// Host a RandomX stratum pool from the node (v1.3.0+ daemons). Empty allow-IP = loopback only (safe).
|
||||
bool getStratumHost() const { return stratum_host_enabled_; }
|
||||
void setStratumHost(bool v) { stratum_host_enabled_ = v; }
|
||||
const std::string& getStratumAllowIp() const { return stratum_allowip_; }
|
||||
void setStratumAllowIp(const std::string& v) { stratum_allowip_ = v; }
|
||||
|
||||
// Lite wallet server selection
|
||||
LiteServerSelectionPreferenceMode getLiteServerSelectionMode() const { return lite_server_selection_mode_; }
|
||||
@@ -525,21 +243,6 @@ public:
|
||||
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; }
|
||||
// Servers shipped as defaults. Also merged into an existing install's saved list on load
|
||||
// (see Settings::load), so upgrades surface newly-added servers (e.g. lite6/lite7).
|
||||
static std::vector<LiteServerPreference> defaultLiteServers();
|
||||
|
||||
// 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; }
|
||||
// Re-attach groups stuck on a legacy (address-set-hash) scope to the stable wallet id `scopeId`.
|
||||
// A legacy group is claimed when ANY of: its scope == identityHash (the current address-set hash, exact
|
||||
// match); at least one of its own addresses is in `walletAddresses` (groups carry their addresses, so
|
||||
// this attributes correctly in multi-wallet installs); or `singleWallet` (only one known wallet, so
|
||||
// attribution is unambiguous - same rule contacts use). Anything else stays legacy (fail-open visible).
|
||||
// Idempotent. Does NOT save: the caller saves once if the return value is > 0. Returns #entries moved.
|
||||
int reattachLegacyPortfolioScopes(const std::string& scopeId, const std::string& identityHash,
|
||||
const std::set<std::string>& walletAddresses, bool singleWallet);
|
||||
|
||||
// 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_; }
|
||||
@@ -586,10 +289,6 @@ public:
|
||||
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
|
||||
std::string getSelectedPair() const { return selected_pair_; }
|
||||
void setSelectedPair(const std::string& v) { selected_pair_ = v; }
|
||||
int getChartInterval() const { return chart_interval_; } // Market chart range 0=Live..4=1M
|
||||
void setChartInterval(int v) { chart_interval_ = v; }
|
||||
int getChartStyle() const { return chart_style_; } // 0 = line, 1 = candlestick
|
||||
void setChartStyle(int v) { chart_style_ = v; }
|
||||
|
||||
// Pool mining
|
||||
std::string getPoolUrl() const { return pool_url_; }
|
||||
@@ -606,8 +305,6 @@ public:
|
||||
void setPoolHugepages(bool v) { pool_hugepages_ = v; }
|
||||
bool getPoolMode() const { return pool_mode_; }
|
||||
void setPoolMode(bool v) { pool_mode_ = v; }
|
||||
PoolSelectMode getPoolSelectMode() const { return pool_select_mode_; }
|
||||
void setPoolSelectMode(PoolSelectMode v) { pool_select_mode_ = v; }
|
||||
|
||||
// Installed DRG-XMRig release tag (for in-app miner update detection); empty if unknown/bundled.
|
||||
std::string getXmrigVersion() const { return xmrig_version_; }
|
||||
@@ -667,73 +364,14 @@ public:
|
||||
bool needsUpgradeSave() const { return needs_upgrade_save_; }
|
||||
void clearUpgradeSave() { needs_upgrade_save_ = false; }
|
||||
|
||||
// UI-sweep ONLY. sweepBegin() snapshots the values the full sweep's demo surfaces change: the active
|
||||
// wallet file, the contacts view mode, and the pool URL/worker (the Mining tab defaults an empty
|
||||
// worker to the first DEMO z-address). Until sweepEnd(), save() returns true without writing, so
|
||||
// neither those nor the demo portfolio can reach disk, even through the shutdown save if the app
|
||||
// quits mid-sweep. sweepEnd() restores the snapshot and, only if a save was suppressed, saves once
|
||||
// so a real change made meanwhile still lands. The caller restores everything else it changed
|
||||
// (the portfolio) BEFORE sweepEnd(). Both are idempotent.
|
||||
void sweepBegin() {
|
||||
if (sweep_active_) return;
|
||||
sweep_active_wallet_file_ = active_wallet_file_;
|
||||
sweep_contacts_view_mode_ = contacts_view_mode_;
|
||||
sweep_pool_url_ = pool_url_;
|
||||
sweep_pool_worker_ = pool_worker_;
|
||||
sweep_suppressed_save_ = false;
|
||||
sweep_active_ = true;
|
||||
}
|
||||
void sweepEnd() {
|
||||
if (!sweep_active_) return;
|
||||
sweep_active_ = false;
|
||||
active_wallet_file_ = sweep_active_wallet_file_;
|
||||
contacts_view_mode_ = sweep_contacts_view_mode_;
|
||||
pool_url_ = sweep_pool_url_;
|
||||
pool_worker_ = sweep_pool_worker_;
|
||||
if (sweep_suppressed_save_) {
|
||||
sweep_suppressed_save_ = false;
|
||||
save();
|
||||
}
|
||||
}
|
||||
bool sweepActive() const { return sweep_active_; }
|
||||
|
||||
private:
|
||||
// UI-sweep latch (see sweepBegin()).
|
||||
bool sweep_active_ = false;
|
||||
bool sweep_suppressed_save_ = false;
|
||||
std::string sweep_active_wallet_file_;
|
||||
int sweep_contacts_view_mode_ = 0;
|
||||
std::string sweep_pool_url_, sweep_pool_worker_;
|
||||
|
||||
std::string settings_path_;
|
||||
util::CoalescingWriter* writer_ = nullptr; // null -> synchronous saves (tests, tools, early startup)
|
||||
// Armed by the fund/security-critical setters (seed-migration stage, encryption_pending, pin_enabled,
|
||||
// active_wallet_file, wizard_completed): the next save() waits until the file is on disk before it
|
||||
// returns, so e.g. the sweep-submitted marker is durable before the sweep is broadcast. Cleared by a
|
||||
// successful save. A NEW setter for crash-critical state must arm it too.
|
||||
bool durable_next_save_ = false;
|
||||
|
||||
// Settings values
|
||||
std::string theme_ = "dragonx";
|
||||
std::string skin_id_ = "dragonx";
|
||||
std::string chat_reply_zaddr_;
|
||||
std::vector<std::string> muted_chat_cids_; // muted chat conversations by cid (Q10)
|
||||
std::vector<std::string> hidden_chat_cids_; // hidden chat conversations by cid
|
||||
std::vector<BlockedChatConv> blocked_chat_convs_; // blocked chat conversations (cid + last-known name)
|
||||
std::vector<SensitiveChatConv> sensitive_chat_convs_; // sensitive chat conversations (cid + last-known name)
|
||||
// Chat-tab customization (chat settings modal + Settings → Chat & Contacts).
|
||||
bool chat_emoji_color_ = true; // true = color (needs FreeType; falls back to mono if absent), false = monochrome
|
||||
float chat_poll_rate_sec_ = 2.5f; // 0-conf chat fast-scan cadence (full node)
|
||||
int chat_bubble_style_ = 0; // 0 = rounded, 1 = square, 2 = minimal
|
||||
int chat_bubble_accent_ = 0; // outgoing-bubble accent preset (0 = theme primary)
|
||||
int chat_density_ = 0; // 0 = comfortable, 1 = compact
|
||||
float chat_font_scale_ = 1.0f; // message text scale
|
||||
int chat_time_format_ = 0; // 0 = follow global, 1 = 24h, 2 = 12h (Chat tab only)
|
||||
bool chat_enter_sends_ = true; // Enter sends (vs. inserts newline; Ctrl+Enter sends)
|
||||
int time_format_ = 0; // global clock: 0 = 24h, 1 = 12h
|
||||
bool save_ztxs_ = true;
|
||||
bool auto_shield_ = true;
|
||||
bool auto_consolidate_ = true;
|
||||
bool use_tor_ = false;
|
||||
bool allow_custom_fees_ = false;
|
||||
double default_fee_ = 0.0001;
|
||||
@@ -745,59 +383,26 @@ private:
|
||||
int acrylic_quality_ = 2;
|
||||
float blur_multiplier_ = 0.10f;
|
||||
float noise_opacity_ = 0.5f;
|
||||
float favorite_opacity_ = 1.0f; // Favorite gold outline/star opacity multiplier (0.15–1.0, 1.0 = theme default)
|
||||
bool gradient_background_ = false;
|
||||
#ifdef _WIN32
|
||||
float ui_opacity_ = 0.50f; // Card/sidebar opacity (0.3–1.0, 1.0 = opaque)
|
||||
float window_opacity_ = 0.90f; // Background alpha (0.3–1.0, <1 = desktop visible)
|
||||
float window_opacity_ = 0.75f; // Background alpha (0.3–1.0, <1 = desktop visible)
|
||||
#else
|
||||
float ui_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
||||
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
|
||||
#endif
|
||||
std::string balance_layout_ = "classic";
|
||||
int portfolio_style_ = 0; // Market portfolio row style (0 Table / 1 Cards / 2 Spotlight)
|
||||
int contacts_view_mode_ = 0; // Contacts address-list view (0 cards / 1 list / 2 table)
|
||||
int contacts_avatar_shape_ = 0; // 0 = circle, 1 = rounded square, 2 = full-height left tab
|
||||
float contacts_list_scale_ = 1.0f; // card/list row scale (does not affect the table view)
|
||||
bool animate_avatars_ = true; // play animated (GIF/WebP) contact avatars
|
||||
bool scanline_enabled_ = true;
|
||||
bool console_line_accents_ = true; // left color accent bars in console output
|
||||
bool console_text_color_ = true; // per-channel text coloring in console output
|
||||
float console_zoom_ = 1.0f; // console output font zoom factor
|
||||
bool console_auto_focus_ = false; // focus the command input when the Console tab is opened (opt-in)
|
||||
std::set<std::string> hidden_addresses_;
|
||||
std::set<std::string> favorite_addresses_;
|
||||
std::set<std::string> earnings_addresses_; // per-address earnings column opt-in (when show-all off)
|
||||
int earnings_interval_ = 1; // 0=Hour 1=Day 2=Month 3=Year
|
||||
int earnings_mode_ = 0; // 0=Inflows 1=Net
|
||||
int earnings_format_ = 1; // 0=DRGX 1=DRGX+fiat 2=Fiat
|
||||
bool earnings_show_all_ = false;
|
||||
std::map<std::string, AddressMeta> address_meta_;
|
||||
bool wizard_completed_ = false;
|
||||
bool seed_backup_reminded_ = false;
|
||||
bool chat_intro_dismissed_ = false; // one-time chat "how it works" intro card
|
||||
int lock_failed_attempts_ = 0; // persisted unlock-failure count (lockout curve)
|
||||
long long lockout_until_epoch_ = 0; // unix time the current lockout expires (0 = none)
|
||||
std::string lock_state_wallet_; // wallet file the above two apply to
|
||||
bool large_wallet_warned_ = false;
|
||||
std::set<std::string> empty_wallet_warning_acked_; // wallet files whose empty-wallet warning was dismissed
|
||||
bool encryption_pending_ = 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_;
|
||||
std::string seed_migration_sweep_opid_;
|
||||
bool seed_migration_sweep_submitted_ = false;
|
||||
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
|
||||
bool stratum_host_enabled_ = false; // host a RandomX stratum pool from the node (v1.3.0+ daemons)
|
||||
std::string stratum_allowip_; // -stratumallowip filter (empty = daemon default: loopback only)
|
||||
|
||||
// Lite wallet server preferences. These are user/server settings only;
|
||||
// wallet secrets, wallet files, and lifecycle state are never stored here.
|
||||
@@ -808,20 +413,23 @@ private:
|
||||
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_ = defaultLiteServers();
|
||||
std::vector<LiteServerPreference> lite_servers_ = {
|
||||
{"https://lite.dragonx.is", "DragonX Lite", true},
|
||||
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
|
||||
{"https://lite2.dragonx.is", "DragonX Lite 2", true},
|
||||
{"https://lite3.dragonx.is", "DragonX Lite 3", true},
|
||||
{"https://lite4.dragonx.is", "DragonX Lite 4", true},
|
||||
{"https://lite5.dragonx.is", "DragonX Lite 5", true}
|
||||
};
|
||||
std::set<std::string> lite_hidden_servers_; // server URLs hidden from the Network tab
|
||||
|
||||
std::vector<PortfolioEntry> portfolio_entries_; // Market tab custom portfolio groups
|
||||
|
||||
bool verbose_logging_ = false;
|
||||
std::set<std::string> debug_categories_;
|
||||
bool theme_effects_enabled_ = true;
|
||||
bool low_spec_mode_ = false;
|
||||
bool reduce_motion_ = false;
|
||||
std::string selected_exchange_ = "Nonkyc.io";
|
||||
std::string selected_pair_ = "DRGX/USDT";
|
||||
int chart_interval_ = 4; // Market chart range (0=Live 1=1H 2=1D 3=1W 4=1M)
|
||||
int chart_style_ = 1; // Market chart style (0=line, 1=candlestick)
|
||||
std::string selected_exchange_ = "TradeOgre";
|
||||
std::string selected_pair_ = "DRGX/BTC";
|
||||
|
||||
// Pool mining
|
||||
std::string pool_url_ = "pool.dragonx.is:3433";
|
||||
@@ -831,7 +439,6 @@ private:
|
||||
bool pool_tls_ = false;
|
||||
bool pool_hugepages_ = true;
|
||||
bool pool_mode_ = false; // false=solo, true=pool
|
||||
PoolSelectMode pool_select_mode_ = PoolSelectMode::Manual; // manual vs auto-balance pool choice
|
||||
std::string xmrig_version_; // installed DRG-XMRig release tag (update detection)
|
||||
bool mine_when_idle_ = false; // auto-start mining when system idle
|
||||
int mine_idle_delay_= 120; // seconds of idle before mining starts
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// Is the node busy with something that stopping it would throw away? Tracks dragonxd's own log markers,
|
||||
// in order, for two independent states:
|
||||
// - rebuilding: a witness-cache rebuild (BuildWitnessCache). It holds cs_main for up to an hour on a
|
||||
// big wallet, its second phase logs nothing and ignores shutdown, and a node killed mid-rebuild redoes
|
||||
// it (after replaying everything since its last flush) on the next start;
|
||||
// - replaying: the startup replay ("Activating best chain..."). Stopping there is safe — the daemon
|
||||
// checks for shutdown between steps and flushes — it just takes a moment.
|
||||
// A rebuild can start in the middle of a replay, so its end must not clear `replaying`, and the state is
|
||||
// order-based: it never compares timestamps (POSIX nodes log to stdout without them). Pure and
|
||||
// header-only, so the test target can feed it lines.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
|
||||
/**
|
||||
* "YYYY-MM-DD HH:MM:SS ..." (dragonxd's debug.log prefix, which is UTC) -> seconds since the epoch, or 0
|
||||
* when the line has no such prefix. Days-from-civil arithmetic, not mktime(): mktime reads local time.
|
||||
*/
|
||||
inline std::int64_t parseDaemonLogUtc(const std::string& line)
|
||||
{
|
||||
int y = 0, mo = 0, d = 0, h = 0, mi = 0, s = 0;
|
||||
if (line.size() < 19 || std::sscanf(line.c_str(), "%4d-%2d-%2d %2d:%2d:%2d", &y, &mo, &d, &h, &mi, &s) != 6)
|
||||
return 0;
|
||||
if (mo < 1 || mo > 12 || d < 1 || d > 31 || h > 23 || mi > 59 || s > 60) return 0;
|
||||
// Howard Hinnant's days_from_civil.
|
||||
const int yy = y - (mo <= 2 ? 1 : 0);
|
||||
const int era = (yy >= 0 ? yy : yy - 399) / 400;
|
||||
const unsigned yoe = static_cast<unsigned>(yy - era * 400);
|
||||
const unsigned doy = (153u * static_cast<unsigned>(mo + (mo > 2 ? -3 : 9)) + 2u) / 5u + static_cast<unsigned>(d) - 1u;
|
||||
const unsigned doe = yoe * 365u + yoe / 4u - yoe / 100u + doy;
|
||||
const std::int64_t days = static_cast<std::int64_t>(era) * 146097 + static_cast<std::int64_t>(doe) - 719468;
|
||||
return days * 86400 + h * 3600 + mi * 60 + s;
|
||||
}
|
||||
|
||||
class DaemonBusyTracker {
|
||||
public:
|
||||
// Feed raw output: any chunking, a line split across two chunks is reassembled.
|
||||
void feed(const std::string& chunk)
|
||||
{
|
||||
for (char c : chunk) {
|
||||
if (c == '\n') {
|
||||
feedLine(carry_);
|
||||
carry_.clear();
|
||||
} else if (c != '\r') {
|
||||
carry_.push_back(c);
|
||||
if (carry_.size() > 64 * 1024) carry_.clear(); // not a log line; don't grow without bound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void feedLine(const std::string& line)
|
||||
{
|
||||
// A new daemon process, or the end of the last one: forget everything.
|
||||
if (contains(line, "DragonX version ") || contains(line, "Hush version ") || contains(line, "Shutdown: done")) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
if (contains(line, "init message: Activating best chain")) {
|
||||
replaying_ = true;
|
||||
replayStartUtc_ = parseDaemonLogUtc(line);
|
||||
lastMarker_ = line;
|
||||
return;
|
||||
}
|
||||
if (contains(line, "init message: Full Node Done Loading") || contains(line, "init message: Done loading")) {
|
||||
replaying_ = false;
|
||||
lastMarker_ = line;
|
||||
return;
|
||||
}
|
||||
const size_t read = line.find("Reading blocks for witness rebuild: ");
|
||||
if (read != std::string::npos || contains(line, "Building Witnesses for block ")) {
|
||||
if (!rebuilding_) rebuildStartUtc_ = parseDaemonLogUtc(line);
|
||||
rebuilding_ = true;
|
||||
if (read != std::string::npos) {
|
||||
long done = 0, total = 0;
|
||||
if (std::sscanf(line.c_str() + read, "Reading blocks for witness rebuild: %ld / %ld", &done, &total) == 2) {
|
||||
readDone_ = done;
|
||||
readTotal_ = total;
|
||||
}
|
||||
}
|
||||
lastMarker_ = line;
|
||||
return;
|
||||
}
|
||||
// The rebuild's end. UpdateTip is logged under cs_main, so one after a rebuild marker means the
|
||||
// rebuild has released it (exceptions and non-bulk paths print no end line).
|
||||
if (rebuilding_ && (contains(line, "note witness cache(s) to height") ||
|
||||
contains(line, "aborting witness rebuild") ||
|
||||
contains(line, "aborted during witness rebuild") ||
|
||||
contains(line, "aborting building witnesses") ||
|
||||
contains(line, "UpdateTip: "))) {
|
||||
rebuilding_ = false;
|
||||
readDone_ = readTotal_ = 0;
|
||||
rebuildStartUtc_ = 0;
|
||||
lastMarker_ = line;
|
||||
}
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
rebuilding_ = replaying_ = false;
|
||||
readDone_ = readTotal_ = 0;
|
||||
rebuildStartUtc_ = replayStartUtc_ = 0;
|
||||
lastMarker_.clear();
|
||||
}
|
||||
|
||||
bool rebuilding() const { return rebuilding_; }
|
||||
bool replaying() const { return replaying_; }
|
||||
bool busy() const { return rebuilding_ || replaying_; }
|
||||
long readDone() const { return readDone_; } // the last Phase-1 "reading blocks" progress, 0 if none
|
||||
long readTotal() const { return readTotal_; }
|
||||
std::int64_t rebuildStartUtc() const { return rebuildStartUtc_; } // 0: unknown (no timestamps)
|
||||
std::int64_t replayStartUtc() const { return replayStartUtc_; }
|
||||
const std::string& lastMarker() const { return lastMarker_; }
|
||||
|
||||
private:
|
||||
static bool contains(const std::string& s, const char* needle) { return s.find(needle) != std::string::npos; }
|
||||
|
||||
std::string carry_;
|
||||
bool rebuilding_ = false;
|
||||
bool replaying_ = false;
|
||||
long readDone_ = 0;
|
||||
long readTotal_ = 0;
|
||||
std::int64_t rebuildStartUtc_ = 0;
|
||||
std::int64_t replayStartUtc_ = 0;
|
||||
std::string lastMarker_;
|
||||
};
|
||||
|
||||
/**
|
||||
* The "keep the node running?" guard on quit. Prompt only when this quit would stop the node and it is
|
||||
* rebuilding witnesses, or has been alive but too busy to answer RPC for a while (a rebuild that logged
|
||||
* no marker). A replay alone never prompts: stopping it is safe. No daemon version suppresses the prompt
|
||||
* (v1.3.0 does not checkpoint a witness rebuild; nothing survives a kill).
|
||||
*/
|
||||
inline bool shouldConfirmStop(bool stopsDaemon, const DaemonBusyTracker& tracker, bool rpcBusyWhileAlive)
|
||||
{
|
||||
return stopsDaemon && (tracker.rebuilding() || rpcBusyWhileAlive);
|
||||
}
|
||||
|
||||
// Retry schedule after failing to find a pid's banner in debug.log: re-scan fast during the fresh-node grace
|
||||
// window, then back off 4 s doubling to a 60 s cap. `backoff` is the caller's running state (0 until grace ends).
|
||||
inline double nextExternalLogRetryDelay(double ageSinceMiss, double& backoff,
|
||||
double graceWindow = 15.0, double fast = 2.0,
|
||||
double base = 4.0, double cap = 60.0)
|
||||
{
|
||||
if (ageSinceMiss < graceWindow) return fast;
|
||||
backoff = (backoff < base) ? base : (backoff * 2.0 < cap ? backoff * 2.0 : cap);
|
||||
return backoff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where to start feeding a DaemonBusyTracker from the last `window` of the debug.log of a node someone else
|
||||
* started at `processStartUtc` (UTC seconds). Only lines that process wrote count: a node started with
|
||||
* -printtoconsole leaves an old debug.log behind, whose last run may have died mid-rebuild. Returns the
|
||||
* offset of that process's startup banner in the window; 0 when its banner is older than the window
|
||||
* (`windowStartsMidFile`) and the window's first stamped line postdates the process start; npos when the
|
||||
* log isn't that process's.
|
||||
*/
|
||||
inline size_t externalLogFeedOffset(const std::string& window, bool windowStartsMidFile, std::int64_t processStartUtc)
|
||||
{
|
||||
size_t lastBanner = std::string::npos;
|
||||
std::int64_t lastBannerAt = 0, firstStamp = 0;
|
||||
size_t pos = 0;
|
||||
while (pos < window.size()) {
|
||||
size_t end = window.find('\n', pos);
|
||||
if (end == std::string::npos) end = window.size();
|
||||
const std::string line = window.substr(pos, end - pos);
|
||||
const std::int64_t at = parseDaemonLogUtc(line);
|
||||
if (at > 0 && firstStamp == 0) firstStamp = at;
|
||||
if (line.find("DragonX version ") != std::string::npos || line.find("Hush version ") != std::string::npos) {
|
||||
lastBanner = pos;
|
||||
lastBannerAt = at;
|
||||
}
|
||||
pos = end + 1;
|
||||
}
|
||||
if (processStartUtc <= 0) return std::string::npos;
|
||||
const std::int64_t since = processStartUtc - 5; // the log's whole seconds vs the process's start time
|
||||
if (lastBanner != std::string::npos) return (lastBannerAt > 0 && lastBannerAt >= since) ? lastBanner : std::string::npos;
|
||||
if (windowStartsMidFile && firstStamp > 0 && firstStamp >= since) return 0;
|
||||
return std::string::npos;
|
||||
}
|
||||
|
||||
// What the "node is starting / not answering" overlay should say, most specific first.
|
||||
enum class DaemonStatusKind {
|
||||
Rebuilding, // its own log says it's rebuilding note witnesses
|
||||
Replaying, // its own log says it's catching up on blocks it saved before (startup replay)
|
||||
InitPhase, // a recognisable init phase line (loading the block index, verifying, ...)
|
||||
Busy, // it answers the connection but not the request (reply timeout / queue full)
|
||||
Launching, // nothing bound to the RPC port yet
|
||||
};
|
||||
|
||||
inline DaemonStatusKind chooseDaemonStatus(bool hasInitPhaseLine, bool reachableButBusy,
|
||||
const DaemonBusyTracker& tracker)
|
||||
{
|
||||
if (tracker.rebuilding()) return DaemonStatusKind::Rebuilding;
|
||||
if (tracker.replaying()) return DaemonStatusKind::Replaying;
|
||||
if (hasInitPhaseLine) return DaemonStatusKind::InitPhase;
|
||||
if (reachableButBusy) return DaemonStatusKind::Busy;
|
||||
return DaemonStatusKind::Launching;
|
||||
}
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
@@ -1,10 +1,7 @@
|
||||
#include "daemon_controller.h"
|
||||
#include "../config/settings.h"
|
||||
#include "../util/platform.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
@@ -26,25 +23,12 @@ void DaemonController::syncSettings(const config::Settings* settings)
|
||||
if (!settings) return;
|
||||
daemon_->setDebugCategories(settings->getDebugCategories());
|
||||
daemon_->setMaxConnections(settings->getMaxConnections());
|
||||
daemon_->setStratumHosting(settings->getStratumHost(), settings->getStratumAllowIp());
|
||||
|
||||
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, int lockWaitPolls)
|
||||
bool DaemonController::start(const config::Settings* settings)
|
||||
{
|
||||
syncSettings(settings);
|
||||
return daemon_->start("", lockWaitPolls);
|
||||
return daemon_->start();
|
||||
}
|
||||
|
||||
void DaemonController::stop(int waitMs)
|
||||
@@ -52,11 +36,6 @@ void DaemonController::stop(int waitMs)
|
||||
daemon_->stop(waitMs);
|
||||
}
|
||||
|
||||
void DaemonController::markStopRequested()
|
||||
{
|
||||
daemon_->markStopRequested();
|
||||
}
|
||||
|
||||
bool DaemonController::isRunning() const
|
||||
{
|
||||
return daemon_->isRunning();
|
||||
@@ -67,21 +46,14 @@ bool DaemonController::externalDaemonDetected() const
|
||||
return daemon_->externalDaemonDetected();
|
||||
}
|
||||
|
||||
void DaemonController::clearExternalDaemonDetected()
|
||||
{
|
||||
daemon_->clearExternalDaemonDetected();
|
||||
}
|
||||
|
||||
DaemonController::State DaemonController::state() const
|
||||
{
|
||||
return daemon_->getState();
|
||||
}
|
||||
|
||||
std::string DaemonController::lastError() const
|
||||
const std::string& DaemonController::lastError() const
|
||||
{
|
||||
// By value — getLastError() now returns a mutex-locked COPY, so forwarding it by reference would
|
||||
// dangle (bind a reference to that temporary). (M-04 follow-through)
|
||||
return daemon_ ? daemon_->getLastError() : std::string();
|
||||
return daemon_->getLastError();
|
||||
}
|
||||
|
||||
int DaemonController::crashCount() const
|
||||
@@ -99,18 +71,11 @@ double DaemonController::memoryUsageMB() const
|
||||
return daemon_ ? daemon_->getMemoryUsageMB() : 0.0;
|
||||
}
|
||||
|
||||
std::function<double()> DaemonController::memoryProbe() const
|
||||
{
|
||||
return daemon_ ? daemon_->memoryProbe() : std::function<double()>{};
|
||||
}
|
||||
|
||||
std::vector<std::string> DaemonController::recentLines(std::size_t count) const
|
||||
{
|
||||
return daemon_ ? daemon_->getRecentLines(count) : std::vector<std::string>{};
|
||||
}
|
||||
|
||||
std::size_t DaemonController::outputEnd() const { return daemon_ ? daemon_->getOutputEnd() : 0; }
|
||||
|
||||
std::string DaemonController::outputSince(std::size_t& offset) const
|
||||
{
|
||||
return daemon_ ? daemon_->getOutputSince(offset) : std::string{};
|
||||
@@ -131,33 +96,12 @@ bool DaemonController::rescanOnNextStart() const
|
||||
return daemon_->rescanOnNextStart();
|
||||
}
|
||||
|
||||
void DaemonController::setZapOnNextStart(bool enabled)
|
||||
{
|
||||
daemon_->setZapOnNextStart(enabled);
|
||||
}
|
||||
|
||||
void DaemonController::setSalvageOnNextStart(bool enabled)
|
||||
{
|
||||
daemon_->setSalvageOnNextStart(enabled);
|
||||
}
|
||||
|
||||
void DaemonController::setReindexOnNextStart(bool enabled)
|
||||
{
|
||||
daemon_->setReindexOnNextStart(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(
|
||||
|
||||
+12
-257
@@ -1,12 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "embedded_daemon.h"
|
||||
#include "stop_delivery.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -34,7 +31,6 @@ public:
|
||||
enum class LifecycleOperation {
|
||||
ManualRestart,
|
||||
Rescan,
|
||||
RepairWallet, // restart with -zapwallettxes=2 (wipe & rebuild wallet tx records)
|
||||
DeleteBlockchainData,
|
||||
BootstrapStop
|
||||
};
|
||||
@@ -50,7 +46,6 @@ public:
|
||||
bool setRescanOnNextStart = false;
|
||||
bool disconnectRpc = false;
|
||||
int restartDelayMs = 0;
|
||||
bool setZapOnNextStart = false;
|
||||
};
|
||||
|
||||
class LifecycleTaskContext {
|
||||
@@ -64,21 +59,17 @@ public:
|
||||
class LifecycleRuntime {
|
||||
public:
|
||||
virtual ~LifecycleRuntime() = default;
|
||||
// Stop the node and wait (bounded, never killing it) for it to exit. False: it is still running
|
||||
// (or we stopped waiting), and nothing that needs it gone may run.
|
||||
virtual bool stopDaemonWithPolicy() = 0;
|
||||
virtual void stopDaemonWithPolicy() = 0;
|
||||
virtual bool startDaemon() = 0;
|
||||
virtual int deleteBlockchainData() = 0;
|
||||
virtual void resetOutputOffset() = 0;
|
||||
// Disconnect and stop the node off this thread; whoever needs it gone waits for the process to exit.
|
||||
virtual void beginBackgroundStop(const char* reason) = 0;
|
||||
virtual void requestRpcStopAndDisconnect(const char* context, const char* reason) = 0;
|
||||
};
|
||||
|
||||
struct LifecycleExecutionResult {
|
||||
bool completed = false;
|
||||
bool cancelled = false;
|
||||
bool stopped = false;
|
||||
bool stopFailed = false; // the node didn't exit: the operation was cancelled before changing anything
|
||||
bool started = false;
|
||||
int deletedItems = 0;
|
||||
};
|
||||
@@ -95,30 +86,22 @@ public:
|
||||
void setStateCallback(StateCallback callback);
|
||||
void syncSettings(const config::Settings* settings);
|
||||
|
||||
bool start(const config::Settings* settings, int lockWaitPolls = 3);
|
||||
bool start(const config::Settings* settings);
|
||||
void stop(int waitMs);
|
||||
void markStopRequested();
|
||||
|
||||
bool isRunning() const;
|
||||
bool externalDaemonDetected() const;
|
||||
void clearExternalDaemonDetected();
|
||||
State state() const;
|
||||
std::string lastError() const; // by value: EmbeddedDaemon::getLastError() returns a locked copy (M-04)
|
||||
const std::string& lastError() const;
|
||||
int crashCount() const;
|
||||
int lastBlockHeight() const;
|
||||
double memoryUsageMB() const;
|
||||
std::function<double()> memoryProbe() const; // EmbeddedDaemon::memoryProbe(); empty without a daemon
|
||||
std::vector<std::string> recentLines(std::size_t count) const;
|
||||
std::string outputSince(std::size_t& offset) const;
|
||||
std::size_t outputEnd() const;
|
||||
|
||||
void resetCrashCount();
|
||||
void setRescanOnNextStart(bool enabled);
|
||||
bool rescanOnNextStart() const;
|
||||
void setZapOnNextStart(bool enabled);
|
||||
bool zapOnNextStart() const;
|
||||
void setSalvageOnNextStart(bool enabled);
|
||||
void setReindexOnNextStart(bool enabled); // -reindex: rebuild the block DB from raw blocks on next start
|
||||
|
||||
static ShutdownDecision evaluateShutdownPolicy(bool hasDaemon,
|
||||
bool externalDaemonDetected,
|
||||
@@ -158,13 +141,6 @@ public:
|
||||
}
|
||||
return {operation, true, daemonRunning, "rescan-blockchain", "Starting rescan...", "",
|
||||
false, true, false, 3000};
|
||||
case LifecycleOperation::RepairWallet:
|
||||
if (!usingEmbeddedDaemon || !hasDaemon) {
|
||||
return {operation, false, daemonRunning, "", "",
|
||||
"Wallet repair requires embedded daemon. Restart your daemon with -zapwallettxes=2 manually."};
|
||||
}
|
||||
return {operation, true, daemonRunning, "repair-wallet", "Repairing wallet...", "",
|
||||
false, false, false, 3000, true};
|
||||
case LifecycleOperation::DeleteBlockchainData:
|
||||
if (!usingEmbeddedDaemon || !hasDaemon) {
|
||||
return {operation, false, daemonRunning, "", "",
|
||||
@@ -201,24 +177,21 @@ public:
|
||||
|
||||
switch (decision.operation) {
|
||||
case LifecycleOperation::BootstrapStop:
|
||||
// The download takes minutes; the bootstrap worker waits for the process to exit before it
|
||||
// touches chain data.
|
||||
if (decision.wasRunning) {
|
||||
runtime.beginBackgroundStop("Bootstrap");
|
||||
runtime.requestRpcStopAndDisconnect("Bootstrap daemon stop", "Bootstrap");
|
||||
result.stopped = true;
|
||||
}
|
||||
result.completed = true;
|
||||
return result;
|
||||
case LifecycleOperation::ManualRestart:
|
||||
case LifecycleOperation::Rescan:
|
||||
case LifecycleOperation::RepairWallet:
|
||||
case LifecycleOperation::DeleteBlockchainData:
|
||||
if (decision.operation == LifecycleOperation::ManualRestart && !decision.wasRunning) break;
|
||||
// Never start a second node, or delete chain data, under one that hasn't exited.
|
||||
if (!runtime.stopDaemonWithPolicy()) {
|
||||
result.stopFailed = true;
|
||||
return result;
|
||||
if (decision.wasRunning) {
|
||||
runtime.stopDaemonWithPolicy();
|
||||
result.stopped = true;
|
||||
}
|
||||
break;
|
||||
case LifecycleOperation::Rescan:
|
||||
case LifecycleOperation::DeleteBlockchainData:
|
||||
runtime.stopDaemonWithPolicy();
|
||||
result.stopped = true;
|
||||
break;
|
||||
}
|
||||
@@ -233,7 +206,6 @@ public:
|
||||
}
|
||||
|
||||
if (decision.operation == LifecycleOperation::Rescan ||
|
||||
decision.operation == LifecycleOperation::RepairWallet ||
|
||||
decision.operation == LifecycleOperation::DeleteBlockchainData) {
|
||||
runtime.resetOutputOffset();
|
||||
}
|
||||
@@ -245,223 +217,6 @@ public:
|
||||
ShutdownDecision shutdownDecision(bool keepDaemonRunning,
|
||||
bool stopExternalDaemon) const;
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Stopping the node: deliver "stop" until it lands, then wait for the process to exit. Never kill it
|
||||
// on our own: a node that is busy (a witness rebuild can hold it for an hour) or flushing would lose
|
||||
// everything since its last save and replay it on the next start. Only an explicit user "Kill",
|
||||
// offered once the node has gone idle, ends it by force; "Leave running" lets the wallet close while
|
||||
// the node finishes on its own.
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
using StopUserChoice = daemon::StopUserChoice;
|
||||
using StopOutcome = daemon::StopOutcome;
|
||||
|
||||
struct StopPolicy {
|
||||
bool owned = true; // we launched it: watch the process, signal it, offer Kill
|
||||
bool gracefulSignal = false; // POSIX: a group SIGTERM is a stop channel (dragonxd treats it as stop)
|
||||
double maxWaitSec = 0.0; // stop waiting (never kill) after this long; 0 = no limit
|
||||
bool userChoices = false; // the shutdown screen offers "Leave running" / "Kill"
|
||||
bool abandonOnShutdown = false; // stop waiting once the app starts shutting down (background callers)
|
||||
double idleOfferSec = 600.0; // delivered + no CPU/I-O progress for this long: offer Kill
|
||||
};
|
||||
// A bounded stop that has no way to reach the node (a node we don't own refused our credentials, or our
|
||||
// own did with every source and there is no signal) gives up after this long rather than its full budget.
|
||||
static constexpr double kUndeliverableGraceSec = 5.0;
|
||||
|
||||
// What the stop loop reports for display.
|
||||
struct StopStatus {
|
||||
bool delivered = false; // stop accepted (or the node is shutting down, or was signalled)
|
||||
bool heard = false; // the node has the request (possibly still queued)
|
||||
bool signalled = false; // a graceful signal (POSIX SIGTERM) went out
|
||||
double elapsedSec = 0.0;
|
||||
double cpuPercent = -1.0; // < 0: unknown
|
||||
double idleSec = 0.0;
|
||||
bool killOffered = false;
|
||||
bool rebuildActive = false;
|
||||
StopSendResult lastSend = StopSendResult::Other;
|
||||
int attempts = 0;
|
||||
};
|
||||
|
||||
class StopRuntime {
|
||||
public:
|
||||
virtual ~StopRuntime() = default;
|
||||
virtual double nowSec() = 0;
|
||||
virtual bool processAlive() = 0;
|
||||
virtual StopSendResult trySendStop() = 0;
|
||||
virtual bool nextCredentials() = 0; // switch to the next stop credentials; false when none left
|
||||
virtual bool sendGracefulSignal() = 0; // POSIX group SIGTERM; false where there is none (Windows)
|
||||
virtual double cpuSeconds() = 0; // cumulative CPU of the node; < 0 if unknown
|
||||
virtual double ioBytes() = 0; // cumulative I/O bytes of the node; < 0 if unknown
|
||||
virtual bool rebuildActive() = 0; // a witness rebuild is running: never offer Kill
|
||||
virtual StopUserChoice userChoice() = 0;
|
||||
virtual void offerForceKill(bool offered) = 0;
|
||||
virtual void forceKill() = 0;
|
||||
virtual void setStatus(const StopStatus& status) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* "Making progress" = >= 0.05 cores of CPU or >= 1 MB of I/O over the last >= 60s. A flush that
|
||||
* fsyncs a big wallet is mostly I/O at low CPU; a rebuild is mostly CPU. Idle time counts from the
|
||||
* first sample. With no readings at all (unknown), it never reports idle, so Kill is never offered.
|
||||
*/
|
||||
class StopProgressTracker {
|
||||
public:
|
||||
static constexpr double kWindowSec = 60.0;
|
||||
static constexpr double kMinCores = 0.05;
|
||||
static constexpr double kMinIoBytes = 1024.0 * 1024.0;
|
||||
|
||||
void add(double t, double cpuSec, double ioBytes)
|
||||
{
|
||||
if (cpuSec >= 0 || ioBytes >= 0) measurable_ = true;
|
||||
if (samples_.empty()) lastProgressAt_ = t;
|
||||
samples_.push_back({t, cpuSec, ioBytes});
|
||||
// Keep the newest sample that is at least a full window old as the comparison base.
|
||||
while (samples_.size() > 2 && samples_[1].t <= t - kWindowSec) samples_.pop_front();
|
||||
const Sample& base = samples_.front();
|
||||
const double span = t - base.t;
|
||||
if (span >= kWindowSec) {
|
||||
const bool cpuMoved = cpuSec >= 0 && base.cpu >= 0 && (cpuSec - base.cpu) / span >= kMinCores;
|
||||
const bool ioMoved = ioBytes >= 0 && base.io >= 0 && (ioBytes - base.io) >= kMinIoBytes;
|
||||
if (cpuMoved || ioMoved) lastProgressAt_ = t;
|
||||
}
|
||||
}
|
||||
double idleSec(double t) const { return measurable_ ? std::max(0.0, t - lastProgressAt_) : 0.0; }
|
||||
// CPU over the most recent sample interval (100 = one core), for display; < 0 unknown.
|
||||
double cpuPercent() const
|
||||
{
|
||||
if (samples_.size() < 2) return -1.0;
|
||||
const Sample& a = samples_[samples_.size() - 2];
|
||||
const Sample& b = samples_.back();
|
||||
if (a.cpu < 0 || b.cpu < 0 || b.t <= a.t) return -1.0;
|
||||
return std::max(0.0, (b.cpu - a.cpu) / (b.t - a.t) * 100.0);
|
||||
}
|
||||
|
||||
private:
|
||||
struct Sample { double t, cpu, io; };
|
||||
std::deque<Sample> samples_;
|
||||
double lastProgressAt_ = 0.0;
|
||||
bool measurable_ = false;
|
||||
};
|
||||
|
||||
enum class StopWaitDecision { Done, Wait, GiveUp, Detach, OfferForceKill, ForceKill };
|
||||
|
||||
struct StopWaitSample {
|
||||
bool processAlive = true;
|
||||
bool delivered = false;
|
||||
double elapsedSec = 0.0;
|
||||
double idleSec = 0.0;
|
||||
bool rebuildActive = false;
|
||||
StopUserChoice choice = StopUserChoice::None;
|
||||
bool killOffered = false; // the Kill offer is on screen right now
|
||||
bool shuttingDown = false; // the app began shutting down / the task was cancelled
|
||||
bool undeliverable = false; // no stop delivered, none will be sent, and no signal to send
|
||||
};
|
||||
|
||||
static inline StopWaitDecision evaluateStopWait(const StopPolicy& p, const StopWaitSample& s)
|
||||
{
|
||||
if (!s.processAlive) return StopWaitDecision::Done;
|
||||
if (p.userChoices && s.choice == StopUserChoice::Detach) return StopWaitDecision::Detach;
|
||||
if (p.userChoices && p.owned && s.choice == StopUserChoice::Kill && s.killOffered)
|
||||
return StopWaitDecision::ForceKill;
|
||||
if (p.abandonOnShutdown && s.shuttingDown) return StopWaitDecision::GiveUp;
|
||||
if (p.maxWaitSec > 0 && s.elapsedSec >= p.maxWaitSec) return StopWaitDecision::GiveUp;
|
||||
if (p.maxWaitSec > 0 && s.undeliverable && s.elapsedSec >= kUndeliverableGraceSec)
|
||||
return StopWaitDecision::GiveUp;
|
||||
if (p.userChoices && p.owned && s.delivered && !s.rebuildActive && s.idleSec >= p.idleOfferSec)
|
||||
return StopWaitDecision::OfferForceKill;
|
||||
return StopWaitDecision::Wait;
|
||||
}
|
||||
|
||||
static constexpr int kStopTickMs = 250;
|
||||
|
||||
static inline StopOutcome executeDaemonStop(const StopPolicy& policy, StopRuntime& rt,
|
||||
LifecycleTaskContext& task)
|
||||
{
|
||||
if (!rt.processAlive()) return StopOutcome::Exited; // already gone: not a single RPC call
|
||||
|
||||
const double t0 = rt.nowSec();
|
||||
StopDeliveryPolicy delivery(policy.owned);
|
||||
StopProgressTracker progress;
|
||||
bool signalled = false;
|
||||
bool offered = false;
|
||||
auto withdrawOffer = [&] {
|
||||
if (offered) { offered = false; rt.offerForceKill(false); }
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
double now = rt.nowSec() - t0;
|
||||
if (delivery.shouldSend(now)) {
|
||||
const StopSendResult r = delivery.record(rt.trySendStop(), now);
|
||||
if (r == StopSendResult::Auth && policy.owned) {
|
||||
// Our own node refusing our credentials means they're stale (e.g. a regenerated
|
||||
// .cookie): try the next source; out of sources, the graceful signal below remains.
|
||||
if (rt.nextCredentials()) delivery.retryNow(rt.nowSec() - t0);
|
||||
else delivery.giveUp();
|
||||
}
|
||||
// POSIX: a group SIGTERM bypasses the RPC queue altogether (dragonxd's handler sets the same
|
||||
// flag as stop). Send it as soon as an attempt shows stop didn't get through.
|
||||
if (policy.gracefulSignal && !signalled && !delivery.delivered() &&
|
||||
r != StopSendResult::Queued && rt.sendGracefulSignal()) {
|
||||
signalled = true;
|
||||
delivery.markDelivered();
|
||||
}
|
||||
}
|
||||
if (!rt.processAlive()) {
|
||||
withdrawOffer();
|
||||
return StopOutcome::Exited;
|
||||
}
|
||||
|
||||
now = rt.nowSec() - t0;
|
||||
progress.add(now, rt.cpuSeconds(), rt.ioBytes());
|
||||
StopWaitSample s;
|
||||
s.delivered = delivery.delivered();
|
||||
s.elapsedSec = now;
|
||||
s.idleSec = progress.idleSec(now);
|
||||
s.rebuildActive = rt.rebuildActive();
|
||||
s.choice = rt.userChoice();
|
||||
s.killOffered = offered;
|
||||
s.shuttingDown = task.cancelled() || task.shuttingDown();
|
||||
s.undeliverable = delivery.finished() && !delivery.delivered() && !signalled &&
|
||||
!(policy.gracefulSignal && policy.owned);
|
||||
|
||||
switch (evaluateStopWait(policy, s)) {
|
||||
case StopWaitDecision::Done:
|
||||
withdrawOffer();
|
||||
return StopOutcome::Exited;
|
||||
case StopWaitDecision::Detach:
|
||||
withdrawOffer();
|
||||
return StopOutcome::Detached;
|
||||
case StopWaitDecision::ForceKill:
|
||||
withdrawOffer();
|
||||
rt.forceKill();
|
||||
return StopOutcome::Killed;
|
||||
case StopWaitDecision::GiveUp:
|
||||
withdrawOffer();
|
||||
return StopOutcome::StillRunning;
|
||||
case StopWaitDecision::OfferForceKill:
|
||||
if (!offered) { offered = true; rt.offerForceKill(true); }
|
||||
break;
|
||||
case StopWaitDecision::Wait:
|
||||
withdrawOffer(); // progress resumed (or a rebuild started): the node isn't stuck after all
|
||||
break;
|
||||
}
|
||||
|
||||
StopStatus st;
|
||||
st.delivered = delivery.delivered();
|
||||
st.heard = delivery.heard();
|
||||
st.signalled = signalled;
|
||||
st.elapsedSec = now;
|
||||
st.cpuPercent = progress.cpuPercent();
|
||||
st.idleSec = s.idleSec;
|
||||
st.killOffered = offered;
|
||||
st.rebuildActive = s.rebuildActive;
|
||||
st.lastSend = delivery.last();
|
||||
st.attempts = delivery.attempts();
|
||||
rt.setStatus(st);
|
||||
task.sleepForMs(kStopTickMs);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<EmbeddedDaemon> daemon_;
|
||||
};
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// daemon_startup_diagnosis.h — pure classifiers over a crashed daemon's captured console output,
|
||||
// so the app can offer a targeted one-click fix instead of a bare "daemon crashed" / silent no-funds.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
|
||||
// True when dragonxd aborted because its BLOCK DATABASE could not be loaded — either a
|
||||
// daemon-vs-chaindata serialization-format mismatch after a daemon update (the deterministic
|
||||
// "non-canonical optional discriminant" → "Error loading block database" → "Aborted block database
|
||||
// rebuild. Exiting." sequence) or a genuinely corrupt/incomplete block index. In BOTH cases the fix
|
||||
// is the same: `-reindex` rebuilds the index + chainstate from the intact raw blocks (blk*.dat).
|
||||
// This is what otherwise silently presents as a wallet with zero balance — the node never starts.
|
||||
inline bool blockDbOutputLooksBroken(const std::string& out)
|
||||
{
|
||||
return out.find("Error loading block database") != std::string::npos
|
||||
|| out.find("non-canonical optional discriminant") != std::string::npos
|
||||
|| out.find("Aborted block database rebuild") != std::string::npos
|
||||
|| out.find("LoadBlockIndex()") != std::string::npos; // "... : failed to read value"
|
||||
}
|
||||
|
||||
// True when dragonxd AUTO-RECOVERED the wallet on startup: on any BDB-verify failure it moves the
|
||||
// original wallet.dat to "wallet.{timestamp}.bak", salvages readable keys into a fresh wallet.dat, and
|
||||
// keeps running — no flag required (CWallet::Verify → CDBEnv::Verify(walletFile, CWalletDB::Recover)).
|
||||
// The salvage can be incomplete (or a false positive from stale/cross-platform BDB env state), so the
|
||||
// node silently comes up on a possibly-empty wallet — which reads as fund loss unless we surface it.
|
||||
inline bool walletAutoRecovered(const std::string& out)
|
||||
{
|
||||
// Cover BOTH salvage outcomes. A successful salvage prints the "data salvaged"/"saved as wallet.<ts>.bak"
|
||||
// warning; a FAILED one (e.g. an inconsistent-but-readable file where aggressive salvage finds no
|
||||
// records) prints "salvage failed"/"found no records". In every case CWalletDB::Recover first logs
|
||||
// "Renamed <wallet> to wallet.<ts>.bak" and CDBEnv::Salvage logs its own banner — those two fire the
|
||||
// instant a salvage begins, before the daemon may abort, so they're the earliest reliable signal.
|
||||
return out.find("CDBEnv::Salvage") != std::string::npos // salvage is running
|
||||
|| out.find("wallet.dat corrupt, data salvaged") != std::string::npos // RECOVER_OK
|
||||
|| out.find("Original wallet.dat saved as wallet.") != std::string::npos
|
||||
|| out.find("wallet.dat corrupt, salvage failed") != std::string::npos // RECOVER_FAIL
|
||||
|| out.find("found no records in wallet") != std::string::npos // aggressive salvage empty
|
||||
|| (out.find("Renamed ") != std::string::npos && out.find(" to wallet.") != std::string::npos
|
||||
&& out.find(".bak") != std::string::npos); // Recover moved wallet.dat aside
|
||||
}
|
||||
|
||||
// True when dragonxd (v1.3.0+) opened the wallet in DEGRADED mode: a wallet.dat that lost its hdchain
|
||||
// record (e.g. an old `-salvagewallet` output) now OPENS — existing keys stay intact and spendable —
|
||||
// instead of aborting, but the daemon can no longer derive NEW HD keys, so z_getnewaddress /
|
||||
// z_shieldcoinbase / a t->z z_sendmany fail with "HD seed not found". The only signal is a startup log
|
||||
// line; pre-1.3.0 daemons never emit it, so this classifier is naturally a no-op against them.
|
||||
inline bool walletOpenedDegraded(const std::string& out)
|
||||
{
|
||||
return out.find("Wallet opened in DEGRADED mode") != std::string::npos;
|
||||
}
|
||||
|
||||
// If `name` is a daemon salvage backup "wallet.<unixtime>.bak", return its timestamp; else -1.
|
||||
inline long long parseWalletSalvageBakTs(const std::string& name)
|
||||
{
|
||||
if (name.rfind("wallet.", 0) != 0) return -1; // must start "wallet."
|
||||
if (name.size() < 12 || name.compare(name.size() - 4, 4, ".bak") != 0) return -1; // ...and end ".bak"
|
||||
const std::string mid = name.substr(7, name.size() - 7 - 4); // digits between the dots
|
||||
if (mid.empty() || mid.size() > 18) return -1;
|
||||
for (char c : mid) if (c < '0' || c > '9') return -1;
|
||||
long long ts = 0;
|
||||
for (char c : mid) ts = ts * 10 + (c - '0');
|
||||
return ts;
|
||||
}
|
||||
|
||||
// Most RECENT salvage backup (highest timestamp). Pure, testable.
|
||||
inline std::string newestWalletSalvageBak(const std::vector<std::string>& filenames)
|
||||
{
|
||||
long long best = -1;
|
||||
std::string bestName;
|
||||
for (const auto& f : filenames) {
|
||||
const long long ts = parseWalletSalvageBakTs(f);
|
||||
if (ts > best) { best = ts; bestName = f; }
|
||||
}
|
||||
return bestName;
|
||||
}
|
||||
|
||||
// LARGEST salvage backup, from (filename, fileSize) pairs — the least-salvaged one, i.e. the original.
|
||||
// This is what "Restore original wallet" should use: a salvage CASCADE shrinks the wallet each round, so
|
||||
// the newest .bak is the WORST and the largest is the pristine pre-salvage original (an emptied salvage
|
||||
// is tiny; a real wallet is large). Ties break toward the newest timestamp. Returns "" if none present.
|
||||
inline std::string largestWalletSalvageBak(const std::vector<std::pair<std::string, unsigned long long>>& files)
|
||||
{
|
||||
std::string bestName;
|
||||
unsigned long long bestSize = 0;
|
||||
long long bestTs = -1;
|
||||
for (const auto& fp : files) {
|
||||
const long long ts = parseWalletSalvageBakTs(fp.first);
|
||||
if (ts < 0) continue;
|
||||
if (fp.second > bestSize || (fp.second == bestSize && ts > bestTs)) {
|
||||
bestSize = fp.second; bestTs = ts; bestName = fp.first;
|
||||
}
|
||||
}
|
||||
return bestName;
|
||||
}
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
+213
-1013
File diff suppressed because it is too large
Load Diff
+17
-350
@@ -4,23 +4,15 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <chrono>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include "output_window.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
@@ -58,71 +50,15 @@ public:
|
||||
/**
|
||||
* @brief Start the embedded dragonxd daemon
|
||||
* @param binary_path Path to dragonxd binary (auto-detect if empty)
|
||||
* @return true if process started successfully (or is already running); false while a stop
|
||||
* session (beginStop() .. endStop()) is still in progress
|
||||
* @return true if process started successfully
|
||||
*/
|
||||
static constexpr int kDatadirLockWaitMaxPolls = 3; // ~300ms total, breaks early on exit
|
||||
|
||||
/** @param lockWaitPolls max 100ms datadir-lock waits (0 = single check, never sleeps; for the UI thread) */
|
||||
bool start(const std::string& binary_path = "", int lockWaitPolls = kDatadirLockWaitMaxPolls);
|
||||
bool start(const std::string& binary_path = "");
|
||||
|
||||
/**
|
||||
* @brief Stop the daemon process this object launched (never any other dragonxd)
|
||||
* @brief Stop the running daemon
|
||||
* @param wait_ms Maximum milliseconds to wait for graceful shutdown
|
||||
* @return true if it exited on its own (or there was nothing to stop), false if it had to be
|
||||
* force-killed — then its wallet/chain files may not have been closed cleanly
|
||||
*/
|
||||
bool stop(int wait_ms = 5000);
|
||||
|
||||
/**
|
||||
* @brief Record that we are about to ask the daemon to stop (e.g. over RPC)
|
||||
*
|
||||
* Call it before sending the stop. If the daemon then exits before stop() runs, the monitor
|
||||
* records a clean stop instead of a crash (crash_count_ drives auto-restart). stop() sets it
|
||||
* too; start() clears it.
|
||||
*/
|
||||
void markStopRequested() { stop_requested_ = true; }
|
||||
|
||||
// --- A stop session, driven by DaemonController::executeDaemonStop through the app's StopRuntime ---
|
||||
|
||||
/** Marks the stop as requested and stops the monitor, so the session is the only reaper.
|
||||
* @return true if a process this object launched is still alive; then finish with endStop(). */
|
||||
bool beginStop();
|
||||
/** Polls our own process (reaping it on POSIX) and drains its output; false once it has exited. */
|
||||
bool pollOwnedProcess();
|
||||
/** POSIX: SIGTERM our process group (dragonxd treats it like RPC stop). Windows has no such channel. */
|
||||
bool signalGracefulStop();
|
||||
/** Ends our own process by force (SIGKILL / TerminateProcess) and reaps it. Explicit user choice only. */
|
||||
void forceKillOwnedProcess();
|
||||
struct ResourceUsage {
|
||||
double cpuSeconds = -1.0; // cumulative; < 0 if unknown
|
||||
double ioBytes = -1.0; // cumulative storage I/O; < 0 if unknown
|
||||
};
|
||||
/** CPU and I/O used so far by our process (its whole process group on Linux). */
|
||||
ResourceUsage ownedResourceUsage() const;
|
||||
/** Finishes a stop session. The process is gone: release everything. Still running: keepWatching
|
||||
* resumes the monitor (its exit is still seen, and booked as a clean stop); otherwise let it go —
|
||||
* the user chose to leave it running. */
|
||||
void endStop(bool keepWatching);
|
||||
|
||||
/**
|
||||
* @brief Parse a /proc/<pid>/stat line: process group (field 5) and utime+stime ticks (fields 14+15)
|
||||
*
|
||||
* comm (field 2) is parenthesised and may itself contain spaces and ')', so fields are read from
|
||||
* after the LAST ')'.
|
||||
*/
|
||||
static bool parseProcStatLine(const std::string& line, long& pgrp, unsigned long long& cpuTicks)
|
||||
{
|
||||
const size_t close = line.rfind(')');
|
||||
if (close == std::string::npos) return false;
|
||||
unsigned long long utime = 0, stime = 0;
|
||||
// state ppid pgrp session tty_nr tpgid flags minflt cminflt majflt cmajflt utime stime
|
||||
if (std::sscanf(line.c_str() + close + 1, " %*c %*d %ld %*d %*d %*d %*u %*u %*u %*u %*u %llu %llu",
|
||||
&pgrp, &utime, &stime) != 3)
|
||||
return false;
|
||||
cpuTicks = utime + stime;
|
||||
return true;
|
||||
}
|
||||
void stop(int wait_ms = 5000);
|
||||
|
||||
/**
|
||||
* @brief Check if daemon process is running
|
||||
@@ -137,25 +73,13 @@ public:
|
||||
/**
|
||||
* @brief Get daemon process memory usage (RSS) in MB
|
||||
* @return Memory usage in MB, or 0 if unavailable
|
||||
*
|
||||
* Synchronous (Linux scans /proc, macOS forks `ps`): UI code uses memoryProbe() through a
|
||||
* util::BackgroundSampler instead.
|
||||
*/
|
||||
double getMemoryUsageMB() const;
|
||||
|
||||
/**
|
||||
* @brief A self-contained RSS probe (MB) for another thread. Snapshots what it needs on the calling
|
||||
* thread (the pid; on Windows the value itself: one cheap call on the owned handle); the callable
|
||||
* touches no EmbeddedDaemon state. Empty when there is no process.
|
||||
*/
|
||||
std::function<double()> memoryProbe() const;
|
||||
|
||||
/**
|
||||
* @brief Get last error message
|
||||
*/
|
||||
// Copy under lock: last_error_ is written from the monitor thread (setState on an unexpected exit)
|
||||
// while the UI thread reads it — a reference would be a torn-read / use-after-free race (M-04).
|
||||
std::string getLastError() const { std::lock_guard<std::mutex> lk(error_mutex_); return last_error_; }
|
||||
const std::string& getLastError() const { return last_error_; }
|
||||
|
||||
/**
|
||||
* @brief Get dragonxd process output (thread-safe copy)
|
||||
@@ -167,25 +91,17 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Get new output since a given offset (thread-safe).
|
||||
* Returns the new text and advances offset. Offsets are absolute positions in everything ever
|
||||
* appended (start from 0), so trimming old output never shifts them: a reader that falls behind a
|
||||
* trim only loses the trimmed part, not output that is still buffered.
|
||||
* Returns the new text and updates offset to the current size.
|
||||
*/
|
||||
std::string getOutputSince(size_t& offset) const {
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
return outputSince(process_output_, output_total_, offset);
|
||||
if (offset >= process_output_.size()) {
|
||||
offset = process_output_.size();
|
||||
return {};
|
||||
}
|
||||
|
||||
/** getOutputSince() on a buffer holding the LAST buffer.size() of `total` bytes ever appended. */
|
||||
static std::string outputSince(const std::string& buffer, size_t total, size_t& offset)
|
||||
{
|
||||
return outputWindowSince(buffer, total, offset);
|
||||
}
|
||||
|
||||
/** Absolute end of everything ever appended (a valid getOutputSince() offset). */
|
||||
size_t getOutputEnd() const {
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
return output_total_;
|
||||
std::string result = process_output_.substr(offset);
|
||||
offset = process_output_.size();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,10 +142,6 @@ public:
|
||||
* When true the wallet should connect to it instead of showing an error.
|
||||
*/
|
||||
bool externalDaemonDetected() const { return external_daemon_detected_; }
|
||||
// Clear the adopted-external latch before relaunching our OWN process (e.g. a wallet switch that
|
||||
// stopped the adopted daemon), so the freshly spawned daemon is treated as owned. start() also
|
||||
// clears it on the port-free fall-through, but only if not short-circuited by an early guard.
|
||||
void clearExternalDaemonDetected() { external_daemon_detected_ = false; }
|
||||
|
||||
/**
|
||||
* @brief Set callback for state changes
|
||||
@@ -264,7 +176,6 @@ public:
|
||||
* @brief Set maximum peer connections (0 = use daemon default)
|
||||
*/
|
||||
void setMaxConnections(int v) { max_connections_ = v; }
|
||||
void setStratumHosting(bool enabled, const std::string& allowIp) { stratum_enabled_ = enabled; stratum_allowip_ = allowIp; }
|
||||
|
||||
/**
|
||||
* @brief Request a blockchain rescan on the next daemon start
|
||||
@@ -272,230 +183,14 @@ public:
|
||||
void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; }
|
||||
bool rescanOnNextStart() const { return rescan_on_next_start_.load(); }
|
||||
|
||||
// Active wallet file (multi-wallet). Passed to the daemon as -wallet=<name> so it loads
|
||||
// <datadir>/<name>; empty or "wallet.dat" keeps the daemon default (no arg). Must be a plain
|
||||
// filename in the datadir — the daemon rejects paths.
|
||||
void setWalletFile(const std::string& v) { wallet_file_ = v; }
|
||||
std::string walletFile() const { return wallet_file_; }
|
||||
|
||||
/**
|
||||
* @brief Request a wallet repair (-zapwallettxes=2) on the next daemon start. This deletes all
|
||||
* wallet transaction/note records and rebuilds them from the chain (keys are kept); the
|
||||
* daemon implicitly rescans afterwards. One-shot, like the rescan flag.
|
||||
*/
|
||||
void setZapOnNextStart(bool v) { zap_on_next_start_ = v; }
|
||||
bool zapOnNextStart() const { return zap_on_next_start_.load(); }
|
||||
|
||||
// -salvagewallet: attempt to recover keys from a corrupt wallet.dat on startup (implies -rescan in
|
||||
// the daemon). One-shot, consumed on the next start. Used to repair a wallet a switch flagged corrupt.
|
||||
void setSalvageOnNextStart(bool v) { salvage_on_next_start_ = v; }
|
||||
bool salvageOnNextStart() const { return salvage_on_next_start_.load(); }
|
||||
|
||||
// -reindex: rebuild the block index + chainstate from the raw blocks (blk*.dat) on startup. One-shot,
|
||||
// consumed on the next start. Offered when the node aborts on an unreadable block database (a
|
||||
// daemon-vs-chaindata format mismatch after an update, or a corrupt index). It implies a wallet
|
||||
// rescan, so it's the block-DB analogue of -salvagewallet and coexists with the wallet-repair flags.
|
||||
void setReindexOnNextStart(bool v) { reindex_on_next_start_ = v; }
|
||||
bool reindexOnNextStart() const { return reindex_on_next_start_.load(); }
|
||||
|
||||
/**
|
||||
* @brief One-shot isolated-datadir override for the NEXT start(): run the daemon against a
|
||||
* different datadir (with its own DRAGONX.conf) plus the given extra args. Used by the
|
||||
* "migrate to a seed wallet" flow to mint a fresh mnemonic wallet in a throwaway datadir
|
||||
* without touching the real one. Consumed on the next start(); later starts are normal.
|
||||
* The caller must serialize this with start() (no concurrent starts).
|
||||
*/
|
||||
void setNextStartOverride(const std::string& datadir, std::vector<std::string> extraArgs) {
|
||||
override_datadir_ = datadir;
|
||||
override_extra_args_ = std::move(extraArgs);
|
||||
}
|
||||
void clearNextStartOverride() { override_datadir_.clear(); override_extra_args_.clear(); }
|
||||
|
||||
/**
|
||||
* @brief Skip the "default RPC port already in use → external daemon" bail in start().
|
||||
* Set true only for an isolated instance running on its OWN (non-default) port
|
||||
* alongside the main daemon (migrate-to-seed flow).
|
||||
*/
|
||||
void setSkipPortCheck(bool v) { skip_port_check_ = v; }
|
||||
|
||||
/**
|
||||
* @brief True while ANY dragonxd process is running (by process name), regardless of who started it.
|
||||
* Unlike isRpcPortInUse()/isRunning(), this reflects the actual PROCESS still being alive — a
|
||||
* graceful shutdown stops accepting RPC (port reads "free") but keeps the datadir lock until the
|
||||
* process exits, which can take up to ~90s. Use this to know a stopped node has FULLY released
|
||||
* the datadir before starting a replacement. Matches the daemon binary name on all platforms.
|
||||
*/
|
||||
static bool isDaemonProcessRunning();
|
||||
|
||||
/**
|
||||
* @brief Does a /proc entry (its comm + its exe link's basename) belong to a dragonxd? (Linux)
|
||||
*
|
||||
* comm alone misses a node in its final shutdown phase: Shutdown() renames the main thread to
|
||||
* "hush-shutoff" while the process still holds the datadir lock. The exe link keeps the binary
|
||||
* name, with " (deleted)" appended once the binary was replaced underneath it (reinstall/update).
|
||||
*/
|
||||
static bool isDaemonProcessIdentity(const std::string& comm, std::string exeName)
|
||||
{
|
||||
static const std::string kDeleted = " (deleted)";
|
||||
if (exeName.size() > kDeleted.size() &&
|
||||
exeName.compare(exeName.size() - kDeleted.size(), kDeleted.size(), kDeleted) == 0)
|
||||
exeName.erase(exeName.size() - kDeleted.size());
|
||||
return comm == "dragonxd" || exeName == "dragonxd";
|
||||
}
|
||||
|
||||
// ---- Ownership record --------------------------------------------------------------------------
|
||||
// <datadir>/obsidiandragon-dragonxd.pid names the node this wallet launched, so a later session that
|
||||
// finds it still running (after "Leave running") knows it is ours: it may stop it gracefully and wait
|
||||
// for it, never kill it. The start token (when that process started) defeats PID reuse and makes a
|
||||
// record from before a reboot stale.
|
||||
struct OwnershipRecord {
|
||||
long pid = 0;
|
||||
std::string startToken;
|
||||
std::string exePath;
|
||||
};
|
||||
static std::string formatOwnershipRecord(const OwnershipRecord& r)
|
||||
{
|
||||
return "pid=" + std::to_string(r.pid) + "\nstart=" + r.startToken + "\nexe=" + r.exePath + "\n";
|
||||
}
|
||||
static bool parseOwnershipRecord(const std::string& text, OwnershipRecord& out)
|
||||
{
|
||||
OwnershipRecord r;
|
||||
size_t pos = 0;
|
||||
while (pos < text.size()) {
|
||||
size_t end = text.find('\n', pos);
|
||||
if (end == std::string::npos) end = text.size();
|
||||
std::string line = text.substr(pos, end - pos);
|
||||
pos = end + 1;
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
if (line.rfind("pid=", 0) == 0) r.pid = std::strtol(line.c_str() + 4, nullptr, 10);
|
||||
else if (line.rfind("start=", 0) == 0) r.startToken = line.substr(6);
|
||||
else if (line.rfind("exe=", 0) == 0) r.exePath = line.substr(4);
|
||||
}
|
||||
if (r.pid <= 0 || r.startToken.empty()) return false;
|
||||
out = r;
|
||||
return true;
|
||||
}
|
||||
/** Linux /proc/<pid>/stat: the process state and its start time (clock ticks after boot, field 22). */
|
||||
static bool parseProcStatStart(const std::string& line, char& state, unsigned long long& startTicks)
|
||||
{
|
||||
const size_t close = line.rfind(')');
|
||||
if (close == std::string::npos) return false;
|
||||
// state ppid pgrp session tty_nr tpgid flags minflt cminflt majflt cmajflt utime stime cutime cstime
|
||||
// priority nice num_threads itrealvalue starttime
|
||||
return std::sscanf(line.c_str() + close + 1,
|
||||
" %c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %*u %*u %*d %*d %*d %*d %*d %*d %llu",
|
||||
&state, &startTicks) == 2;
|
||||
}
|
||||
/**
|
||||
* dragonxd shrinks a large debug.log at startup (it keeps the tail), so the file can become smaller
|
||||
* than where we were reading. Resume at THIS run's startup banner — the first banner line stamped at or
|
||||
* after `launchUtc` (UTC) — not at 0, which would replay the previous run's lines (old rebuild markers,
|
||||
* old corruption errors) as if they were new. No such banner yet: skip to the end (it follows).
|
||||
*/
|
||||
static size_t shrinkResumeOffset(const std::string& content, std::int64_t launchUtc);
|
||||
|
||||
/** When process `pid` started, as an opaque token; "" if it isn't running (or is a zombie). */
|
||||
static std::string processStartToken(long pid);
|
||||
/** When process `pid` started, in UTC seconds; 0 if unknown. */
|
||||
static std::int64_t processStartUtc(long pid);
|
||||
/** Is process `pid` a dragonxd (by name / executable)? */
|
||||
static bool processIsDaemon(long pid);
|
||||
static std::string ownershipRecordPath(); // <datadir>/obsidiandragon-dragonxd.pid
|
||||
static void writeOwnershipRecord(const std::string& path, const OwnershipRecord& r);
|
||||
/** The record at `path`, if its process is still running; a stale or unreadable record is deleted. */
|
||||
static bool liveOwnershipRecord(const std::string& path, OwnershipRecord* out = nullptr);
|
||||
|
||||
/**
|
||||
* Does this DRAGONX.conf text set `key` on an active line ("key=value", spaces allowed around '=';
|
||||
* '#' starts a comment)? Used before adding a flag of our own: a command-line flag overrides the conf,
|
||||
* so the user's own setting must win.
|
||||
*/
|
||||
static bool confSetsKey(const std::string& confText, const std::string& key)
|
||||
{
|
||||
size_t pos = 0;
|
||||
while (pos < confText.size()) {
|
||||
size_t end = confText.find('\n', pos);
|
||||
if (end == std::string::npos) end = confText.size();
|
||||
std::string line = confText.substr(pos, end - pos);
|
||||
pos = end + 1;
|
||||
const size_t hash = line.find('#');
|
||||
if (hash != std::string::npos) line.erase(hash);
|
||||
const size_t eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
size_t b = line.find_first_not_of(" \t\r");
|
||||
size_t e = line.find_last_not_of(" \t\r", eq == 0 ? 0 : eq - 1);
|
||||
if (b == std::string::npos || b >= eq || e == std::string::npos || e < b) continue;
|
||||
if (line.compare(b, e - b + 1, key) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the wallet's own -dbcache= pin from `args` when the user's DRAGONX.conf sets dbcache (a
|
||||
* command-line flag overrides the conf, so the user's own value must win). Pure; no logging.
|
||||
*/
|
||||
static void dropPinnedDbcacheIfConfSets(std::vector<std::string>& args, const std::string& conf)
|
||||
{
|
||||
if (!confSetsKey(conf, "dbcache")) return;
|
||||
args.erase(std::remove_if(args.begin(), args.end(),
|
||||
[](const std::string& a) { return a.rfind("-dbcache=", 0) == 0; }),
|
||||
args.end());
|
||||
}
|
||||
|
||||
/** Decision returned by evaluateDatadirLockGate(): whether start() may spawn now. */
|
||||
struct StartLockGateDecision {
|
||||
bool proceed = true; // false => bail before spawning
|
||||
const char* errorMessage = ""; // set (a string literal) when proceed == false
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Pure decision for start(): bail because a previous dragonxd still holds the
|
||||
* shared datadir lock? Isolated instances (skip_port_check_ / an active -datadir
|
||||
* override) are exempt — they run their own throwaway datadir+port and can coexist
|
||||
* with the main daemon. Does no process/fs I/O itself (the caller does the probing),
|
||||
* so it is directly unit-testable; defined inline so tests need only this header.
|
||||
*/
|
||||
static StartLockGateDecision evaluateDatadirLockGate(bool skipPortCheck,
|
||||
bool isolatedOverride,
|
||||
bool stillRunningAfterWait)
|
||||
{
|
||||
if (skipPortCheck || isolatedOverride) return {true, ""};
|
||||
if (stillRunningAfterWait) {
|
||||
return {false,
|
||||
"A previous dragonxd is still shutting down and holding the data "
|
||||
"directory lock. Retrying shortly…"};
|
||||
}
|
||||
return {true, ""};
|
||||
}
|
||||
|
||||
/** @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) */
|
||||
void resetCrashCount() { crash_count_ = 0; }
|
||||
/** Unexpected exits since this object was created; never reset (unlike the crash count), so a
|
||||
* reader can tell a new crash from an error it has already reported. */
|
||||
std::uint64_t getUnexpectedExitCount() const { return unexpected_exits_.load(); }
|
||||
|
||||
/** Monitor poll interval: stay `fast` for the first 30 s after the thread starts and for 3 s after any
|
||||
* output; otherwise double per idle tick up to `cap`. Any output resets to `fast`. Pure. */
|
||||
static std::chrono::milliseconds nextMonitorInterval(std::chrono::milliseconds cur, bool bytesRead,
|
||||
std::chrono::milliseconds sinceStart, std::chrono::milliseconds sinceLastData,
|
||||
std::chrono::milliseconds fast, std::chrono::milliseconds cap)
|
||||
{
|
||||
using ms = std::chrono::milliseconds;
|
||||
if (bytesRead || sinceStart < ms(30000) || sinceLastData < ms(3000)) return fast;
|
||||
const ms next = cur * 2;
|
||||
return next > cap ? cap : (next < fast ? fast : next);
|
||||
}
|
||||
|
||||
private:
|
||||
void setState(State s, const std::string& message = "");
|
||||
void monitorProcess();
|
||||
void requestMonitorStop(); // set should_stop_ under monitor_mutex_ and wake the monitor
|
||||
size_t outputTotalSnapshot() const; // output_total_ under output_mutex_ (monitor idle detection)
|
||||
void drainOutput(); // read any pending stdout into process_output_
|
||||
void appendOutput(const char* data, size_t len); // append + trim (caller holds output_mutex_)
|
||||
bool startProcess(const std::string& binary_path, const std::vector<std::string>& args);
|
||||
@@ -503,58 +198,30 @@ private:
|
||||
std::atomic<State> state_{State::Stopped};
|
||||
std::atomic<bool> external_daemon_detected_{false};
|
||||
std::string last_error_;
|
||||
mutable std::mutex error_mutex_; // protects last_error_ (written by main + monitor threads)
|
||||
// Locked writer for last_error_ (startProcess() runs on the caller thread while the monitor may call setState()).
|
||||
void setLastError(std::string message) { std::lock_guard<std::mutex> lk(error_mutex_); last_error_ = std::move(message); }
|
||||
mutable std::mutex output_mutex_; // protects process_output_
|
||||
std::string process_output_;
|
||||
size_t output_total_ = 0; // bytes ever appended (the absolute end of process_output_)
|
||||
StateCallback state_callback_;
|
||||
|
||||
// Process handle
|
||||
#ifdef _WIN32
|
||||
std::atomic<HANDLE> process_handle_{nullptr}; // written by start/stop/monitor threads, read by accessors
|
||||
HANDLE process_handle_ = nullptr;
|
||||
HANDLE stdout_read_ = nullptr; // pipe handle (Linux-style pipe, unused when tailing debug.log)
|
||||
std::string debug_log_path_; // path to daemon's debug.log for output tailing
|
||||
size_t debug_log_offset_ = 0; // read offset into debug.log
|
||||
std::string launch_cmd_; // full command line used to launch daemon (for diagnostics)
|
||||
std::string launch_binary_; // binary path used (for diagnostics)
|
||||
std::string launch_workdir_; // working directory used (for diagnostics)
|
||||
#else
|
||||
std::atomic<pid_t> process_pid_{0}; // written by start/stop paths and the monitor (reap), read by accessors
|
||||
std::atomic<int> stdout_fd_{-1}; // the child's stdout+stderr (crash diagnostics; the log itself is debug.log)
|
||||
void tailDebugLog();
|
||||
|
||||
// Waits up to timeout_ms for `pid` to exit, draining its output; true once reaped. Only valid
|
||||
// after monitor_thread_ is joined, so that stop() is the sole reaper.
|
||||
bool reapWithin(pid_t pid, int timeout_ms, int& status);
|
||||
pid_t process_pid_ = 0;
|
||||
int stdout_fd_ = -1;
|
||||
#endif
|
||||
|
||||
// The node's output is its <datadir>/debug.log (timestamped; it outlives us if the node is left running).
|
||||
std::string debug_log_path_;
|
||||
size_t debug_log_offset_ = 0; // read offset into debug.log
|
||||
std::int64_t launch_utc_ = 0; // when we launched it (to find this run's banner after a log shrink)
|
||||
|
||||
std::thread monitor_thread_;
|
||||
std::atomic<bool> should_stop_{false};
|
||||
std::mutex monitor_mutex_; // pairs with monitor_cv_ so stop wakes the monitor at once
|
||||
std::condition_variable monitor_cv_;
|
||||
std::atomic<bool> stop_requested_{false}; // a stop was asked for: the next exit is not a crash
|
||||
// Serializes start/beginStop/stop/endStop over monitor_thread_, should_stop_ and stop_requested_ (two
|
||||
// threads joining one std::thread is undefined). Never taken by the monitor thread or the state callback.
|
||||
std::mutex lifecycle_mutex_;
|
||||
std::set<std::string> debug_categories_;
|
||||
int max_connections_ = 0; // 0 = daemon default
|
||||
bool stratum_enabled_ = false; // -stratum: host a RandomX pool (v1.3.0+; older daemons ignore it)
|
||||
std::string stratum_allowip_; // -stratumallowip subnet (empty = daemon default: loopback only)
|
||||
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<std::uint64_t> unexpected_exits_{0}; // all unexpected exits (see getUnexpectedExitCount)
|
||||
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
||||
std::atomic<bool> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start
|
||||
std::atomic<bool> salvage_on_next_start_{false}; // -salvagewallet flag for next start
|
||||
std::atomic<bool> reindex_on_next_start_{false}; // -reindex flag for next start (rebuild block DB)
|
||||
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
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Shared absolute-offset output ring helpers (daemon debug.log feed, xmrig console feed).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
|
||||
// getOutputSince() over a buffer holding the LAST buffer.size() of `total` bytes ever appended.
|
||||
inline std::string outputWindowSince(const std::string& buffer, std::size_t total, std::size_t& offset)
|
||||
{
|
||||
const std::size_t base = total - buffer.size(); // absolute position of buffer[0]
|
||||
if (offset < base) offset = base; // trimmed before read: resume at what's left
|
||||
if (offset >= total) {
|
||||
offset = total;
|
||||
return {};
|
||||
}
|
||||
std::string result = buffer.substr(offset - base);
|
||||
offset = total;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Append `n` bytes, advance absolute `total`, and trim to ~cap at a newline once size exceeds 1.25x cap.
|
||||
// The `nl < size-1` guard keeps a lone trailing newline from erasing the whole buffer.
|
||||
inline void appendBounded(std::string& buffer, std::size_t& total, const char* data, std::size_t n, std::size_t cap)
|
||||
{
|
||||
buffer.append(data, n);
|
||||
total += n;
|
||||
if (buffer.size() > cap + cap / 4) {
|
||||
const std::size_t trim_to = buffer.size() - cap;
|
||||
const std::size_t nl = buffer.find('\n', trim_to);
|
||||
if (nl != std::string::npos && nl < buffer.size() - 1) buffer.erase(0, nl + 1);
|
||||
else buffer.erase(0, trim_to);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
@@ -1,193 +0,0 @@
|
||||
#include "daemon/seed_wallet_creator.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
|
||||
#include <sodium.h>
|
||||
|
||||
#include "daemon/embedded_daemon.h"
|
||||
#include "rpc/rpc_client.h"
|
||||
#include "util/platform.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
|
||||
namespace {
|
||||
|
||||
// Asks the isolated node to stop over its own RPC (port + throwaway credentials, never the main node's)
|
||||
// and waits for it to exit. Without the RPC stop it was only ever force-killed, holding the wallet.dat
|
||||
// it had just created. Returns false if it still had to be force-killed.
|
||||
bool stopIsolatedNode(EmbeddedDaemon& node, rpc::RPCClient& cli, int port, const std::string& user,
|
||||
const std::string& pass, int waitMs)
|
||||
{
|
||||
node.markStopRequested();
|
||||
// connect() also succeeds against a node still in warmup, which accepts stop too.
|
||||
if (cli.isConnected() || cli.connect("127.0.0.1", std::to_string(port), user, pass, /*useTls=*/false)) {
|
||||
try { cli.call("stop", nlohmann::json::array(), 10L); } catch (const std::exception&) {}
|
||||
}
|
||||
cli.disconnect();
|
||||
return node.stop(waitMs);
|
||||
}
|
||||
|
||||
// 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";
|
||||
// W3-2: never blindly wipe a pre-existing temp seed wallet. A prior migration that swept funds into
|
||||
// it but was abandoned or crashed before adopting would otherwise have its (fund-bearing) wallet
|
||||
// destroyed here. A completed migration removes this dir on adopt, so a leftover means an unfinished
|
||||
// one — refuse and point the user at it rather than silently destroying it.
|
||||
if (fs::exists(dataDir + "/wallet.dat")) {
|
||||
r.error = "A previous seed migration looks unfinished — its temporary wallet is still at\n" + base +
|
||||
"\nResume or cancel it first. If you are certain its funds are already in your main "
|
||||
"wallet, delete that folder and try again.";
|
||||
return r;
|
||||
}
|
||||
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.";
|
||||
stopIsolatedNode(temp, cli, port, user, pass, 20000); // datadir is thrown away: a forced stop is harmless
|
||||
fs::remove_all(base, ec);
|
||||
return r;
|
||||
}
|
||||
|
||||
// 6. Export the new seed phrase + a fresh shielded receive address (the future sweep target).
|
||||
try {
|
||||
auto m = cli.callSecret("z_exportmnemonic"); // zero the raw body too (B7)
|
||||
if (m.contains("mnemonic") && m["mnemonic"].is_string()) {
|
||||
// Take our copy, then scrub the json node's own copy so it isn't freed in the clear (B7).
|
||||
auto& mn = m["mnemonic"].get_ref<std::string&>();
|
||||
r.seedPhrase = mn;
|
||||
if (!mn.empty()) sodium_memzero(&mn[0], mn.size());
|
||||
}
|
||||
r.destAddress = cli.call("z_getnewaddress").get<std::string>();
|
||||
r.ok = !r.seedPhrase.empty() && !r.destAddress.empty();
|
||||
if (!r.ok) r.error = "The isolated node returned an empty seed or address.";
|
||||
} catch (const std::exception& e) {
|
||||
const std::string what = e.what();
|
||||
// "Method not found" (JSON-RPC -32601) means this dragonxd predates mnemonic support —
|
||||
// it has no z_exportmnemonic RPC (the older bundled binary). Migrate-to-seed can't work
|
||||
// until the daemon is updated, so give an actionable message, not the raw RPC error.
|
||||
if (what.find("Method not found") != std::string::npos ||
|
||||
what.find("-32601") != std::string::npos) {
|
||||
r.error = "This DragonX daemon is too old to create a seed wallet — it lacks mnemonic "
|
||||
"support (the z_exportmnemonic RPC). Update to the latest DragonX daemon "
|
||||
"(Settings -> NODE & SECURITY -> Check for updates, or Install bundled), then "
|
||||
"try again.";
|
||||
} else {
|
||||
r.error = std::string("Seed export failed: ") + what;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Stop the isolated node (graceful; it flushes its tiny empty chain quickly). Adopt later copies
|
||||
// this wallet.dat, so it must have been closed cleanly: a node that had to be force-killed
|
||||
// fails the creation (nothing has been swept into it yet, so nothing is lost by retrying).
|
||||
if (!stopIsolatedNode(temp, cli, port, user, pass, 60000) && r.ok) {
|
||||
r.ok = false;
|
||||
r.error = "The isolated node had to be force-stopped, so its new wallet can't be trusted. "
|
||||
"Please try again.";
|
||||
}
|
||||
|
||||
// W1-2: never hand back a live seed on a failure path. If the mnemonic was exported but a
|
||||
// later step failed (empty address, z_getnewaddress threw, or an unclean stop), the caller
|
||||
// discards this result without wiping it, which would leave the seed resident. Success keeps
|
||||
// it deliberately.
|
||||
if (!r.ok && !r.seedPhrase.empty()) {
|
||||
sodium_memzero(&r.seedPhrase[0], r.seedPhrase.size());
|
||||
r.seedPhrase.clear();
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
|
||||
struct SeedWalletResult {
|
||||
bool ok = false;
|
||||
std::string seedPhrase; // SECRET — the caller must wipe it after use
|
||||
std::string destAddress; // new shielded z-address (the Phase 2 sweep target)
|
||||
std::string tempDatadir; // datadir holding the new wallet.dat (kept iff keepDatadir + ok)
|
||||
std::string error;
|
||||
};
|
||||
|
||||
// Mint a fresh BIP39 mnemonic wallet in an ISOLATED throwaway datadir by running a second
|
||||
// dragonxd on its own port with no network, export its seed + a new z-address, then stop it.
|
||||
//
|
||||
// This is the safe first step of "migrate to a seed wallet": it moves no funds and never touches
|
||||
// the main daemon or the real wallet.dat. Blocking — call it on a background thread.
|
||||
//
|
||||
// keepDatadir: keep the temp datadir + its wallet.dat so a later sweep/adopt step can use it, or
|
||||
// delete it immediately (used when only the seed itself is wanted).
|
||||
class SeedWalletCreator {
|
||||
public:
|
||||
static SeedWalletResult create(bool keepDatadir,
|
||||
const std::function<void(const std::string&)>& progress = {});
|
||||
};
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
@@ -1,155 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// Delivering "stop" to dragonxd: what one attempt achieved, and when to send another. Pure and
|
||||
// header-only so the test target can drive it without the RPC client.
|
||||
//
|
||||
// Why retrying matters: dragonxd's HTTP server has 8 workers and a 16-deep queue. While a long
|
||||
// operation holds cs_main, the GUI's own abandoned polls can fill every slot, and each new request is
|
||||
// refused at once with "Work queue depth exceeded" until a slot frees. The old code made one or two
|
||||
// attempts, gated on a getinfo probe that needs cs_main, so stop was never delivered and the node was
|
||||
// then killed.
|
||||
|
||||
#include "rpc/rpc_failure.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace daemon {
|
||||
|
||||
// What the user picked on the shutdown screen while the node was stopping.
|
||||
enum class StopUserChoice { None, Detach, Kill };
|
||||
|
||||
// How a stop session ended. The wallet never kills the node unless the user chose Kill.
|
||||
enum class StopOutcome {
|
||||
Exited, // the process is gone
|
||||
StillRunning, // we stopped waiting (time budget, app shutdown, cancellation); never killed
|
||||
Detached, // the user chose to leave it running
|
||||
Killed, // the user chose Kill
|
||||
};
|
||||
|
||||
inline const char* stopOutcomeName(StopOutcome o)
|
||||
{
|
||||
switch (o) {
|
||||
case StopOutcome::Exited: return "Exited";
|
||||
case StopOutcome::StillRunning: return "StillRunning";
|
||||
case StopOutcome::Detached: return "Detached";
|
||||
case StopOutcome::Killed: return "Killed";
|
||||
}
|
||||
return "Exited";
|
||||
}
|
||||
|
||||
enum class StopSendResult {
|
||||
Accepted, // HTTP 200: the daemon ran stop
|
||||
Queued, // reply timeout: it reached the daemon and waits for a worker; it will run
|
||||
QueueFull, // "Work queue depth exceeded": refused before queueing (holds no slot)
|
||||
NotListening, // refused / connect timeout: RPC isn't bound (not up yet, or already unbound)
|
||||
ShuttingDown, // 503 / "Unhandled request": the daemon is already shutting down
|
||||
Auth, // 401: our credentials are wrong or stale
|
||||
Other,
|
||||
};
|
||||
|
||||
inline const char* stopSendResultName(StopSendResult r)
|
||||
{
|
||||
switch (r) {
|
||||
case StopSendResult::Accepted: return "Accepted";
|
||||
case StopSendResult::Queued: return "Queued";
|
||||
case StopSendResult::QueueFull: return "QueueFull";
|
||||
case StopSendResult::NotListening: return "NotListening";
|
||||
case StopSendResult::ShuttingDown: return "ShuttingDown";
|
||||
case StopSendResult::Auth: return "Auth";
|
||||
case StopSendResult::Other: return "Other";
|
||||
}
|
||||
return "Other";
|
||||
}
|
||||
|
||||
// What a stop call's failure kind (RpcFailureKind::None = HTTP 200) means for delivery.
|
||||
inline StopSendResult stopSendResultFromKind(rpc::RpcFailureKind kind)
|
||||
{
|
||||
using K = rpc::RpcFailureKind;
|
||||
switch (kind) {
|
||||
case K::None: return StopSendResult::Accepted;
|
||||
case K::ReplyTimeout: return StopSendResult::Queued;
|
||||
case K::QueueFull: return StopSendResult::QueueFull;
|
||||
case K::NotListening:
|
||||
case K::ConnectTimeout: return StopSendResult::NotListening;
|
||||
case K::ShuttingDown:
|
||||
case K::Unhandled: return StopSendResult::ShuttingDown;
|
||||
case K::Auth: return StopSendResult::Auth;
|
||||
default: return StopSendResult::Other;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When to (re)send stop. Times are seconds on the caller's clock.
|
||||
*
|
||||
* - Accepted / ShuttingDown: delivered, never resend.
|
||||
* - Queued: the daemon has it. Resend at most every 30s: each resend takes another queue slot, and
|
||||
* concurrent stops race on the miner threads (stop's GenerateBitcoins(false)).
|
||||
* - QueueFull / NotListening / Other: not delivered, cheap to retry: again after 1.5s.
|
||||
* - NotListening AFTER the daemon had heard us means it unbound its listener to shut down: delivered.
|
||||
* - Auth: give up for a node we don't own. For our own node the caller may switch credentials and
|
||||
* retry at once (retryNow), or give up (giveUp).
|
||||
*/
|
||||
class StopDeliveryPolicy {
|
||||
public:
|
||||
static constexpr double kRetrySec = 1.5;
|
||||
static constexpr double kQueuedResendSec = 30.0;
|
||||
|
||||
explicit StopDeliveryPolicy(bool owned) : owned_(owned) {}
|
||||
|
||||
bool shouldSend(double now) const { return !finished_ && now >= nextSendAt_; }
|
||||
|
||||
// Records one attempt; returns it as interpreted (a refusal after we were heard reads as ShuttingDown).
|
||||
StopSendResult record(StopSendResult r, double now)
|
||||
{
|
||||
++attempts_;
|
||||
if (r == StopSendResult::NotListening && heard_) r = StopSendResult::ShuttingDown;
|
||||
switch (r) {
|
||||
case StopSendResult::Accepted:
|
||||
heard_ = true;
|
||||
markDelivered();
|
||||
break;
|
||||
case StopSendResult::ShuttingDown:
|
||||
markDelivered();
|
||||
break;
|
||||
case StopSendResult::Queued:
|
||||
heard_ = true;
|
||||
nextSendAt_ = now + kQueuedResendSec;
|
||||
break;
|
||||
case StopSendResult::Auth:
|
||||
authRejected_ = true;
|
||||
if (!owned_) finished_ = true;
|
||||
nextSendAt_ = now + kRetrySec;
|
||||
break;
|
||||
case StopSendResult::QueueFull:
|
||||
case StopSendResult::NotListening:
|
||||
case StopSendResult::Other:
|
||||
nextSendAt_ = now + kRetrySec;
|
||||
break;
|
||||
}
|
||||
last_ = r;
|
||||
return r;
|
||||
}
|
||||
|
||||
void markDelivered() { delivered_ = finished_ = true; } // also for a graceful signal (POSIX SIGTERM)
|
||||
void retryNow(double now) { nextSendAt_ = now; }
|
||||
void giveUp() { finished_ = true; }
|
||||
|
||||
bool delivered() const { return delivered_; } // the daemon is stopping (or was signalled)
|
||||
bool heard() const { return heard_; } // it has (or ran) our request
|
||||
bool finished() const { return finished_; } // no more attempts
|
||||
bool authRejected() const { return authRejected_; }
|
||||
int attempts() const { return attempts_; }
|
||||
StopSendResult last() const { return last_; }
|
||||
|
||||
private:
|
||||
bool owned_ = true;
|
||||
bool delivered_ = false;
|
||||
bool heard_ = false;
|
||||
bool finished_ = false;
|
||||
bool authRejected_ = false;
|
||||
int attempts_ = 0;
|
||||
double nextSendAt_ = 0.0;
|
||||
StopSendResult last_ = StopSendResult::Other;
|
||||
};
|
||||
|
||||
} // namespace daemon
|
||||
} // namespace dragonx
|
||||
+93
-222
@@ -2,13 +2,12 @@
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// xmrig_manager.cpp — Pool mining process management via drg-xmrig.
|
||||
// xmrig_manager.cpp — Pool mining process management via xmrig-hac.
|
||||
// Spawns xmrig, monitors via HTTP API, tracks hashrate and shares.
|
||||
|
||||
#include "xmrig_manager.h"
|
||||
#include "../resources/embedded_resources.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -23,8 +22,6 @@
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include "../util/logger.h"
|
||||
#include "../util/platform.h"
|
||||
#include "../util/pool_registry.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
@@ -90,32 +87,8 @@ static std::string getConfigDir() {
|
||||
// libcurl write callback
|
||||
static size_t curlWriteCb(void* ptr, size_t sz, size_t n, void* userdata) {
|
||||
auto* s = static_cast<std::string*>(userdata);
|
||||
const size_t add = sz * n;
|
||||
// Stats JSON (local xmrig HTTP API + pool API) is tiny; refuse an unbounded body from a hostile or
|
||||
// MITM'd endpoint so it can't grow this string until OOM. Returning < add aborts the transfer. (L-02)
|
||||
constexpr size_t kMaxStatsBytes = 1u << 20; // 1 MiB
|
||||
if (s->size() + add > kMaxStatsBytes) return 0;
|
||||
s->append(static_cast<char*>(ptr), add);
|
||||
return add;
|
||||
}
|
||||
|
||||
// True if `host` (already stripped of scheme+port) is a loopback/private/link-local/single-label target
|
||||
// that a public mining pool would never be — used to refuse a background stats GET to it (M-09).
|
||||
static bool hostLooksInternal(const std::string& host) {
|
||||
if (host.empty() || host == "localhost") return true;
|
||||
if (host.rfind("127.", 0) == 0 || host.rfind("10.", 0) == 0 ||
|
||||
host.rfind("192.168.", 0) == 0 || host.rfind("169.254.", 0) == 0) return true;
|
||||
if (host.rfind("172.", 0) == 0) { // 172.16.0.0 - 172.31.255.255
|
||||
const int second = std::atoi(host.c_str() + 4);
|
||||
if (second >= 16 && second <= 31) return true;
|
||||
}
|
||||
if (host.find(':') != std::string::npos) { // IPv6 literal: loopback / ULA / link-local
|
||||
if (host == "::1" || host.rfind("fc", 0) == 0 || host.rfind("fd", 0) == 0 ||
|
||||
host.rfind("fe80", 0) == 0) return true;
|
||||
}
|
||||
if (host.size() >= 6 && host.compare(host.size() - 6, 6, ".local") == 0) return true;
|
||||
if (host.find('.') == std::string::npos) return true; // bare single-label name = LAN/hosts, not a pool
|
||||
return false;
|
||||
s->append(static_cast<char*>(ptr), sz * n);
|
||||
return sz * n;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -125,14 +98,9 @@ static bool hostLooksInternal(const std::string& host) {
|
||||
XmrigManager::XmrigManager() = default;
|
||||
|
||||
XmrigManager::~XmrigManager() {
|
||||
should_stop_ = true;
|
||||
if (isRunning()) {
|
||||
stop(3000);
|
||||
}
|
||||
// Join a monitor thread left joinable by an unexpected xmrig exit (State::Error, so isRunning() is
|
||||
// false and stop() above was skipped) — std::thread's destructor would otherwise std::terminate(). (M-04)
|
||||
if (monitor_thread_.joinable())
|
||||
monitor_thread_.join();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -146,18 +114,32 @@ std::string XmrigManager::findXmrigBinary() {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Fallback: system PATH — windowless so it never flashes a console.
|
||||
// Fallback: system PATH
|
||||
#ifdef _WIN32
|
||||
std::string out = util::Platform::runHiddenCapture("where xmrig.exe");
|
||||
FILE* f = _popen("where xmrig.exe 2>nul", "r");
|
||||
#else
|
||||
std::string out = util::Platform::runHiddenCapture("which xmrig");
|
||||
FILE* f = popen("which xmrig 2>/dev/null", "r");
|
||||
#endif
|
||||
if (f) {
|
||||
char line[512];
|
||||
if (fgets(line, sizeof(line), f)) {
|
||||
std::string s(line);
|
||||
while (!s.empty() && (s.back() == '\n' || s.back() == '\r'))
|
||||
s.pop_back();
|
||||
if (!s.empty() && fs::exists(s)) {
|
||||
#ifdef _WIN32
|
||||
_pclose(f);
|
||||
#else
|
||||
pclose(f);
|
||||
#endif
|
||||
return s;
|
||||
}
|
||||
}
|
||||
#ifdef _WIN32
|
||||
_pclose(f);
|
||||
#else
|
||||
pclose(f);
|
||||
#endif
|
||||
{
|
||||
std::string s = out;
|
||||
const auto nl = s.find_first_of("\r\n"); // first line only
|
||||
if (nl != std::string::npos) s.erase(nl);
|
||||
while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) s.pop_back();
|
||||
if (!s.empty() && fs::exists(s)) return s;
|
||||
}
|
||||
|
||||
return {};
|
||||
@@ -173,10 +155,9 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath)
|
||||
|
||||
int hw = (int)std::thread::hardware_concurrency();
|
||||
if (hw < 1) hw = 1;
|
||||
// Use explicit thread count (not just a hint). One store: the UI thread reads it (getRequestedThreads).
|
||||
int t = (cfg.threads > 0) ? cfg.threads : std::max(1, hw / 2);
|
||||
if (t > hw) t = hw;
|
||||
threads_ = t;
|
||||
// Use explicit thread count (not just a hint)
|
||||
threads_ = (cfg.threads > 0) ? cfg.threads : std::max(1, hw / 2);
|
||||
if (threads_ > hw) threads_ = hw;
|
||||
|
||||
json j;
|
||||
j["autosave"] = false;
|
||||
@@ -225,43 +206,22 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath)
|
||||
|
||||
try {
|
||||
fs::create_directories(fs::path(outPath).parent_path());
|
||||
const std::string dumped = j.dump(4);
|
||||
#ifndef _WIN32
|
||||
// Create the config 0600 AT CREATION (open with mode) so the API token + wallet address are never
|
||||
// in a world/group-readable file — even for a local attacker who opened it in the old
|
||||
// create-then-chmod window and held the fd open across the chmod. (L-01)
|
||||
int fd = ::open(outPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (fd < 0) {
|
||||
setLastError("Cannot write xmrig config: " + outPath);
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
size_t off = 0;
|
||||
bool wrote = true;
|
||||
while (off < dumped.size()) {
|
||||
ssize_t nw = ::write(fd, dumped.data() + off, dumped.size() - off);
|
||||
if (nw <= 0) { wrote = false; break; }
|
||||
off += static_cast<size_t>(nw);
|
||||
}
|
||||
::close(fd);
|
||||
if (!wrote) {
|
||||
setLastError("Cannot write xmrig config: " + outPath);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
std::ofstream ofs(outPath, std::ios::trunc);
|
||||
std::ofstream ofs(outPath);
|
||||
if (!ofs.is_open()) {
|
||||
setLastError("Cannot write xmrig config: " + outPath);
|
||||
last_error_ = "Cannot write xmrig config: " + outPath;
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
ofs << dumped;
|
||||
ofs << j.dump(4);
|
||||
ofs.close();
|
||||
return true;
|
||||
|
||||
#ifndef _WIN32
|
||||
// 0600 permissions — only owner can read/write
|
||||
chmod(outPath.c_str(), 0600);
|
||||
#endif
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
setLastError(std::string("Config write error: ") + e.what());
|
||||
last_error_ = std::string("Config write error: ") + e.what();
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -273,22 +233,19 @@ bool XmrigManager::generateConfig(const Config& cfg, const std::string& outPath)
|
||||
|
||||
bool XmrigManager::start(const Config& cfg) {
|
||||
if (state_ == State::Running || state_ == State::Starting) {
|
||||
setLastError("Already running");
|
||||
last_error_ = "Already running";
|
||||
DEBUG_LOGF("[WARN] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
state_ = State::Starting;
|
||||
should_stop_ = false;
|
||||
setLastError(std::string());
|
||||
last_error_.clear();
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
process_output_.clear();
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(stats_mutex_);
|
||||
stats_ = PoolStats{};
|
||||
}
|
||||
|
||||
// Extract pool hostname for stats API queries
|
||||
{
|
||||
@@ -305,7 +262,7 @@ bool XmrigManager::start(const Config& cfg) {
|
||||
// Find binary
|
||||
std::string binary = findXmrigBinary();
|
||||
if (binary.empty()) {
|
||||
setLastError("xmrig binary not found");
|
||||
last_error_ = "xmrig binary not found";
|
||||
state_ = State::Error;
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: xmrig binary not found\n");
|
||||
return false;
|
||||
@@ -325,7 +282,7 @@ bool XmrigManager::start(const Config& cfg) {
|
||||
return false;
|
||||
}
|
||||
DEBUG_LOGF("[INFO] XmrigManager: config written to %s (API port %d, threads %d)\n",
|
||||
cfgPath.c_str(), api_port_, threads_.load());
|
||||
cfgPath.c_str(), api_port_, threads_);
|
||||
|
||||
// Spawn process
|
||||
if (!startProcess(binary, cfgPath, threads_)) {
|
||||
@@ -333,11 +290,7 @@ bool XmrigManager::start(const Config& cfg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Join a prior monitor thread before move-assigning: if xmrig exited unexpectedly, monitorProcess set
|
||||
// State::Error and returned, leaving monitor_thread_ joinable — move-assigning over a joinable
|
||||
// std::thread calls std::terminate() and aborts the whole wallet. (M-04)
|
||||
if (monitor_thread_.joinable())
|
||||
monitor_thread_.join();
|
||||
// Start monitor thread
|
||||
monitor_thread_ = std::thread(&XmrigManager::monitorProcess, this);
|
||||
state_ = State::Running;
|
||||
DEBUG_LOGF("[INFO] XmrigManager: started\n");
|
||||
@@ -356,6 +309,12 @@ void XmrigManager::stop(int wait_ms) {
|
||||
// Try graceful termination first
|
||||
TerminateProcess(process_handle_, 0);
|
||||
WaitForSingleObject(process_handle_, wait_ms);
|
||||
CloseHandle(process_handle_);
|
||||
process_handle_ = nullptr;
|
||||
}
|
||||
if (stdout_read_) {
|
||||
CloseHandle(stdout_read_);
|
||||
stdout_read_ = nullptr;
|
||||
}
|
||||
#else
|
||||
if (process_pid_ > 0) {
|
||||
@@ -380,29 +339,15 @@ void XmrigManager::stop(int wait_ms) {
|
||||
}
|
||||
process_pid_ = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (monitor_thread_.joinable())
|
||||
monitor_thread_.join();
|
||||
|
||||
// Close the pipe only after the monitor thread has finished with it (it
|
||||
// drains stdout); closing earlier let read() hit a closed/reused fd.
|
||||
#ifdef _WIN32
|
||||
if (process_handle_) {
|
||||
CloseHandle(process_handle_);
|
||||
process_handle_ = nullptr;
|
||||
}
|
||||
if (stdout_read_) {
|
||||
CloseHandle(stdout_read_);
|
||||
stdout_read_ = nullptr;
|
||||
}
|
||||
#else
|
||||
if (stdout_fd_ >= 0) {
|
||||
close(stdout_fd_);
|
||||
stdout_fd_ = -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (monitor_thread_.joinable())
|
||||
monitor_thread_.join();
|
||||
|
||||
state_ = State::Stopped;
|
||||
DEBUG_LOGF("[INFO] XmrigManager: stopped\n");
|
||||
}
|
||||
@@ -420,7 +365,7 @@ bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string&
|
||||
|
||||
HANDLE hRead = nullptr, hWrite = nullptr;
|
||||
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) {
|
||||
setLastError("CreatePipe failed");
|
||||
last_error_ = "CreatePipe failed";
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -452,7 +397,7 @@ bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string&
|
||||
char errBuf[256];
|
||||
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
NULL, err, 0, errBuf, sizeof(errBuf), NULL);
|
||||
setLastError("CreateProcess failed for xmrig (error " + std::to_string(err) + "): " + errBuf);
|
||||
last_error_ = "CreateProcess failed for xmrig (error " + std::to_string(err) + "): " + errBuf;
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\nCommand: %s\n", last_error_.c_str(), cmdLine.c_str());
|
||||
return false;
|
||||
}
|
||||
@@ -483,7 +428,6 @@ double XmrigManager::getMemoryUsageMB() const {
|
||||
}
|
||||
|
||||
void XmrigManager::drainOutput() {
|
||||
std::lock_guard<std::mutex> io_lk(io_mutex_);
|
||||
if (!stdout_read_) return;
|
||||
char buf[4096];
|
||||
DWORD avail = 0;
|
||||
@@ -504,14 +448,14 @@ void XmrigManager::drainOutput() {
|
||||
bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads) {
|
||||
int pipefd[2];
|
||||
if (pipe(pipefd) != 0) {
|
||||
setLastError("pipe() failed");
|
||||
last_error_ = "pipe() failed";
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
setLastError("fork() failed");
|
||||
last_error_ = "fork() failed";
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: %s\n", last_error_.c_str());
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
@@ -574,7 +518,7 @@ double XmrigManager::getMemoryUsageMB() const {
|
||||
#ifdef __APPLE__
|
||||
// macOS: use ps to read RSS for xmrig process
|
||||
char cmd[128];
|
||||
snprintf(cmd, sizeof(cmd), "ps -o rss= -p %d 2>/dev/null", (int)process_pid_.load(std::memory_order_relaxed));
|
||||
snprintf(cmd, sizeof(cmd), "ps -o rss= -p %d 2>/dev/null", process_pid_);
|
||||
FILE* fp = popen(cmd, "r");
|
||||
if (!fp) return 0.0;
|
||||
char line[64];
|
||||
@@ -587,7 +531,7 @@ double XmrigManager::getMemoryUsageMB() const {
|
||||
return mb;
|
||||
#else
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "/proc/%d/statm", (int)process_pid_.load(std::memory_order_relaxed));
|
||||
snprintf(path, sizeof(path), "/proc/%d/statm", process_pid_);
|
||||
FILE* fp = fopen(path, "r");
|
||||
if (!fp) return 0.0;
|
||||
long dummy = 0, pages = 0;
|
||||
@@ -601,7 +545,6 @@ double XmrigManager::getMemoryUsageMB() const {
|
||||
}
|
||||
|
||||
void XmrigManager::drainOutput() {
|
||||
std::lock_guard<std::mutex> io_lk(io_mutex_);
|
||||
if (stdout_fd_ < 0) return;
|
||||
char buf[4096];
|
||||
while (true) {
|
||||
@@ -623,7 +566,17 @@ void XmrigManager::drainOutput() {
|
||||
|
||||
void XmrigManager::appendOutput(const char* data, size_t len) {
|
||||
// Caller must hold output_mutex_
|
||||
appendBounded(process_output_, output_total_, data, len, 1024 * 1024); // 1 MB cap (1.25x hysteresis)
|
||||
static constexpr size_t MAX_OUTPUT = 1024 * 1024; // 1 MB cap
|
||||
process_output_.append(data, len);
|
||||
if (process_output_.size() > MAX_OUTPUT) {
|
||||
// Trim from the front at a newline boundary
|
||||
size_t cut = process_output_.size() - MAX_OUTPUT;
|
||||
auto pos = process_output_.find('\n', cut);
|
||||
if (pos != std::string::npos)
|
||||
process_output_.erase(0, pos + 1);
|
||||
else
|
||||
process_output_.erase(0, cut);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> XmrigManager::getRecentLines(int maxLines) const {
|
||||
@@ -673,7 +626,7 @@ void XmrigManager::monitorProcess() {
|
||||
if (GetExitCodeProcess(process_handle_, &exitCode) && exitCode != STILL_ACTIVE) {
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: process exited (code %lu)\n", exitCode);
|
||||
state_ = State::Error;
|
||||
setLastError("xmrig process exited unexpectedly");
|
||||
last_error_ = "xmrig process exited unexpectedly";
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -684,7 +637,7 @@ void XmrigManager::monitorProcess() {
|
||||
if (ret == process_pid_ || ret < 0) {
|
||||
DEBUG_LOGF("[ERROR] XmrigManager: process exited (waitpid=%d)\n", ret);
|
||||
state_ = State::Error;
|
||||
setLastError("xmrig process exited unexpectedly");
|
||||
last_error_ = "xmrig process exited unexpectedly";
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -712,8 +665,9 @@ void XmrigManager::monitorProcess() {
|
||||
// ============================================================================
|
||||
|
||||
void XmrigManager::pollStats() {
|
||||
// No-op on UI thread — stats and the stdout drain both run on the monitor
|
||||
// thread (a second reader here raced stop() closing the fd).
|
||||
// No-op on UI thread — stats are fetched by the monitor thread.
|
||||
// Just drain stdout so log lines stay fresh.
|
||||
drainOutput();
|
||||
}
|
||||
|
||||
void XmrigManager::fetchStatsHttp() {
|
||||
@@ -739,23 +693,18 @@ void XmrigManager::fetchStatsHttp() {
|
||||
headers = curl_slist_append(headers, authHeader.c_str());
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROXY, "*"); // 127.0.0.1 miner API: never via an env proxy
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 2000L); // 2s timeout — generous for loaded system
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 1000L); // 1s connect timeout
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); // Avoid signal issues in threads
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, &XmrigManager::abortCb);
|
||||
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
if (should_stop_.load()) return; // aborted by stop(): not a failure
|
||||
static int s_fail_count = 0;
|
||||
if (++s_fail_count <= 5 || s_fail_count % 30 == 0) {
|
||||
DEBUG_LOGF("[WARN] XmrigManager::pollStats: curl failed (%d): %s url=%s\n",
|
||||
@@ -769,11 +718,6 @@ void XmrigManager::fetchStatsHttp() {
|
||||
|
||||
std::lock_guard<std::mutex> lk(stats_mutex_);
|
||||
|
||||
// Miner version (top-level in /2/summary) — lets the UI show the actually
|
||||
// running miner's version even when no release tag was persisted (bundled miner).
|
||||
if (resp.contains("version") && resp["version"].is_string())
|
||||
stats_.version = resp["version"].get<std::string>();
|
||||
|
||||
if (resp.contains("hashrate") && resp["hashrate"].contains("total")) {
|
||||
auto& total = resp["hashrate"]["total"];
|
||||
if (total.is_array() && total.size() >= 3) {
|
||||
@@ -824,19 +768,10 @@ void XmrigManager::fetchStatsHttp() {
|
||||
void XmrigManager::fetchPoolApiStats() {
|
||||
if (state_ != State::Running || pool_host_.empty()) return;
|
||||
|
||||
// Resolve the stats endpoint + JSON schema for this pool. Known pools carry their
|
||||
// own API shape (pool.dragonx.is = custom /api/stats; pool.dragonx.cc = Miningcore
|
||||
// /api/pools); unknown/custom hosts fall back to the .is convention.
|
||||
const util::KnownPool* known = util::findKnownPoolByUrl(pool_host_);
|
||||
// SSRF guard: for an UNKNOWN (user-typed) pool host, don't let the wallet issue a background GET to a
|
||||
// loopback/private/link-local/single-label target — those aren't public mining pools, and a
|
||||
// paste-a-pool-config lure could otherwise point us at an internal host. Known pools use their trusted
|
||||
// registry statsUrl and are exempt. (M-09)
|
||||
if (!known && hostLooksInternal(pool_host_)) return;
|
||||
const std::string url = known ? known->statsUrl
|
||||
: ("https://" + pool_host_ + "/api/stats");
|
||||
|
||||
// Query the pool's public stats API
|
||||
std::string url = "https://" + pool_host_ + "/api/stats";
|
||||
std::string responseData;
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) return;
|
||||
|
||||
@@ -847,95 +782,31 @@ void XmrigManager::fetchPoolApiStats() {
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
// pool.dragonx.cc sits behind Cloudflare and 403s odd User-Agents.
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; ObsidianDragon)");
|
||||
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
|
||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, &XmrigManager::abortCb);
|
||||
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) return;
|
||||
|
||||
bool ok = false;
|
||||
const double poolHR = util::parsePoolHashrate(
|
||||
known ? known->schema : util::PoolStatsSchema::DragonXIs,
|
||||
responseData, known ? known->miningcorePoolId : std::string{}, ok);
|
||||
if (!ok) return;
|
||||
try {
|
||||
json resp = json::parse(responseData);
|
||||
|
||||
// Pool stats API format: { "pools": { "<name>": { "hashrate": ... } } }
|
||||
double poolHR = 0;
|
||||
if (resp.contains("pools") && resp["pools"].is_object()) {
|
||||
for (auto& [key, pool] : resp["pools"].items()) {
|
||||
if (pool.contains("hashrate") && pool["hashrate"].is_number()) {
|
||||
poolHR = pool["hashrate"].get<double>();
|
||||
break; // Use the first pool entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lk(stats_mutex_);
|
||||
stats_.pool_hashrate = poolHR;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Installed-miner version detection (`<binary> --version`, cached)
|
||||
// ============================================================================
|
||||
|
||||
namespace {
|
||||
std::mutex g_installed_ver_mutex;
|
||||
std::string g_installed_ver;
|
||||
std::atomic<bool> g_ver_detect_started{false};
|
||||
|
||||
// Extract the first "D.D[.D...]" version token from `--version` output (skips the
|
||||
// build date, which uses '-' separators). Returns e.g. "6.21.0", or "" if none.
|
||||
std::string parseMinerVersion(const std::string& out)
|
||||
{
|
||||
for (size_t i = 0; i < out.size(); ++i) {
|
||||
if (std::isdigit(static_cast<unsigned char>(out[i]))) {
|
||||
size_t j = i;
|
||||
int dots = 0;
|
||||
while (j < out.size() &&
|
||||
(std::isdigit(static_cast<unsigned char>(out[j])) || out[j] == '.')) {
|
||||
if (out[j] == '.') ++dots;
|
||||
++j;
|
||||
} catch (...) {
|
||||
// Malformed response — ignore
|
||||
}
|
||||
if (dots >= 1 && (j - i) >= 3) {
|
||||
// Include a trailing build suffix like "-hac" / "-drg1" (e.g. "6.25.1-hac"),
|
||||
// matching what the running miner's API reports.
|
||||
size_t end = j;
|
||||
if (end < out.size() && out[end] == '-') {
|
||||
size_t k = end + 1;
|
||||
while (k < out.size() && std::isalnum(static_cast<unsigned char>(out[k]))) ++k;
|
||||
if (k > end + 1) end = k;
|
||||
}
|
||||
return out.substr(i, end - i);
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void XmrigManager::startVersionDetection()
|
||||
{
|
||||
if (g_ver_detect_started.exchange(true)) return; // one-shot
|
||||
std::thread([]() {
|
||||
const std::string bin = findXmrigBinary();
|
||||
std::string ver;
|
||||
// Don't hand a path containing shell/cmd metacharacters to popen()'s shell — bin is normally an
|
||||
// app-controlled path, but this closes command injection if it ever isn't. (M-10)
|
||||
// Reject only chars that stay shell-special INSIDE the double-quotes we wrap bin in ("\"" + bin + "\"")
|
||||
// on cmd.exe or /bin/sh. Parens are inert when quoted, so they're excluded — otherwise common Windows
|
||||
// paths like "C:\Program Files (x86)\..." would be rejected and version detection would silently fail. (M-10)
|
||||
const bool binShellSafe =
|
||||
!bin.empty() && bin.find_first_of("\"'`$;&|<>^%\n\r") == std::string::npos;
|
||||
if (binShellSafe) {
|
||||
// Windowless capture (mergeStderr: xmrig may print --version to stderr) — never flashes.
|
||||
const std::string cmd = "\"" + bin + "\" --version";
|
||||
const std::string out = util::Platform::runHiddenCapture(cmd, /*mergeStderr=*/true);
|
||||
if (!out.empty()) ver = parseMinerVersion(out);
|
||||
}
|
||||
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
|
||||
g_installed_ver = ver;
|
||||
}).detach();
|
||||
}
|
||||
|
||||
std::string XmrigManager::installedVersion()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
|
||||
return g_installed_ver;
|
||||
}
|
||||
|
||||
} // namespace daemon
|
||||
|
||||
+14
-33
@@ -11,8 +11,6 @@
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
#include "output_window.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
@@ -45,7 +43,6 @@ public:
|
||||
double pool_diff = 0;
|
||||
std::string pool_url;
|
||||
std::string algo;
|
||||
std::string version; // miner version reported by the running xmrig API
|
||||
bool connected = false;
|
||||
// Memory usage
|
||||
int64_t memory_free = 0; // bytes
|
||||
@@ -88,10 +85,8 @@ public:
|
||||
bool isRunning() const;
|
||||
State getState() const { return state_.load(std::memory_order_relaxed); }
|
||||
|
||||
// Return COPIES under lock: stats_ and last_error_ are mutated by the monitor thread while the UI
|
||||
// thread reads them, so handing out a reference is a torn-read / use-after-free race (M-03, M-04).
|
||||
PoolStats getStats() const { std::lock_guard<std::mutex> lk(stats_mutex_); return stats_; }
|
||||
std::string getLastError() const { std::lock_guard<std::mutex> lk(error_mutex_); return last_error_; }
|
||||
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.
|
||||
@@ -108,7 +103,13 @@ public:
|
||||
*/
|
||||
std::string getOutputSince(size_t& offset) const {
|
||||
std::lock_guard<std::mutex> lk(output_mutex_);
|
||||
return outputWindowSince(process_output_, output_total_, offset);
|
||||
if (offset >= process_output_.size()) {
|
||||
offset = process_output_.size();
|
||||
return {};
|
||||
}
|
||||
std::string result = process_output_.substr(offset);
|
||||
offset = process_output_.size();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,57 +137,37 @@ 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);
|
||||
void monitorProcess();
|
||||
void drainOutput();
|
||||
void appendOutput(const char* data, size_t len);
|
||||
// Set last_error_ under error_mutex_ (writers run on both the main thread and the monitor thread).
|
||||
void setLastError(std::string e) { std::lock_guard<std::mutex> lk(error_mutex_); last_error_ = std::move(e); }
|
||||
void fetchStatsHttp(); // Blocking HTTP call — runs on monitor thread only
|
||||
// curl xfer-info callback: abort an in-flight stats fetch once stop() is requested.
|
||||
static int abortCb(void* p, int64_t, int64_t, int64_t, int64_t)
|
||||
{ return static_cast<const XmrigManager*>(p)->should_stop_.load() ? 1 : 0; }
|
||||
void fetchPoolApiStats(); // Fetch pool-side stats (hashrate) from pool HTTP API
|
||||
|
||||
std::atomic<State> state_{State::Stopped};
|
||||
std::string last_error_;
|
||||
mutable std::mutex error_mutex_; // guards last_error_ (written by main + monitor threads)
|
||||
|
||||
mutable std::mutex output_mutex_;
|
||||
std::string process_output_;
|
||||
std::size_t output_total_ = 0; // bytes ever appended (absolute end; guarded by output_mutex_, kept across restarts)
|
||||
|
||||
// xmrig HTTP API credentials (random per session)
|
||||
int api_port_ = 0;
|
||||
std::string api_token_;
|
||||
std::atomic<int> threads_{0}; // Thread count for mining (written on the mining-control thread, read by the UI)
|
||||
int threads_ = 0; // Thread count for mining
|
||||
std::string pool_host_; // Pool hostname for stats API
|
||||
PoolStats stats_;
|
||||
mutable std::mutex stats_mutex_;
|
||||
|
||||
// Process handles
|
||||
#ifdef _WIN32
|
||||
std::atomic<HANDLE> process_handle_{nullptr};
|
||||
std::atomic<HANDLE> stdout_read_{nullptr};
|
||||
HANDLE process_handle_ = nullptr;
|
||||
HANDLE stdout_read_ = nullptr;
|
||||
#else
|
||||
std::atomic<pid_t> process_pid_{0};
|
||||
std::atomic<int> stdout_fd_{-1};
|
||||
pid_t process_pid_ = 0;
|
||||
int stdout_fd_ = -1;
|
||||
#endif
|
||||
std::mutex io_mutex_; // serializes drainOutput(); lock order io_mutex_ -> output_mutex_
|
||||
|
||||
std::thread monitor_thread_;
|
||||
std::atomic<bool> should_stop_{false};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user