Compare commits
356 Commits
v1.2.0-rc1
...
b75801f5c4
| Author | SHA1 | Date | |
|---|---|---|---|
| b75801f5c4 | |||
| 6ddca6aed0 | |||
| ca1ac22b17 | |||
| 6bfb56aeb9 | |||
| 6c08a08127 | |||
| f5bde67f64 | |||
| 516e5ec688 | |||
| 008cc82ee5 | |||
| 2e6188a36d | |||
| e191680782 | |||
| 4db609fb52 | |||
| 980a100edd | |||
| 46eec37013 | |||
| 1738468f8c | |||
| ba03de938e | |||
| d043538e2f | |||
| 2502487e44 | |||
| cec330ee47 | |||
| 0ff56bf5a4 | |||
| ddd53dc006 | |||
| ee59a9e2cd | |||
| 62998179ec | |||
| 061db16004 | |||
| ec48538881 | |||
| 658c0f355b | |||
| 41f5548593 | |||
| 1dbbd44759 | |||
| b0cc6bcef4 | |||
| dd9bc0bd2b | |||
| b2a8c4487e | |||
| 65bb98cd09 | |||
| 129a8e6449 | |||
| df14533ad3 | |||
| 24bb32eb61 | |||
| d3dcb5a015 | |||
| 7249d11899 | |||
| ee6903ae56 | |||
| 1c5bc0d4d6 | |||
| dfca9bbd92 | |||
| 840ead3645 | |||
| 1c8e97c0a4 | |||
| f1e66c08d1 | |||
| c83348ab63 | |||
| da56bb73a0 | |||
| ad59d62b82 | |||
| 07f6d2efc9 | |||
| e84cea27d9 | |||
| a457f0262e | |||
| 4c466d78d1 | |||
| 0d4eaa02e7 | |||
| dddf1b6ec7 | |||
| a523611779 | |||
| 7c90d72ded | |||
| bb2337b552 | |||
| 19e31388d9 | |||
| 30139a5698 | |||
| 94b57e91e3 | |||
| 10289b33b5 | |||
| 42df658275 | |||
| 9d2c4a8ad3 | |||
| 65c06d0e6b | |||
| c7f89e5cd3 | |||
| 34ded6b441 | |||
| 0ea423dcea | |||
| 61738ef24a | |||
| ccbb0783b5 | |||
| c6b32e86c0 | |||
| 5caffc84b0 | |||
| ebf01bd858 | |||
| d91be3b34e | |||
| 4b46d4a20c | |||
| 69cd8ba048 | |||
| 4543cdc495 | |||
| 71fb81670f | |||
| a092097141 | |||
| 50873d1112 | |||
| 7e8628f94c | |||
| 0ae52ee095 | |||
| 529a74f6a7 | |||
| 88f1415bcc | |||
| b8cfc1c79c | |||
| a0fd9aff82 | |||
| 14b08d1358 | |||
| d689b02de9 | |||
| 84498b1312 | |||
| 8c2b0521a6 | |||
| 4411cb45e2 | |||
| 7db6564a0e | |||
| faf8e4e430 | |||
| 1affea0273 | |||
| 32d8d5663a | |||
| 3568b12dbf | |||
| e74989ec2c | |||
| fe18628216 | |||
| 4dc6b2ef8f | |||
| 5afb339ba7 | |||
| 0e8816e220 | |||
| 2035f71c65 | |||
| d007f3de39 | |||
| 1fdb6656aa | |||
| 3305be6318 | |||
| 19482214f1 | |||
| 4aed72c73b | |||
| 3e6987d72f | |||
| 42c511b13a | |||
| ef9141c248 | |||
| 8c88afe16e | |||
| d48c6db21c | |||
| ced1773e70 | |||
| d76c109063 | |||
| 5aa3cc4d51 | |||
| 4e15a2b1ba | |||
| 021ba88522 | |||
| c1d5f79502 | |||
| db212a81c6 | |||
| 67f6156030 | |||
| 76ed09fad0 | |||
| 77219c5ce7 | |||
| b26aff392b | |||
| b0ded528b1 | |||
| 301b3df77e | |||
| ae4e6486c6 | |||
| d838243caa | |||
| 75f33e41eb | |||
| 1263058ba7 | |||
| 9bcdf51895 | |||
| 67feaea767 | |||
| 3e1cdd15f3 | |||
| af2db4366c | |||
| cb74fbbff0 | |||
| 121a79e768 | |||
| 262891229a | |||
| dcd09dc542 | |||
| 4635a56e89 | |||
| 9c533fa485 | |||
| de11850c74 | |||
| 280a71e973 | |||
| 7891c689fb | |||
| f8150434d4 | |||
| 530bf24abf | |||
| e3b53258b7 | |||
| f9e4db4abf | |||
| bdccddfeee | |||
| d68bc27aac | |||
| 51667b5a27 | |||
| 96e05b4ba6 | |||
| 36386b2b84 | |||
| 2fc577fb18 | |||
| 5180f74a68 | |||
| fc5ce019e2 | |||
| 7826be4b13 | |||
| 5d6e78c719 | |||
| a1e4acfe86 | |||
| 419f8bb9cb | |||
| c5ef9e42ed | |||
| f0243307f1 | |||
| 95ddfa05ee | |||
| ecc0043559 | |||
| 3a9ff5f021 | |||
| 1faaa54a07 | |||
| 61f8c54742 | |||
| 48fc9ba8bb | |||
| 2552234610 | |||
| ff20a8a44e | |||
| 245d9bf976 | |||
| 1c248d727a | |||
| 57291ef28c | |||
| 18c1de87db | |||
| 0cdbbd024e | |||
| 779c9682d4 | |||
| 33735b1d52 | |||
| 19e83eb12a | |||
| f12153cdd0 | |||
| 8abc0ec113 | |||
| 5a677f6202 | |||
| efec3a50a1 | |||
| 4ed69d4e9f | |||
| e0d6d6bc4d | |||
| e9121fe49e | |||
| b5001ad4db | |||
| 1266b6e68e | |||
| 57f52b7a6c | |||
| a6964621a8 | |||
| 14f5fdc559 | |||
| 1a00ec3359 | |||
| 5c490c8d53 | |||
| d0586e5e75 | |||
| 24e8fb4942 | |||
| 1393d9ae1a | |||
| 3ce62326f9 | |||
| b20e7efb16 | |||
| 4473e7e00a | |||
| 2e8e214689 | |||
| 69a6fb3e64 | |||
| ed6120eef9 | |||
| a5bd2dadd7 | |||
| 25ee1496b4 | |||
| e2bc3623b6 | |||
| b32fe07cb1 | |||
| a0532275dd | |||
| 7df00b0909 | |||
| 090e288c44 | |||
| 6544c10ac1 | |||
| b2e104358d | |||
| de70e68472 | |||
| 0fe12d65df | |||
| 37c8287a12 | |||
| 6ff80354df | |||
| f58d009703 | |||
| 0e2c786ebf | |||
| d54c7f9e11 | |||
| b3251e9244 | |||
| c40f4d5815 | |||
| 5547ab1cac | |||
| f0867084f3 | |||
| 5167b52cbd | |||
| e7d11f620a | |||
| 168cae9306 | |||
| 317d9028a3 | |||
| 09ab8d52c5 | |||
| 101c835c46 | |||
| c71c3c3378 | |||
| 2e115aef39 | |||
| 574307f6ac | |||
| 88851f5eea | |||
| 5796664b51 | |||
| b71f8ae0a8 | |||
| 555f541c84 | |||
| 2b70ee5cd8 | |||
| d8c055c125 | |||
| 2470675746 | |||
| 3a597482da | |||
| b9b2d469d4 | |||
| 25fef8ff4d | |||
| bc788d008e | |||
| 9ee8f9a43b | |||
| bf91c4eb6c | |||
| 4d78ca0d7d | |||
| 00ee61fe64 | |||
| 3a4998f57c | |||
| d27017daeb | |||
| 2182c060e6 | |||
| 4ee830c5dd | |||
| b6567ee196 | |||
| 1a8d6fd30f | |||
| 9389859ee9 | |||
| e63aba6959 | |||
| fa240e7b99 | |||
| e21f7bf8aa | |||
| 47f228fa47 | |||
| 9f82bba260 | |||
| 323cb341f1 | |||
| ee6cac41c4 | |||
| 094771af81 | |||
| e40962cdf2 | |||
| 63b3a04716 | |||
| cf77c6cbe0 | |||
| 560f2bcf91 | |||
| 255d9399fa | |||
| 1fb6dc44d9 | |||
| 2ba8a799ff | |||
| 41b380449e | |||
| 4a65dce947 | |||
| c8183241c3 | |||
| 0bf80d2757 | |||
| 6f9123f651 | |||
| 320c659689 | |||
| 6ff1fda870 | |||
| 8c51b092f8 | |||
| 788d71a549 | |||
| 3d4b013b0c | |||
| dc07491abb | |||
| 7e568e4bf1 | |||
| 85a1080b52 | |||
| dbeae3ac98 | |||
| 9ff5508989 | |||
| 79e5adcbd3 | |||
| 6531d0c4d2 | |||
| 142a6826af | |||
| 070a516f4e | |||
| 3cec333d84 | |||
| fc438ab962 | |||
| 8c2c1c2aaf | |||
| a605e35409 | |||
| 6ed80d2d79 | |||
| e978db85ca | |||
| e00772db6e | |||
| 8f22db5eea | |||
| 5c883d4b91 | |||
| 274f7ea1af | |||
| 3799330bb0 | |||
| 53a10e149d | |||
| 1bc7f5c8cd | |||
| 20b22410e9 | |||
| 7195c25376 | |||
| a6921bca60 | |||
| 8ba4233b9b | |||
| 732d892d4d | |||
| afd612be7e | |||
| 4d769c8719 | |||
| 0e1b19d0f2 | |||
| b24212fb8f | |||
| 64fe8fc6c9 | |||
| b9881278af | |||
| 85b53baeaf | |||
| eece57c025 | |||
| 8765fdf362 | |||
| 98e0cce8ec | |||
| 5c87bc6e87 | |||
| 946958b591 | |||
| f5561c0dac | |||
| b3c2282b53 | |||
| ca14aaddc7 | |||
| a5da5562cf | |||
| c676ec8287 | |||
| f474b0d633 | |||
| af252575cf | |||
| 74bd22958a | |||
| cd60bded9f | |||
| 59b8c4da81 | |||
| 950d7ace50 | |||
| d52d3d1b7f | |||
| 9569b0ba43 | |||
| 50b0419dfe | |||
| 4f7a4fb38e | |||
| f511c0d509 | |||
| 235504657d | |||
| 76c2ac5db8 | |||
| 89bd21018a | |||
| 9d7054b245 | |||
| 70608fcb7a | |||
| 0c819afdd4 | |||
| db4778e6a7 | |||
| eb6114ee19 | |||
| 5d317f6be3 | |||
| a677c09984 | |||
| 6611d57147 | |||
| 6a4e98b7ed | |||
| 4b9d6f7db5 | |||
| 043cdc7128 | |||
| 2daea67a1e | |||
| e6b91ca661 | |||
| c6e28fc4da | |||
| 268eba6321 | |||
| 3119440cd9 | |||
| 59c55e33f8 | |||
| a8b5d2f6a3 | |||
| 012341b1a4 | |||
| 5586f334a4 | |||
| 863d015628 | |||
| a78a13edf3 | |||
| e95ad50e41 | |||
| 975743f754 | |||
| 948ef419ac | |||
| 9edab31728 | |||
| ee8a08e569 |
13
.gitignore
vendored
13
.gitignore
vendored
@@ -41,3 +41,16 @@ asmap.dat
|
|||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
# Local-only archive of superseded lite-wallet design/planning docs (untracked)
|
||||||
|
docs/_archive/
|
||||||
|
|
||||||
|
# ed25519 release-signing keys — the secret key must NEVER be committed
|
||||||
|
*.ed25519.key
|
||||||
|
*.ed25519.pub.b64
|
||||||
|
|
||||||
|
|
||||||
|
# Lite-backend deps are fetched (or `cargo vendor`-ed locally for offline); not committed.
|
||||||
|
third_party/silentdragonxlite/lib/vendor/
|
||||||
|
|
||||||
|
# Generated by configure_file from res/ObsidianDragon.manifest.in (do not track)
|
||||||
|
res/ObsidianDragon.manifest
|
||||||
|
|||||||
102
CLAUDE.md
Normal file
102
CLAUDE.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
|
||||||
|
ObsidianDragon is a portable, full-node GUI wallet for DragonX (DRGX), written in C++17 using SDL3 + Dear ImGui (immediate-mode). It drives a `dragonxd` full node over JSON-RPC and can embed/extract the daemon itself. A separate **Lite** variant (`ObsidianDragonLite`) drops the full node and instead talks to an external lite-wallet backend library.
|
||||||
|
|
||||||
|
## Build & run
|
||||||
|
|
||||||
|
`build.sh` is the single entry point for all builds. `setup.sh` (repo root) installs/validates dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh # Dev build (native, no packaging) -> build/linux/bin/ObsidianDragon
|
||||||
|
./build.sh --lite # Dev build of the Lite variant -> build/linux/bin/ObsidianDragonLite
|
||||||
|
./build.sh --clean # Wipe the build dir first
|
||||||
|
./build.sh --linux-release # Release zip + AppImage -> release/linux/
|
||||||
|
./build.sh --win-release # Windows cross-compile (mingw-w64) -> release/windows/
|
||||||
|
./build.sh --mac-release # macOS .app bundle + DMG
|
||||||
|
./setup.sh --check # Report missing build deps without installing
|
||||||
|
```
|
||||||
|
|
||||||
|
Dev builds use `build/linux/` (or `build/mac/`). To re-build incrementally without re-running CMake config: `cmake --build build/linux -j$(nproc)`.
|
||||||
|
|
||||||
|
The wallet connects to the daemon using credentials in `~/.hush/DRAGONX/DRAGONX.conf` (`rpcuser`/`rpcpassword`/`rpcport`). It searches for `dragonxd`/`dragonx-cli` binaries in the **executable's own directory first**, so dropping custom node builds next to the wallet binary overrides the bundled ones.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Tests live in `tests/test_phase4.cpp` — a single large translation unit using a custom assertion harness (`EXPECT_TRUE`/`EXPECT_EQ`/`EXPECT_NEAR` macros, one `main()`, exit code = failure count). `include(CTest)` enables `BUILD_TESTING=ON` by default, so the `ObsidianDragonTests` executable is built alongside the app.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd build/linux && ctest --output-on-failure # run the suite
|
||||||
|
./build/linux/bin/ObsidianDragonTests # run the binary directly (same thing)
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no per-test filtering — it is one binary that runs every assertion. The suite exercises the services layer, lite-wallet bridge, and pure helpers (parsers, formatters, model classes) without launching the GUI. Fixtures are under `tests/fixtures/` (path injected as `DRAGONX_TEST_FIXTURE_DIR`).
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Entry & main loop.** `src/main.cpp` owns SDL3 window creation, ImGui/OpenGL(or DX11 on Windows) setup, and the frame loop. The `App` class is the central controller; because it is large it is split across four files that all implement the same class:
|
||||||
|
- `src/app.cpp` — core lifecycle, the per-frame `render()`, tab dispatch
|
||||||
|
- `src/app_network.cpp` — RPC orchestration, sync, peers, daemon lifecycle
|
||||||
|
- `src/app_security.cpp` — encryption, PIN/lock screen, key import/export, backup
|
||||||
|
- `src/app_wizard.cpp` — first-run wizard
|
||||||
|
|
||||||
|
**RPC.** All daemon calls go through `src/rpc/` (`rpc_client`, `connection`, `rpc_worker`). **Never block the main/UI thread with synchronous network I/O — dispatch through `RPCWorker`** (async). `rpc/types.h` holds the shared DTOs.
|
||||||
|
|
||||||
|
**Services** (`src/services/`) hold the non-UI state machines that the `App` owns: `NetworkRefreshService` + `RefreshScheduler` (polling/refresh of balance, peers, txs on intervals) and the `WalletSecurity*` controller/workflow stack (encryption & unlock flows).
|
||||||
|
|
||||||
|
**Data model** (`src/data/`): `WalletState`, `address_book`, `transaction_history_cache`, `exchange_info`. UI reads from these.
|
||||||
|
|
||||||
|
**UI** (`src/ui/`): `windows/` are the tabs and dialogs (one pair per screen, e.g. `send_tab`, `mining_tab`, `console_tab`), `pages/` are multi-section screens (Settings), `material/` is the design-system layer (the live helpers `color_theme`, `colors`, `type`/`typography`, `draw_helpers`, `layout`, `project_icons`, `components/buttons`), `schema/` loads the TOML UI schema/skins, `effects/` is GL post-processing (blur/acrylic).
|
||||||
|
|
||||||
|
**Lite wallet** (`src/wallet/`): the bridge to an external `litelib_*` C-ABI backend. `lite_client_bridge` loads the backend (via direct `litelib_*` externs in `linkedSdxl()`) and owns each Rust string through `lite_owned_string` (copy-before-free / free-once). On top sit `lite_connection_service`, `lite_sync_service`, `lite_result_parsers`, `lite_wallet_gateway`, `lite_wallet_state_mapper`, and `lite_wallet_lifecycle_service`, all driven by `lite_wallet_controller`. The real frontend entry points are `lite_wallet_lifecycle_ui_adapter` and `lite_wallet_server_selection_adapter` (used by `src/ui/pages/settings_page.cpp`); everything else is reachable through them. (The prebuilt-backend symbol check for `DRAGONX_ENABLE_LITE_BACKEND` is done in CMake against the symbols inventory — see below — not in C++.)
|
||||||
|
|
||||||
|
> ⚠️ **Do not regrow the `_plan`/`_batch` churn.** This directory previously held ~160 dead `lite_wallet_*_plan` / `*_batch*_receipt_custody_acceptance_confirmation_archive_handoff_*` files (filenames up to 250 chars) — auto-generated scaffolding that never reached the shipping binary. They were deleted. When extending lite-wallet behavior, **edit the named service/bridge/runtime files in place**; never add another "promotion/receipt/custody/handoff/stewardship" wrapper layer. `scripts/check-source-hygiene.sh` (wired as a `.git/hooks/pre-commit` hook) blocks >80-char filenames and chained churn-token names — run it in CI too.
|
||||||
|
|
||||||
|
**Chat** (`src/chat/*`): the HushChat protocol port (Contacts/Chat tabs, seed-derived identity, secretstream crypto, seed-encrypted sqlite store, two-variant send/receive transport). Runtime behavior is gated by `DRAGONX_ENABLE_CHAT`, now **default ON** (the sources always compile; the flag folds the feature away at runtime via `hushChatFeatureEnabledAtBuild()`). Full-node chat derives its identity from the wallet's mnemonic (`z_exportmnemonic`, portable/SDXLite-compatible) or, for legacy/non-mnemonic wallets, a stable z-address spending key (`z_exportkey`).
|
||||||
|
|
||||||
|
## Build variants & feature gating
|
||||||
|
|
||||||
|
Variants are selected with CMake options (set by `build.sh` flags), surfaced to C++ as compile definitions:
|
||||||
|
- `DRAGONX_BUILD_LITE` (`--lite`) → `DRAGONX_LITE_BUILD` define; renames the app to `ObsidianDragonLite` and excludes embedded-daemon / full-node assets (Sapling params, asmap, dragonxd).
|
||||||
|
- `DRAGONX_ENABLE_LITE_BACKEND` → links a real external lite backend. Requires `--lite`, link mode `imported`, ABI `sdxl-c-v1`, and a symbols inventory file (built by `scripts/build-lite-backend-artifact.sh`); CMake hard-fails if any required `litelib_*` symbol is missing. The backend **source is vendored in-tree** at `third_party/silentdragonxlite/` — the `qtlib` C-ABI wrapper (`lib/`, produces `libsilentdragonxlite.a`) and the `silentdragonxlitelib` core (`silentdragonxlite-cli/lib/`, with `proto/` + `res/`). `build-lite-backend-artifact.sh` defaults `--backend-dir` there, so the lite wallet builds **without** the upstream SilentDragonXLite repo. External build inputs are limited to the **Rust toolchain (rustc/cargo 1.63)** plus two project-controlled sources on `git.dragonx.is`: the librustzcash crates come from the mirror `git.dragonx.is/DragonX/librustzcash` (the 6 `git =` deps in the core `Cargo.toml`, pinned to rev `acff1444…`), and the **Sapling params are not committed** (gitignored) — the build fetches them from the `git.dragonx.is/DragonX/zcash-params` release `sapling-v1` and verifies their SHA-256 before rust-embed bakes them in (`ensure_sapling_params`; override the URL with `SAPLING_PARAMS_BASE_URL`). Other crate deps come from crates.io. For a fully offline build, `cargo vendor` into `third_party/silentdragonxlite/lib/vendor/` and add a `vendored-sources` redirect to `lib/.cargo/config.toml` (the build script symlinks `vendor/` into its prepared dir if present); `vendor/` is gitignored.
|
||||||
|
- `DRAGONX_ENABLE_CHAT` → `DRAGONX_ENABLE_CHAT` define gating the chat module.
|
||||||
|
|
||||||
|
Guard full-node-only code paths with `#if DRAGONX_LITE_BUILD` / chat code with `DRAGONX_ENABLE_CHAT`.
|
||||||
|
|
||||||
|
## Lite wallet status
|
||||||
|
|
||||||
|
The Lite variant is **functionally complete and runtime-verified on Linux + Windows** (work lives on branch `cleanup/lite-plan-churn`, **local-only — not pushed yet**):
|
||||||
|
- **Implemented:** lifecycle (create/open/restore + auto-open on startup), sync, refresh, send / shield / import / export / seed, persistence (the backend does *not* auto-save after sync/send/shield — the controller triggers `save` at those points), and passphrase **encryption** (encrypt/unlock/lock/decrypt + Settings UI + send-time & startup unlock; the backend locks immediately on `encrypt`). All controller-tested against the fake backend (`tests/fake_lite_backend.h`) and smoke-verified against the real SDXL backend via `tools/lite_smoke` (incl. a full sync). GUI is wired end-to-end with lite-appropriate wording; the full-node RPC connect loop / wizard / daemon strings are gated out of lite (lite "online" is derived from `lite_wallet_->walletOpen()`, not RPC).
|
||||||
|
- **Packaging:** `./build.sh --lite-backend --linux-release` (zip + AppImage, **verified**) and `--win-release` (cross-compiled `.exe`, **verified**; first build the Windows backend artifact with `scripts/build-lite-backend-artifact.sh --platform windows`). macOS `--lite-backend --mac-release` is **wired but not yet verified on this Linux box** (needs macOS/osxcross): the `.app`/launcher/rpath/`CFBundleExecutable` follow `ObsidianDragonLite`, full-node assets are skipped, and the lite variant gets its own `CFBundleName` ("DragonX Wallet Lite"), bundle id (`is.hush.dragonx.lite`), and DMG name so it can coexist with the full-node app. All variants correctly exclude full-node assets.
|
||||||
|
- **Rollout / kill-switch (implemented):** `wallet/lite_rollout_policy.{h,cpp}` is a pure, fail-open gate (local-only, no network) feeding `LiteWalletLifecycleService::availability()` (new `RolloutDisabled` reason). Inputs: the emergency env var `DRAGONX_LITE_KILL_SWITCH` (absolute — not even `force_on` bypasses it); a `lite_rollout` setting (`auto`/`force_on`/`force_off`); and an optional **locally-cached** manifest at `<config-dir>/lite_rollout.json` (`global_enabled`, `min_version`/`max_version`, `blocked_versions`, `rollout_permille`, `message`) keyed for staged rollout on a hashed, never-transmitted per-install id. A signed remote fetcher can populate that cache later without touching the policy. Resolved in `App::rebuildLiteWallet()`; the disable message surfaces via the lifecycle status. Unit-tested + runtime-verified (env / manifest / control).
|
||||||
|
- **Remaining (M5b):** verify the wired macOS `--lite` packaging on a Mac/osxcross, CI backend-artifact build + signing.
|
||||||
|
- **To publish:** rename branch → `feat/lite-wallet`, base the PR on `dev` (the full-node UX is already there), and handle the dormant gated-OFF HushChat content bundled in commit `af06b8b`.
|
||||||
|
|
||||||
|
The detailed milestone plan and design history (the v2 plan, backend artifact/ABI/signing design docs, the v1 plan, chat specs, etc.) are kept **untracked** under `docs/_archive/`.
|
||||||
|
|
||||||
|
## Miner updater (xmrig)
|
||||||
|
|
||||||
|
The mining tab's pool section has an **"Update miner…"** button that downloads/verifies/installs the latest DRG-XMRig from the project Gitea (`util/XmrigUpdater` + `ui/windows/xmrig_download_dialog.h`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest` → pick the asset for this platform (`linux-x64` / `win-x64` / `macos-x86_64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (from the release body) **and** a detached **ed25519 signature** → miniz-extract the binary (flattening the versioned subdir) into `resources::getDaemonDirectory()`. The whole archive is verified, so extracted members are trusted by transitivity (no per-member hash check). The pure, no-I/O core is split into `xmrig_updater_core.cpp` for unit tests; an env-gated (`DRAGONX_TEST_NETWORK=1`) test exercises the worker live. A **"Browse all releases…"** button (the `/releases` list, newest first, pre-releases included) lets users pin an older or pre-release build — same verify/install path via `startInstallRelease()`; the picker UI is shared with the daemon updater (`ui/windows/release_list_view.h`).
|
||||||
|
|
||||||
|
**Signature verification is enforced** (`kXmrigRequireSignature = true` in `src/util/xmrig_updater.h`), checked against the public key pinned in `kXmrigSignaturePublicKeyBase64`. **Consequence for releases:** every `drg-xmrig` release MUST ship a detached signature per archive or the in-app updater refuses it. To cut a release: build the archives, then `scripts/sign-xmrig-release.sh sign <secret.key> <archive.zip>...` (OpenSSL-based, no extra deps) and upload each `<archive>.sig` as a release asset alongside its `.zip`. The signing **secret key must stay offline** (it is gitignored: `*.ed25519.key`); only its base64 public key is pinned in the source. To rotate the key, regenerate (`scripts/sign-xmrig-release.sh keygen`) and update `kXmrigSignaturePublicKeyBase64`. An emergency env override is not provided — disabling verification means setting `kXmrigSignaturePublicKeyBase64` empty (and rebuilding).
|
||||||
|
|
||||||
|
## Daemon updater (dragonxd)
|
||||||
|
|
||||||
|
Settings → **NODE & SECURITY → DAEMON BINARY** has a **"Check for updates…"** button that downloads/verifies/installs the latest **dragonxd full node** from the project Gitea — the full-node sibling of the xmrig updater (`util/DaemonUpdater` + `ui/windows/daemon_download_dialog.h`, pure no-I/O core in `daemon_updater_core.cpp`; gated full-node-only via `supportsFullNodeLifecycleActions()`). Flow: query `git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/latest` → pick the archive for this platform (`linux-amd64` / `macos` / `win64`; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive **SHA-256** (parsed from the release body's markdown **checksum table**, not xmrig's `<hash> <name>` lines) **and** a detached **ed25519 signature** → miniz-extract the three executables (`dragonxd`/`dragonx-cli`/`dragonx-tx`, flattening the versioned subdir) into `resources::getDaemonDirectory()`. The archive also bundles Sapling params/asmap, which the updater deliberately leaves to the wallet's own resource extraction. Install is **atomic and safe while the node runs** (POSIX `rename()` replaces the in-use binary; Windows moves the locked `.exe` aside to `.old`); the new binary takes effect on the **next daemon start**, so the Done screen offers **"Restart daemon now"** (`App::restartDaemon()`). A **"Browse all releases…"** button (shared `release_list_view.h` picker) lets users pin a specific/older/pre-release node build via `startInstallRelease()` — with a downgrade caution, since an older binary may not match current chain data.
|
||||||
|
|
||||||
|
**Signature verification is enforced** (`kDaemonRequireSignature = true` in `src/util/daemon_updater.h`), checked against `kDaemonSignaturePublicKeyBase64`. **Consequence for releases:** every `dragonx` release MUST ship a detached `<archive>.sig` per platform archive or the in-app updater refuses it (as of v1.0.2 the releases publish SHA-256 but **no** signatures yet — sign + upload them to enable in-app updates). To cut a release: `scripts/sign-daemon-release.sh sign <secret.key> dragonx-<ver>-{linux-amd64,macos,win64}.zip` (OpenSSL-based) and upload each `.sig` next to its `.zip`. The signing **secret key stays offline** (gitignored `*.ed25519.key`; this repo's is `dragonx-daemon.ed25519.key`); only the base64 public key is pinned. To rotate: `scripts/sign-daemon-release.sh keygen` and update `kDaemonSignaturePublicKeyBase64`. The generic SHA-256 / ed25519 primitives are shared with the miner updater (`util::sha256Hex` / `util::verifyXmrigSignature`).
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
The version has a **single source of truth**: `project(... VERSION 1.2.0 ...)` plus `DRAGONX_VERSION_SUFFIX` in `CMakeLists.txt`. CMake generates `build/.../generated/dragonx_generated_version.h` from `src/config/version.h.in`. Do not hand-edit generated version output or hardcode version strings — bump the `project()` version in `CMakeLists.txt`.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **C++17.** Match the surrounding code's style per file.
|
||||||
|
- **Icons:** use the Material Design icon font defines (`ICON_MD_*`); never raw Unicode glyphs.
|
||||||
|
- **UI layout values** belong in `res/themes/ui.toml`, read via `schema::UI()` — do not hardcode pixel sizes/offsets in code.
|
||||||
|
- **i18n:** user-facing strings are translated via `src/util/i18n`; 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`.
|
||||||
515
CMakeLists.txt
515
CMakeLists.txt
@@ -15,13 +15,19 @@ if(APPLE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
project(ObsidianDragon
|
project(ObsidianDragon
|
||||||
VERSION 1.2.0
|
VERSION 2.0.0
|
||||||
LANGUAGES C CXX
|
LANGUAGES C CXX
|
||||||
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
DESCRIPTION "DragonX Cryptocurrency Wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pre-release suffix (e.g. "-rc1", "-beta2"). Leave empty for stable releases.
|
# Pre-release suffix (e.g. "-rc1", "-beta2"). Leave empty for stable releases.
|
||||||
set(DRAGONX_VERSION_SUFFIX "-rc1")
|
set(DRAGONX_VERSION_SUFFIX "")
|
||||||
|
|
||||||
|
# ObsidianDragonLite is versioned INDEPENDENTLY of the full-node app above. The active variant's
|
||||||
|
# version flows to the generated header, the Windows .rc/manifest, and build.sh's release names via
|
||||||
|
# DRAGONX_APP_VERSION* (resolved in the lite/full block below).
|
||||||
|
set(DRAGONX_LITE_VERSION "1.0.0")
|
||||||
|
set(DRAGONX_LITE_VERSION_SUFFIX "")
|
||||||
|
|
||||||
# C++17 standard
|
# C++17 standard
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
@@ -36,6 +42,132 @@ endif()
|
|||||||
# Options
|
# Options
|
||||||
option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON)
|
option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON)
|
||||||
option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON)
|
option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON)
|
||||||
|
option(DRAGONX_BUILD_LITE "Build ObsidianDragonLite variant without full-node features" OFF)
|
||||||
|
option(DRAGONX_ENABLE_LITE_BACKEND "Enable real lite wallet backend integration" OFF)
|
||||||
|
option(DRAGONX_ENABLE_CHAT "Enable the HushChat protocol/UI integration" ON)
|
||||||
|
set(DRAGONX_LITE_BACKEND_LIBRARY "" CACHE FILEPATH "Path to a prebuilt SDXL-compatible lite backend library")
|
||||||
|
set(DRAGONX_LITE_BACKEND_INCLUDE_DIR "" CACHE PATH "Optional include directory for SDXL-compatible lite backend headers")
|
||||||
|
set(DRAGONX_LITE_BACKEND_EXTRA_LIBS "" CACHE STRING "Additional libraries needed by the SDXL-compatible lite backend")
|
||||||
|
set(DRAGONX_LITE_BACKEND_LINK_MODE "imported" CACHE STRING "Lite backend link mode; Phase 1 supports imported only")
|
||||||
|
set_property(CACHE DRAGONX_LITE_BACKEND_LINK_MODE PROPERTY STRINGS imported)
|
||||||
|
set(DRAGONX_LITE_BACKEND_ABI "sdxl-c-v1" CACHE STRING "Expected lite backend C ABI version")
|
||||||
|
set(DRAGONX_LITE_BACKEND_SYMBOLS_FILE "" CACHE FILEPATH "Path to generated lite backend exported-symbol inventory")
|
||||||
|
set(DRAGONX_LITE_BACKEND_MANIFEST "" CACHE FILEPATH "Optional path to generated lite backend artifact manifest")
|
||||||
|
option(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE "Require verified signature metadata in the lite backend artifact manifest" OFF)
|
||||||
|
set(DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS
|
||||||
|
litelib_wallet_exists
|
||||||
|
litelib_initialize_new
|
||||||
|
litelib_initialize_new_from_phrase
|
||||||
|
litelib_initialize_existing
|
||||||
|
litelib_execute
|
||||||
|
litelib_rust_free_string
|
||||||
|
litelib_check_server_online
|
||||||
|
litelib_shutdown
|
||||||
|
)
|
||||||
|
|
||||||
|
if(DRAGONX_BUILD_LITE)
|
||||||
|
set(DRAGONX_APP_NAME "ObsidianDragonLite")
|
||||||
|
set(DRAGONX_BINARY_NAME "ObsidianDragonLite")
|
||||||
|
# NOTE: do NOT FORCE-write DRAGONX_ENABLE_EMBEDDED_DAEMON=OFF into the cache here. A forced
|
||||||
|
# cache write persists into a later full-node reconfigure of the same build dir and silently
|
||||||
|
# disables the embedded daemon — the binary still embeds/extracts, but isUsingEmbeddedDaemon()
|
||||||
|
# returns false, so it "unpacks dragonxd but never starts" (the 1.3.0 regression). It is also
|
||||||
|
# redundant: makeWalletCapabilities() already forces the embedded-daemon capability off for any
|
||||||
|
# lite build via `fullNodeBuild && embeddedDaemonCompiled`, so lite never launches a daemon
|
||||||
|
# regardless of this flag. build.sh sets the flag explicitly per variant to defeat stale caches.
|
||||||
|
set(DRAGONX_APP_VERSION "${DRAGONX_LITE_VERSION}")
|
||||||
|
set(DRAGONX_APP_VERSION_SUFFIX "${DRAGONX_LITE_VERSION_SUFFIX}")
|
||||||
|
else()
|
||||||
|
set(DRAGONX_APP_NAME "ObsidianDragon")
|
||||||
|
set(DRAGONX_BINARY_NAME "ObsidianDragon")
|
||||||
|
set(DRAGONX_APP_VERSION "${PROJECT_VERSION}")
|
||||||
|
set(DRAGONX_APP_VERSION_SUFFIX "${DRAGONX_VERSION_SUFFIX}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Split the active version into numeric components for the generated header + Windows VERSIONINFO.
|
||||||
|
string(REPLACE "." ";" _dragonx_ver_parts "${DRAGONX_APP_VERSION}")
|
||||||
|
list(GET _dragonx_ver_parts 0 DRAGONX_APP_VERSION_MAJOR)
|
||||||
|
list(GET _dragonx_ver_parts 1 DRAGONX_APP_VERSION_MINOR)
|
||||||
|
list(GET _dragonx_ver_parts 2 DRAGONX_APP_VERSION_PATCH)
|
||||||
|
|
||||||
|
set(DRAGONX_LITE_BACKEND_READY OFF)
|
||||||
|
if(DRAGONX_ENABLE_LITE_BACKEND)
|
||||||
|
if(NOT DRAGONX_BUILD_LITE)
|
||||||
|
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND is only supported with DRAGONX_BUILD_LITE=ON")
|
||||||
|
endif()
|
||||||
|
if(NOT DRAGONX_LITE_BACKEND_LINK_MODE STREQUAL "imported")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_LINK_MODE currently supports only 'imported'; runtime dynamic loading is a later bridge-runtime phase")
|
||||||
|
endif()
|
||||||
|
if(NOT DRAGONX_LITE_BACKEND_ABI STREQUAL "sdxl-c-v1")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_ABI must be sdxl-c-v1")
|
||||||
|
endif()
|
||||||
|
if(NOT DRAGONX_LITE_BACKEND_LIBRARY)
|
||||||
|
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND requires DRAGONX_LITE_BACKEND_LIBRARY to point at an SDXL-compatible artifact")
|
||||||
|
endif()
|
||||||
|
if(NOT EXISTS "${DRAGONX_LITE_BACKEND_LIBRARY}")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_LIBRARY does not exist: ${DRAGONX_LITE_BACKEND_LIBRARY}")
|
||||||
|
endif()
|
||||||
|
if(NOT DRAGONX_LITE_BACKEND_SYMBOLS_FILE)
|
||||||
|
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND requires DRAGONX_LITE_BACKEND_SYMBOLS_FILE generated by scripts/build-lite-backend-artifact.sh")
|
||||||
|
endif()
|
||||||
|
if(NOT EXISTS "${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE does not exist: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
|
||||||
|
endif()
|
||||||
|
file(STRINGS "${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}" DRAGONX_LITE_BACKEND_SYMBOL_LINES)
|
||||||
|
if(NOT DRAGONX_LITE_BACKEND_SYMBOL_LINES)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE is empty: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
|
||||||
|
endif()
|
||||||
|
foreach(DRAGONX_LITE_REQUIRED_SYMBOL IN LISTS DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS)
|
||||||
|
list(FIND DRAGONX_LITE_BACKEND_SYMBOL_LINES "${DRAGONX_LITE_REQUIRED_SYMBOL}" DRAGONX_LITE_SYMBOL_INDEX)
|
||||||
|
if(DRAGONX_LITE_SYMBOL_INDEX EQUAL -1)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE is missing required symbol: ${DRAGONX_LITE_REQUIRED_SYMBOL}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
if(DRAGONX_LITE_BACKEND_MANIFEST AND NOT EXISTS "${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST does not exist: ${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||||
|
endif()
|
||||||
|
if(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE)
|
||||||
|
if(NOT DRAGONX_LITE_BACKEND_MANIFEST)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires DRAGONX_LITE_BACKEND_MANIFEST")
|
||||||
|
endif()
|
||||||
|
file(READ "${DRAGONX_LITE_BACKEND_MANIFEST}" DRAGONX_LITE_BACKEND_MANIFEST_JSON)
|
||||||
|
string(JSON DRAGONX_LITE_SIGNATURE_STATUS ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_STATUS_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_status)
|
||||||
|
if(DRAGONX_LITE_SIGNATURE_STATUS_ERROR)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing signature verification status")
|
||||||
|
endif()
|
||||||
|
if(NOT DRAGONX_LITE_SIGNATURE_STATUS STREQUAL "verified")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verified signature metadata")
|
||||||
|
endif()
|
||||||
|
string(JSON DRAGONX_LITE_SIGNATURE_VERIFIED_SHA ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verified_artifact_sha256)
|
||||||
|
string(JSON DRAGONX_LITE_ARTIFACT_SHA ERROR_VARIABLE DRAGONX_LITE_ARTIFACT_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" artifact sha256)
|
||||||
|
if(DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR OR DRAGONX_LITE_ARTIFACT_SHA_ERROR)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing artifact/signature SHA-256 metadata")
|
||||||
|
endif()
|
||||||
|
if(NOT DRAGONX_LITE_SIGNATURE_VERIFIED_SHA STREQUAL DRAGONX_LITE_ARTIFACT_SHA)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST signature metadata does not verify the artifact SHA-256")
|
||||||
|
endif()
|
||||||
|
string(JSON DRAGONX_LITE_SIGNATURE_PERFORMED ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_performed)
|
||||||
|
if(DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR OR NOT DRAGONX_LITE_SIGNATURE_PERFORMED)
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verification_performed=true")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(dragonx_lite_backend UNKNOWN IMPORTED)
|
||||||
|
set_target_properties(dragonx_lite_backend PROPERTIES
|
||||||
|
IMPORTED_LOCATION "${DRAGONX_LITE_BACKEND_LIBRARY}"
|
||||||
|
)
|
||||||
|
if(DRAGONX_LITE_BACKEND_INCLUDE_DIR)
|
||||||
|
if(NOT IS_DIRECTORY "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
|
||||||
|
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_INCLUDE_DIR does not exist: ${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
|
||||||
|
endif()
|
||||||
|
set_target_properties(dragonx_lite_backend PROPERTIES
|
||||||
|
INTERFACE_INCLUDE_DIRECTORIES "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
set(DRAGONX_LITE_BACKEND_READY ON)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include(CTest)
|
||||||
|
|
||||||
# Output directories
|
# Output directories
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||||
@@ -108,6 +240,32 @@ FetchContent_Declare(
|
|||||||
)
|
)
|
||||||
FetchContent_MakeAvailable(tomlplusplus)
|
FetchContent_MakeAvailable(tomlplusplus)
|
||||||
|
|
||||||
|
# SQLite amalgamation - local Explorer block-summary cache
|
||||||
|
FetchContent_Declare(
|
||||||
|
sqlite3
|
||||||
|
URL https://www.sqlite.org/2024/sqlite-amalgamation-3450300.zip
|
||||||
|
URL_HASH SHA256=ea170e73e447703e8359308ca2e4366a3ae0c4304a8665896f068c736781c651
|
||||||
|
)
|
||||||
|
FetchContent_GetProperties(sqlite3)
|
||||||
|
if(NOT sqlite3_POPULATED)
|
||||||
|
FetchContent_Populate(sqlite3)
|
||||||
|
endif()
|
||||||
|
file(GLOB SQLITE3_AMALGAMATION_C CONFIGURE_DEPENDS
|
||||||
|
${sqlite3_SOURCE_DIR}/sqlite3.c
|
||||||
|
${sqlite3_SOURCE_DIR}/*/sqlite3.c
|
||||||
|
)
|
||||||
|
if(NOT SQLITE3_AMALGAMATION_C)
|
||||||
|
message(FATAL_ERROR "SQLite amalgamation source not found")
|
||||||
|
endif()
|
||||||
|
list(GET SQLITE3_AMALGAMATION_C 0 SQLITE3_SOURCE_FILE)
|
||||||
|
get_filename_component(SQLITE3_INCLUDE_DIR ${SQLITE3_SOURCE_FILE} DIRECTORY)
|
||||||
|
add_library(sqlite3_amalgamation STATIC ${SQLITE3_SOURCE_FILE})
|
||||||
|
target_include_directories(sqlite3_amalgamation PUBLIC ${SQLITE3_INCLUDE_DIR})
|
||||||
|
target_compile_definitions(sqlite3_amalgamation PRIVATE
|
||||||
|
SQLITE_THREADSAFE=1
|
||||||
|
SQLITE_OMIT_LOAD_EXTENSION
|
||||||
|
)
|
||||||
|
|
||||||
# libcurl for HTTPS RPC connections (more reliable than cpp-httplib with OpenSSL 3.x)
|
# libcurl for HTTPS RPC connections (more reliable than cpp-httplib with OpenSSL 3.x)
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
# For Windows cross-compilation, fetch and build libcurl statically
|
# For Windows cross-compilation, fetch and build libcurl statically
|
||||||
@@ -247,22 +405,70 @@ set(APP_SOURCES
|
|||||||
src/app_network.cpp
|
src/app_network.cpp
|
||||||
src/app_security.cpp
|
src/app_security.cpp
|
||||||
src/app_wizard.cpp
|
src/app_wizard.cpp
|
||||||
|
src/services/network_refresh_service.cpp
|
||||||
|
src/services/refresh_scheduler.cpp
|
||||||
|
src/services/wallet_security_controller.cpp
|
||||||
|
src/services/wallet_security_workflow.cpp
|
||||||
|
src/services/wallet_security_workflow_executor.cpp
|
||||||
|
src/chat/chat_protocol.cpp
|
||||||
|
src/chat/chat_crypto.cpp
|
||||||
|
src/chat/chat_identity.cpp
|
||||||
|
src/chat/chat_store.cpp
|
||||||
|
src/chat/chat_service.cpp
|
||||||
|
src/chat/chat_database.cpp
|
||||||
|
src/chat/chat_outgoing.cpp
|
||||||
|
src/wallet/lite_owned_string.cpp
|
||||||
|
src/wallet/lite_rollout_policy.cpp
|
||||||
|
src/wallet/lite_client_bridge.cpp
|
||||||
|
src/wallet/lite_connection_service.cpp
|
||||||
|
src/wallet/lite_diagnostics.cpp
|
||||||
|
src/wallet/lite_wallet_controller.cpp
|
||||||
|
src/wallet/lite_result_parsers.cpp
|
||||||
|
src/wallet/lite_sync_service.cpp
|
||||||
|
src/wallet/lite_wallet_gateway.cpp
|
||||||
|
src/wallet/lite_wallet_state_mapper.cpp
|
||||||
|
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
|
||||||
|
src/wallet/lite_wallet_server_selection_adapter.cpp
|
||||||
|
src/wallet/lite_wallet_lifecycle_service.cpp
|
||||||
src/data/wallet_state.cpp
|
src/data/wallet_state.cpp
|
||||||
|
src/data/transaction_history_cache.cpp
|
||||||
src/ui/theme.cpp
|
src/ui/theme.cpp
|
||||||
src/ui/theme_loader.cpp
|
src/ui/theme_loader.cpp
|
||||||
|
src/ui/explorer/explorer_block_cache.cpp
|
||||||
src/ui/material/color_theme.cpp
|
src/ui/material/color_theme.cpp
|
||||||
src/ui/material/typography.cpp
|
src/ui/material/typography.cpp
|
||||||
src/ui/notifications.cpp
|
src/ui/notifications.cpp
|
||||||
src/ui/windows/main_window.cpp
|
src/ui/windows/main_window.cpp
|
||||||
src/ui/windows/balance_tab.cpp
|
src/ui/windows/balance_tab.cpp
|
||||||
|
src/ui/windows/balance_components.cpp
|
||||||
|
src/ui/windows/balance_address_list.cpp
|
||||||
|
src/ui/windows/balance_recent_tx.cpp
|
||||||
|
src/ui/windows/balance_tab_helpers.cpp
|
||||||
src/ui/windows/send_tab.cpp
|
src/ui/windows/send_tab.cpp
|
||||||
src/ui/windows/receive_tab.cpp
|
src/ui/windows/receive_tab.cpp
|
||||||
src/ui/windows/transactions_tab.cpp
|
src/ui/windows/transactions_tab.cpp
|
||||||
src/ui/windows/mining_tab.cpp
|
src/ui/windows/mining_tab.cpp
|
||||||
|
src/ui/windows/mining_earnings.cpp
|
||||||
|
src/ui/windows/mining_stats.cpp
|
||||||
|
src/ui/windows/mining_controls.cpp
|
||||||
|
src/ui/windows/mining_mode_toggle.cpp
|
||||||
|
src/ui/windows/mining_benchmark.cpp
|
||||||
|
src/ui/windows/mining_pool_panel.cpp
|
||||||
|
src/ui/windows/mining_tab_helpers.cpp
|
||||||
src/ui/windows/peers_tab.cpp
|
src/ui/windows/peers_tab.cpp
|
||||||
|
src/ui/windows/network_tab.cpp
|
||||||
src/ui/windows/explorer_tab.cpp
|
src/ui/windows/explorer_tab.cpp
|
||||||
src/ui/windows/market_tab.cpp
|
src/ui/windows/market_tab.cpp
|
||||||
src/ui/windows/console_tab.cpp
|
src/ui/windows/console_tab.cpp
|
||||||
|
src/ui/windows/console_command_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/windows/settings_window.cpp
|
||||||
src/ui/pages/settings_page.cpp
|
src/ui/pages/settings_page.cpp
|
||||||
src/ui/windows/about_dialog.cpp
|
src/ui/windows/about_dialog.cpp
|
||||||
@@ -270,14 +476,13 @@ set(APP_SOURCES
|
|||||||
src/ui/windows/transaction_details_dialog.cpp
|
src/ui/windows/transaction_details_dialog.cpp
|
||||||
src/ui/windows/qr_popup_dialog.cpp
|
src/ui/windows/qr_popup_dialog.cpp
|
||||||
src/ui/windows/validate_address_dialog.cpp
|
src/ui/windows/validate_address_dialog.cpp
|
||||||
src/ui/windows/address_book_dialog.cpp
|
src/ui/windows/contacts_tab.cpp
|
||||||
|
src/ui/windows/chat_tab.cpp
|
||||||
src/ui/windows/shield_dialog.cpp
|
src/ui/windows/shield_dialog.cpp
|
||||||
src/ui/windows/request_payment_dialog.cpp
|
src/ui/windows/request_payment_dialog.cpp
|
||||||
src/ui/windows/block_info_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_all_keys_dialog.cpp
|
||||||
src/ui/windows/export_transactions_dialog.cpp
|
src/ui/windows/export_transactions_dialog.cpp
|
||||||
src/ui/windows/backup_wallet_dialog.cpp
|
|
||||||
src/ui/widgets/qr_code.cpp
|
src/ui/widgets/qr_code.cpp
|
||||||
src/rpc/rpc_client.cpp
|
src/rpc/rpc_client.cpp
|
||||||
src/rpc/rpc_worker.cpp
|
src/rpc/rpc_worker.cpp
|
||||||
@@ -286,16 +491,30 @@ set(APP_SOURCES
|
|||||||
src/data/address_book.cpp
|
src/data/address_book.cpp
|
||||||
src/data/exchange_info.cpp
|
src/data/exchange_info.cpp
|
||||||
src/util/logger.cpp
|
src/util/logger.cpp
|
||||||
|
src/util/async_task_manager.cpp
|
||||||
|
src/util/amount_format.cpp
|
||||||
|
src/util/address_validation.cpp
|
||||||
src/util/base64.cpp
|
src/util/base64.cpp
|
||||||
src/util/single_instance.cpp
|
src/util/single_instance.cpp
|
||||||
src/util/i18n.cpp
|
src/util/i18n.cpp
|
||||||
|
src/util/text_format.cpp
|
||||||
src/util/platform.cpp
|
src/util/platform.cpp
|
||||||
src/util/payment_uri.cpp
|
src/util/payment_uri.cpp
|
||||||
src/util/texture_loader.cpp
|
src/util/texture_loader.cpp
|
||||||
src/util/noise_texture.cpp
|
src/util/noise_texture.cpp
|
||||||
src/daemon/embedded_daemon.cpp
|
src/daemon/embedded_daemon.cpp
|
||||||
|
src/daemon/daemon_controller.cpp
|
||||||
|
src/daemon/lifecycle_adapters.cpp
|
||||||
src/daemon/xmrig_manager.cpp
|
src/daemon/xmrig_manager.cpp
|
||||||
src/util/bootstrap.cpp
|
src/util/bootstrap.cpp
|
||||||
|
src/util/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/util/secure_vault.cpp
|
||||||
src/ui/effects/framebuffer.cpp
|
src/ui/effects/framebuffer.cpp
|
||||||
src/ui/effects/blur_shader.cpp
|
src/ui/effects/blur_shader.cpp
|
||||||
@@ -326,34 +545,77 @@ endif()
|
|||||||
|
|
||||||
set(APP_HEADERS
|
set(APP_HEADERS
|
||||||
src/app.h
|
src/app.h
|
||||||
|
src/services/network_refresh_service.h
|
||||||
|
src/services/refresh_scheduler.h
|
||||||
|
src/services/wallet_security_controller.h
|
||||||
|
src/services/wallet_security_workflow.h
|
||||||
|
src/services/wallet_security_workflow_executor.h
|
||||||
|
src/wallet/wallet_capabilities.h
|
||||||
|
src/wallet/wallet_backend.h
|
||||||
|
src/wallet/lite_owned_string.h
|
||||||
|
src/wallet/lite_rollout_policy.h
|
||||||
|
src/wallet/lite_client_bridge.h
|
||||||
|
src/wallet/lite_connection_service.h
|
||||||
|
src/wallet/lite_result_parsers.h
|
||||||
|
src/wallet/lite_sync_service.h
|
||||||
|
src/wallet/lite_wallet_gateway.h
|
||||||
|
src/wallet/lite_wallet_state_mapper.h
|
||||||
|
src/wallet/lite_wallet_lifecycle_ui_adapter.h
|
||||||
|
src/wallet/lite_wallet_server_selection_adapter.h
|
||||||
|
src/wallet/lite_wallet_lifecycle_service.h
|
||||||
|
src/chat/chat_protocol.h
|
||||||
|
src/chat/chat_crypto.h
|
||||||
|
src/chat/chat_identity.h
|
||||||
|
src/chat/chat_message.h
|
||||||
|
src/chat/chat_store.h
|
||||||
|
src/chat/chat_service.h
|
||||||
|
src/chat/chat_database.h
|
||||||
|
src/chat/chat_outgoing.h
|
||||||
src/config/version.h
|
src/config/version.h
|
||||||
src/data/wallet_state.h
|
src/data/wallet_state.h
|
||||||
|
src/data/transaction_history_cache.h
|
||||||
src/ui/theme.h
|
src/ui/theme.h
|
||||||
src/ui/theme_loader.h
|
src/ui/theme_loader.h
|
||||||
|
src/ui/explorer/explorer_block_cache.h
|
||||||
src/ui/notifications.h
|
src/ui/notifications.h
|
||||||
src/ui/windows/main_window.h
|
src/ui/windows/main_window.h
|
||||||
src/ui/windows/balance_tab.h
|
src/ui/windows/balance_tab.h
|
||||||
|
src/ui/windows/balance_address_list.h
|
||||||
|
src/ui/windows/balance_recent_tx.h
|
||||||
|
src/ui/windows/balance_tab_helpers.h
|
||||||
src/ui/windows/send_tab.h
|
src/ui/windows/send_tab.h
|
||||||
src/ui/windows/receive_tab.h
|
src/ui/windows/receive_tab.h
|
||||||
src/ui/windows/transactions_tab.h
|
src/ui/windows/transactions_tab.h
|
||||||
src/ui/windows/mining_tab.h
|
src/ui/windows/mining_tab.h
|
||||||
|
src/ui/windows/mining_benchmark.h
|
||||||
|
src/ui/windows/mining_pool_panel.h
|
||||||
|
src/ui/windows/mining_tab_helpers.h
|
||||||
src/ui/windows/peers_tab.h
|
src/ui/windows/peers_tab.h
|
||||||
src/ui/windows/explorer_tab.h
|
src/ui/windows/explorer_tab.h
|
||||||
src/ui/windows/market_tab.h
|
src/ui/windows/market_tab.h
|
||||||
|
src/ui/windows/console_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/settings_window.h
|
||||||
src/ui/windows/about_dialog.h
|
src/ui/windows/about_dialog.h
|
||||||
src/ui/windows/key_export_dialog.h
|
src/ui/windows/key_export_dialog.h
|
||||||
src/ui/windows/transaction_details_dialog.h
|
src/ui/windows/transaction_details_dialog.h
|
||||||
src/ui/windows/qr_popup_dialog.h
|
src/ui/windows/qr_popup_dialog.h
|
||||||
src/ui/windows/validate_address_dialog.h
|
src/ui/windows/validate_address_dialog.h
|
||||||
src/ui/windows/address_book_dialog.h
|
src/ui/windows/contacts_tab.h
|
||||||
|
src/ui/windows/chat_tab.h
|
||||||
|
src/ui/windows/contact_picker.h
|
||||||
src/ui/windows/shield_dialog.h
|
src/ui/windows/shield_dialog.h
|
||||||
src/ui/windows/request_payment_dialog.h
|
src/ui/windows/request_payment_dialog.h
|
||||||
src/ui/windows/block_info_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_all_keys_dialog.h
|
||||||
src/ui/windows/export_transactions_dialog.h
|
src/ui/windows/export_transactions_dialog.h
|
||||||
src/ui/windows/backup_wallet_dialog.h
|
|
||||||
src/ui/widgets/qr_code.h
|
src/ui/widgets/qr_code.h
|
||||||
src/rpc/rpc_client.h
|
src/rpc/rpc_client.h
|
||||||
src/rpc/rpc_worker.h
|
src/rpc/rpc_worker.h
|
||||||
@@ -363,6 +625,8 @@ set(APP_HEADERS
|
|||||||
src/data/address_book.h
|
src/data/address_book.h
|
||||||
src/data/exchange_info.h
|
src/data/exchange_info.h
|
||||||
src/util/logger.h
|
src/util/logger.h
|
||||||
|
src/util/async_task_manager.h
|
||||||
|
src/util/amount_format.h
|
||||||
src/util/base64.h
|
src/util/base64.h
|
||||||
src/util/single_instance.h
|
src/util/single_instance.h
|
||||||
src/util/i18n.h
|
src/util/i18n.h
|
||||||
@@ -370,6 +634,8 @@ set(APP_HEADERS
|
|||||||
src/util/payment_uri.h
|
src/util/payment_uri.h
|
||||||
src/util/secure_vault.h
|
src/util/secure_vault.h
|
||||||
src/daemon/embedded_daemon.h
|
src/daemon/embedded_daemon.h
|
||||||
|
src/daemon/daemon_controller.h
|
||||||
|
src/daemon/lifecycle_adapters.h
|
||||||
src/daemon/xmrig_manager.h
|
src/daemon/xmrig_manager.h
|
||||||
src/ui/effects/framebuffer.h
|
src/ui/effects/framebuffer.h
|
||||||
src/ui/effects/blur_shader.h
|
src/ui/effects/blur_shader.h
|
||||||
@@ -408,10 +674,12 @@ if(WIN32)
|
|||||||
set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc)
|
set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Generate version.h from the single project(VERSION ...) declaration
|
# Generate version values from the single project(VERSION ...) declaration.
|
||||||
|
# Keep the build-specific app name in the build tree so full/lite configures do
|
||||||
|
# not rewrite a tracked source header.
|
||||||
configure_file(
|
configure_file(
|
||||||
${CMAKE_SOURCE_DIR}/src/config/version.h.in
|
${CMAKE_SOURCE_DIR}/src/config/version.h.in
|
||||||
${CMAKE_SOURCE_DIR}/src/config/version.h
|
${CMAKE_BINARY_DIR}/generated/dragonx_generated_version.h
|
||||||
@ONLY
|
@ONLY
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -432,7 +700,9 @@ set_source_files_properties(
|
|||||||
"${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-R.ttf;\
|
"${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-R.ttf;\
|
||||||
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
|
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
|
||||||
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.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/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/NotoSansCJK-Subset.ttf"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -446,6 +716,8 @@ add_executable(ObsidianDragon
|
|||||||
${WIN_RC_FILE}
|
${WIN_RC_FILE}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set_target_properties(ObsidianDragon PROPERTIES OUTPUT_NAME "${DRAGONX_BINARY_NAME}")
|
||||||
|
|
||||||
target_include_directories(ObsidianDragon PRIVATE
|
target_include_directories(ObsidianDragon PRIVATE
|
||||||
${CMAKE_SOURCE_DIR}/src
|
${CMAKE_SOURCE_DIR}/src
|
||||||
${CMAKE_SOURCE_DIR}/src/embedded
|
${CMAKE_SOURCE_DIR}/src/embedded
|
||||||
@@ -465,10 +737,63 @@ target_link_libraries(ObsidianDragon PRIVATE
|
|||||||
SDL3::SDL3
|
SDL3::SDL3
|
||||||
nlohmann_json::nlohmann_json
|
nlohmann_json::nlohmann_json
|
||||||
tomlplusplus::tomlplusplus
|
tomlplusplus::tomlplusplus
|
||||||
|
sqlite3_amalgamation
|
||||||
${CURL_LIBRARIES}
|
${CURL_LIBRARIES}
|
||||||
${SODIUM_LIBRARY}
|
${SODIUM_LIBRARY}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(DRAGONX_LITE_BACKEND_READY)
|
||||||
|
target_link_libraries(ObsidianDragon PRIVATE dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS})
|
||||||
|
|
||||||
|
# Real-backend smoke tool (only built when a real lite backend is linked).
|
||||||
|
add_executable(lite_smoke
|
||||||
|
tools/lite_smoke.cpp
|
||||||
|
src/wallet/lite_client_bridge.cpp
|
||||||
|
src/wallet/lite_owned_string.cpp
|
||||||
|
src/wallet/lite_rollout_policy.cpp
|
||||||
|
src/wallet/lite_connection_service.cpp
|
||||||
|
src/wallet/lite_result_parsers.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(lite_smoke PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
${CMAKE_BINARY_DIR}/generated
|
||||||
|
${SODIUM_INCLUDE_DIR}
|
||||||
|
)
|
||||||
|
target_compile_definitions(lite_smoke PRIVATE DRAGONX_ENABLE_LITE_BACKEND=1)
|
||||||
|
target_link_libraries(lite_smoke PRIVATE
|
||||||
|
dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS}
|
||||||
|
nlohmann_json::nlohmann_json
|
||||||
|
${SODIUM_LIBRARY}
|
||||||
|
)
|
||||||
|
if(UNIX)
|
||||||
|
target_link_libraries(lite_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Real-backend SEND smoke tool — drives the exact GUI send path (bridge.execute("send", ...)).
|
||||||
|
add_executable(lite_send_smoke
|
||||||
|
tools/lite_send_smoke.cpp
|
||||||
|
src/wallet/lite_client_bridge.cpp
|
||||||
|
src/wallet/lite_owned_string.cpp
|
||||||
|
src/wallet/lite_rollout_policy.cpp
|
||||||
|
src/wallet/lite_connection_service.cpp
|
||||||
|
src/wallet/lite_result_parsers.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(lite_send_smoke PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
${CMAKE_BINARY_DIR}/generated
|
||||||
|
${SODIUM_INCLUDE_DIR}
|
||||||
|
)
|
||||||
|
target_compile_definitions(lite_send_smoke PRIVATE DRAGONX_ENABLE_LITE_BACKEND=1)
|
||||||
|
target_link_libraries(lite_send_smoke PRIVATE
|
||||||
|
dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS}
|
||||||
|
nlohmann_json::nlohmann_json
|
||||||
|
${SODIUM_LIBRARY}
|
||||||
|
)
|
||||||
|
if(UNIX)
|
||||||
|
target_link_libraries(lite_send_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
# Platform-specific settings
|
# Platform-specific settings
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi iphlpapi d3d11 dxgi d3dcompiler dcomp)
|
target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi iphlpapi d3d11 dxgi d3dcompiler dcomp)
|
||||||
@@ -497,6 +822,10 @@ endif()
|
|||||||
# Compile definitions
|
# Compile definitions
|
||||||
target_compile_definitions(ObsidianDragon PRIVATE
|
target_compile_definitions(ObsidianDragon PRIVATE
|
||||||
DRAGONX_DEBUG
|
DRAGONX_DEBUG
|
||||||
|
DRAGONX_LITE_BUILD=$<BOOL:${DRAGONX_BUILD_LITE}>
|
||||||
|
DRAGONX_ENABLE_EMBEDDED_DAEMON=$<BOOL:${DRAGONX_ENABLE_EMBEDDED_DAEMON}>
|
||||||
|
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
|
||||||
|
DRAGONX_ENABLE_CHAT=$<BOOL:${DRAGONX_ENABLE_CHAT}>
|
||||||
)
|
)
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11)
|
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11)
|
||||||
@@ -504,6 +833,26 @@ else()
|
|||||||
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAS_GLAD)
|
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAS_GLAD)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
add_executable(HushChatFixtureCheck
|
||||||
|
tools/hushchat_fixture_check.cpp
|
||||||
|
src/chat/chat_protocol.cpp
|
||||||
|
src/chat/chat_fixture_tooling.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(HushChatFixtureCheck PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
${SODIUM_INCLUDE_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(HushChatFixtureCheck PRIVATE
|
||||||
|
nlohmann_json::nlohmann_json
|
||||||
|
${SODIUM_LIBRARY}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(HushChatFixtureCheck PRIVATE
|
||||||
|
DRAGONX_ENABLE_CHAT=0
|
||||||
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Copy resources
|
# Copy resources
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -527,6 +876,12 @@ endif()
|
|||||||
# so edits to res/lang/*.json are picked up by 'make' without re-running cmake.
|
# so edits to res/lang/*.json are picked up by 'make' without re-running cmake.
|
||||||
file(GLOB LANG_FILES ${CMAKE_SOURCE_DIR}/res/lang/*.json)
|
file(GLOB LANG_FILES ${CMAKE_SOURCE_DIR}/res/lang/*.json)
|
||||||
if(LANG_FILES)
|
if(LANG_FILES)
|
||||||
|
find_program(XXD_EXECUTABLE NAMES xxd)
|
||||||
|
if(NOT XXD_EXECUTABLE)
|
||||||
|
message(WARNING "xxd not found; runtime language JSON files will be copied, but embedded build/generated/embedded/lang_*.h files will not be regenerated")
|
||||||
|
endif()
|
||||||
|
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/generated/embedded)
|
||||||
|
|
||||||
foreach(LANG_FILE ${LANG_FILES})
|
foreach(LANG_FILE ${LANG_FILES})
|
||||||
get_filename_component(LANG_FILENAME ${LANG_FILE} NAME)
|
get_filename_component(LANG_FILENAME ${LANG_FILE} NAME)
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
@@ -540,16 +895,18 @@ if(LANG_FILES)
|
|||||||
list(APPEND LANG_OUTPUTS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME})
|
list(APPEND LANG_OUTPUTS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME})
|
||||||
|
|
||||||
# Also regenerate the embedded header so the binary always has fresh translations
|
# Also regenerate the embedded header so the binary always has fresh translations
|
||||||
get_filename_component(LANG_CODE ${LANG_FILENAME} NAME_WE)
|
if(XXD_EXECUTABLE)
|
||||||
set(LANG_HEADER ${CMAKE_SOURCE_DIR}/src/embedded/lang_${LANG_CODE}.h)
|
get_filename_component(LANG_CODE ${LANG_FILENAME} NAME_WE)
|
||||||
add_custom_command(
|
set(LANG_HEADER ${CMAKE_BINARY_DIR}/generated/embedded/lang_${LANG_CODE}.h)
|
||||||
OUTPUT ${LANG_HEADER}
|
add_custom_command(
|
||||||
COMMAND xxd -i "res/lang/${LANG_FILENAME}" > "src/embedded/lang_${LANG_CODE}.h"
|
OUTPUT ${LANG_HEADER}
|
||||||
DEPENDS ${LANG_FILE}
|
COMMAND ${XXD_EXECUTABLE} -i "res/lang/${LANG_FILENAME}" > "${LANG_HEADER}"
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
DEPENDS ${LANG_FILE}
|
||||||
COMMENT "Embedding lang_${LANG_CODE}.h"
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
)
|
COMMENT "Embedding lang_${LANG_CODE}.h"
|
||||||
list(APPEND LANG_OUTPUTS ${LANG_HEADER})
|
)
|
||||||
|
list(APPEND LANG_OUTPUTS ${LANG_HEADER})
|
||||||
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
add_custom_target(copy_langs ALL DEPENDS ${LANG_OUTPUTS})
|
add_custom_target(copy_langs ALL DEPENDS ${LANG_OUTPUTS})
|
||||||
add_dependencies(ObsidianDragon copy_langs)
|
add_dependencies(ObsidianDragon copy_langs)
|
||||||
@@ -596,6 +953,7 @@ if(THEME_FILES AND Python3_EXECUTABLE)
|
|||||||
message(STATUS " Theme files: ${THEME_FILES} (build-time expansion via Python)")
|
message(STATUS " Theme files: ${THEME_FILES} (build-time expansion via Python)")
|
||||||
elseif(THEME_FILES)
|
elseif(THEME_FILES)
|
||||||
# Fallback: plain copy if Python is not available
|
# Fallback: plain copy if Python is not available
|
||||||
|
message(WARNING "Python3 not found; copying theme files without expand_themes.py layout merge")
|
||||||
foreach(THEME_FILE ${THEME_FILES})
|
foreach(THEME_FILE ${THEME_FILES})
|
||||||
get_filename_component(THEME_FILENAME ${THEME_FILE} NAME)
|
get_filename_component(THEME_FILENAME ${THEME_FILE} NAME)
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
@@ -645,20 +1003,133 @@ install(TARGETS ObsidianDragon
|
|||||||
)
|
)
|
||||||
|
|
||||||
install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res
|
install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res
|
||||||
DESTINATION share/ObsidianDragon
|
DESTINATION share/${DRAGONX_BINARY_NAME}
|
||||||
OPTIONAL
|
OPTIONAL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Tests
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
add_executable(ObsidianDragonTests
|
||||||
|
tests/test_phase4.cpp
|
||||||
|
src/services/network_refresh_service.cpp
|
||||||
|
src/services/refresh_scheduler.cpp
|
||||||
|
src/services/wallet_security_controller.cpp
|
||||||
|
src/services/wallet_security_workflow.cpp
|
||||||
|
src/services/wallet_security_workflow_executor.cpp
|
||||||
|
src/chat/chat_protocol.cpp
|
||||||
|
src/chat/chat_crypto.cpp
|
||||||
|
src/chat/chat_identity.cpp
|
||||||
|
src/chat/chat_store.cpp
|
||||||
|
src/chat/chat_service.cpp
|
||||||
|
src/chat/chat_database.cpp
|
||||||
|
src/chat/chat_outgoing.cpp
|
||||||
|
src/wallet/lite_owned_string.cpp
|
||||||
|
src/wallet/lite_rollout_policy.cpp
|
||||||
|
src/wallet/lite_client_bridge.cpp
|
||||||
|
src/wallet/lite_connection_service.cpp
|
||||||
|
src/wallet/lite_diagnostics.cpp
|
||||||
|
src/wallet/lite_wallet_controller.cpp
|
||||||
|
src/wallet/lite_result_parsers.cpp
|
||||||
|
src/wallet/lite_sync_service.cpp
|
||||||
|
src/wallet/lite_wallet_gateway.cpp
|
||||||
|
src/wallet/lite_wallet_state_mapper.cpp
|
||||||
|
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
|
||||||
|
src/wallet/lite_wallet_server_selection_adapter.cpp
|
||||||
|
src/wallet/lite_wallet_lifecycle_service.cpp
|
||||||
|
src/ui/explorer/explorer_block_cache.cpp
|
||||||
|
src/ui/windows/balance_address_list.cpp
|
||||||
|
src/ui/windows/balance_recent_tx.cpp
|
||||||
|
src/ui/windows/console_input_model.cpp
|
||||||
|
src/ui/windows/console_model.cpp
|
||||||
|
src/ui/windows/console_output_model.cpp
|
||||||
|
src/ui/windows/console_scroll_controller.cpp
|
||||||
|
src/ui/windows/console_selection_controller.cpp
|
||||||
|
src/ui/windows/console_tab_helpers.cpp
|
||||||
|
src/ui/windows/console_text_layout.cpp
|
||||||
|
src/ui/windows/mining_benchmark.cpp
|
||||||
|
src/ui/windows/mining_pool_panel.cpp
|
||||||
|
src/ui/windows/mining_tab_helpers.cpp
|
||||||
|
src/util/payment_uri.cpp
|
||||||
|
src/util/amount_format.cpp
|
||||||
|
src/util/address_validation.cpp
|
||||||
|
src/util/i18n.cpp
|
||||||
|
src/util/text_format.cpp
|
||||||
|
src/data/wallet_state.cpp
|
||||||
|
src/data/transaction_history_cache.cpp
|
||||||
|
src/daemon/lifecycle_adapters.cpp
|
||||||
|
src/rpc/connection.cpp
|
||||||
|
src/config/settings.cpp
|
||||||
|
src/resources/embedded_resources.cpp
|
||||||
|
src/util/secure_vault.cpp
|
||||||
|
src/util/platform.cpp
|
||||||
|
src/util/logger.cpp
|
||||||
|
src/util/lite_server_probe.cpp
|
||||||
|
src/util/pool_registry_core.cpp
|
||||||
|
src/util/http_download.cpp
|
||||||
|
src/util/xmrig_updater.cpp
|
||||||
|
src/util/xmrig_updater_core.cpp
|
||||||
|
src/util/daemon_updater.cpp
|
||||||
|
src/util/daemon_updater_core.cpp
|
||||||
|
${MINIZ_SOURCES}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(ObsidianDragonTests PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/src
|
||||||
|
${CMAKE_SOURCE_DIR}/src/resources
|
||||||
|
${CMAKE_SOURCE_DIR}/libs
|
||||||
|
${CMAKE_BINARY_DIR}/generated
|
||||||
|
${IMGUI_DIR}
|
||||||
|
${SODIUM_INCLUDE_DIR}
|
||||||
|
${CURL_INCLUDE_DIRS}
|
||||||
|
${MINIZ_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(ObsidianDragonTests PRIVATE
|
||||||
|
nlohmann_json::nlohmann_json
|
||||||
|
sqlite3_amalgamation
|
||||||
|
${SODIUM_LIBRARY}
|
||||||
|
${CURL_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(ObsidianDragonTests PRIVATE
|
||||||
|
DRAGONX_ENABLE_CHAT=$<BOOL:${DRAGONX_ENABLE_CHAT}>
|
||||||
|
DRAGONX_LITE_BUILD=$<BOOL:${DRAGONX_BUILD_LITE}>
|
||||||
|
DRAGONX_ENABLE_EMBEDDED_DAEMON=$<BOOL:${DRAGONX_ENABLE_EMBEDDED_DAEMON}>
|
||||||
|
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
|
||||||
|
DRAGONX_TEST_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(DRAGONX_LITE_BACKEND_READY)
|
||||||
|
target_link_libraries(ObsidianDragonTests PRIVATE dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(UNIX)
|
||||||
|
target_link_libraries(ObsidianDragonTests PRIVATE ${CMAKE_DL_LIBS})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME ObsidianDragonPhase4Tests COMMAND ObsidianDragonTests)
|
||||||
|
endif()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Summary
|
# Summary
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
message(STATUS "")
|
message(STATUS "")
|
||||||
message(STATUS "DragonX ImGui Wallet Configuration:")
|
message(STATUS "DragonX ImGui Wallet Configuration:")
|
||||||
message(STATUS " Version: ${PROJECT_VERSION}")
|
message(STATUS " Version: ${DRAGONX_APP_VERSION}${DRAGONX_APP_VERSION_SUFFIX} (${DRAGONX_APP_NAME})")
|
||||||
message(STATUS " Build type: ${CMAKE_BUILD_TYPE}")
|
message(STATUS " Build type: ${CMAKE_BUILD_TYPE}")
|
||||||
message(STATUS " C++ Standard: ${CMAKE_CXX_STANDARD}")
|
message(STATUS " C++ Standard: ${CMAKE_CXX_STANDARD}")
|
||||||
message(STATUS " ImGui dir: ${IMGUI_DIR}")
|
message(STATUS " ImGui dir: ${IMGUI_DIR}")
|
||||||
message(STATUS " SDL3 found: ${SDL3_FOUND}")
|
message(STATUS " SDL3 found: ${SDL3_FOUND}")
|
||||||
message(STATUS " Sodium lib: ${SODIUM_LIBRARY}")
|
message(STATUS " Sodium lib: ${SODIUM_LIBRARY}")
|
||||||
|
message(STATUS " Lite build: ${DRAGONX_BUILD_LITE}")
|
||||||
|
message(STATUS " Lite requested: ${DRAGONX_ENABLE_LITE_BACKEND}")
|
||||||
|
message(STATUS " Lite backend: ${DRAGONX_LITE_BACKEND_READY}")
|
||||||
|
message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}")
|
||||||
|
message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
|
||||||
|
message(STATUS " Lite manifest: ${DRAGONX_LITE_BACKEND_MANIFEST}")
|
||||||
|
message(STATUS " Lite signature: ${DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE}")
|
||||||
message(STATUS "")
|
message(STATUS "")
|
||||||
|
|||||||
49
README.md
49
README.md
@@ -1,6 +1,8 @@
|
|||||||
# DragonX Wallet - ImGui Edition
|
# ObsidianDragon - DragonX Wallet
|
||||||
|
|
||||||
A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
|
A lightweight, portable full-node cryptocurrency wallet for DragonX (DRGX), built with Dear ImGui.
|
||||||
|
|
||||||
|
Current pre-release: **1.2.0-rc1**.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@@ -9,10 +11,13 @@ A lightweight, portable cryptocurrency wallet for DragonX (DRGX), built with Dea
|
|||||||
|
|
||||||
- **Full Node Support**: Connects to dragonxd for complete blockchain verification
|
- **Full Node Support**: Connects to dragonxd for complete blockchain verification
|
||||||
- **Shielded Transactions**: Full z-address support with encrypted memos
|
- **Shielded Transactions**: Full z-address support with encrypted memos
|
||||||
- **Integrated Mining**: CPU mining controls with hashrate monitoring
|
- **Address Management**: Labels, icons, favorites, hidden addresses, and address-to-address transfers
|
||||||
|
- **Integrated Mining**: Solo CPU mining plus pool mining through xmrig, with idle-mining controls
|
||||||
|
- **Explorer Tools**: Block/transaction lookup and bootstrap snapshot download
|
||||||
- **Market Data**: Real-time price charts from CoinGecko
|
- **Market Data**: Real-time price charts from CoinGecko
|
||||||
- **QR Codes**: Generate and display QR codes for receiving addresses
|
- **QR Codes**: Generate and display QR codes for receiving addresses
|
||||||
- **Multi-language**: i18n support (English, Spanish, more coming)
|
- **Multi-language**: i18n support for English, German, Spanish, French, Japanese, Korean, Portuguese, Russian, and Chinese
|
||||||
|
- **CJK Fonts**: Bundled CJK subset font for translated interfaces
|
||||||
- **Lightweight**: ~5-10MB binary vs ~50MB+ for Qt version
|
- **Lightweight**: ~5-10MB binary vs ~50MB+ for Qt version
|
||||||
- **Fast Builds**: Compiles in seconds, not minutes
|
- **Fast Builds**: Compiles in seconds, not minutes
|
||||||
|
|
||||||
@@ -116,7 +121,8 @@ cd ObsidianDragon/
|
|||||||
./ObsidianDragon
|
./ObsidianDragon
|
||||||
```
|
```
|
||||||
|
|
||||||
The wallet will automatically connect to the daemon using credentials from \`~/.hush/DRAGONX/DRAGONX.conf\`.
|
The wallet will automatically connect to the daemon using credentials from `~/.hush/DRAGONX/DRAGONX.conf`.
|
||||||
|
|
||||||
### Using Custom Node Binaries
|
### Using Custom Node Binaries
|
||||||
|
|
||||||
The wallet checks its **own directory first** when looking for DragonX node binaries. This means you can test new or different branch builds of `hush-arrakis-chain`/`hushd` without waiting for a new wallet release:
|
The wallet checks its **own directory first** when looking for DragonX node binaries. This means you can test new or different branch builds of `hush-arrakis-chain`/`hushd` without waiting for a new wallet release:
|
||||||
@@ -131,9 +137,10 @@ The wallet checks its **own directory first** when looking for DragonX node bina
|
|||||||
3. System-wide locations (`/usr/local/bin`, `~/dragonx/src`, etc.)
|
3. System-wide locations (`/usr/local/bin`, `~/dragonx/src`, etc.)
|
||||||
|
|
||||||
This is useful for testing new branches or hotfixes to the node software before they are bundled into a wallet release.
|
This is useful for testing new branches or hotfixes to the node software before they are bundled into a wallet release.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Configuration is stored in \`~/.hush/DRAGONX/DRAGONX.conf\`:
|
Configuration is stored in `~/.hush/DRAGONX/DRAGONX.conf`:
|
||||||
|
|
||||||
```
|
```
|
||||||
rpcuser=your_rpc_user
|
rpcuser=your_rpc_user
|
||||||
@@ -148,44 +155,46 @@ ObsidianDragon/
|
|||||||
├── src/
|
├── src/
|
||||||
│ ├── main.cpp # Entry point, SDL/ImGui setup
|
│ ├── main.cpp # Entry point, SDL/ImGui setup
|
||||||
│ ├── app.cpp/h # Main application class
|
│ ├── app.cpp/h # Main application class
|
||||||
│ ├── wallet_state.h # Wallet data structures
|
│ ├── data/ # WalletState, address book, exchange info
|
||||||
│ ├── version.h # Version definitions
|
│ ├── config/ # Settings persistence and committed/generated version.h
|
||||||
│ ├── ui/
|
│ ├── ui/
|
||||||
│ │ ├── theme.cpp/h # DragonX theme
|
│ │ ├── schema/ # TOML UI schema and skin manager
|
||||||
│ │ └── windows/ # UI tabs and dialogs
|
│ │ ├── material/ # Material components, typography, layout
|
||||||
|
│ │ ├── windows/ # Tabs and dialogs
|
||||||
|
│ │ └── pages/ # Multi-page screens such as Settings
|
||||||
│ ├── rpc/
|
│ ├── rpc/
|
||||||
│ │ ├── rpc_client.cpp # JSON-RPC client
|
│ │ ├── rpc_client.cpp # JSON-RPC client
|
||||||
│ │ └── connection.cpp # Daemon connection
|
│ │ └── connection.cpp # Daemon connection
|
||||||
│ ├── config/
|
│ ├── resources/ # Embedded resource extraction
|
||||||
│ │ └── settings.cpp # Settings persistence
|
│ ├── platform/ # Windows DX11/backdrop helpers
|
||||||
│ ├── util/
|
│ ├── util/
|
||||||
│ │ ├── i18n.cpp # Internationalization
|
│ │ ├── i18n.cpp # Internationalization
|
||||||
│ │ └── ...
|
│ │ └── ...
|
||||||
│ └── daemon/
|
│ └── daemon/
|
||||||
│ └── embedded_daemon.cpp
|
│ └── embedded_daemon.cpp
|
||||||
├── res/
|
├── res/
|
||||||
│ ├── fonts/ # Ubuntu font
|
│ ├── fonts/ # Ubuntu, icon, and CJK fonts
|
||||||
│ └── lang/ # Translation files
|
│ └── lang/ # Translation files
|
||||||
├── libs/
|
├── libs/
|
||||||
│ └── qrcode/ # QR code generation
|
│ └── qrcode/ # QR code generation
|
||||||
├── CMakeLists.txt
|
├── CMakeLists.txt
|
||||||
├── build-release.sh # Build script
|
├── build.sh # Release/cross-platform build script
|
||||||
└── create-appimage.sh # AppImage packaging
|
└── scripts/create-appimage.sh # AppImage packaging
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
Fetched automatically by CMake (no manual install needed):
|
Fetched or discovered by CMake:
|
||||||
|
|
||||||
- **[SDL3](https://github.com/libsdl-org/SDL)** — Cross-platform windowing/input
|
- **[SDL3](https://github.com/libsdl-org/SDL)** — Cross-platform windowing/input
|
||||||
- **[nlohmann/json](https://github.com/nlohmann/json)** — JSON parsing
|
- **[nlohmann/json](https://github.com/nlohmann/json)** — JSON parsing
|
||||||
- **[toml++](https://github.com/marzer/tomlplusplus)** — TOML parsing (UI schema/themes)
|
- **[toml++](https://github.com/marzer/tomlplusplus)** — TOML parsing (UI schema/themes)
|
||||||
- **[libcurl](https://curl.se/libcurl/)** — HTTPS RPC transport (system on Linux, fetched on Windows)
|
- **[libcurl](https://curl.se/libcurl/)** — HTTP/HTTPS transport for daemon RPC and network calls (system on Linux/macOS, fetched on Windows)
|
||||||
|
|
||||||
Bundled in `libs/`:
|
Bundled in `libs/`:
|
||||||
|
|
||||||
- **[Dear ImGui](https://github.com/ocornut/imgui)** — Immediate mode GUI
|
- **[Dear ImGui](https://github.com/ocornut/imgui)** — Immediate mode GUI
|
||||||
- **[libsodium](https://libsodium.org)** — Cryptographic operations (fetched by `scripts/fetch-libsodium.sh`)
|
- **[libsodium](https://libsodium.org)** — Cryptographic operations (system on Linux or fetched by `scripts/fetch-libsodium.sh`)
|
||||||
- **[QR-Code-generator](https://github.com/nayuki/QR-Code-generator)** — QR code rendering
|
- **[QR-Code-generator](https://github.com/nayuki/QR-Code-generator)** — QR code rendering
|
||||||
- **[miniz](https://github.com/richgel999/miniz)** — ZIP compression
|
- **[miniz](https://github.com/richgel999/miniz)** — ZIP compression
|
||||||
- **[GLAD](https://glad.dav1d.de/)** — OpenGL loader (Linux/macOS)
|
- **[GLAD](https://glad.dav1d.de/)** — OpenGL loader (Linux/macOS)
|
||||||
@@ -202,9 +211,11 @@ Bundled in `libs/`:
|
|||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
|
Current language files live in `res/lang/` as `de`, `es`, `fr`, `ja`, `ko`, `pt`, `ru`, and `zh` JSON files, with built-in English fallbacks.
|
||||||
|
|
||||||
To add a new language:
|
To add a new language:
|
||||||
|
|
||||||
1. Copy \`res/lang/es.json\` to \`res/lang/<code>.json\`
|
1. Copy `res/lang/es.json` to `res/lang/<code>.json`
|
||||||
2. Translate all strings
|
2. Translate all strings
|
||||||
3. The language will appear in Settings automatically
|
3. The language will appear in Settings automatically
|
||||||
|
|
||||||
|
|||||||
@@ -361,7 +361,22 @@ https://www.apache.org/licenses/LICENSE-2.0
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. IconFontCppHeaders
|
## 13. Material Design Icons Pickaxe Subset Font
|
||||||
|
|
||||||
|
- **Location:** `res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf`
|
||||||
|
- **Source:** https://github.com/Templarian/MaterialDesign-Webfont
|
||||||
|
- **Derived from:** Pictogrammers Material Design Icons webfont (`materialdesignicons-webfont.ttf`)
|
||||||
|
- **Copyright:** Pictogrammers contributors
|
||||||
|
- **License:** Apache License 2.0
|
||||||
|
|
||||||
|
This bundled font is a local one-glyph subset containing only the MDI pickaxe
|
||||||
|
icon, remapped onto a BMP private-use codepoint for Dear ImGui compatibility.
|
||||||
|
The full text of the Apache License 2.0 is available at:
|
||||||
|
https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. IconFontCppHeaders
|
||||||
|
|
||||||
- **Location:** `src/embedded/IconsMaterialDesign.h`
|
- **Location:** `src/embedded/IconsMaterialDesign.h`
|
||||||
- **Source:** https://github.com/juliettef/IconFontCppHeaders
|
- **Source:** https://github.com/juliettef/IconFontCppHeaders
|
||||||
@@ -390,7 +405,7 @@ freely, subject to the following restrictions:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14. Ubuntu Font Family
|
## 15. Ubuntu Font Family
|
||||||
|
|
||||||
- **Location:** `res/fonts/Ubuntu-Light.ttf`, `Ubuntu-Medium.ttf`, `Ubuntu-R.ttf`
|
- **Location:** `res/fonts/Ubuntu-Light.ttf`, `Ubuntu-Medium.ttf`, `Ubuntu-R.ttf`
|
||||||
- **Source:** https://design.ubuntu.com/font
|
- **Source:** https://design.ubuntu.com/font
|
||||||
|
|||||||
437
build.sh
437
build.sh
@@ -20,7 +20,9 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
VERSION="1.2.0-rc1"
|
# VERSION is resolved per-variant from CMakeLists.txt (the single source of truth) after arg
|
||||||
|
# parsing — see the APP_BASENAME block below. Placeholder until then.
|
||||||
|
VERSION=""
|
||||||
|
|
||||||
# ── Colours ──────────────────────────────────────────────────────────────────
|
# ── Colours ──────────────────────────────────────────────────────────────────
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -41,6 +43,8 @@ DO_DEV=false
|
|||||||
DO_LINUX=false
|
DO_LINUX=false
|
||||||
DO_WIN=false
|
DO_WIN=false
|
||||||
DO_MAC=false
|
DO_MAC=false
|
||||||
|
DO_LITE=false
|
||||||
|
DO_LITE_BACKEND=false
|
||||||
CLEAN=false
|
CLEAN=false
|
||||||
BUILD_TYPE="Release"
|
BUILD_TYPE="Release"
|
||||||
|
|
||||||
@@ -54,6 +58,10 @@ Targets (at least one required, or none for dev build):
|
|||||||
--linux-release Linux release (zip + AppImage) -> release/linux/
|
--linux-release Linux release (zip + AppImage) -> release/linux/
|
||||||
--win-release Windows cross-compile (mingw-w64) -> release/windows/
|
--win-release Windows cross-compile (mingw-w64) -> release/windows/
|
||||||
--mac-release macOS .app bundle + DMG -> release/mac/
|
--mac-release macOS .app bundle + DMG -> release/mac/
|
||||||
|
--lite Build ObsidianDragonLite variant (no embedded daemon/full-node features)
|
||||||
|
--lite-backend Like --lite, and link the real SDXL litelib backend artifact
|
||||||
|
(auto-discovers build/lite-backend/<platform>/; build it with
|
||||||
|
scripts/build-lite-backend-artifact.sh, or set DRAGONX_LITE_BACKEND_DIR)
|
||||||
|
|
||||||
Build trees are stored under build/{linux,windows,mac}/
|
Build trees are stored under build/{linux,windows,mac}/
|
||||||
|
|
||||||
@@ -74,6 +82,7 @@ Examples:
|
|||||||
$0 --linux-release # Linux release (zip + AppImage)
|
$0 --linux-release # Linux release (zip + AppImage)
|
||||||
$0 --win-release # Windows cross-compile
|
$0 --win-release # Windows cross-compile
|
||||||
$0 --mac-release # macOS bundle + DMG (native or osxcross)
|
$0 --mac-release # macOS bundle + DMG (native or osxcross)
|
||||||
|
$0 --lite-backend --mac-release # macOS ObsidianDragonLite.app + DMG (lite backend)
|
||||||
$0 --clean --linux-release --win-release # Clean + both
|
$0 --clean --linux-release --win-release # Clean + both
|
||||||
EOF
|
EOF
|
||||||
exit 0
|
exit 0
|
||||||
@@ -85,6 +94,8 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--linux-release) DO_LINUX=true; shift ;;
|
--linux-release) DO_LINUX=true; shift ;;
|
||||||
--win-release) DO_WIN=true; shift ;;
|
--win-release) DO_WIN=true; shift ;;
|
||||||
--mac-release) DO_MAC=true; shift ;;
|
--mac-release) DO_MAC=true; shift ;;
|
||||||
|
--lite) DO_LITE=true; shift ;;
|
||||||
|
--lite-backend) DO_LITE=true; DO_LITE_BACKEND=true; shift ;;
|
||||||
-c|--clean) CLEAN=true; shift ;;
|
-c|--clean) CLEAN=true; shift ;;
|
||||||
-d|--debug) BUILD_TYPE="Debug"; shift ;;
|
-d|--debug) BUILD_TYPE="Debug"; shift ;;
|
||||||
-j) JOBS="$2"; shift 2 ;;
|
-j) JOBS="$2"; shift 2 ;;
|
||||||
@@ -98,6 +109,92 @@ if ! $DO_LINUX && ! $DO_WIN && ! $DO_MAC; then
|
|||||||
DO_DEV=true
|
DO_DEV=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
APP_BASENAME="ObsidianDragon"
|
||||||
|
CMAKE_LITE_ARGS=()
|
||||||
|
# Always set the variant flag EXPLICITLY (ON and OFF) so switching variants in a shared build dir
|
||||||
|
# can't reuse a stale cached value (e.g. a prior --lite build leaving DRAGONX_BUILD_LITE=ON).
|
||||||
|
if $DO_LITE; then
|
||||||
|
APP_BASENAME="ObsidianDragonLite"
|
||||||
|
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=ON")
|
||||||
|
# Lite never embeds/launches a daemon; set it explicitly too for cache hygiene.
|
||||||
|
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=OFF")
|
||||||
|
info "Lite mode enabled: building ${APP_BASENAME}"
|
||||||
|
else
|
||||||
|
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=OFF")
|
||||||
|
# Re-assert the embedded daemon ON for full-node builds, EXPLICITLY, so a build dir whose cache
|
||||||
|
# was poisoned OFF by a prior --lite configure (or any stale value) is healed — otherwise the
|
||||||
|
# full-node app extracts dragonxd but never launches it (isUsingEmbeddedDaemon() == false).
|
||||||
|
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=ON")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve the release version string for the active variant from CMakeLists.txt (single source of
|
||||||
|
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
|
||||||
|
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
|
||||||
|
_cml="$SCRIPT_DIR/CMakeLists.txt"
|
||||||
|
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
|
||||||
|
_full_suffix=$(sed -n 's/^set(DRAGONX_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||||
|
_lite_ver=$(sed -n 's/^set(DRAGONX_LITE_VERSION[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||||
|
_lite_suffix=$(sed -n 's/^set(DRAGONX_LITE_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
|
||||||
|
if $DO_LITE; then
|
||||||
|
VERSION="${_lite_ver}${_lite_suffix}"
|
||||||
|
else
|
||||||
|
VERSION="${_full_ver}${_full_suffix}"
|
||||||
|
fi
|
||||||
|
[ -n "$_full_ver" ] && [ -n "$VERSION" ] || { err "Could not parse version from CMakeLists.txt"; exit 1; }
|
||||||
|
info "Release version: ${VERSION} (${APP_BASENAME})"
|
||||||
|
|
||||||
|
# ── Lite backend (real SDXL litelib) linking ─────────────────────────────────
|
||||||
|
# Enables DRAGONX_ENABLE_LITE_BACKEND with an imported artifact produced by
|
||||||
|
# scripts/build-lite-backend-artifact.sh. Auto-discovers build/lite-backend/<platform>/;
|
||||||
|
# override the directory with DRAGONX_LITE_BACKEND_DIR.
|
||||||
|
if $DO_LITE_BACKEND; then
|
||||||
|
# Artifact platform follows the cross target when exactly one non-host release is requested,
|
||||||
|
# so `--lite-backend --win-release` links the Windows backend (not the host's) automatically.
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux) lb_platform="linux" ;;
|
||||||
|
Darwin) lb_platform="macos" ;;
|
||||||
|
*) lb_platform="linux" ;;
|
||||||
|
esac
|
||||||
|
if $DO_WIN && ! $DO_LINUX && ! $DO_MAC; then lb_platform="windows"; fi
|
||||||
|
if $DO_MAC && ! $DO_LINUX && ! $DO_WIN; then lb_platform="macos"; fi
|
||||||
|
lb_dir="${DRAGONX_LITE_BACKEND_DIR:-$SCRIPT_DIR/build/lite-backend/$lb_platform}"
|
||||||
|
lb_lib=""
|
||||||
|
for cand in "$lb_dir"/libsilentdragonxlite.a "$lb_dir"/libsilentdragonxlite.so "$lb_dir"/silentdragonxlite.lib; do
|
||||||
|
[[ -f "$cand" ]] && { lb_lib="$cand"; break; }
|
||||||
|
done
|
||||||
|
lb_symbols="$lb_dir/lite-backend-symbols.txt"
|
||||||
|
lb_manifest="$lb_dir/lite-backend-artifact-manifest.json"
|
||||||
|
if [[ -z "$lb_lib" || ! -f "$lb_symbols" ]]; then
|
||||||
|
err "Lite backend artifact not found under: $lb_dir"
|
||||||
|
err "Build it first: ./scripts/build-lite-backend-artifact.sh --platform $lb_platform"
|
||||||
|
err "Or set DRAGONX_LITE_BACKEND_DIR to an existing artifact directory."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
CMAKE_LITE_ARGS+=(
|
||||||
|
"-DDRAGONX_ENABLE_LITE_BACKEND=ON"
|
||||||
|
"-DDRAGONX_LITE_BACKEND_LIBRARY=$lb_lib"
|
||||||
|
"-DDRAGONX_LITE_BACKEND_SYMBOLS_FILE=$lb_symbols"
|
||||||
|
"-DDRAGONX_LITE_BACKEND_LINK_MODE=imported"
|
||||||
|
"-DDRAGONX_LITE_BACKEND_ABI=sdxl-c-v1"
|
||||||
|
)
|
||||||
|
[[ -f "$lb_manifest" ]] && CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_MANIFEST=$lb_manifest")
|
||||||
|
# A Rust x86_64-pc-windows-gnu staticlib pulls in Win32 system libs (rustls/schannel, ring,
|
||||||
|
# dirs, std) that the app doesn't already link. The set is rustc's `--print native-static-libs`
|
||||||
|
# for the backend (winapi_* shims mapped to the real mingw import libs); all exist in mingw-w64.
|
||||||
|
if [[ "$lb_platform" == "windows" ]]; then
|
||||||
|
CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_EXTRA_LIBS=advapi32;ws2_32;kernel32;bcrypt;cfgmgr32;credui;crypt32;cryptnet;fwpuclnt;gdi32;msimg32;ncrypt;ntdll;ole32;opengl32;secur32;shell32;synchronization;user32;winspool;userenv")
|
||||||
|
fi
|
||||||
|
info "Lite backend enabled ($lb_platform): $lb_lib"
|
||||||
|
else
|
||||||
|
# Explicit OFF so a prior --lite-backend configure in a shared build dir can't leave it ON
|
||||||
|
# (which would then fail the BUILD_LITE=OFF guard in CMake).
|
||||||
|
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_LITE_BACKEND=OFF")
|
||||||
|
fi
|
||||||
|
|
||||||
|
should_bundle_full_node_assets() {
|
||||||
|
! $DO_LITE
|
||||||
|
}
|
||||||
|
|
||||||
# ── Helper: find resource files ──────────────────────────────────────────────
|
# ── Helper: find resource files ──────────────────────────────────────────────
|
||||||
find_sapling_params() {
|
find_sapling_params() {
|
||||||
local dirs=(
|
local dirs=(
|
||||||
@@ -215,13 +312,14 @@ build_dev() {
|
|||||||
cmake "$SCRIPT_DIR" \
|
cmake "$SCRIPT_DIR" \
|
||||||
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
|
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
|
||||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=ON
|
-DDRAGONX_USE_SYSTEM_SDL3=ON \
|
||||||
|
"${CMAKE_LITE_ARGS[@]}"
|
||||||
|
|
||||||
info "Building with $JOBS jobs ..."
|
info "Building with $JOBS jobs ..."
|
||||||
cmake --build . -j "$JOBS"
|
cmake --build . -j "$JOBS"
|
||||||
|
|
||||||
[[ -f "bin/ObsidianDragon" ]] || { err "Build failed"; exit 1; }
|
[[ -f "bin/${APP_BASENAME}" ]] || { err "Build failed"; exit 1; }
|
||||||
info "Dev binary: $bd/bin/ObsidianDragon ($(du -h bin/ObsidianDragon | cut -f1))"
|
info "Dev binary: $bd/bin/${APP_BASENAME} ($(du -h "bin/${APP_BASENAME}" | cut -f1))"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -242,42 +340,51 @@ build_release_linux() {
|
|||||||
cmake "$SCRIPT_DIR" \
|
cmake "$SCRIPT_DIR" \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=ON
|
-DDRAGONX_USE_SYSTEM_SDL3=ON \
|
||||||
|
"${CMAKE_LITE_ARGS[@]}"
|
||||||
|
|
||||||
info "Building with $JOBS jobs ..."
|
info "Building with $JOBS jobs ..."
|
||||||
cmake --build . -j "$JOBS"
|
cmake --build . -j "$JOBS"
|
||||||
|
|
||||||
[[ -f "bin/ObsidianDragon" ]] || { err "Linux build failed"; exit 1; }
|
[[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; }
|
||||||
|
|
||||||
info "Stripping ..."
|
info "Stripping ..."
|
||||||
strip bin/ObsidianDragon
|
strip "bin/${APP_BASENAME}"
|
||||||
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
|
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||||
|
|
||||||
# ── Bundle daemon ────────────────────────────────────────────────────────
|
if should_bundle_full_node_assets; then
|
||||||
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
|
# ── Bundle daemon ────────────────────────────────────────────────────
|
||||||
|
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
|
||||||
|
|
||||||
# ── Bundle Sapling params ────────────────────────────────────────────────
|
# ── Bundle Sapling params ────────────────────────────────────────────
|
||||||
SAPLING_SPEND="" SAPLING_OUTPUT=""
|
SAPLING_SPEND="" SAPLING_OUTPUT=""
|
||||||
find_sapling_params && {
|
find_sapling_params && {
|
||||||
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
|
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
|
||||||
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
|
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
|
||||||
info "Bundled Sapling params"
|
info "Bundled Sapling params"
|
||||||
} || warn "Sapling params not found — not bundled"
|
} || warn "Sapling params not found — not bundled"
|
||||||
|
else
|
||||||
|
info "Lite mode: skipping daemon and Sapling/asmap bundling"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Package: release/linux/ ──────────────────────────────────────────────
|
# ── Package: release/linux/ ──────────────────────────────────────────────
|
||||||
rm -rf "$out"
|
# Remove only THIS variant's prior artifacts so full-node and lite releases can coexist in the
|
||||||
|
# same output dir (both ObsidianDragon* and ObsidianDragonLite* end up under release/linux/).
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
|
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.AppImage"
|
||||||
|
|
||||||
local DIST="ObsidianDragon-${VERSION}-Linux-x64"
|
local DIST="${APP_BASENAME}-${VERSION}-Linux-x64"
|
||||||
local dist_dir="$out/$DIST"
|
local dist_dir="$out/$DIST"
|
||||||
mkdir -p "$dist_dir"
|
mkdir -p "$dist_dir"
|
||||||
|
|
||||||
cp bin/ObsidianDragon "$dist_dir/"
|
cp "bin/${APP_BASENAME}" "$dist_dir/"
|
||||||
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/"
|
if should_bundle_full_node_assets; then
|
||||||
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/"
|
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/"
|
||||||
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
|
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/"
|
||||||
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
|
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
|
||||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
|
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
|
||||||
|
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
|
||||||
|
fi
|
||||||
# Bundle xmrig for mining support
|
# Bundle xmrig for mining support
|
||||||
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/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"
|
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
|
||||||
@@ -299,27 +406,29 @@ build_release_linux() {
|
|||||||
"$APPDIR/usr/share/icons/hicolor/256x256/apps" \
|
"$APPDIR/usr/share/icons/hicolor/256x256/apps" \
|
||||||
"$APPDIR/usr/share/ObsidianDragon/res"
|
"$APPDIR/usr/share/ObsidianDragon/res"
|
||||||
|
|
||||||
cp bin/ObsidianDragon "$APPDIR/usr/bin/"
|
cp "bin/${APP_BASENAME}" "$APPDIR/usr/bin/"
|
||||||
cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true
|
cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true
|
||||||
|
|
||||||
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
|
if should_bundle_full_node_assets; then
|
||||||
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$APPDIR/usr/bin/"
|
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
|
||||||
# Daemon data files must be alongside the daemon binary (usr/bin/)
|
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$APPDIR/usr/bin/"
|
||||||
# because dragonxd searches relative to its own directory.
|
# Daemon data files must be alongside the daemon binary (usr/bin/)
|
||||||
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
|
# because dragonxd searches relative to its own directory.
|
||||||
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
|
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
|
||||||
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
|
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
|
||||||
|
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
|
||||||
|
fi
|
||||||
# Bundle xmrig for mining support
|
# Bundle xmrig for mining support
|
||||||
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/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"; }
|
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
|
||||||
|
|
||||||
# Desktop entry
|
# Desktop entry
|
||||||
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<'DESK'
|
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<DESK
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=DragonX Wallet
|
Name=DragonX Wallet
|
||||||
Comment=DragonX Cryptocurrency Wallet
|
Comment=DragonX Cryptocurrency Wallet
|
||||||
Exec=ObsidianDragon
|
Exec=${APP_BASENAME}
|
||||||
Icon=ObsidianDragon
|
Icon=ObsidianDragon
|
||||||
Categories=Finance;Network;
|
Categories=Finance;Network;
|
||||||
Terminal=false
|
Terminal=false
|
||||||
@@ -350,14 +459,14 @@ SVG
|
|||||||
cp "$APPDIR/ObsidianDragon.svg" "$APPDIR/ObsidianDragon.png" 2>/dev/null || true
|
cp "$APPDIR/ObsidianDragon.svg" "$APPDIR/ObsidianDragon.png" 2>/dev/null || true
|
||||||
|
|
||||||
# AppRun
|
# AppRun
|
||||||
cat > "$APPDIR/AppRun" <<'APPRUN'
|
cat > "$APPDIR/AppRun" <<APPRUN
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
SELF=$(readlink -f "$0")
|
SELF=\$(readlink -f "\$0")
|
||||||
HERE=${SELF%/*}
|
HERE=\${SELF%/*}
|
||||||
export DRAGONX_RES_PATH="${HERE}/usr/share/ObsidianDragon/res"
|
export DRAGONX_RES_PATH="\${HERE}/usr/share/ObsidianDragon/res"
|
||||||
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}"
|
export LD_LIBRARY_PATH="\${HERE}/usr/lib:\${LD_LIBRARY_PATH}"
|
||||||
cd "${HERE}/usr/share/ObsidianDragon"
|
cd "\${HERE}/usr/share/ObsidianDragon"
|
||||||
exec "${HERE}/usr/bin/ObsidianDragon" "$@"
|
exec "\${HERE}/usr/bin/${APP_BASENAME}" "\$@"
|
||||||
APPRUN
|
APPRUN
|
||||||
chmod +x "$APPDIR/AppRun"
|
chmod +x "$APPDIR/AppRun"
|
||||||
|
|
||||||
@@ -386,9 +495,9 @@ APPRUN
|
|||||||
local ARCH
|
local ARCH
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
cd "$bd"
|
cd "$bd"
|
||||||
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
|
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
|
||||||
cp "ObsidianDragon-${VERSION}-${ARCH}.AppImage" "$out/ObsidianDragon-${VERSION}.AppImage"
|
cp "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" "$out/${APP_BASENAME}-${VERSION}.AppImage"
|
||||||
info "AppImage: $out/ObsidianDragon-${VERSION}.AppImage ($(du -h "$out/ObsidianDragon-${VERSION}.AppImage" | cut -f1))"
|
info "AppImage: $out/${APP_BASENAME}-${VERSION}.AppImage ($(du -h "$out/${APP_BASENAME}-${VERSION}.AppImage" | cut -f1))"
|
||||||
} || warn "AppImage creation failed — binaries zip still in release/linux/"
|
} || warn "AppImage creation failed — binaries zip still in release/linux/"
|
||||||
|
|
||||||
info "Linux release artifacts: $out/"
|
info "Linux release artifacts: $out/"
|
||||||
@@ -497,26 +606,48 @@ HDR
|
|||||||
|
|
||||||
# ── Daemon binaries ──────────────────────────────────────────────
|
# ── Daemon binaries ──────────────────────────────────────────────
|
||||||
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
||||||
if [[ -d "$DD" && -f "$DD/dragonxd.exe" ]]; then
|
if should_bundle_full_node_assets; then
|
||||||
info "Embedding daemon binaries ..."
|
if [[ -d "$DD" && -f "$DD/dragonxd.exe" ]]; then
|
||||||
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
|
info "Embedding daemon binaries ..."
|
||||||
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
|
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
|
||||||
local sym=$(echo "$f" | sed 's/[^a-zA-Z0-9]/_/g')
|
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
|
||||||
if [[ -f "$DD/$f" ]]; then
|
local sym=$(echo "$f" | sed 's/[^a-zA-Z0-9]/_/g')
|
||||||
cp -f "$DD/$f" "$RES/$f"
|
if [[ -f "$DD/$f" ]]; then
|
||||||
info " Staged $f ($(du -h "$DD/$f" | cut -f1))"
|
cp -f "$DD/$f" "$RES/$f"
|
||||||
echo "INCBIN(${sym}, \"$RES/$f\");" >> "$GEN/embedded_data.h"
|
info " Staged $f ($(du -h "$DD/$f" | cut -f1))"
|
||||||
else
|
echo "INCBIN(${sym}, \"$RES/$f\");" >> "$GEN/embedded_data.h"
|
||||||
echo "extern \"C\" { static const uint8_t* g_${sym}_data = nullptr; }" >> "$GEN/embedded_data.h"
|
else
|
||||||
echo "static const unsigned int g_${sym}_size = 0;" >> "$GEN/embedded_data.h"
|
echo "extern \"C\" { static const uint8_t* g_${sym}_data = nullptr; }" >> "$GEN/embedded_data.h"
|
||||||
fi
|
echo "static const unsigned int g_${sym}_size = 0;" >> "$GEN/embedded_data.h"
|
||||||
done
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
warn "prebuilt-binaries/dragonxd-win/ not found — wallet-only build"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
warn "prebuilt-binaries/dragonxd-win/ not found — wallet-only build"
|
info "Lite mode: skipping embedded daemon binaries"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
|
||||||
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
|
||||||
|
# The published DRG-XMRig archives ship the binary inside a versioned subdir, not as a flat
|
||||||
|
# xmrig.exe. Extract it from the matching win-x64 zip if it isn't already staged — otherwise
|
||||||
|
# the embed below never fires (HAS_EMBEDDED_XMRIG stays undefined) and the wallet ships with
|
||||||
|
# no miner ("xmrig binary not found" at runtime).
|
||||||
|
if [[ ! -f "$XMRIG_DIR/xmrig.exe" ]]; then
|
||||||
|
local _xz; _xz=$(ls "$XMRIG_DIR"/drg-xmrig-*-win-x64.zip 2>/dev/null | head -1)
|
||||||
|
if [[ -n "$_xz" ]] && command -v unzip >/dev/null 2>&1; then
|
||||||
|
local _xtmp; _xtmp=$(mktemp -d)
|
||||||
|
# -j flattens the versioned subdir; check the file (not unzip's exit code, which is
|
||||||
|
# non-zero if a pattern matches nothing).
|
||||||
|
unzip -j -o "$_xz" '*xmrig.exe' -d "$_xtmp" >/dev/null 2>&1 || true
|
||||||
|
if [[ -f "$_xtmp/xmrig.exe" ]]; then
|
||||||
|
cp -f "$_xtmp/xmrig.exe" "$XMRIG_DIR/xmrig.exe"
|
||||||
|
info " Extracted xmrig.exe from $(basename "$_xz")"
|
||||||
|
fi
|
||||||
|
rm -rf "$_xtmp"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
if [[ -f "$XMRIG_DIR/xmrig.exe" ]]; then
|
if [[ -f "$XMRIG_DIR/xmrig.exe" ]]; then
|
||||||
cp -f "$XMRIG_DIR/xmrig.exe" "$RES/xmrig.exe"
|
cp -f "$XMRIG_DIR/xmrig.exe" "$RES/xmrig.exe"
|
||||||
info " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
|
info " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
|
||||||
@@ -599,34 +730,40 @@ HDR
|
|||||||
cmake "$SCRIPT_DIR" \
|
cmake "$SCRIPT_DIR" \
|
||||||
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
|
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF
|
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||||
|
"${CMAKE_LITE_ARGS[@]}"
|
||||||
|
|
||||||
info "Building with $JOBS jobs ..."
|
info "Building with $JOBS jobs ..."
|
||||||
cmake --build . -j "$JOBS"
|
cmake --build . -j "$JOBS"
|
||||||
|
|
||||||
[[ -f "bin/ObsidianDragon.exe" ]] || { err "Windows build failed"; exit 1; }
|
[[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; }
|
||||||
info "Binary: $(du -h bin/ObsidianDragon.exe | cut -f1)"
|
info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)"
|
||||||
|
|
||||||
# ── Package: release/windows/ ────────────────────────────────────────────
|
# ── Package: release/windows/ ────────────────────────────────────────────
|
||||||
rm -rf "$out"
|
# Remove only THIS variant's prior artifacts so full-node and lite releases coexist here.
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
|
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.exe"
|
||||||
|
|
||||||
local DIST="ObsidianDragon-${VERSION}-Windows-x64"
|
local DIST="${APP_BASENAME}-${VERSION}-Windows-x64"
|
||||||
local dist_dir="$out/$DIST"
|
local dist_dir="$out/$DIST"
|
||||||
mkdir -p "$dist_dir"
|
mkdir -p "$dist_dir"
|
||||||
cp bin/ObsidianDragon.exe "$dist_dir/"
|
cp "bin/${APP_BASENAME}.exe" "$dist_dir/"
|
||||||
|
|
||||||
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
|
||||||
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
|
if should_bundle_full_node_assets; then
|
||||||
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
|
||||||
done
|
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
||||||
|
done
|
||||||
|
|
||||||
# Bundle Sapling params + asmap for the zip distribution
|
# Bundle Sapling params + asmap for the zip distribution
|
||||||
# (The single-file exe has these embedded via INCBIN, but the zip
|
# (The single-file exe has these embedded via INCBIN, but the zip
|
||||||
# needs them on disk so the daemon can find them in its work dir.)
|
# needs them on disk so the daemon can find them in its work dir.)
|
||||||
for f in sapling-spend.params sapling-output.params asmap.dat; do
|
for f in sapling-spend.params sapling-output.params asmap.dat; do
|
||||||
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
|
||||||
done
|
done
|
||||||
|
else
|
||||||
|
info "Lite mode: skipping daemon and Sapling/asmap assets in Windows zip"
|
||||||
|
fi
|
||||||
|
|
||||||
# Bundle xmrig for mining support
|
# Bundle xmrig for mining support
|
||||||
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
|
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
|
||||||
@@ -635,8 +772,8 @@ HDR
|
|||||||
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
cp -r bin/res "$dist_dir/" 2>/dev/null || true
|
||||||
|
|
||||||
# ── Single-file exe (all resources embedded) ────────────────────────────
|
# ── Single-file exe (all resources embedded) ────────────────────────────
|
||||||
cp bin/ObsidianDragon.exe "$out/ObsidianDragon-${VERSION}.exe"
|
cp "bin/${APP_BASENAME}.exe" "$out/${APP_BASENAME}-${VERSION}.exe"
|
||||||
info "Single-file exe: $out/ObsidianDragon-${VERSION}.exe ($(du -h "$out/ObsidianDragon-${VERSION}.exe" | cut -f1))"
|
info "Single-file exe: $out/${APP_BASENAME}-${VERSION}.exe ($(du -h "$out/${APP_BASENAME}-${VERSION}.exe" | cut -f1))"
|
||||||
|
|
||||||
# ── Zip ──────────────────────────────────────────────────────────────────
|
# ── Zip ──────────────────────────────────────────────────────────────────
|
||||||
if command -v zip &>/dev/null; then
|
if command -v zip &>/dev/null; then
|
||||||
@@ -818,7 +955,8 @@ TOOLCHAIN
|
|||||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||||
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"}
|
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} \
|
||||||
|
"${CMAKE_LITE_ARGS[@]}"
|
||||||
else
|
else
|
||||||
# Build libsodium as universal if needed
|
# Build libsodium as universal if needed
|
||||||
local need_sodium=false
|
local need_sodium=false
|
||||||
@@ -844,39 +982,40 @@ TOOLCHAIN
|
|||||||
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
|
||||||
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
|
||||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
|
||||||
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
|
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
|
||||||
|
"${CMAKE_LITE_ARGS[@]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
info "Building with $JOBS jobs ..."
|
info "Building with $JOBS jobs ..."
|
||||||
cmake --build . -j "$JOBS"
|
cmake --build . -j "$JOBS"
|
||||||
|
|
||||||
[[ -f "bin/ObsidianDragon" ]] || { err "macOS build failed"; exit 1; }
|
[[ -f "bin/${APP_BASENAME}" ]] || { err "macOS build failed"; exit 1; }
|
||||||
|
|
||||||
# Strip — use osxcross strip for cross-builds
|
# Strip — use osxcross strip for cross-builds
|
||||||
if $IS_CROSS; then
|
if $IS_CROSS; then
|
||||||
local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip"
|
local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip"
|
||||||
if [[ -x "$STRIP_CMD" ]]; then
|
if [[ -x "$STRIP_CMD" ]]; then
|
||||||
info "Stripping (osxcross) ..."
|
info "Stripping (osxcross) ..."
|
||||||
"$STRIP_CMD" bin/ObsidianDragon
|
"$STRIP_CMD" "bin/${APP_BASENAME}"
|
||||||
else
|
else
|
||||||
warn "osxcross strip not found at $STRIP_CMD — skipping"
|
warn "osxcross strip not found at $STRIP_CMD — skipping"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
info "Stripping ..."
|
info "Stripping ..."
|
||||||
strip bin/ObsidianDragon
|
strip "bin/${APP_BASENAME}"
|
||||||
# Verify universal binary
|
# Verify universal binary
|
||||||
if command -v lipo &>/dev/null; then
|
if command -v lipo &>/dev/null; then
|
||||||
info "Architecture info:"
|
info "Architecture info:"
|
||||||
lipo -info bin/ObsidianDragon
|
lipo -info "bin/${APP_BASENAME}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
|
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
|
||||||
|
|
||||||
# ── Create .app bundle ───────────────────────────────────────────────────
|
# ── Create .app bundle ───────────────────────────────────────────────────
|
||||||
rm -rf "$out"
|
rm -rf "$out"
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
|
|
||||||
local APP="$out/ObsidianDragon.app"
|
local APP="$out/${APP_BASENAME}.app"
|
||||||
local CONTENTS="$APP/Contents"
|
local CONTENTS="$APP/Contents"
|
||||||
local MACOS="$CONTENTS/MacOS"
|
local MACOS="$CONTENTS/MacOS"
|
||||||
local RESOURCES="$CONTENTS/Resources"
|
local RESOURCES="$CONTENTS/Resources"
|
||||||
@@ -885,39 +1024,43 @@ TOOLCHAIN
|
|||||||
mkdir -p "$MACOS" "$RESOURCES/res" "$FRAMEWORKS"
|
mkdir -p "$MACOS" "$RESOURCES/res" "$FRAMEWORKS"
|
||||||
|
|
||||||
# Main binary
|
# Main binary
|
||||||
cp bin/ObsidianDragon "$MACOS/"
|
cp "bin/${APP_BASENAME}" "$MACOS/"
|
||||||
chmod +x "$MACOS/ObsidianDragon"
|
chmod +x "$MACOS/${APP_BASENAME}"
|
||||||
|
|
||||||
# Resources
|
# Resources
|
||||||
cp -r bin/res/* "$RESOURCES/res/" 2>/dev/null || true
|
cp -r bin/res/* "$RESOURCES/res/" 2>/dev/null || true
|
||||||
|
|
||||||
# Daemon binaries (macOS native, from dragonxd-mac/)
|
if should_bundle_full_node_assets; then
|
||||||
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
|
# Daemon binaries (macOS native, from dragonxd-mac/)
|
||||||
if [[ -d "$daemon_dir" ]]; then
|
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
|
||||||
for f in dragonxd dragonx-cli dragonx-tx; do
|
if [[ -d "$daemon_dir" ]]; then
|
||||||
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; chmod +x "$MACOS/$f"; info " Bundled $f"; }
|
for f in dragonxd dragonx-cli dragonx-tx; do
|
||||||
done
|
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; chmod +x "$MACOS/$f"; info " Bundled $f"; }
|
||||||
for f in sapling-spend.params sapling-output.params; do
|
done
|
||||||
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; info " Bundled $f"; }
|
for f in sapling-spend.params sapling-output.params; do
|
||||||
done
|
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; info " Bundled $f"; }
|
||||||
elif ! $IS_CROSS; then
|
done
|
||||||
# Native macOS: try standard paths
|
elif ! $IS_CROSS; then
|
||||||
local daemon_paths=(
|
# Native macOS: try standard paths
|
||||||
"$SCRIPT_DIR/../dragonxd"
|
local daemon_paths=(
|
||||||
"$HOME/dragonx/src/dragonxd"
|
"$SCRIPT_DIR/../dragonxd"
|
||||||
)
|
"$HOME/dragonx/src/dragonxd"
|
||||||
for p in "${daemon_paths[@]}"; do
|
)
|
||||||
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonxd"; chmod +x "$MACOS/dragonxd"; info " Bundled dragonxd"; break; }
|
for p in "${daemon_paths[@]}"; do
|
||||||
done
|
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonxd"; chmod +x "$MACOS/dragonxd"; info " Bundled dragonxd"; break; }
|
||||||
local cli_paths=(
|
done
|
||||||
"$SCRIPT_DIR/../dragonx-cli"
|
local cli_paths=(
|
||||||
"$HOME/dragonx/src/dragonx-cli"
|
"$SCRIPT_DIR/../dragonx-cli"
|
||||||
)
|
"$HOME/dragonx/src/dragonx-cli"
|
||||||
for p in "${cli_paths[@]}"; do
|
)
|
||||||
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonx-cli"; chmod +x "$MACOS/dragonx-cli"; info " Bundled dragonx-cli"; break; }
|
for p in "${cli_paths[@]}"; do
|
||||||
done
|
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonx-cli"; chmod +x "$MACOS/dragonx-cli"; info " Bundled dragonx-cli"; break; }
|
||||||
|
done
|
||||||
|
else
|
||||||
|
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
|
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
|
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
|
||||||
@@ -930,11 +1073,13 @@ TOOLCHAIN
|
|||||||
warn "xmrig not found — mining unavailable in .app"
|
warn "xmrig not found — mining unavailable in .app"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# asmap.dat — placed in MacOS/ so the daemon finds it next to its binary
|
if should_bundle_full_node_assets; then
|
||||||
find_asmap 2>/dev/null && {
|
# asmap.dat — placed in MacOS/ so the daemon finds it next to its binary
|
||||||
cp "$ASMAP_DAT" "$MACOS/asmap.dat"
|
find_asmap 2>/dev/null && {
|
||||||
info " Bundled asmap.dat"
|
cp "$ASMAP_DAT" "$MACOS/asmap.dat"
|
||||||
}
|
info " Bundled asmap.dat"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
# Bundle SDL3 dylib
|
# Bundle SDL3 dylib
|
||||||
local sdl_dylib=""
|
local sdl_dylib=""
|
||||||
@@ -954,26 +1099,34 @@ TOOLCHAIN
|
|||||||
# Fix the rpath so the binary finds SDL3 in Frameworks/
|
# Fix the rpath so the binary finds SDL3 in Frameworks/
|
||||||
if $IS_CROSS; then
|
if $IS_CROSS; then
|
||||||
local INSTALL_NAME_TOOL="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-install_name_tool"
|
local INSTALL_NAME_TOOL="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-install_name_tool"
|
||||||
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/ObsidianDragon" 2>/dev/null || true
|
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
|
||||||
else
|
else
|
||||||
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/ObsidianDragon" 2>/dev/null || true
|
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
info " Bundled $sdl_name"
|
info " Bundled $sdl_name"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Launcher script (ensures working dir + dylib path)
|
# Launcher script (ensures working dir + dylib path). Uses ${APP_BASENAME} so the lite
|
||||||
mv "$MACOS/ObsidianDragon" "$MACOS/ObsidianDragon.bin"
|
# variant (ObsidianDragonLite) gets a correctly-named launcher + .bin pair.
|
||||||
cat > "$MACOS/ObsidianDragon" <<'LAUNCH'
|
mv "$MACOS/${APP_BASENAME}" "$MACOS/${APP_BASENAME}.bin"
|
||||||
|
cat > "$MACOS/${APP_BASENAME}" <<LAUNCH
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
DIR="\$(cd "\$(dirname "\$0")" && pwd)"
|
||||||
export DYLD_LIBRARY_PATH="$DIR/../Frameworks:$DYLD_LIBRARY_PATH"
|
export DYLD_LIBRARY_PATH="\$DIR/../Frameworks:\$DYLD_LIBRARY_PATH"
|
||||||
export DRAGONX_RES_PATH="$DIR/../Resources/res"
|
export DRAGONX_RES_PATH="\$DIR/../Resources/res"
|
||||||
cd "$DIR/../Resources"
|
cd "\$DIR/../Resources"
|
||||||
exec "$DIR/ObsidianDragon.bin" "$@"
|
exec "\$DIR/${APP_BASENAME}.bin" "\$@"
|
||||||
LAUNCH
|
LAUNCH
|
||||||
chmod +x "$MACOS/ObsidianDragon"
|
chmod +x "$MACOS/${APP_BASENAME}"
|
||||||
|
|
||||||
# Info.plist
|
# Info.plist — display name + bundle id differ per variant so lite and full-node .apps
|
||||||
|
# can coexist; the executable matches the launcher (${APP_BASENAME}); the icon is shared.
|
||||||
|
local APP_DISPLAY_NAME="DragonX Wallet"
|
||||||
|
local APP_BUNDLE_ID="is.hush.dragonx"
|
||||||
|
if $DO_LITE; then
|
||||||
|
APP_DISPLAY_NAME="DragonX Wallet Lite"
|
||||||
|
APP_BUNDLE_ID="is.hush.dragonx.lite"
|
||||||
|
fi
|
||||||
cat > "$CONTENTS/Info.plist" <<PLIST
|
cat > "$CONTENTS/Info.plist" <<PLIST
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||||
@@ -981,17 +1134,17 @@ LAUNCH
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>DragonX Wallet</string>
|
<string>${APP_DISPLAY_NAME}</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>DragonX Wallet</string>
|
<string>${APP_DISPLAY_NAME}</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>is.hush.dragonx</string>
|
<string>${APP_BUNDLE_ID}</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>${VERSION}</string>
|
<string>${VERSION}</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>${VERSION}</string>
|
<string>${VERSION}</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>ObsidianDragon</string>
|
<string>${APP_BASENAME}</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>ObsidianDragon</string>
|
<string>ObsidianDragon</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
@@ -1053,25 +1206,27 @@ PLIST
|
|||||||
info ".app bundle created: $APP"
|
info ".app bundle created: $APP"
|
||||||
|
|
||||||
# ── Zip the .app bundle ──────────────────────────────────────────────────
|
# ── Zip the .app bundle ──────────────────────────────────────────────────
|
||||||
local APP_ZIP="ObsidianDragon-${VERSION}-macOS-${MAC_ARCH}.app.zip"
|
local APP_ZIP="${APP_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.app.zip"
|
||||||
if command -v zip &>/dev/null; then
|
if command -v zip &>/dev/null; then
|
||||||
(cd "$out" && zip -r "$APP_ZIP" "ObsidianDragon.app")
|
(cd "$out" && zip -r "$APP_ZIP" "${APP_BASENAME}.app")
|
||||||
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
|
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Create DMG ───────────────────────────────────────────────────────────
|
# ── Create DMG ───────────────────────────────────────────────────────────
|
||||||
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
local DMG_BASENAME="DragonX_Wallet"
|
||||||
|
$DO_LITE && DMG_BASENAME="DragonX_Wallet_Lite"
|
||||||
|
local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg"
|
||||||
|
|
||||||
if command -v create-dmg &>/dev/null; then
|
if command -v create-dmg &>/dev/null; then
|
||||||
# create-dmg (works on macOS; also available on Linux via npm)
|
# create-dmg (works on macOS; also available on Linux via npm)
|
||||||
info "Creating DMG with create-dmg ..."
|
info "Creating DMG with create-dmg ..."
|
||||||
create-dmg \
|
create-dmg \
|
||||||
--volname "DragonX Wallet" \
|
--volname "${APP_DISPLAY_NAME}" \
|
||||||
--volicon "$RESOURCES/ObsidianDragon.icns" \
|
--volicon "$RESOURCES/ObsidianDragon.icns" \
|
||||||
--window-pos 200 120 \
|
--window-pos 200 120 \
|
||||||
--window-size 600 400 \
|
--window-size 600 400 \
|
||||||
--icon-size 100 \
|
--icon-size 100 \
|
||||||
--icon "ObsidianDragon.app" 150 190 \
|
--icon "${APP_BASENAME}.app" 150 190 \
|
||||||
--app-drop-link 450 190 \
|
--app-drop-link 450 190 \
|
||||||
--no-internet-enable \
|
--no-internet-enable \
|
||||||
"$out/$DMG_NAME" \
|
"$out/$DMG_NAME" \
|
||||||
@@ -1086,7 +1241,7 @@ PLIST
|
|||||||
mkdir -p "$staging"
|
mkdir -p "$staging"
|
||||||
cp -a "$APP" "$staging/"
|
cp -a "$APP" "$staging/"
|
||||||
ln -s /Applications "$staging/Applications"
|
ln -s /Applications "$staging/Applications"
|
||||||
hdiutil create -volname "DragonX Wallet" \
|
hdiutil create -volname "${APP_DISPLAY_NAME}" \
|
||||||
-srcfolder "$staging" \
|
-srcfolder "$staging" \
|
||||||
-ov -format UDZO \
|
-ov -format UDZO \
|
||||||
"$out/$DMG_NAME" 2>/dev/null && {
|
"$out/$DMG_NAME" 2>/dev/null && {
|
||||||
@@ -1102,7 +1257,7 @@ PLIST
|
|||||||
cp -a "$APP" "$staging/"
|
cp -a "$APP" "$staging/"
|
||||||
# Can't create a real symlink to /Applications in an ISO, but the .app
|
# Can't create a real symlink to /Applications in an ISO, but the .app
|
||||||
# is the important part — users drag it to Applications manually.
|
# is the important part — users drag it to Applications manually.
|
||||||
genisoimage -V "DragonX Wallet" \
|
genisoimage -V "${APP_DISPLAY_NAME}" \
|
||||||
-D -R -apple -no-pad \
|
-D -R -apple -no-pad \
|
||||||
-o "$out/$DMG_NAME" \
|
-o "$out/$DMG_NAME" \
|
||||||
"$staging" 2>/dev/null && {
|
"$staging" 2>/dev/null && {
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
|
||||||
|
|
||||||
<!-- Application identity —————————————————————————————— -->
|
|
||||||
<assemblyIdentity
|
|
||||||
type="win32"
|
|
||||||
name="DragonX.ObsidianDragon.Wallet"
|
|
||||||
version="1.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>
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<assemblyIdentity
|
<assemblyIdentity
|
||||||
type="win32"
|
type="win32"
|
||||||
name="DragonX.ObsidianDragon.Wallet"
|
name="DragonX.ObsidianDragon.Wallet"
|
||||||
version="@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0"
|
version="@DRAGONX_APP_VERSION_MAJOR@.@DRAGONX_APP_VERSION_MINOR@.@DRAGONX_APP_VERSION_PATCH@.0"
|
||||||
processorArchitecture="amd64"
|
processorArchitecture="amd64"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
#include <winver.h>
|
#include <winver.h>
|
||||||
|
|
||||||
VS_VERSION_INFO VERSIONINFO
|
VS_VERSION_INFO VERSIONINFO
|
||||||
FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
|
FILEVERSION @DRAGONX_APP_VERSION_MAJOR@,@DRAGONX_APP_VERSION_MINOR@,@DRAGONX_APP_VERSION_PATCH@,0
|
||||||
PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
|
PRODUCTVERSION @DRAGONX_APP_VERSION_MAJOR@,@DRAGONX_APP_VERSION_MINOR@,@DRAGONX_APP_VERSION_PATCH@,0
|
||||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
FILEFLAGS 0x0L
|
FILEFLAGS 0x0L
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
@@ -32,13 +32,13 @@ BEGIN
|
|||||||
BLOCK "040904B0" // US-English, Unicode
|
BLOCK "040904B0" // US-English, Unicode
|
||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", "DragonX Developers\0"
|
VALUE "CompanyName", "DragonX Developers\0"
|
||||||
VALUE "FileDescription", "ObsidianDragon Wallet\0"
|
VALUE "FileDescription", "@DRAGONX_APP_NAME@ Wallet\0"
|
||||||
VALUE "FileVersion", "@PROJECT_VERSION@\0"
|
VALUE "FileVersion", "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@\0"
|
||||||
VALUE "InternalName", "ObsidianDragon\0"
|
VALUE "InternalName", "@DRAGONX_APP_NAME@\0"
|
||||||
VALUE "LegalCopyright", "Copyright 2024-2026 DragonX Developers. GPLv3.\0"
|
VALUE "LegalCopyright", "Copyright 2024-2026 DragonX Developers. GPLv3.\0"
|
||||||
VALUE "OriginalFilename", "ObsidianDragon.exe\0"
|
VALUE "OriginalFilename", "@DRAGONX_APP_NAME@.exe\0"
|
||||||
VALUE "ProductName", "ObsidianDragon\0"
|
VALUE "ProductName", "@DRAGONX_APP_NAME@\0"
|
||||||
VALUE "ProductVersion", "@PROJECT_VERSION@\0"
|
VALUE "ProductVersion", "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@\0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
BIN
res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf
Normal file
BIN
res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
res/fonts/UbuntuMono-R.ttf
Normal file
BIN
res/fonts/UbuntuMono-R.ttf
Normal file
Binary file not shown.
@@ -110,6 +110,28 @@
|
|||||||
"bootstrap_warning": "Vorhandene Blockdaten (blocks, chainstate, notarizations) werden gelöscht und ersetzt. Ihre wallet.dat wird NICHT verändert oder gelöscht.",
|
"bootstrap_warning": "Vorhandene Blockdaten (blocks, chainstate, notarizations) werden gelöscht und ersetzt. Ihre wallet.dat wird NICHT verändert oder gelöscht.",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"characters": "Zeichen",
|
"characters": "Zeichen",
|
||||||
|
"chat": "Chat",
|
||||||
|
"chat_cancel": "Abbrechen",
|
||||||
|
"chat_contact_request": "kontaktanfrage",
|
||||||
|
"chat_empty_hint": "Noch keine Unterhaltungen. Nachrichten, die du erhältst, erscheinen hier.",
|
||||||
|
"chat_locked_hint": "Entsperre deine Wallet, um deine Chats zu laden.",
|
||||||
|
"chat_new_button": "Neue Unterhaltung",
|
||||||
|
"chat_new_message": "Nachricht",
|
||||||
|
"chat_new_send": "Anfrage senden",
|
||||||
|
"chat_new_title": "Neue Unterhaltung",
|
||||||
|
"chat_new_zaddr": "z-Adresse des Empfängers",
|
||||||
|
"chat_select_hint": "Wähle eine Unterhaltung aus, um sie anzuzeigen.",
|
||||||
|
"chat_send": "Senden",
|
||||||
|
"chat_send_failed": "nicht gesendet",
|
||||||
|
"chat_toast_compose_failed": "Nachricht konnte nicht erstellt werden (zu lang?).",
|
||||||
|
"chat_toast_lite_busy": "Es wird bereits gesendet, oder es ist keine Wallet geöffnet.",
|
||||||
|
"chat_toast_no_zaddr": "Keine z-Adresse verfügbar, um den Chat zu senden.",
|
||||||
|
"chat_toast_not_connected": "Nicht verbunden – Nachricht nicht gesendet.",
|
||||||
|
"chat_toast_request_compose_failed": "Kontaktanfrage konnte nicht erstellt werden (ungültige Adresse / ungültiger Text?).",
|
||||||
|
"chat_toast_request_queued": "Kontaktanfrage in Warteschlange.",
|
||||||
|
"chat_toast_waiting_reply": "Warte auf die Antwort des Kontakts, bevor du ihm schreiben kannst.",
|
||||||
|
"chat_waiting_reply": "Warte auf die Antwort dieses Kontakts – sobald er antwortet, kannst du ihm schreiben.",
|
||||||
|
"chat_you": "Du",
|
||||||
"choose_icon": "Symbol wählen",
|
"choose_icon": "Symbol wählen",
|
||||||
"clear": "Leeren",
|
"clear": "Leeren",
|
||||||
"clear_all_bans": "Alle Sperren aufheben",
|
"clear_all_bans": "Alle Sperren aufheben",
|
||||||
@@ -346,6 +368,7 @@
|
|||||||
"market_now": "Jetzt",
|
"market_now": "Jetzt",
|
||||||
"market_pct_shielded": "%.0f%% Abgeschirmt",
|
"market_pct_shielded": "%.0f%% Abgeschirmt",
|
||||||
"market_portfolio": "PORTFOLIO",
|
"market_portfolio": "PORTFOLIO",
|
||||||
|
"market_price_loading": "Preisdaten werden geladen...",
|
||||||
"market_price_unavailable": "Preisdaten nicht verfügbar",
|
"market_price_unavailable": "Preisdaten nicht verfügbar",
|
||||||
"market_refresh_price": "Preisdaten aktualisieren",
|
"market_refresh_price": "Preisdaten aktualisieren",
|
||||||
"market_trade_on": "Handeln auf %s",
|
"market_trade_on": "Handeln auf %s",
|
||||||
|
|||||||
@@ -110,6 +110,28 @@
|
|||||||
"bootstrap_warning": "Los datos de bloques existentes (blocks, chainstate, notarizations) se eliminarán y reemplazarán. Su wallet.dat NO será modificado ni eliminado.",
|
"bootstrap_warning": "Los datos de bloques existentes (blocks, chainstate, notarizations) se eliminarán y reemplazarán. Su wallet.dat NO será modificado ni eliminado.",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"characters": "caracteres",
|
"characters": "caracteres",
|
||||||
|
"chat": "Chat",
|
||||||
|
"chat_cancel": "Cancelar",
|
||||||
|
"chat_contact_request": "solicitud de contacto",
|
||||||
|
"chat_empty_hint": "Aún no hay conversaciones. Los mensajes que recibas aparecerán aquí.",
|
||||||
|
"chat_locked_hint": "Desbloquea tu monedero para cargar tus chats.",
|
||||||
|
"chat_new_button": "Nueva conversación",
|
||||||
|
"chat_new_message": "Mensaje",
|
||||||
|
"chat_new_send": "Enviar solicitud",
|
||||||
|
"chat_new_title": "Nueva conversación",
|
||||||
|
"chat_new_zaddr": "Dirección z del destinatario",
|
||||||
|
"chat_select_hint": "Selecciona una conversación para verla.",
|
||||||
|
"chat_send": "Enviar",
|
||||||
|
"chat_send_failed": "no enviado",
|
||||||
|
"chat_toast_compose_failed": "No se pudo componer el mensaje (¿demasiado largo?).",
|
||||||
|
"chat_toast_lite_busy": "Ya hay un envío en curso, o no hay ningún monedero abierto.",
|
||||||
|
"chat_toast_no_zaddr": "No hay ninguna dirección z disponible desde la que enviar el chat.",
|
||||||
|
"chat_toast_not_connected": "Sin conexión: mensaje de chat no enviado.",
|
||||||
|
"chat_toast_request_compose_failed": "No se pudo componer la solicitud de contacto (¿dirección o texto no válidos?).",
|
||||||
|
"chat_toast_request_queued": "Solicitud de contacto en cola.",
|
||||||
|
"chat_toast_waiting_reply": "Espera a que este contacto responda antes de poder escribirle.",
|
||||||
|
"chat_waiting_reply": "Esperando a que este contacto responda: podrás escribirle una vez lo haga.",
|
||||||
|
"chat_you": "Tú",
|
||||||
"choose_icon": "Elegir Icono",
|
"choose_icon": "Elegir Icono",
|
||||||
"clear": "Limpiar",
|
"clear": "Limpiar",
|
||||||
"clear_all_bans": "Limpiar Todos los Bloqueos",
|
"clear_all_bans": "Limpiar Todos los Bloqueos",
|
||||||
@@ -346,6 +368,7 @@
|
|||||||
"market_now": "Ahora",
|
"market_now": "Ahora",
|
||||||
"market_pct_shielded": "%.0f%% Protegido",
|
"market_pct_shielded": "%.0f%% Protegido",
|
||||||
"market_portfolio": "PORTAFOLIO",
|
"market_portfolio": "PORTAFOLIO",
|
||||||
|
"market_price_loading": "Cargando datos de precio...",
|
||||||
"market_price_unavailable": "Datos de precio no disponibles",
|
"market_price_unavailable": "Datos de precio no disponibles",
|
||||||
"market_refresh_price": "Actualizar datos de precio",
|
"market_refresh_price": "Actualizar datos de precio",
|
||||||
"market_trade_on": "Operar en %s",
|
"market_trade_on": "Operar en %s",
|
||||||
|
|||||||
@@ -110,6 +110,28 @@
|
|||||||
"bootstrap_warning": "Les données de blocs existantes (blocks, chainstate, notarizations) seront supprimées et remplacées. Votre wallet.dat ne sera PAS modifié ni supprimé.",
|
"bootstrap_warning": "Les données de blocs existantes (blocks, chainstate, notarizations) seront supprimées et remplacées. Votre wallet.dat ne sera PAS modifié ni supprimé.",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"characters": "caractères",
|
"characters": "caractères",
|
||||||
|
"chat": "Discussion",
|
||||||
|
"chat_cancel": "Annuler",
|
||||||
|
"chat_contact_request": "demande de contact",
|
||||||
|
"chat_empty_hint": "Aucune conversation pour l'instant. Les messages que vous recevez apparaîtront ici.",
|
||||||
|
"chat_locked_hint": "Déverrouillez votre portefeuille pour charger vos discussions.",
|
||||||
|
"chat_new_button": "Nouvelle conversation",
|
||||||
|
"chat_new_message": "Message",
|
||||||
|
"chat_new_send": "Envoyer la demande",
|
||||||
|
"chat_new_title": "Nouvelle conversation",
|
||||||
|
"chat_new_zaddr": "Adresse Z du destinataire",
|
||||||
|
"chat_select_hint": "Sélectionnez une conversation pour l'afficher.",
|
||||||
|
"chat_send": "Envoyer",
|
||||||
|
"chat_send_failed": "non envoyé",
|
||||||
|
"chat_toast_compose_failed": "Impossible de composer le message (trop long ?).",
|
||||||
|
"chat_toast_lite_busy": "Un envoi est déjà en cours, ou aucun portefeuille n'est ouvert.",
|
||||||
|
"chat_toast_no_zaddr": "Aucune adresse Z disponible pour envoyer le message.",
|
||||||
|
"chat_toast_not_connected": "Non connecté — message non envoyé.",
|
||||||
|
"chat_toast_request_compose_failed": "Impossible de composer la demande de contact (adresse / texte invalide ?).",
|
||||||
|
"chat_toast_request_queued": "Demande de contact mise en file d'attente.",
|
||||||
|
"chat_toast_waiting_reply": "En attente de la réponse de ce contact avant de pouvoir lui écrire.",
|
||||||
|
"chat_waiting_reply": "En attente de la réponse de ce contact — vous pourrez lui écrire dès qu'il aura répondu.",
|
||||||
|
"chat_you": "Vous",
|
||||||
"choose_icon": "Choisir une icône",
|
"choose_icon": "Choisir une icône",
|
||||||
"clear": "Effacer",
|
"clear": "Effacer",
|
||||||
"clear_all_bans": "Lever tous les bannissements",
|
"clear_all_bans": "Lever tous les bannissements",
|
||||||
@@ -346,6 +368,7 @@
|
|||||||
"market_now": "Maintenant",
|
"market_now": "Maintenant",
|
||||||
"market_pct_shielded": "%.0f%% Blindé",
|
"market_pct_shielded": "%.0f%% Blindé",
|
||||||
"market_portfolio": "PORTEFEUILLE",
|
"market_portfolio": "PORTEFEUILLE",
|
||||||
|
"market_price_loading": "Chargement des données de prix...",
|
||||||
"market_price_unavailable": "Données de prix indisponibles",
|
"market_price_unavailable": "Données de prix indisponibles",
|
||||||
"market_refresh_price": "Actualiser les données de prix",
|
"market_refresh_price": "Actualiser les données de prix",
|
||||||
"market_trade_on": "Échanger sur %s",
|
"market_trade_on": "Échanger sur %s",
|
||||||
|
|||||||
@@ -110,6 +110,28 @@
|
|||||||
"bootstrap_warning": "既存のブロックデータ(blocks、chainstate、notarizations)は削除され置き換えられます。wallet.dat は変更・削除されません。",
|
"bootstrap_warning": "既存のブロックデータ(blocks、chainstate、notarizations)は削除され置き換えられます。wallet.dat は変更・削除されません。",
|
||||||
"cancel": "キャンセル",
|
"cancel": "キャンセル",
|
||||||
"characters": "文字",
|
"characters": "文字",
|
||||||
|
"chat": "チャット",
|
||||||
|
"chat_cancel": "キャンセル",
|
||||||
|
"chat_contact_request": "連絡リクエスト",
|
||||||
|
"chat_empty_hint": "まだ会話はありません。受信したメッセージはここに表示されます。",
|
||||||
|
"chat_locked_hint": "チャットを読み込むにはウォレットのロックを解除してください。",
|
||||||
|
"chat_new_button": "新しい会話",
|
||||||
|
"chat_new_message": "メッセージ",
|
||||||
|
"chat_new_send": "リクエストを送信",
|
||||||
|
"chat_new_title": "新しい会話",
|
||||||
|
"chat_new_zaddr": "宛先Zアドレス",
|
||||||
|
"chat_select_hint": "表示する会話を選択してください。",
|
||||||
|
"chat_send": "送信",
|
||||||
|
"chat_send_failed": "未送信",
|
||||||
|
"chat_toast_compose_failed": "メッセージを作成できませんでした(長すぎませんか?)。",
|
||||||
|
"chat_toast_lite_busy": "すでに送信処理が進行中か、ウォレットが開かれていません。",
|
||||||
|
"chat_toast_no_zaddr": "送信元に使えるZアドレスがありません。",
|
||||||
|
"chat_toast_not_connected": "未接続 — チャットメッセージは送信されませんでした。",
|
||||||
|
"chat_toast_request_compose_failed": "連絡リクエストを作成できませんでした(アドレスまたはテキストが無効?)。",
|
||||||
|
"chat_toast_request_queued": "連絡リクエストを送信待ちに追加しました。",
|
||||||
|
"chat_toast_waiting_reply": "メッセージを送るには、この相手からの返信を待つ必要があります。",
|
||||||
|
"chat_waiting_reply": "この相手からの返信を待っています — 返信があればメッセージを送れます。",
|
||||||
|
"chat_you": "自分",
|
||||||
"choose_icon": "アイコンを選択",
|
"choose_icon": "アイコンを選択",
|
||||||
"clear": "クリア",
|
"clear": "クリア",
|
||||||
"clear_all_bans": "すべてのブロックを解除",
|
"clear_all_bans": "すべてのブロックを解除",
|
||||||
@@ -346,6 +368,7 @@
|
|||||||
"market_now": "現在",
|
"market_now": "現在",
|
||||||
"market_pct_shielded": "%.0f%% シールド済み",
|
"market_pct_shielded": "%.0f%% シールド済み",
|
||||||
"market_portfolio": "ポートフォリオ",
|
"market_portfolio": "ポートフォリオ",
|
||||||
|
"market_price_loading": "価格データを読み込み中...",
|
||||||
"market_price_unavailable": "価格データが利用できません",
|
"market_price_unavailable": "価格データが利用できません",
|
||||||
"market_refresh_price": "価格データを更新",
|
"market_refresh_price": "価格データを更新",
|
||||||
"market_trade_on": "%s で取引",
|
"market_trade_on": "%s で取引",
|
||||||
|
|||||||
@@ -110,6 +110,28 @@
|
|||||||
"bootstrap_warning": "기존 블록 데이터(blocks, chainstate, notarizations)가 삭제되고 교체됩니다. wallet.dat는 수정되거나 삭제되지 않습니다.",
|
"bootstrap_warning": "기존 블록 데이터(blocks, chainstate, notarizations)가 삭제되고 교체됩니다. wallet.dat는 수정되거나 삭제되지 않습니다.",
|
||||||
"cancel": "취소",
|
"cancel": "취소",
|
||||||
"characters": "문자",
|
"characters": "문자",
|
||||||
|
"chat": "채팅",
|
||||||
|
"chat_cancel": "취소",
|
||||||
|
"chat_contact_request": "연락 요청",
|
||||||
|
"chat_empty_hint": "아직 대화가 없습니다. 받은 메시지가 여기에 표시됩니다.",
|
||||||
|
"chat_locked_hint": "채팅을 불러오려면 지갑 잠금을 해제하세요.",
|
||||||
|
"chat_new_button": "새 대화",
|
||||||
|
"chat_new_message": "메시지",
|
||||||
|
"chat_new_send": "요청 보내기",
|
||||||
|
"chat_new_title": "새 대화",
|
||||||
|
"chat_new_zaddr": "받는 사람 z-주소",
|
||||||
|
"chat_select_hint": "볼 대화를 선택하세요.",
|
||||||
|
"chat_send": "전송",
|
||||||
|
"chat_send_failed": "전송 안 됨",
|
||||||
|
"chat_toast_compose_failed": "메시지를 작성할 수 없습니다 (너무 긴가요?).",
|
||||||
|
"chat_toast_lite_busy": "이미 전송이 진행 중이거나 열린 지갑이 없습니다.",
|
||||||
|
"chat_toast_no_zaddr": "채팅을 보낼 z-주소가 없습니다.",
|
||||||
|
"chat_toast_not_connected": "연결되지 않음 — 채팅 메시지가 전송되지 않았습니다.",
|
||||||
|
"chat_toast_request_compose_failed": "연락 요청을 작성할 수 없습니다 (잘못된 주소 / 텍스트?).",
|
||||||
|
"chat_toast_request_queued": "연락 요청이 대기열에 추가되었습니다.",
|
||||||
|
"chat_toast_waiting_reply": "상대방이 답장해야 메시지를 보낼 수 있습니다.",
|
||||||
|
"chat_waiting_reply": "상대방의 답장을 기다리는 중입니다 — 답장하면 메시지를 보낼 수 있습니다.",
|
||||||
|
"chat_you": "나",
|
||||||
"choose_icon": "아이콘 선택",
|
"choose_icon": "아이콘 선택",
|
||||||
"clear": "지우기",
|
"clear": "지우기",
|
||||||
"clear_all_bans": "모든 차단 해제",
|
"clear_all_bans": "모든 차단 해제",
|
||||||
@@ -346,6 +368,7 @@
|
|||||||
"market_now": "현재",
|
"market_now": "현재",
|
||||||
"market_pct_shielded": "%.0f%% 차폐됨",
|
"market_pct_shielded": "%.0f%% 차폐됨",
|
||||||
"market_portfolio": "포트폴리오",
|
"market_portfolio": "포트폴리오",
|
||||||
|
"market_price_loading": "가격 데이터를 불러오는 중...",
|
||||||
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
|
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
|
||||||
"market_refresh_price": "가격 데이터 새로고침",
|
"market_refresh_price": "가격 데이터 새로고침",
|
||||||
"market_trade_on": "%s에서 거래",
|
"market_trade_on": "%s에서 거래",
|
||||||
|
|||||||
@@ -110,6 +110,28 @@
|
|||||||
"bootstrap_warning": "Os dados de blocos existentes (blocks, chainstate, notarizations) serão excluídos e substituídos. Seu wallet.dat NÃO será modificado ou excluído.",
|
"bootstrap_warning": "Os dados de blocos existentes (blocks, chainstate, notarizations) serão excluídos e substituídos. Seu wallet.dat NÃO será modificado ou excluído.",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"characters": "caracteres",
|
"characters": "caracteres",
|
||||||
|
"chat": "Chat",
|
||||||
|
"chat_cancel": "Cancelar",
|
||||||
|
"chat_contact_request": "solicitação de contato",
|
||||||
|
"chat_empty_hint": "Nenhuma conversa ainda. As mensagens que você receber aparecerão aqui.",
|
||||||
|
"chat_locked_hint": "Desbloqueie sua carteira para carregar suas conversas.",
|
||||||
|
"chat_new_button": "Nova conversa",
|
||||||
|
"chat_new_message": "Mensagem",
|
||||||
|
"chat_new_send": "Enviar solicitação",
|
||||||
|
"chat_new_title": "Nova conversa",
|
||||||
|
"chat_new_zaddr": "Endereço-z do destinatário",
|
||||||
|
"chat_select_hint": "Selecione uma conversa para visualizá-la.",
|
||||||
|
"chat_send": "Enviar",
|
||||||
|
"chat_send_failed": "não enviada",
|
||||||
|
"chat_toast_compose_failed": "Não foi possível compor a mensagem (muito longa?).",
|
||||||
|
"chat_toast_lite_busy": "Já há um envio em andamento, ou nenhuma carteira está aberta.",
|
||||||
|
"chat_toast_no_zaddr": "Nenhum endereço-z disponível para enviar o chat.",
|
||||||
|
"chat_toast_not_connected": "Não conectado — mensagem de chat não enviada.",
|
||||||
|
"chat_toast_request_compose_failed": "Não foi possível compor a solicitação de contato (endereço / texto inválido?).",
|
||||||
|
"chat_toast_request_queued": "Solicitação de contato na fila.",
|
||||||
|
"chat_toast_waiting_reply": "Aguardando a resposta do contato antes que você possa enviar mensagens a ele.",
|
||||||
|
"chat_waiting_reply": "Aguardando a resposta deste contato — você poderá enviar mensagens assim que ele responder.",
|
||||||
|
"chat_you": "Você",
|
||||||
"choose_icon": "Escolher Ícone",
|
"choose_icon": "Escolher Ícone",
|
||||||
"clear": "Limpar",
|
"clear": "Limpar",
|
||||||
"clear_all_bans": "Remover Todos os Banimentos",
|
"clear_all_bans": "Remover Todos os Banimentos",
|
||||||
@@ -346,6 +368,7 @@
|
|||||||
"market_now": "Agora",
|
"market_now": "Agora",
|
||||||
"market_pct_shielded": "%.0f%% Blindado",
|
"market_pct_shielded": "%.0f%% Blindado",
|
||||||
"market_portfolio": "PORTFÓLIO",
|
"market_portfolio": "PORTFÓLIO",
|
||||||
|
"market_price_loading": "Carregando dados de preço...",
|
||||||
"market_price_unavailable": "Dados de preço indisponíveis",
|
"market_price_unavailable": "Dados de preço indisponíveis",
|
||||||
"market_refresh_price": "Atualizar dados de preço",
|
"market_refresh_price": "Atualizar dados de preço",
|
||||||
"market_trade_on": "Negociar no %s",
|
"market_trade_on": "Negociar no %s",
|
||||||
|
|||||||
@@ -110,6 +110,28 @@
|
|||||||
"bootstrap_warning": "Существующие данные блоков (blocks, chainstate, notarizations) будут удалены и заменены. Ваш wallet.dat НЕ будет изменён или удалён.",
|
"bootstrap_warning": "Существующие данные блоков (blocks, chainstate, notarizations) будут удалены и заменены. Ваш wallet.dat НЕ будет изменён или удалён.",
|
||||||
"cancel": "Отмена",
|
"cancel": "Отмена",
|
||||||
"characters": "символов",
|
"characters": "символов",
|
||||||
|
"chat": "Чат",
|
||||||
|
"chat_cancel": "Отмена",
|
||||||
|
"chat_contact_request": "запрос контакта",
|
||||||
|
"chat_empty_hint": "Пока нет переписок. Полученные сообщения появятся здесь.",
|
||||||
|
"chat_locked_hint": "Разблокируйте кошелёк, чтобы загрузить переписку.",
|
||||||
|
"chat_new_button": "Новая переписка",
|
||||||
|
"chat_new_message": "Сообщение",
|
||||||
|
"chat_new_send": "Отправить запрос",
|
||||||
|
"chat_new_title": "Новая переписка",
|
||||||
|
"chat_new_zaddr": "Z-адрес получателя",
|
||||||
|
"chat_select_hint": "Выберите переписку для просмотра.",
|
||||||
|
"chat_send": "Отправить",
|
||||||
|
"chat_send_failed": "не отправлено",
|
||||||
|
"chat_toast_compose_failed": "Не удалось составить сообщение (слишком длинное?).",
|
||||||
|
"chat_toast_lite_busy": "Отправка уже выполняется, или кошелёк не открыт.",
|
||||||
|
"chat_toast_no_zaddr": "Нет доступного Z-адреса для отправки сообщений.",
|
||||||
|
"chat_toast_not_connected": "Нет подключения — сообщение не отправлено.",
|
||||||
|
"chat_toast_request_compose_failed": "Не удалось составить запрос контакта (неверный адрес / текст?).",
|
||||||
|
"chat_toast_request_queued": "Запрос контакта поставлен в очередь.",
|
||||||
|
"chat_toast_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему только после этого.",
|
||||||
|
"chat_waiting_reply": "Ожидание ответа от контакта — вы сможете писать ему, как только он ответит.",
|
||||||
|
"chat_you": "Вы",
|
||||||
"choose_icon": "Выбрать иконку",
|
"choose_icon": "Выбрать иконку",
|
||||||
"clear": "Очистить",
|
"clear": "Очистить",
|
||||||
"clear_all_bans": "Снять все блокировки",
|
"clear_all_bans": "Снять все блокировки",
|
||||||
@@ -346,6 +368,7 @@
|
|||||||
"market_now": "Сейчас",
|
"market_now": "Сейчас",
|
||||||
"market_pct_shielded": "%.0f%% Экранировано",
|
"market_pct_shielded": "%.0f%% Экранировано",
|
||||||
"market_portfolio": "ПОРТФЕЛЬ",
|
"market_portfolio": "ПОРТФЕЛЬ",
|
||||||
|
"market_price_loading": "Загрузка данных о ценах...",
|
||||||
"market_price_unavailable": "Данные о ценах недоступны",
|
"market_price_unavailable": "Данные о ценах недоступны",
|
||||||
"market_refresh_price": "Обновить данные о ценах",
|
"market_refresh_price": "Обновить данные о ценах",
|
||||||
"market_trade_on": "Торговать на %s",
|
"market_trade_on": "Торговать на %s",
|
||||||
|
|||||||
@@ -110,6 +110,28 @@
|
|||||||
"bootstrap_warning": "现有区块数据(blocks、chainstate、notarizations)将被删除并替换。您的 wallet.dat 不会被修改或删除。",
|
"bootstrap_warning": "现有区块数据(blocks、chainstate、notarizations)将被删除并替换。您的 wallet.dat 不会被修改或删除。",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"characters": "字符",
|
"characters": "字符",
|
||||||
|
"chat": "聊天",
|
||||||
|
"chat_cancel": "取消",
|
||||||
|
"chat_contact_request": "联系人请求",
|
||||||
|
"chat_empty_hint": "暂无对话。您收到的消息将显示在此处。",
|
||||||
|
"chat_locked_hint": "解锁钱包以加载您的聊天记录。",
|
||||||
|
"chat_new_button": "新建对话",
|
||||||
|
"chat_new_message": "消息",
|
||||||
|
"chat_new_send": "发送请求",
|
||||||
|
"chat_new_title": "新建对话",
|
||||||
|
"chat_new_zaddr": "收款方 z 地址",
|
||||||
|
"chat_select_hint": "选择一个对话以查看。",
|
||||||
|
"chat_send": "发送",
|
||||||
|
"chat_send_failed": "未发送",
|
||||||
|
"chat_toast_compose_failed": "无法编写该消息(内容过长?)。",
|
||||||
|
"chat_toast_lite_busy": "已有发送正在进行中,或未打开任何钱包。",
|
||||||
|
"chat_toast_no_zaddr": "没有可用于发送聊天的 z 地址。",
|
||||||
|
"chat_toast_not_connected": "未连接——聊天消息未发送。",
|
||||||
|
"chat_toast_request_compose_failed": "无法编写联系人请求(地址或文本无效?)。",
|
||||||
|
"chat_toast_request_queued": "联系人请求已排队。",
|
||||||
|
"chat_toast_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
|
||||||
|
"chat_waiting_reply": "等待该联系人回复——对方回复后您即可向其发送消息。",
|
||||||
|
"chat_you": "我",
|
||||||
"choose_icon": "选择图标",
|
"choose_icon": "选择图标",
|
||||||
"clear": "清除",
|
"clear": "清除",
|
||||||
"clear_all_bans": "解除所有封禁",
|
"clear_all_bans": "解除所有封禁",
|
||||||
@@ -346,6 +368,7 @@
|
|||||||
"market_now": "现在",
|
"market_now": "现在",
|
||||||
"market_pct_shielded": "%.0f%% 屏蔽",
|
"market_pct_shielded": "%.0f%% 屏蔽",
|
||||||
"market_portfolio": "投资组合",
|
"market_portfolio": "投资组合",
|
||||||
|
"market_price_loading": "正在加载价格数据...",
|
||||||
"market_price_unavailable": "价格数据不可用",
|
"market_price_unavailable": "价格数据不可用",
|
||||||
"market_refresh_price": "刷新价格数据",
|
"market_refresh_price": "刷新价格数据",
|
||||||
"market_trade_on": "在 %s 交易",
|
"market_trade_on": "在 %s 交易",
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ images = { background_image = "backgrounds/texture/pop-dark_bg.png", logo = "log
|
|||||||
--on-secondary = "#FFFFFF"
|
--on-secondary = "#FFFFFF"
|
||||||
--on-background = "#E8E6F0"
|
--on-background = "#E8E6F0"
|
||||||
--on-surface = "#E8E6F0"
|
--on-surface = "#E8E6F0"
|
||||||
--on-surface-medium = "rgba(232,230,240,0.72)"
|
--on-surface-medium = "rgba(232,230,240,0.85)"
|
||||||
--on-surface-disabled = "rgba(232,230,240,0.40)"
|
--on-surface-disabled = "rgba(232,230,240,0.58)"
|
||||||
--error = "#FF5C72"
|
--error = "#FF5C72"
|
||||||
--on-error = "#000000"
|
--on-error = "#000000"
|
||||||
--success = "#3DE8A0"
|
--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-badge = "rgba(232,230,240,1.0)"
|
||||||
--sidebar-divider = "rgba(200,190,240,0.05)"
|
--sidebar-divider = "rgba(200,190,240,0.05)"
|
||||||
--chart-line = "rgba(124,108,255,0.12)"
|
--chart-line = "rgba(124,108,255,0.12)"
|
||||||
--window-control = "rgba(232,230,240,0.72)"
|
--window-control = "rgba(232,230,240,0.85)"
|
||||||
--window-control-hover = "rgba(124,108,255,0.12)"
|
--window-control-hover = "rgba(124,108,255,0.12)"
|
||||||
--window-close-hover = "rgba(255,92,114,0.75)"
|
--window-close-hover = "rgba(255,92,114,0.75)"
|
||||||
--spinner-track = "rgba(200,190,240,0.08)"
|
--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-secondary = "#FFFFFF"
|
||||||
--on-background = "#1E1E2A"
|
--on-background = "#1E1E2A"
|
||||||
--on-surface = "#1E1E2A"
|
--on-surface = "#1E1E2A"
|
||||||
--on-surface-medium = "rgba(30,30,42,0.72)"
|
--on-surface-medium = "rgba(30,30,42,0.86)"
|
||||||
--on-surface-disabled = "rgba(30,30,42,0.38)"
|
--on-surface-disabled = "rgba(30,30,42,0.62)"
|
||||||
--error = "#E0304A"
|
--error = "#E0304A"
|
||||||
--on-error = "#FFFFFF"
|
--on-error = "#FFFFFF"
|
||||||
--success = "#18A860"
|
--success = "#18A860"
|
||||||
--on-success = "#FFFFFF"
|
--on-success = "#FFFFFF"
|
||||||
--warning = "#E09020"
|
--warning = "#E09020"
|
||||||
--on-warning = "#000000"
|
--on-warning = "#000000"
|
||||||
--divider = "rgba(30,30,60,0.12)"
|
--divider = "rgba(30,30,60,0.20)"
|
||||||
--outline = "rgba(30,30,60,0.15)"
|
--outline = "rgba(30,30,60,0.24)"
|
||||||
--scrim = "rgba(0,0,0,0.45)"
|
--scrim = "rgba(0,0,0,0.45)"
|
||||||
--surface-hover = "rgba(96,64,224,0.05)"
|
--surface-hover = "rgba(96,64,224,0.05)"
|
||||||
--surface-alt = "rgba(96,64,224,0.02)"
|
--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)"
|
--chart-hover-ring = "rgba(96,64,224,0.30)"
|
||||||
--tooltip-bg = "rgba(36,34,52,0.94)"
|
--tooltip-bg = "rgba(36,34,52,0.94)"
|
||||||
--tooltip-border = "rgba(96,64,224,0.16)"
|
--tooltip-border = "rgba(96,64,224,0.16)"
|
||||||
--glass-fill = "rgba(255,255,255,0.55)"
|
--glass-fill = "rgba(255,255,255,0.20)"
|
||||||
--glass-border = "rgba(30,30,60,0.10)"
|
--glass-border = "rgba(30,30,60,0.10)"
|
||||||
--glass-noise-tint = "rgba(96,64,224,0.02)"
|
--glass-noise-tint = "rgba(96,64,224,0.02)"
|
||||||
--tactile-top = "rgba(255,255,255,0.40)"
|
--tactile-top = "rgba(255,255,255,0.40)"
|
||||||
--tactile-bottom = "rgba(255,255,255,0.05)"
|
--tactile-bottom = "rgba(255,255,255,0.05)"
|
||||||
--hover-overlay = "rgba(96,64,224,0.04)"
|
--hover-overlay = "rgba(96,64,224,0.10)"
|
||||||
--active-overlay = "rgba(96,64,224,0.08)"
|
--active-overlay = "rgba(96,64,224,0.08)"
|
||||||
--rim-light = "rgba(96,64,224,0.06)"
|
--rim-light = "rgba(30,30,42,0.22)"
|
||||||
--status-divider = "rgba(30,30,60,0.08)"
|
--status-divider = "rgba(30,30,60,0.08)"
|
||||||
--sidebar-hover = "rgba(96,64,224,0.07)"
|
--sidebar-hover = "rgba(96,64,224,0.07)"
|
||||||
--sidebar-icon = "rgba(30,30,42,0.50)"
|
--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-secondary = "#000000"
|
||||||
--on-background = "#D0D0D4"
|
--on-background = "#D0D0D4"
|
||||||
--on-surface = "#D0D0D4"
|
--on-surface = "#D0D0D4"
|
||||||
--on-surface-medium = "rgba(208,208,212,0.75)"
|
--on-surface-medium = "rgba(208,208,212,0.85)"
|
||||||
--on-surface-disabled = "rgba(208,208,212,0.45)"
|
--on-surface-disabled = "rgba(208,208,212,0.58)"
|
||||||
--error = "#B07080"
|
--error = "#B07080"
|
||||||
--on-error = "#000000"
|
--on-error = "#000000"
|
||||||
--success = "#7AAE7C"
|
--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-badge = "rgba(208,208,212,1.0)"
|
||||||
--sidebar-divider = "rgba(220,220,225,0.05)"
|
--sidebar-divider = "rgba(220,220,225,0.05)"
|
||||||
--chart-line = "rgba(220,220,225,0.08)"
|
--chart-line = "rgba(220,220,225,0.08)"
|
||||||
--window-control = "rgba(208,208,212,0.75)"
|
--window-control = "rgba(208,208,212,0.85)"
|
||||||
--window-control-hover = "rgba(220,220,225,0.10)"
|
--window-control-hover = "rgba(220,220,225,0.10)"
|
||||||
--window-close-hover = "rgba(200,50,60,0.70)"
|
--window-close-hover = "rgba(200,50,60,0.70)"
|
||||||
--spinner-track = "rgba(220,220,225,0.08)"
|
--spinner-track = "rgba(220,220,225,0.08)"
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FDF8F0", --elevation-1 = "#F5EDE0", --elevation-
|
|||||||
--on-secondary = "#FFFFFF"
|
--on-secondary = "#FFFFFF"
|
||||||
--on-background = "#3A2E22"
|
--on-background = "#3A2E22"
|
||||||
--on-surface = "#3A2E22"
|
--on-surface = "#3A2E22"
|
||||||
--on-surface-medium = "rgba(58,46,34,0.68)"
|
--on-surface-medium = "rgba(58,46,34,0.86)"
|
||||||
--on-surface-disabled = "rgba(58,46,34,0.38)"
|
--on-surface-disabled = "rgba(58,46,34,0.62)"
|
||||||
--error = "#A0524A"
|
--error = "#A0524A"
|
||||||
--on-error = "#FFFFFF"
|
--on-error = "#FFFFFF"
|
||||||
--success = "#6A8A5C"
|
--success = "#4E8A42"
|
||||||
--on-success = "#FFFFFF"
|
--on-success = "#FFFFFF"
|
||||||
--warning = "#C08840"
|
--warning = "#C08840"
|
||||||
--on-warning = "#000000"
|
--on-warning = "#000000"
|
||||||
--divider = "rgba(140,110,70,0.14)"
|
--divider = "rgba(140,110,70,0.20)"
|
||||||
--outline = "rgba(140,110,70,0.16)"
|
--outline = "rgba(140,110,70,0.24)"
|
||||||
--scrim = "rgba(30,20,10,0.45)"
|
--scrim = "rgba(30,20,10,0.45)"
|
||||||
--surface-hover = "rgba(176,120,64,0.06)"
|
--surface-hover = "rgba(176,120,64,0.06)"
|
||||||
--surface-alt = "rgba(176,120,64,0.03)"
|
--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)"
|
--chart-hover-ring = "rgba(176,120,64,0.28)"
|
||||||
--tooltip-bg = "rgba(50,38,24,0.94)"
|
--tooltip-bg = "rgba(50,38,24,0.94)"
|
||||||
--tooltip-border = "rgba(176,120,64,0.12)"
|
--tooltip-border = "rgba(176,120,64,0.12)"
|
||||||
--glass-fill = "rgba(255,252,245,0.58)"
|
--glass-fill = "rgba(255,252,245,0.20)"
|
||||||
--glass-border = "rgba(176,120,64,0.14)"
|
--glass-border = "rgba(176,120,64,0.14)"
|
||||||
--glass-noise-tint = "rgba(180,140,80,0.03)"
|
--glass-noise-tint = "rgba(180,140,80,0.03)"
|
||||||
--tactile-top = "rgba(255,255,248,0.50)"
|
--tactile-top = "rgba(255,255,248,0.50)"
|
||||||
--tactile-bottom = "rgba(255,255,248,0.08)"
|
--tactile-bottom = "rgba(255,255,248,0.08)"
|
||||||
--hover-overlay = "rgba(176,120,64,0.05)"
|
--hover-overlay = "rgba(176,120,64,0.10)"
|
||||||
--active-overlay = "rgba(176,120,64,0.10)"
|
--active-overlay = "rgba(176,120,64,0.10)"
|
||||||
--rim-light = "rgba(212,160,108,0.10)"
|
--rim-light = "rgba(58,46,34,0.22)"
|
||||||
--status-divider = "rgba(176,120,64,0.10)"
|
--status-divider = "rgba(176,120,64,0.10)"
|
||||||
--sidebar-hover = "rgba(176,120,64,0.08)"
|
--sidebar-hover = "rgba(176,120,64,0.08)"
|
||||||
--sidebar-icon = "rgba(58,46,34,0.50)"
|
--sidebar-icon = "rgba(58,46,34,0.50)"
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FDFBFF", --elevation-1 = "#F5F0FA", --elevation-
|
|||||||
--on-secondary = "#FFFFFF"
|
--on-secondary = "#FFFFFF"
|
||||||
--on-background = "#1C1525"
|
--on-background = "#1C1525"
|
||||||
--on-surface = "#1C1525"
|
--on-surface = "#1C1525"
|
||||||
--on-surface-medium = "rgba(28,21,37,0.72)"
|
--on-surface-medium = "rgba(28,21,37,0.86)"
|
||||||
--on-surface-disabled = "rgba(28,21,37,0.40)"
|
--on-surface-disabled = "rgba(28,21,37,0.62)"
|
||||||
--error = "#C62828"
|
--error = "#C62828"
|
||||||
--on-error = "#FFFFFF"
|
--on-error = "#FFFFFF"
|
||||||
--success = "#2E7D32"
|
--success = "#2E7D32"
|
||||||
--on-success = "#FFFFFF"
|
--on-success = "#FFFFFF"
|
||||||
--warning = "#E65100"
|
--warning = "#E65100"
|
||||||
--on-warning = "#000000"
|
--on-warning = "#000000"
|
||||||
--divider = "rgba(120,80,160,0.12)"
|
--divider = "rgba(120,80,160,0.20)"
|
||||||
--outline = "rgba(120,80,160,0.14)"
|
--outline = "rgba(120,80,160,0.24)"
|
||||||
--scrim = "rgba(20,10,30,0.45)"
|
--scrim = "rgba(20,10,30,0.45)"
|
||||||
--surface-hover = "rgba(140,107,175,0.06)"
|
--surface-hover = "rgba(140,107,175,0.06)"
|
||||||
--surface-alt = "rgba(140,107,175,0.03)"
|
--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)"
|
--chart-hover-ring = "rgba(140,107,175,0.28)"
|
||||||
--tooltip-bg = "rgba(32,24,48,0.94)"
|
--tooltip-bg = "rgba(32,24,48,0.94)"
|
||||||
--tooltip-border = "rgba(140,107,175,0.12)"
|
--tooltip-border = "rgba(140,107,175,0.12)"
|
||||||
--glass-fill = "rgba(255,255,255,0.55)"
|
--glass-fill = "rgba(255,255,255,0.20)"
|
||||||
--glass-border = "rgba(140,107,175,0.14)"
|
--glass-border = "rgba(140,107,175,0.14)"
|
||||||
--glass-noise-tint = "rgba(180,140,220,0.03)"
|
--glass-noise-tint = "rgba(180,140,220,0.03)"
|
||||||
--tactile-top = "rgba(255,255,255,0.50)"
|
--tactile-top = "rgba(255,255,255,0.50)"
|
||||||
--tactile-bottom = "rgba(255,255,255,0.08)"
|
--tactile-bottom = "rgba(255,255,255,0.08)"
|
||||||
--hover-overlay = "rgba(140,107,175,0.05)"
|
--hover-overlay = "rgba(140,107,175,0.10)"
|
||||||
--active-overlay = "rgba(140,107,175,0.10)"
|
--active-overlay = "rgba(140,107,175,0.10)"
|
||||||
--rim-light = "rgba(180,140,255,0.10)"
|
--rim-light = "rgba(28,21,37,0.22)"
|
||||||
--status-divider = "rgba(140,107,175,0.10)"
|
--status-divider = "rgba(140,107,175,0.10)"
|
||||||
--sidebar-hover = "rgba(140,107,175,0.08)"
|
--sidebar-hover = "rgba(140,107,175,0.08)"
|
||||||
--sidebar-icon = "rgba(28,21,37,0.50)"
|
--sidebar-icon = "rgba(28,21,37,0.50)"
|
||||||
|
|||||||
@@ -19,16 +19,16 @@ elevation = { --elevation-0 = "#FAFAFA", --elevation-1 = "#F2F3F5", --elevation-
|
|||||||
--on-secondary = "#FFFFFF"
|
--on-secondary = "#FFFFFF"
|
||||||
--on-background = "#2A2C30"
|
--on-background = "#2A2C30"
|
||||||
--on-surface = "#2A2C30"
|
--on-surface = "#2A2C30"
|
||||||
--on-surface-medium = "rgba(42,44,48,0.68)"
|
--on-surface-medium = "rgba(42,44,48,0.86)"
|
||||||
--on-surface-disabled = "rgba(42,44,48,0.38)"
|
--on-surface-disabled = "rgba(42,44,48,0.62)"
|
||||||
--error = "#8C5A62"
|
--error = "#8C5A62"
|
||||||
--on-error = "#FFFFFF"
|
--on-error = "#FFFFFF"
|
||||||
--success = "#5A7E5C"
|
--success = "#3D7A42"
|
||||||
--on-success = "#FFFFFF"
|
--on-success = "#FFFFFF"
|
||||||
--warning = "#8A7A52"
|
--warning = "#9A7A2E"
|
||||||
--on-warning = "#000000"
|
--on-warning = "#000000"
|
||||||
--divider = "rgba(42,44,48,0.12)"
|
--divider = "rgba(42,44,48,0.20)"
|
||||||
--outline = "rgba(42,44,48,0.14)"
|
--outline = "rgba(42,44,48,0.24)"
|
||||||
--scrim = "rgba(0,0,0,0.42)"
|
--scrim = "rgba(0,0,0,0.42)"
|
||||||
--surface-hover = "rgba(42,44,48,0.04)"
|
--surface-hover = "rgba(42,44,48,0.04)"
|
||||||
--surface-alt = "rgba(42,44,48,0.02)"
|
--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)"
|
--chart-hover-ring = "rgba(42,44,48,0.24)"
|
||||||
--tooltip-bg = "rgba(50,52,58,0.92)"
|
--tooltip-bg = "rgba(50,52,58,0.92)"
|
||||||
--tooltip-border = "rgba(42,44,48,0.10)"
|
--tooltip-border = "rgba(42,44,48,0.10)"
|
||||||
--glass-fill = "rgba(255,255,255,0.55)"
|
--glass-fill = "rgba(255,255,255,0.20)"
|
||||||
--glass-border = "rgba(42,44,48,0.10)"
|
--glass-border = "rgba(42,44,48,0.10)"
|
||||||
--glass-noise-tint = "rgba(42,44,48,0.015)"
|
--glass-noise-tint = "rgba(42,44,48,0.015)"
|
||||||
--tactile-top = "rgba(255,255,255,0.35)"
|
--tactile-top = "rgba(255,255,255,0.35)"
|
||||||
--tactile-bottom = "rgba(255,255,255,0.04)"
|
--tactile-bottom = "rgba(255,255,255,0.04)"
|
||||||
--hover-overlay = "rgba(42,44,48,0.04)"
|
--hover-overlay = "rgba(42,44,48,0.10)"
|
||||||
--active-overlay = "rgba(42,44,48,0.08)"
|
--active-overlay = "rgba(42,44,48,0.08)"
|
||||||
--rim-light = "rgba(42,44,48,0.06)"
|
--rim-light = "rgba(42,44,48,0.22)"
|
||||||
--status-divider = "rgba(42,44,48,0.08)"
|
--status-divider = "rgba(42,44,48,0.08)"
|
||||||
--sidebar-hover = "rgba(42,44,48,0.05)"
|
--sidebar-hover = "rgba(42,44,48,0.05)"
|
||||||
--sidebar-icon = "rgba(42,44,48,0.45)"
|
--sidebar-icon = "rgba(42,44,48,0.45)"
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ elevation = { --elevation-0 = "#FAFAF8", --elevation-1 = "#F0EEEC", --elevation-
|
|||||||
--on-secondary = "#FFFFFF"
|
--on-secondary = "#FFFFFF"
|
||||||
--on-background = "#2C2A28"
|
--on-background = "#2C2A28"
|
||||||
--on-surface = "#2C2A28"
|
--on-surface = "#2C2A28"
|
||||||
--on-surface-medium = "rgba(44,42,40,0.68)"
|
--on-surface-medium = "rgba(44,42,40,0.86)"
|
||||||
--on-surface-disabled = "rgba(44,42,40,0.38)"
|
--on-surface-disabled = "rgba(44,42,40,0.62)"
|
||||||
--error = "#8C5250"
|
--error = "#8C5250"
|
||||||
--on-error = "#FFFFFF"
|
--on-error = "#FFFFFF"
|
||||||
--success = "#5C7A62"
|
--success = "#3F7A48"
|
||||||
--on-success = "#FFFFFF"
|
--on-success = "#FFFFFF"
|
||||||
--warning = "#8A7A4C"
|
--warning = "#9A7A2E"
|
||||||
--on-warning = "#000000"
|
--on-warning = "#000000"
|
||||||
--divider = "rgba(80,75,68,0.12)"
|
--divider = "rgba(80,75,68,0.20)"
|
||||||
--outline = "rgba(80,75,68,0.14)"
|
--outline = "rgba(80,75,68,0.24)"
|
||||||
--scrim = "rgba(20,18,16,0.42)"
|
--scrim = "rgba(20,18,16,0.42)"
|
||||||
--surface-hover = "rgba(110,117,128,0.05)"
|
--surface-hover = "rgba(110,117,128,0.05)"
|
||||||
--surface-alt = "rgba(110,117,128,0.025)"
|
--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)"
|
--chart-hover-ring = "rgba(110,117,128,0.24)"
|
||||||
--tooltip-bg = "rgba(44,42,40,0.94)"
|
--tooltip-bg = "rgba(44,42,40,0.94)"
|
||||||
--tooltip-border = "rgba(110,117,128,0.10)"
|
--tooltip-border = "rgba(110,117,128,0.10)"
|
||||||
--glass-fill = "rgba(255,255,254,0.62)"
|
--glass-fill = "rgba(255,255,254,0.20)"
|
||||||
--glass-border = "rgba(110,117,128,0.10)"
|
--glass-border = "rgba(110,117,128,0.10)"
|
||||||
--glass-noise-tint = "rgba(80,75,68,0.02)"
|
--glass-noise-tint = "rgba(80,75,68,0.02)"
|
||||||
--tactile-top = "rgba(255,255,255,0.45)"
|
--tactile-top = "rgba(255,255,255,0.45)"
|
||||||
--tactile-bottom = "rgba(255,255,255,0.06)"
|
--tactile-bottom = "rgba(255,255,255,0.06)"
|
||||||
--hover-overlay = "rgba(110,117,128,0.04)"
|
--hover-overlay = "rgba(110,117,128,0.10)"
|
||||||
--active-overlay = "rgba(110,117,128,0.08)"
|
--active-overlay = "rgba(110,117,128,0.08)"
|
||||||
--rim-light = "rgba(180,175,168,0.10)"
|
--rim-light = "rgba(44,42,40,0.22)"
|
||||||
--status-divider = "rgba(110,117,128,0.08)"
|
--status-divider = "rgba(110,117,128,0.08)"
|
||||||
--sidebar-hover = "rgba(110,117,128,0.06)"
|
--sidebar-hover = "rgba(110,117,128,0.06)"
|
||||||
--sidebar-icon = "rgba(44,42,40,0.48)"
|
--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-secondary = "#000000"
|
||||||
--on-background = "#E8E0F0"
|
--on-background = "#E8E0F0"
|
||||||
--on-surface = "#E8E0F0"
|
--on-surface = "#E8E0F0"
|
||||||
--on-surface-medium = "rgba(232,224,240,0.75)"
|
--on-surface-medium = "rgba(232,224,240,0.85)"
|
||||||
--on-surface-disabled = "rgba(232,224,240,0.45)"
|
--on-surface-disabled = "rgba(232,224,240,0.58)"
|
||||||
--error = "#CF6679"
|
--error = "#CF6679"
|
||||||
--on-error = "#000000"
|
--on-error = "#000000"
|
||||||
--success = "#81C784"
|
--success = "#81C784"
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/l
|
|||||||
--on-secondary = "#000000"
|
--on-secondary = "#000000"
|
||||||
--on-background = "#F0E0D8"
|
--on-background = "#F0E0D8"
|
||||||
--on-surface = "#F0E0D8"
|
--on-surface = "#F0E0D8"
|
||||||
--on-surface-medium = "rgba(240,224,216,0.7)"
|
--on-surface-medium = "rgba(240,224,216,0.85)"
|
||||||
--on-surface-disabled = "rgba(240,224,216,0.44)"
|
--on-surface-disabled = "rgba(240,224,216,0.58)"
|
||||||
--error = "#FF5252"
|
--error = "#FF5252"
|
||||||
--on-error = "#000000"
|
--on-error = "#000000"
|
||||||
--success = "#81C784"
|
--success = "#81C784"
|
||||||
@@ -485,12 +485,12 @@ suggestion-trunc-len = { size = 50 }
|
|||||||
fee-rounding = { size = 10.0 }
|
fee-rounding = { size = 10.0 }
|
||||||
amount-bar-max-btn-width = { size = 80.0 }
|
amount-bar-max-btn-width = { size = 80.0 }
|
||||||
amount-bar-height = { size = 22.0 }
|
amount-bar-height = { size = 22.0 }
|
||||||
confirm-popup-max-width = { size = 420.0 }
|
confirm-popup-max-width = { size = 560.0 }
|
||||||
confirm-addr-card-height = { size = 28.0 }
|
confirm-addr-card-height = { size = 28.0 }
|
||||||
confirm-amount-card-height = { size = 70.0 }
|
confirm-amount-card-height = { size = 96.0 }
|
||||||
confirm-row-step = { size = 16.0 }
|
confirm-row-step = { size = 24.0 }
|
||||||
confirm-val-col-x = { size = 90.0 }
|
confirm-val-col-x = { size = 150.0 }
|
||||||
confirm-usd-col-x = { size = 80.0 }
|
confirm-usd-col-x = { size = 116.0 }
|
||||||
progress-card-height = { size = 36.0 }
|
progress-card-height = { size = 36.0 }
|
||||||
progress-card-height-txid = { size = 52.0 }
|
progress-card-height-txid = { size = 52.0 }
|
||||||
progress-card-pad-x = { size = 12.0 }
|
progress-card-pad-x = { size = 12.0 }
|
||||||
@@ -516,10 +516,10 @@ error-icon-inset = { size = 20.0 }
|
|||||||
error-btn-rounding = { size = 4.0 }
|
error-btn-rounding = { size = 4.0 }
|
||||||
progress-glass-rounding-ratio = { size = 0.75 }
|
progress-glass-rounding-ratio = { size = 0.75 }
|
||||||
confirm-addr-card-min-height = { size = 24.0 }
|
confirm-addr-card-min-height = { size = 24.0 }
|
||||||
confirm-val-col-min-x = { size = 70.0 }
|
confirm-val-col-min-x = { size = 112.0 }
|
||||||
confirm-usd-col-min-x = { size = 60.0 }
|
confirm-usd-col-min-x = { size = 92.0 }
|
||||||
confirm-amount-card-min-height = { size = 54.0 }
|
confirm-amount-card-min-height = { size = 82.0 }
|
||||||
confirm-row-step-min = { size = 12.0 }
|
confirm-row-step-min = { size = 20.0 }
|
||||||
action-btn-min-height = { size = 26.0 }
|
action-btn-min-height = { size = 26.0 }
|
||||||
recent-icon-min-size = { size = 3.5 }
|
recent-icon-min-size = { size = 3.5 }
|
||||||
recent-icon-size = { size = 5.0 }
|
recent-icon-size = { size = 5.0 }
|
||||||
@@ -566,7 +566,7 @@ txid-trunc-len = { size = 14 }
|
|||||||
txid-label-x-offset = { size = 20.0 }
|
txid-label-x-offset = { size = 20.0 }
|
||||||
txid-copy-btn-right-offset = { size = 50.0 }
|
txid-copy-btn-right-offset = { size = 50.0 }
|
||||||
txid-copy-btn-y-offset = { size = 2.0 }
|
txid-copy-btn-y-offset = { size = 2.0 }
|
||||||
confirm-popup-width-ratio = { size = 0.85 }
|
confirm-popup-width-ratio = { size = 0.92 }
|
||||||
confirm-glass-rounding-ratio = { size = 0.75 }
|
confirm-glass-rounding-ratio = { size = 0.75 }
|
||||||
confirm-addr-trunc-len = { size = 48 }
|
confirm-addr-trunc-len = { size = 48 }
|
||||||
confirm-divider-thickness = { size = 1.0 }
|
confirm-divider-thickness = { size = 1.0 }
|
||||||
@@ -874,7 +874,7 @@ accent-stripe-inset-ratio = { size = 0.0 }
|
|||||||
accent-stripe-left-offset = { size = 0.0 }
|
accent-stripe-left-offset = { size = 0.0 }
|
||||||
accent-stripe-width = { size = 4.0 }
|
accent-stripe-width = { size = 4.0 }
|
||||||
accent-stripe-rounding = { size = 1.5 }
|
accent-stripe-rounding = { size = 1.5 }
|
||||||
chart-y-axis-min-padding = { size = 40.0 }
|
chart-y-axis-min-padding = { size = 54.0 }
|
||||||
chart-y-axis-padding = { size = 70.0 }
|
chart-y-axis-padding = { size = 70.0 }
|
||||||
chart-dot-min-radius = { size = 1.5 }
|
chart-dot-min-radius = { size = 1.5 }
|
||||||
chart-dot-radius = { size = 2.0 }
|
chart-dot-radius = { size = 2.0 }
|
||||||
@@ -908,6 +908,7 @@ scroll-fade-zone = { size = 24.0 }
|
|||||||
|
|
||||||
[tabs.console]
|
[tabs.console]
|
||||||
input-area-padding = 8.0
|
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.0
|
||||||
output = { line-spacing = 2 }
|
output = { line-spacing = 2 }
|
||||||
scroll-multiplier = { size = 3.0 }
|
scroll-multiplier = { size = 3.0 }
|
||||||
@@ -930,7 +931,7 @@ scanline-speed = { size = 40.0 }
|
|||||||
scanline-height = { size = 36.0 }
|
scanline-height = { size = 36.0 }
|
||||||
scanline-alpha = { size = 8.0 }
|
scanline-alpha = { size = 8.0 }
|
||||||
scanline-gap = { size = 2.0 }
|
scanline-gap = { size = 2.0 }
|
||||||
scanline-line-alpha = { size = 4.0 }
|
scanline-line-alpha = { size = 2.0 }
|
||||||
scanline-glow-spread = { size = 4.0 }
|
scanline-glow-spread = { size = 4.0 }
|
||||||
scanline-glow-intensity = { size = 0.6 }
|
scanline-glow-intensity = { size = 0.6 }
|
||||||
scanline-glow-color = { size = 255.0 }
|
scanline-glow-color = { size = 255.0 }
|
||||||
@@ -1218,6 +1219,9 @@ notification-progress = { color = "var(--primary)", height = 4, position = 18 }
|
|||||||
fill-alpha = { size = 12.0 }
|
fill-alpha = { size = 12.0 }
|
||||||
noise-alpha = { size = 14.0 }
|
noise-alpha = { size = 14.0 }
|
||||||
|
|
||||||
|
[components.overlay-dialog]
|
||||||
|
confirm-btn-height = { size = 40.0 }
|
||||||
|
|
||||||
[components.qr-code]
|
[components.qr-code]
|
||||||
module-scale = { size = 4 }
|
module-scale = { size = 4 }
|
||||||
border-modules = { size = 2 }
|
border-modules = { size = 2 }
|
||||||
@@ -1279,7 +1283,7 @@ page-fade-speed = { size = 8.0 }
|
|||||||
collapse-hysteresis = { size = 60.0 }
|
collapse-hysteresis = { size = 60.0 }
|
||||||
header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" }
|
header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" }
|
||||||
coin-icon = { icon = "logos/logo_dragonx_128.png" }
|
coin-icon = { icon = "logos/logo_dragonx_128.png" }
|
||||||
header-title = { font = "subtitle1", size = 14.0, pad-x = 22.0, pad-y = 6.0, logo-gap = 4.0, opacity = 0.7, offset-y = 4.0 }
|
header-title = { font = "subtitle1", size = 12.0, pad-x = 8.0, pad-y = 10.0, logo-gap = 4.0, opacity = 0.7, offset-y = -2.0 }
|
||||||
|
|
||||||
[components.main-window.window]
|
[components.main-window.window]
|
||||||
padding = [12, 36]
|
padding = [12, 36]
|
||||||
@@ -1324,10 +1328,13 @@ wallet-btn-padding = { size = 24.0 }
|
|||||||
rpc-label-min-width = { size = 70.0 }
|
rpc-label-min-width = { size = 70.0 }
|
||||||
rpc-label-width = { size = 85.0 }
|
rpc-label-width = { size = 85.0 }
|
||||||
security-combo-width = { size = 120.0 }
|
security-combo-width = { size = 120.0 }
|
||||||
|
node-grid-breakpoint = { size = 900.0 }
|
||||||
port-input-min-width = { size = 60.0 }
|
port-input-min-width = { size = 60.0 }
|
||||||
port-input-width-ratio = { size = 0.4 }
|
port-input-width-ratio = { size = 0.4 }
|
||||||
idle-combo-width = { size = 64.0 }
|
idle-combo-width = { size = 64.0 }
|
||||||
about-logo-size = { size = 64.0 }
|
# Reserved height basis for the About-card logo; the logo is drawn scaled to the
|
||||||
|
# card's actual height (aspect-preserved) and capped to this * aspect in width.
|
||||||
|
about-logo-size = { size = 150.0 }
|
||||||
|
|
||||||
[components.main-layout]
|
[components.main-layout]
|
||||||
app-bar-height = { size = 64.0 }
|
app-bar-height = { size = 64.0 }
|
||||||
@@ -1378,6 +1385,17 @@ summary = { min-width = 280.0, max-width = 400.0, width-ratio = 0.32, min-height
|
|||||||
side-panel = { min-width = 280.0, max-width = 450.0, width-ratio = 0.4, min-height = 200.0, max-height = 350.0, height-ratio = 0.8 }
|
side-panel = { min-width = 280.0, max-width = 450.0, width-ratio = 0.4, min-height = 200.0, max-height = 350.0, height-ratio = 0.8 }
|
||||||
table = { min-height = 150.0, height-ratio = 0.45, min-remaining = 100.0, default-reserve = 30.0 }
|
table = { min-height = 150.0, height-ratio = 0.45, min-remaining = 100.0, default-reserve = 30.0 }
|
||||||
|
|
||||||
|
[dialog]
|
||||||
|
width-default = 480.0
|
||||||
|
width-lg = 600.0
|
||||||
|
width-xl = 660.0
|
||||||
|
min-width = 280.0
|
||||||
|
form-width = 400.0
|
||||||
|
action-width = 100.0
|
||||||
|
action-gap = 8.0
|
||||||
|
max-height-ratio = 0.94
|
||||||
|
compact-bottom-ratio = 0.64
|
||||||
|
|
||||||
[button]
|
[button]
|
||||||
min-width = 180.0
|
min-width = 180.0
|
||||||
width = 140.0
|
width = 140.0
|
||||||
@@ -1510,7 +1528,6 @@ title = { font = "h5" }
|
|||||||
input = { width = 320.0, height = 40.0 }
|
input = { width = 320.0, height = 40.0 }
|
||||||
unlock-button = { width = 320.0, height = 44.0, font = "subtitle1" }
|
unlock-button = { width = 320.0, height = 44.0, font = "subtitle1" }
|
||||||
error-text = { font = "caption" }
|
error-text = { font = "caption" }
|
||||||
backdrop-alpha = { opacity = 0.0 }
|
|
||||||
mode-toggle = { font = "caption" }
|
mode-toggle = { font = "caption" }
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
801
scripts/build-lite-backend-artifact.sh
Executable file
801
scripts/build-lite-backend-artifact.sh
Executable file
@@ -0,0 +1,801 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
ABI_VERSION="sdxl-c-v1"
|
||||||
|
LINK_MODE="imported"
|
||||||
|
BACKEND_DIR="$PROJECT_ROOT/third_party/silentdragonxlite/lib"
|
||||||
|
BACKEND_SOURCE_DIR=""
|
||||||
|
BUILD_BACKEND_DIR=""
|
||||||
|
BACKEND_DEPENDENCY_DIR=""
|
||||||
|
BACKEND_DEPENDENCY_OVERRIDE_REQUESTED=false
|
||||||
|
OUT_DIR="$PROJECT_ROOT/build/lite-backend"
|
||||||
|
PLATFORM=""
|
||||||
|
RUST_TARGET=""
|
||||||
|
CARGO_TARGET_DIR_VALUE="${CARGO_TARGET_DIR:-}"
|
||||||
|
ARTIFACT_PATH=""
|
||||||
|
BUILD_ARTIFACT=true
|
||||||
|
BUILDER="${DRAGONX_LITE_BACKEND_BUILDER:-local}"
|
||||||
|
JOBS="${JOBS:-}"
|
||||||
|
SOURCE_DATE_EPOCH_VALUE="${SOURCE_DATE_EPOCH:-}"
|
||||||
|
REPRODUCIBLE=false
|
||||||
|
SIGNATURE_REQUIRED=false
|
||||||
|
SIGNATURE_FILE=""
|
||||||
|
SIGNATURE_FORMAT=""
|
||||||
|
SIGNATURE_VERIFICATION_TOOL=""
|
||||||
|
SIGNATURE_VERIFICATION_COMMAND=""
|
||||||
|
SIGNATURE_KEY_FINGERPRINT=""
|
||||||
|
SIGNATURE_CERTIFICATE_IDENTITY=""
|
||||||
|
SIGNATURE_CERTIFICATE_ISSUER=""
|
||||||
|
SIGNATURE_TRANSPARENCY_LOG_URL=""
|
||||||
|
SIGNATURE_VERIFIED_SHA256=""
|
||||||
|
SIGNATURE_POLICY_NAME="dragonx-lite-backend-signature-policy-v1"
|
||||||
|
SIGNATURE_POLICY_DEFINED_MANIFEST_VALUE=true
|
||||||
|
SIGNATURE_REQUIRED_MANIFEST_VALUE=false
|
||||||
|
SIGNATURE_METADATA_PROVIDED=false
|
||||||
|
SIGNATURE_VERIFICATION_PERFORMED=false
|
||||||
|
SIGNATURE_VERIFICATION_STATUS="not-provided"
|
||||||
|
SIGNATURE_FILE_SHA256=""
|
||||||
|
|
||||||
|
REQUIRED_SYMBOLS=(
|
||||||
|
litelib_wallet_exists
|
||||||
|
litelib_initialize_new
|
||||||
|
litelib_initialize_new_from_phrase
|
||||||
|
litelib_initialize_existing
|
||||||
|
litelib_execute
|
||||||
|
litelib_rust_free_string
|
||||||
|
litelib_check_server_online
|
||||||
|
litelib_shutdown
|
||||||
|
)
|
||||||
|
|
||||||
|
EXTRA_CARGO_ARGS=()
|
||||||
|
EXTRA_REMAP_PATH_PREFIXES=()
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Build or inventory the SDXL-compatible lite backend artifact.
|
||||||
|
|
||||||
|
Usage: $0 [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--platform linux|windows|macos Artifact platform. Defaults to host platform.
|
||||||
|
--rust-target TRIPLE Cargo target triple for cross builds.
|
||||||
|
--cargo-target-dir PATH Isolated Cargo target directory for clean builds.
|
||||||
|
--backend-dir PATH SilentDragonXLite/lib source directory.
|
||||||
|
--silentdragonxlitelib-dir PATH Override the wrapper's silentdragonxlitelib dependency path.
|
||||||
|
--out-dir PATH Output directory for copied artifact and metadata.
|
||||||
|
--artifact PATH Inventory an existing artifact instead of building.
|
||||||
|
--no-build Do not run cargo; requires --artifact.
|
||||||
|
--reproducible Add deterministic Rust path remaps for clean builds.
|
||||||
|
--remap-path-prefix FROM=TO Extra rustc path remap used with --reproducible.
|
||||||
|
--builder NAME Redacted builder/provenance label. Default: local.
|
||||||
|
--signature-required Fail if verified signature metadata is not supplied.
|
||||||
|
--signature-file PATH Existing sidecar signature file to record.
|
||||||
|
--signature-format FORMAT Signature format: minisign, gpg, sigstore, external, or other.
|
||||||
|
--signature-verification-tool T Verification tool and version used by the release builder.
|
||||||
|
--signature-verification-command C
|
||||||
|
Verification command already run by the release builder.
|
||||||
|
--signature-key-fingerprint F Reviewed public-key fingerprint, when applicable.
|
||||||
|
--signature-certificate-identity ID
|
||||||
|
Reviewed certificate identity, when applicable.
|
||||||
|
--signature-certificate-issuer I
|
||||||
|
Reviewed certificate issuer, when applicable.
|
||||||
|
--signature-transparency-log-url URL
|
||||||
|
Transparency log entry, when applicable.
|
||||||
|
--signature-verified-sha256 SHA Artifact SHA-256 verified by the signature check.
|
||||||
|
-j, --jobs N Cargo parallel jobs.
|
||||||
|
--cargo-arg ARG Extra argument forwarded to cargo build.
|
||||||
|
-h, --help Show this help.
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
<out>/<platform>/<artifact>
|
||||||
|
<out>/<platform>/lite-backend-symbols.txt
|
||||||
|
<out>/<platform>/lite-backend-artifact-manifest.json
|
||||||
|
|
||||||
|
The script captures symbols, checksums, and optional read-only signature
|
||||||
|
verification metadata only. It does not load the library, resolve function
|
||||||
|
pointers, call SDXL, sign, upload, or publish artifacts.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
info() { printf '[lite-backend] %s\n' "$*"; }
|
||||||
|
warn() { printf '[lite-backend] warning: %s\n' "$*" >&2; }
|
||||||
|
die() { printf '[lite-backend] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
absolute_path() {
|
||||||
|
local path="$1"
|
||||||
|
if [[ "$path" = /* ]]; then
|
||||||
|
printf '%s\n' "$path"
|
||||||
|
else
|
||||||
|
printf '%s/%s\n' "$PWD" "$path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
host_platform() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux) printf 'linux\n' ;;
|
||||||
|
Darwin) printf 'macos\n' ;;
|
||||||
|
MINGW*|MSYS*|CYGWIN*) printf 'windows\n' ;;
|
||||||
|
*) die "unsupported host platform: $(uname -s)" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_platform() {
|
||||||
|
case "$1" in
|
||||||
|
linux|Linux) printf 'linux\n' ;;
|
||||||
|
windows|win|Win|Windows) printf 'windows\n' ;;
|
||||||
|
macos|mac|darwin|Darwin) printf 'macos\n' ;;
|
||||||
|
"") host_platform ;;
|
||||||
|
*) die "unsupported platform: $1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--platform)
|
||||||
|
[[ $# -ge 2 ]] || die "--platform requires a value"
|
||||||
|
PLATFORM="$(normalize_platform "$2")"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--rust-target)
|
||||||
|
[[ $# -ge 2 ]] || die "--rust-target requires a value"
|
||||||
|
RUST_TARGET="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--cargo-target-dir)
|
||||||
|
[[ $# -ge 2 ]] || die "--cargo-target-dir requires a value"
|
||||||
|
CARGO_TARGET_DIR_VALUE="$(absolute_path "$2")"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--backend-dir)
|
||||||
|
[[ $# -ge 2 ]] || die "--backend-dir requires a value"
|
||||||
|
BACKEND_DIR="$(absolute_path "$2")"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--silentdragonxlitelib-dir)
|
||||||
|
[[ $# -ge 2 ]] || die "--silentdragonxlitelib-dir requires a value"
|
||||||
|
BACKEND_DEPENDENCY_DIR="$(absolute_path "$2")"
|
||||||
|
BACKEND_DEPENDENCY_OVERRIDE_REQUESTED=true
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--out-dir)
|
||||||
|
[[ $# -ge 2 ]] || die "--out-dir requires a value"
|
||||||
|
OUT_DIR="$(absolute_path "$2")"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--artifact)
|
||||||
|
[[ $# -ge 2 ]] || die "--artifact requires a value"
|
||||||
|
ARTIFACT_PATH="$(absolute_path "$2")"
|
||||||
|
BUILD_ARTIFACT=false
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--no-build)
|
||||||
|
BUILD_ARTIFACT=false
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--reproducible)
|
||||||
|
REPRODUCIBLE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--remap-path-prefix)
|
||||||
|
[[ $# -ge 2 ]] || die "--remap-path-prefix requires FROM=TO"
|
||||||
|
[[ "$2" == *=* ]] || die "--remap-path-prefix requires FROM=TO"
|
||||||
|
EXTRA_REMAP_PATH_PREFIXES+=("$2")
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--builder)
|
||||||
|
[[ $# -ge 2 ]] || die "--builder requires a value"
|
||||||
|
BUILDER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-required)
|
||||||
|
SIGNATURE_REQUIRED=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--signature-file|--signature-path)
|
||||||
|
[[ $# -ge 2 ]] || die "$1 requires a value"
|
||||||
|
SIGNATURE_FILE="$(absolute_path "$2")"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-format)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-format requires a value"
|
||||||
|
SIGNATURE_FORMAT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-verification-tool|--signature-tool)
|
||||||
|
[[ $# -ge 2 ]] || die "$1 requires a value"
|
||||||
|
SIGNATURE_VERIFICATION_TOOL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-verification-command)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-verification-command requires a value"
|
||||||
|
SIGNATURE_VERIFICATION_COMMAND="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-key-fingerprint)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-key-fingerprint requires a value"
|
||||||
|
SIGNATURE_KEY_FINGERPRINT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-certificate-identity)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-certificate-identity requires a value"
|
||||||
|
SIGNATURE_CERTIFICATE_IDENTITY="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-certificate-issuer)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-certificate-issuer requires a value"
|
||||||
|
SIGNATURE_CERTIFICATE_ISSUER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-transparency-log-url)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-transparency-log-url requires a value"
|
||||||
|
SIGNATURE_TRANSPARENCY_LOG_URL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--signature-verified-sha256)
|
||||||
|
[[ $# -ge 2 ]] || die "--signature-verified-sha256 requires a value"
|
||||||
|
SIGNATURE_VERIFIED_SHA256="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-j|--jobs)
|
||||||
|
[[ $# -ge 2 ]] || die "--jobs requires a value"
|
||||||
|
JOBS="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--cargo-arg)
|
||||||
|
[[ $# -ge 2 ]] || die "--cargo-arg requires a value"
|
||||||
|
EXTRA_CARGO_ARGS+=("$2")
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) die "unknown option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
PLATFORM="$(normalize_platform "$PLATFORM")"
|
||||||
|
BACKEND_SOURCE_DIR="$BACKEND_DIR"
|
||||||
|
BUILD_BACKEND_DIR="$BACKEND_SOURCE_DIR"
|
||||||
|
|
||||||
|
if [[ "$PLATFORM" == "windows" && -z "$RUST_TARGET" ]]; then
|
||||||
|
RUST_TARGET="x86_64-pc-windows-gnu"
|
||||||
|
fi
|
||||||
|
if [[ "$PLATFORM" == "macos" && -z "$RUST_TARGET" && "$(host_platform)" != "macos" ]]; then
|
||||||
|
die "macOS artifacts require --rust-target when not running on macOS"
|
||||||
|
fi
|
||||||
|
if [[ "$BUILD_ARTIFACT" == false && -z "$ARTIFACT_PATH" ]]; then
|
||||||
|
die "--no-build requires --artifact"
|
||||||
|
fi
|
||||||
|
|
||||||
|
backend_dependency_path_from_cargo() {
|
||||||
|
local cargo_toml="$1"
|
||||||
|
awk '
|
||||||
|
/^[[:space:]]*silentdragonxlitelib[[:space:]]*=/ {
|
||||||
|
original = $0
|
||||||
|
path = $0
|
||||||
|
sub(/.*path[[:space:]]*=[[:space:]]*"/, "", path)
|
||||||
|
sub(/".*/, "", path)
|
||||||
|
if (path != original) print path
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$cargo_toml"
|
||||||
|
}
|
||||||
|
|
||||||
|
canonical_dependency_path() {
|
||||||
|
local path="$1"
|
||||||
|
if [[ -d "$path" ]]; then
|
||||||
|
(cd "$path" && pwd -P)
|
||||||
|
else
|
||||||
|
absolute_path "$path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_backend_dependency_source() {
|
||||||
|
[[ -n "$BACKEND_DEPENDENCY_DIR" ]] || return
|
||||||
|
|
||||||
|
if [[ ! -f "$BACKEND_DEPENDENCY_DIR/Cargo.toml" ]]; then
|
||||||
|
if [[ "$BUILD_ARTIFACT" == true || "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
|
||||||
|
die "Cargo.toml not found in $BACKEND_DEPENDENCY_DIR"
|
||||||
|
fi
|
||||||
|
warn "Cargo.toml not found in silentdragonxlitelib source: $BACKEND_DEPENDENCY_DIR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -Eq '^[[:space:]]*name[[:space:]]*=[[:space:]]*"silentdragonxlitelib"' "$BACKEND_DEPENDENCY_DIR/Cargo.toml"; then
|
||||||
|
if [[ "$BUILD_ARTIFACT" == true || "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
|
||||||
|
die "dependency path does not look like silentdragonxlitelib: $BACKEND_DEPENDENCY_DIR"
|
||||||
|
fi
|
||||||
|
warn "dependency path does not look like silentdragonxlitelib: $BACKEND_DEPENDENCY_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure the Sapling proving params are present in the core crate (rust-embed bakes them in at build
|
||||||
|
# time). They are the fixed Zcash trusted-setup output — not buildable — so fetch + verify them from
|
||||||
|
# git.dragonx.is when absent. Override the source with SAPLING_PARAMS_BASE_URL.
|
||||||
|
SAPLING_PARAMS_BASE_URL="${SAPLING_PARAMS_BASE_URL:-https://git.dragonx.is/DragonX/zcash-params/releases/download/sapling-v1}"
|
||||||
|
ensure_sapling_params() {
|
||||||
|
local dir="$1"
|
||||||
|
[[ -n "$dir" ]] || return 0
|
||||||
|
mkdir -p "$dir"
|
||||||
|
local specs=(
|
||||||
|
"sapling-spend.params:8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13"
|
||||||
|
"sapling-output.params:2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"
|
||||||
|
)
|
||||||
|
local spec name want path got
|
||||||
|
for spec in "${specs[@]}"; do
|
||||||
|
name="${spec%%:*}"; want="${spec##*:}"; path="$dir/$name"
|
||||||
|
if [[ -f "$path" ]] && [[ "$(compute_sha256 "$path")" == "$want" ]]; then
|
||||||
|
info "sapling param present and verified: $name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
info "fetching $name from $SAPLING_PARAMS_BASE_URL"
|
||||||
|
curl -fsSL "$SAPLING_PARAMS_BASE_URL/$name" -o "$path" || die "failed to download sapling param: $name"
|
||||||
|
got="$(compute_sha256 "$path")"
|
||||||
|
[[ "$got" == "$want" ]] || { rm -f "$path"; die "sapling param $name sha256 mismatch (got $got, want $want)"; }
|
||||||
|
info "downloaded and verified $name"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_backend_source() {
|
||||||
|
BUILD_BACKEND_DIR="$BACKEND_SOURCE_DIR"
|
||||||
|
|
||||||
|
if [[ "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == false ]]; then
|
||||||
|
if [[ -f "$BACKEND_SOURCE_DIR/Cargo.toml" ]]; then
|
||||||
|
local configured_dependency_path
|
||||||
|
configured_dependency_path="$(backend_dependency_path_from_cargo "$BACKEND_SOURCE_DIR/Cargo.toml")"
|
||||||
|
if [[ -n "$configured_dependency_path" ]]; then
|
||||||
|
if [[ "$configured_dependency_path" = /* ]]; then
|
||||||
|
BACKEND_DEPENDENCY_DIR="$(canonical_dependency_path "$configured_dependency_path")"
|
||||||
|
warn "backend Cargo.toml uses an absolute silentdragonxlitelib path; use --silentdragonxlitelib-dir for portable builders"
|
||||||
|
else
|
||||||
|
BACKEND_DEPENDENCY_DIR="$(canonical_dependency_path "$BACKEND_SOURCE_DIR/$configured_dependency_path")"
|
||||||
|
info "using relative silentdragonxlitelib dependency at $BACKEND_DEPENDENCY_DIR"
|
||||||
|
fi
|
||||||
|
validate_backend_dependency_source
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -f "$BACKEND_SOURCE_DIR/Cargo.toml" ]] || die "Cargo.toml not found in $BACKEND_SOURCE_DIR"
|
||||||
|
validate_backend_dependency_source
|
||||||
|
[[ "$BACKEND_DEPENDENCY_DIR" != *\"* ]] || die "--silentdragonxlitelib-dir path cannot contain a double quote"
|
||||||
|
|
||||||
|
local prepared_root="$OUT_DIR/.prepared-backend/$PLATFORM"
|
||||||
|
[[ "$prepared_root" == */.prepared-backend/* ]] || die "refusing unsafe prepared backend path: $prepared_root"
|
||||||
|
rm -rf "$prepared_root"
|
||||||
|
mkdir -p "$prepared_root"
|
||||||
|
|
||||||
|
ln -s "$BACKEND_SOURCE_DIR/src" "$prepared_root/src"
|
||||||
|
[[ -f "$BACKEND_SOURCE_DIR/Cargo.lock" ]] && ln -s "$BACKEND_SOURCE_DIR/Cargo.lock" "$prepared_root/Cargo.lock"
|
||||||
|
[[ -d "$BACKEND_SOURCE_DIR/.cargo" ]] && ln -s "$BACKEND_SOURCE_DIR/.cargo" "$prepared_root/.cargo"
|
||||||
|
[[ -d "$BACKEND_SOURCE_DIR/libsodium-mingw" ]] && ln -s "$BACKEND_SOURCE_DIR/libsodium-mingw" "$prepared_root/libsodium-mingw"
|
||||||
|
# Vendored crate deps (offline builds): the .cargo/config.toml's vendored-sources directory is
|
||||||
|
# "vendor" relative to the build root, so expose it inside the prepared root too.
|
||||||
|
[[ -d "$BACKEND_SOURCE_DIR/vendor" ]] && ln -s "$BACKEND_SOURCE_DIR/vendor" "$prepared_root/vendor"
|
||||||
|
[[ -f "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" ]] && ln -s "$BACKEND_SOURCE_DIR/silentdragonxlitelib.h" "$prepared_root/silentdragonxlitelib.h"
|
||||||
|
|
||||||
|
local replacement="silentdragonxlitelib = { path = \"$BACKEND_DEPENDENCY_DIR\" }"
|
||||||
|
awk -v replacement="$replacement" '
|
||||||
|
BEGIN { replaced = 0 }
|
||||||
|
/^[[:space:]]*silentdragonxlitelib[[:space:]]*=/ {
|
||||||
|
print replacement
|
||||||
|
replaced = 1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
END { if (replaced != 1) exit 42 }
|
||||||
|
' "$BACKEND_SOURCE_DIR/Cargo.toml" > "$prepared_root/Cargo.toml" \
|
||||||
|
|| die "failed to prepare backend Cargo.toml with portable silentdragonxlitelib path"
|
||||||
|
|
||||||
|
BUILD_BACKEND_DIR="$prepared_root"
|
||||||
|
info "prepared backend source at $BUILD_BACKEND_DIR with silentdragonxlitelib from $BACKEND_DEPENDENCY_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_backend_source
|
||||||
|
|
||||||
|
artifact_kind() {
|
||||||
|
local name="${1##*/}"
|
||||||
|
case "$name" in
|
||||||
|
*.a|*.lib) printf 'static-library\n' ;;
|
||||||
|
*.so|*.dylib|*.dll) printf 'shared-library\n' ;;
|
||||||
|
*) printf 'unknown\n' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
cargo_output_candidates() {
|
||||||
|
local cargo_target_root="$BUILD_BACKEND_DIR/target"
|
||||||
|
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
|
||||||
|
cargo_target_root="$CARGO_TARGET_DIR_VALUE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local base="$cargo_target_root/release"
|
||||||
|
if [[ -n "$RUST_TARGET" ]]; then
|
||||||
|
base="$cargo_target_root/$RUST_TARGET/release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$PLATFORM" in
|
||||||
|
linux)
|
||||||
|
printf '%s\n' "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.a" "$base/libsilentdragonxlite.so"
|
||||||
|
;;
|
||||||
|
windows)
|
||||||
|
printf '%s\n' "$base/silentdragonxlite.lib" "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.dll"
|
||||||
|
;;
|
||||||
|
macos)
|
||||||
|
printf '%s\n' "$base/libsilentdragonxlite.a" "$base/silentdragonxlite.a" "$base/libsilentdragonxlite.dylib" "$base/silentdragonxlite.dylib"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
source_revision_for() {
|
||||||
|
local dir="$1"
|
||||||
|
local revision_file
|
||||||
|
for revision_file in "$dir/DRAGONX_SOURCE_REVISION" "$dir/../DRAGONX_SOURCE_REVISION"; do
|
||||||
|
if [[ -f "$revision_file" ]]; then
|
||||||
|
sed -n '1p' "$revision_file"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
git -C "$dir" rev-parse HEAD 2>/dev/null || printf 'unknown'
|
||||||
|
else
|
||||||
|
printf 'unknown'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
default_source_date_epoch() {
|
||||||
|
if git -C "$PROJECT_ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
git -C "$PROJECT_ROOT" log -1 --format=%ct 2>/dev/null || printf '0'
|
||||||
|
else
|
||||||
|
printf '0'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
append_rustflag() {
|
||||||
|
local rustflag="$1"
|
||||||
|
if [[ -n "${RUSTFLAGS:-}" ]]; then
|
||||||
|
export RUSTFLAGS="${RUSTFLAGS} ${rustflag}"
|
||||||
|
else
|
||||||
|
export RUSTFLAGS="$rustflag"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
append_rust_path_remap() {
|
||||||
|
local from_path="$1"
|
||||||
|
local to_path="$2"
|
||||||
|
[[ -n "$from_path" && -n "$to_path" ]] || return
|
||||||
|
append_rustflag "--remap-path-prefix=${from_path}=${to_path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_reproducible_rustflags() {
|
||||||
|
local cargo_target_root="$BUILD_BACKEND_DIR/target"
|
||||||
|
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
|
||||||
|
cargo_target_root="$CARGO_TARGET_DIR_VALUE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
append_rust_path_remap "$PROJECT_ROOT" "/dragonx-project"
|
||||||
|
append_rust_path_remap "$BACKEND_SOURCE_DIR" "/dragonx-lite-backend"
|
||||||
|
if [[ "$BUILD_BACKEND_DIR" != "$BACKEND_SOURCE_DIR" ]]; then
|
||||||
|
append_rust_path_remap "$BUILD_BACKEND_DIR" "/dragonx-lite-backend"
|
||||||
|
fi
|
||||||
|
append_rust_path_remap "$BACKEND_DEPENDENCY_DIR" "/dragonx-lite-backend-dependency"
|
||||||
|
for path_remap in "${EXTRA_REMAP_PATH_PREFIXES[@]}"; do
|
||||||
|
append_rustflag "--remap-path-prefix=${path_remap}"
|
||||||
|
done
|
||||||
|
|
||||||
|
local cargo_home="${CARGO_HOME:-}"
|
||||||
|
if [[ -z "$cargo_home" && -n "${HOME:-}" ]]; then
|
||||||
|
cargo_home="$HOME/.cargo"
|
||||||
|
fi
|
||||||
|
if [[ -n "$cargo_home" && -d "$cargo_home" ]]; then
|
||||||
|
append_rust_path_remap "$cargo_home" "/cargo-home"
|
||||||
|
fi
|
||||||
|
append_rust_path_remap "$cargo_target_root" "/dragonx-lite-cargo-target"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_with_cargo() {
|
||||||
|
command -v cargo >/dev/null 2>&1 || die "cargo was not found"
|
||||||
|
[[ -f "$BUILD_BACKEND_DIR/Cargo.toml" ]] || die "Cargo.toml not found in $BUILD_BACKEND_DIR"
|
||||||
|
|
||||||
|
if [[ -z "$SOURCE_DATE_EPOCH_VALUE" ]]; then
|
||||||
|
SOURCE_DATE_EPOCH_VALUE="$(default_source_date_epoch)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export CARGO_INCREMENTAL=0
|
||||||
|
export SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH_VALUE"
|
||||||
|
if [[ -n "$CARGO_TARGET_DIR_VALUE" ]]; then
|
||||||
|
export CARGO_TARGET_DIR="$CARGO_TARGET_DIR_VALUE"
|
||||||
|
fi
|
||||||
|
if [[ "$REPRODUCIBLE" == true ]]; then
|
||||||
|
apply_reproducible_rustflags
|
||||||
|
fi
|
||||||
|
if [[ "$PLATFORM" == "windows" && -d "$BUILD_BACKEND_DIR/libsodium-mingw" ]]; then
|
||||||
|
export SODIUM_LIB_DIR="$BUILD_BACKEND_DIR/libsodium-mingw"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "$BACKEND_DEPENDENCY_DIR" ]] && ensure_sapling_params "$BACKEND_DEPENDENCY_DIR/zcash-params"
|
||||||
|
|
||||||
|
local cargo_cmd=(cargo build --locked --lib --release)
|
||||||
|
if [[ -n "$RUST_TARGET" ]]; then
|
||||||
|
cargo_cmd+=(--target "$RUST_TARGET")
|
||||||
|
fi
|
||||||
|
if [[ -n "$JOBS" ]]; then
|
||||||
|
cargo_cmd+=(-j "$JOBS")
|
||||||
|
fi
|
||||||
|
cargo_cmd+=("${EXTRA_CARGO_ARGS[@]}")
|
||||||
|
|
||||||
|
info "building backend in $BUILD_BACKEND_DIR"
|
||||||
|
(cd "$BUILD_BACKEND_DIR" && "${cargo_cmd[@]}")
|
||||||
|
|
||||||
|
while IFS= read -r candidate; do
|
||||||
|
if [[ -f "$candidate" ]]; then
|
||||||
|
ARTIFACT_PATH="$candidate"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done < <(cargo_output_candidates)
|
||||||
|
|
||||||
|
die "cargo finished, but no expected backend artifact was found under $BUILD_BACKEND_DIR/target"
|
||||||
|
}
|
||||||
|
|
||||||
|
select_nm_tool() {
|
||||||
|
if [[ "$PLATFORM" == "windows" ]] && command -v x86_64-w64-mingw32-nm >/dev/null 2>&1; then
|
||||||
|
printf 'x86_64-w64-mingw32-nm\n'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if command -v llvm-nm >/dev/null 2>&1; then
|
||||||
|
printf 'llvm-nm\n'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if command -v nm >/dev/null 2>&1; then
|
||||||
|
printf 'nm\n'
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
die "no symbol inventory tool found; install nm, llvm-nm, or x86_64-w64-mingw32-nm"
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_sha256() {
|
||||||
|
local file="$1"
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
sha256sum "$file" | awk '{print $1}'
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then
|
||||||
|
shasum -a 256 "$file" | awk '{print $1}'
|
||||||
|
else
|
||||||
|
die "sha256sum or shasum is required"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
json_escape() {
|
||||||
|
local value="$1"
|
||||||
|
value="${value//\\/\\\\}"
|
||||||
|
value="${value//\"/\\\"}"
|
||||||
|
value="${value//$'\n'/\\n}"
|
||||||
|
value="${value//$'\r'/}"
|
||||||
|
value="${value//$'\t'/\\t}"
|
||||||
|
printf '"%s"' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
json_array() {
|
||||||
|
local first=true
|
||||||
|
printf '['
|
||||||
|
for value in "$@"; do
|
||||||
|
if [[ "$first" == true ]]; then
|
||||||
|
first=false
|
||||||
|
else
|
||||||
|
printf ','
|
||||||
|
fi
|
||||||
|
json_escape "$value"
|
||||||
|
done
|
||||||
|
printf ']'
|
||||||
|
}
|
||||||
|
|
||||||
|
json_array_from_file() {
|
||||||
|
local file="$1"
|
||||||
|
local values=()
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
mapfile -t values < "$file"
|
||||||
|
fi
|
||||||
|
json_array "${values[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
signature_metadata_requested() {
|
||||||
|
[[ "$SIGNATURE_REQUIRED" == true || \
|
||||||
|
-n "$SIGNATURE_FILE" || \
|
||||||
|
-n "$SIGNATURE_FORMAT" || \
|
||||||
|
-n "$SIGNATURE_VERIFICATION_TOOL" || \
|
||||||
|
-n "$SIGNATURE_VERIFICATION_COMMAND" || \
|
||||||
|
-n "$SIGNATURE_KEY_FINGERPRINT" || \
|
||||||
|
-n "$SIGNATURE_CERTIFICATE_IDENTITY" || \
|
||||||
|
-n "$SIGNATURE_CERTIFICATE_ISSUER" || \
|
||||||
|
-n "$SIGNATURE_TRANSPARENCY_LOG_URL" || \
|
||||||
|
-n "$SIGNATURE_VERIFIED_SHA256" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_signature_metadata() {
|
||||||
|
SIGNATURE_REQUIRED_MANIFEST_VALUE=false
|
||||||
|
if [[ "$SIGNATURE_REQUIRED" == true ]]; then
|
||||||
|
SIGNATURE_REQUIRED_MANIFEST_VALUE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! signature_metadata_requested; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "$SIGNATURE_FILE" ]] || die "signature metadata requires --signature-file"
|
||||||
|
[[ -f "$SIGNATURE_FILE" ]] || die "signature file does not exist: $SIGNATURE_FILE"
|
||||||
|
[[ -n "$SIGNATURE_FORMAT" ]] || die "signature metadata requires --signature-format"
|
||||||
|
case "$SIGNATURE_FORMAT" in
|
||||||
|
minisign|gpg|sigstore|external|other) ;;
|
||||||
|
*) die "unsupported --signature-format: $SIGNATURE_FORMAT" ;;
|
||||||
|
esac
|
||||||
|
[[ -n "$SIGNATURE_VERIFICATION_TOOL" ]] || die "signature metadata requires --signature-verification-tool"
|
||||||
|
[[ -n "$SIGNATURE_VERIFIED_SHA256" ]] || die "signature metadata requires --signature-verified-sha256"
|
||||||
|
[[ "$SIGNATURE_VERIFIED_SHA256" == "$SHA256_DIGEST" ]] || die "signature verified SHA-256 does not match artifact SHA-256"
|
||||||
|
if [[ -z "$SIGNATURE_KEY_FINGERPRINT" && -z "$SIGNATURE_CERTIFICATE_IDENTITY" ]]; then
|
||||||
|
die "signature metadata requires --signature-key-fingerprint or --signature-certificate-identity"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SIGNATURE_METADATA_PROVIDED=true
|
||||||
|
SIGNATURE_VERIFICATION_PERFORMED=true
|
||||||
|
SIGNATURE_VERIFICATION_STATUS="verified"
|
||||||
|
SIGNATURE_FILE_SHA256="$(compute_sha256 "$SIGNATURE_FILE")"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$BUILD_ARTIFACT" == true ]]; then
|
||||||
|
build_with_cargo
|
||||||
|
fi
|
||||||
|
if [[ -z "$SOURCE_DATE_EPOCH_VALUE" ]]; then
|
||||||
|
SOURCE_DATE_EPOCH_VALUE="$(default_source_date_epoch)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -f "$ARTIFACT_PATH" ]] || die "artifact not found: $ARTIFACT_PATH"
|
||||||
|
|
||||||
|
KIND="$(artifact_kind "$ARTIFACT_PATH")"
|
||||||
|
[[ "$KIND" != "unknown" ]] || die "artifact kind is unsupported: $ARTIFACT_PATH"
|
||||||
|
|
||||||
|
PLATFORM_OUT_DIR="$OUT_DIR/$PLATFORM"
|
||||||
|
mkdir -p "$PLATFORM_OUT_DIR"
|
||||||
|
|
||||||
|
ARTIFACT_NAME="$(basename "$ARTIFACT_PATH")"
|
||||||
|
ARTIFACT_OUTPUT="$PLATFORM_OUT_DIR/$ARTIFACT_NAME"
|
||||||
|
if [[ "$(absolute_path "$ARTIFACT_PATH")" != "$(absolute_path "$ARTIFACT_OUTPUT")" ]]; then
|
||||||
|
cp -p "$ARTIFACT_PATH" "$ARTIFACT_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SYMBOLS_FILE="$PLATFORM_OUT_DIR/lite-backend-symbols.txt"
|
||||||
|
RAW_SYMBOLS_FILE="$PLATFORM_OUT_DIR/lite-backend-symbols.raw.txt"
|
||||||
|
NM_TOOL="$(select_nm_tool)"
|
||||||
|
|
||||||
|
info "capturing exported symbols with $NM_TOOL"
|
||||||
|
if ! "$NM_TOOL" -g --defined-only "$ARTIFACT_OUTPUT" > "$RAW_SYMBOLS_FILE" 2> "$PLATFORM_OUT_DIR/lite-backend-symbols.err.txt"; then
|
||||||
|
die "symbol inventory failed; see $PLATFORM_OUT_DIR/lite-backend-symbols.err.txt"
|
||||||
|
fi
|
||||||
|
awk '{print $NF}' "$RAW_SYMBOLS_FILE" \
|
||||||
|
| sed 's/^_//' \
|
||||||
|
| grep -E '^(litelib_[A-Za-z0-9_]*|blake3_PW)$' \
|
||||||
|
| sort -u > "$SYMBOLS_FILE" || true
|
||||||
|
|
||||||
|
[[ -s "$SYMBOLS_FILE" ]] || die "no SDXL C ABI symbols were found in $ARTIFACT_OUTPUT"
|
||||||
|
|
||||||
|
MISSING_SYMBOLS=()
|
||||||
|
for required in "${REQUIRED_SYMBOLS[@]}"; do
|
||||||
|
if ! grep -Fxq "$required" "$SYMBOLS_FILE"; then
|
||||||
|
MISSING_SYMBOLS+=("$required")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ ${#MISSING_SYMBOLS[@]} -ne 0 ]]; then
|
||||||
|
printf '%s\n' "${MISSING_SYMBOLS[@]}" > "$PLATFORM_OUT_DIR/lite-backend-missing-symbols.txt"
|
||||||
|
die "artifact is missing required symbols; see $PLATFORM_OUT_DIR/lite-backend-missing-symbols.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SHA256_DIGEST="$(compute_sha256 "$ARTIFACT_OUTPUT")"
|
||||||
|
validate_signature_metadata
|
||||||
|
ARTIFACT_SIZE_BYTES="$(wc -c < "$ARTIFACT_OUTPUT" | tr -d ' ')"
|
||||||
|
PROJECT_REVISION="$(source_revision_for "$PROJECT_ROOT")"
|
||||||
|
BACKEND_REVISION="$(source_revision_for "$BACKEND_SOURCE_DIR")"
|
||||||
|
BACKEND_DEPENDENCY_REVISION=""
|
||||||
|
if [[ -n "$BACKEND_DEPENDENCY_DIR" ]]; then
|
||||||
|
BACKEND_DEPENDENCY_REVISION="$(source_revision_for "$BACKEND_DEPENDENCY_DIR")"
|
||||||
|
fi
|
||||||
|
ARTIFACT_SET_ID="$PLATFORM-${SHA256_DIGEST:0:16}"
|
||||||
|
REPRODUCIBLE_MANIFEST_VALUE=false
|
||||||
|
if [[ "$BUILD_ARTIFACT" == true && "$REPRODUCIBLE" == true ]]; then
|
||||||
|
REPRODUCIBLE_MANIFEST_VALUE=true
|
||||||
|
fi
|
||||||
|
PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE=false
|
||||||
|
if [[ "$BACKEND_DEPENDENCY_OVERRIDE_REQUESTED" == true ]]; then
|
||||||
|
PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE=true
|
||||||
|
fi
|
||||||
|
FILE_DESCRIPTION="unknown"
|
||||||
|
if command -v file >/dev/null 2>&1; then
|
||||||
|
FILE_DESCRIPTION="$(file -b "$ARTIFACT_OUTPUT")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
MANIFEST_FILE="$PLATFORM_OUT_DIR/lite-backend-artifact-manifest.json"
|
||||||
|
{
|
||||||
|
printf '{\n'
|
||||||
|
printf ' "schema": "dragonx.lite.backend-artifact.v1",\n'
|
||||||
|
printf ' "generated_by": "scripts/build-lite-backend-artifact.sh",\n'
|
||||||
|
printf ' "read_only_inventory": true,\n'
|
||||||
|
printf ' "artifact_mutation_requested": false,\n'
|
||||||
|
printf ' "upload_requested": false,\n'
|
||||||
|
printf ' "signing_requested": false,\n'
|
||||||
|
printf ' "publication_requested": false,\n'
|
||||||
|
printf ' "signature_verification": {\n'
|
||||||
|
printf ' "policy_name": '; json_escape "$SIGNATURE_POLICY_NAME"; printf ',\n'
|
||||||
|
printf ' "policy_defined": %s,\n' "$SIGNATURE_POLICY_DEFINED_MANIFEST_VALUE"
|
||||||
|
printf ' "required_for_release": %s,\n' "$SIGNATURE_REQUIRED_MANIFEST_VALUE"
|
||||||
|
printf ' "metadata_read_only": true,\n'
|
||||||
|
printf ' "metadata_provided": %s,\n' "$SIGNATURE_METADATA_PROVIDED"
|
||||||
|
printf ' "verification_performed": %s,\n' "$SIGNATURE_VERIFICATION_PERFORMED"
|
||||||
|
printf ' "verification_status": '; json_escape "$SIGNATURE_VERIFICATION_STATUS"; printf ',\n'
|
||||||
|
printf ' "signature_format": '; json_escape "$SIGNATURE_FORMAT"; printf ',\n'
|
||||||
|
printf ' "signature_path": '; json_escape "$SIGNATURE_FILE"; printf ',\n'
|
||||||
|
printf ' "signature_file_sha256": '; json_escape "$SIGNATURE_FILE_SHA256"; printf ',\n'
|
||||||
|
printf ' "verification_tool": '; json_escape "$SIGNATURE_VERIFICATION_TOOL"; printf ',\n'
|
||||||
|
printf ' "verification_command": '; json_escape "$SIGNATURE_VERIFICATION_COMMAND"; printf ',\n'
|
||||||
|
printf ' "key_fingerprint": '; json_escape "$SIGNATURE_KEY_FINGERPRINT"; printf ',\n'
|
||||||
|
printf ' "certificate_identity": '; json_escape "$SIGNATURE_CERTIFICATE_IDENTITY"; printf ',\n'
|
||||||
|
printf ' "certificate_issuer": '; json_escape "$SIGNATURE_CERTIFICATE_ISSUER"; printf ',\n'
|
||||||
|
printf ' "transparency_log_url": '; json_escape "$SIGNATURE_TRANSPARENCY_LOG_URL"; printf ',\n'
|
||||||
|
printf ' "verified_artifact_sha256": '; json_escape "$SIGNATURE_VERIFIED_SHA256"; printf '\n'
|
||||||
|
printf ' },\n'
|
||||||
|
printf ' "abi_version": '; json_escape "$ABI_VERSION"; printf ',\n'
|
||||||
|
printf ' "link_mode": '; json_escape "$LINK_MODE"; printf ',\n'
|
||||||
|
printf ' "platform": '; json_escape "$PLATFORM"; printf ',\n'
|
||||||
|
printf ' "rust_target": '; json_escape "$RUST_TARGET"; printf ',\n'
|
||||||
|
printf ' "artifact": {\n'
|
||||||
|
printf ' "path": '; json_escape "$ARTIFACT_OUTPUT"; printf ',\n'
|
||||||
|
printf ' "kind": '; json_escape "$KIND"; printf ',\n'
|
||||||
|
printf ' "size_bytes": %s,\n' "$ARTIFACT_SIZE_BYTES"
|
||||||
|
printf ' "sha256": '; json_escape "$SHA256_DIGEST"; printf ',\n'
|
||||||
|
printf ' "file_description": '; json_escape "$FILE_DESCRIPTION"; printf '\n'
|
||||||
|
printf ' },\n'
|
||||||
|
printf ' "symbol_inventory": {\n'
|
||||||
|
printf ' "tool": '; json_escape "$NM_TOOL"; printf ',\n'
|
||||||
|
printf ' "symbols_path": '; json_escape "$SYMBOLS_FILE"; printf ',\n'
|
||||||
|
printf ' "raw_symbols_path": '; json_escape "$RAW_SYMBOLS_FILE"; printf ',\n'
|
||||||
|
printf ' "required_symbols": '; json_array "${REQUIRED_SYMBOLS[@]}"; printf ',\n'
|
||||||
|
printf ' "exported_symbols": '; json_array_from_file "$SYMBOLS_FILE"; printf ',\n'
|
||||||
|
printf ' "missing_required_symbols": []\n'
|
||||||
|
printf ' },\n'
|
||||||
|
printf ' "provenance": {\n'
|
||||||
|
printf ' "owner_ready": true,\n'
|
||||||
|
printf ' "metadata_provided": true,\n'
|
||||||
|
printf ' "source": '; json_escape "$BACKEND_SOURCE_DIR"; printf ',\n'
|
||||||
|
printf ' "cargo_build_source": '; json_escape "$BUILD_BACKEND_DIR"; printf ',\n'
|
||||||
|
printf ' "portable_dependency_override": %s,\n' "$PORTABLE_DEPENDENCY_OVERRIDE_MANIFEST_VALUE"
|
||||||
|
printf ' "silentdragonxlitelib_source": '; json_escape "$BACKEND_DEPENDENCY_DIR"; printf ',\n'
|
||||||
|
printf ' "builder": '; json_escape "$BUILDER"; printf ',\n'
|
||||||
|
printf ' "source_revision": '; json_escape "$BACKEND_REVISION"; printf ',\n'
|
||||||
|
printf ' "silentdragonxlitelib_revision": '; json_escape "$BACKEND_DEPENDENCY_REVISION"; printf ',\n'
|
||||||
|
printf ' "project_revision": '; json_escape "$PROJECT_REVISION"; printf ',\n'
|
||||||
|
printf ' "artifact_set_id": '; json_escape "$ARTIFACT_SET_ID"; printf ',\n'
|
||||||
|
printf ' "source_date_epoch": '; json_escape "$SOURCE_DATE_EPOCH_VALUE"; printf ',\n'
|
||||||
|
printf ' "reproducible": %s,\n' "$REPRODUCIBLE_MANIFEST_VALUE"
|
||||||
|
printf ' "redacted": true\n'
|
||||||
|
printf ' }\n'
|
||||||
|
printf '}\n'
|
||||||
|
} > "$MANIFEST_FILE"
|
||||||
|
|
||||||
|
info "artifact: $ARTIFACT_OUTPUT"
|
||||||
|
info "symbols: $SYMBOLS_FILE"
|
||||||
|
info "manifest: $MANIFEST_FILE"
|
||||||
|
info "sha256: $SHA256_DIGEST"
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
CMake configure example:
|
||||||
|
cmake -S "$PROJECT_ROOT" -B "$PROJECT_ROOT/build/lite" \\
|
||||||
|
-DDRAGONX_BUILD_LITE=ON \\
|
||||||
|
-DDRAGONX_ENABLE_LITE_BACKEND=ON \\
|
||||||
|
-DDRAGONX_LITE_BACKEND_LIBRARY="$ARTIFACT_OUTPUT" \\
|
||||||
|
-DDRAGONX_LITE_BACKEND_SYMBOLS_FILE="$SYMBOLS_FILE" \\
|
||||||
|
-DDRAGONX_LITE_BACKEND_MANIFEST="$MANIFEST_FILE" \\
|
||||||
|
-DDRAGONX_LITE_BACKEND_LINK_MODE=$LINK_MODE \\
|
||||||
|
-DDRAGONX_LITE_BACKEND_ABI=$ABI_VERSION
|
||||||
|
EOF
|
||||||
56
scripts/check-source-hygiene.sh
Executable file
56
scripts/check-source-hygiene.sh
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Source-tree hygiene guard.
|
||||||
|
#
|
||||||
|
# Blocks two failure modes that an AI coding session previously introduced in
|
||||||
|
# src/wallet/ (the lite-wallet "_plan"/"_batch" churn): pathologically long
|
||||||
|
# filenames (which also break the Windows MAX_PATH 260-char limit during the
|
||||||
|
# cross-build) and the runaway "receipt/custody/handoff/stewardship" naming
|
||||||
|
# explosion where each session wrapped the previous artifact in one more layer.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/check-source-hygiene.sh # check working-tree src/
|
||||||
|
# scripts/check-source-hygiene.sh --staged # check staged files (pre-commit)
|
||||||
|
#
|
||||||
|
# Install as a git pre-commit hook:
|
||||||
|
# ln -sf ../../scripts/check-source-hygiene.sh .git/hooks/pre-commit
|
||||||
|
# # (the hook invokes it with --staged automatically when named pre-commit)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MAX_LEN=80
|
||||||
|
# Naming-explosion tokens. Two or more chained in one basename is the smell.
|
||||||
|
CHURN_RE='receipt|custody|handoff|stewardship|promotion_activation|acceptance_confirmation|archive_handoff|post_closure'
|
||||||
|
|
||||||
|
mode="${1:-}"
|
||||||
|
if [[ "$mode" == "--staged" || "$(basename "$0")" == "pre-commit" ]]; then
|
||||||
|
mapfile -t files < <(git diff --cached --name-only --diff-filter=AR | grep -E '\.(cpp|h|hpp|cc)$' || true)
|
||||||
|
else
|
||||||
|
mapfile -t files < <(git ls-files 'src/**/*.cpp' 'src/**/*.h' 2>/dev/null; \
|
||||||
|
find src -type f \( -name '*.cpp' -o -name '*.h' \) 2>/dev/null)
|
||||||
|
# de-dup
|
||||||
|
mapfile -t files < <(printf '%s\n' "${files[@]}" | sort -u)
|
||||||
|
fi
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
[[ -z "$f" ]] && continue
|
||||||
|
base="$(basename "$f")"
|
||||||
|
len=${#base}
|
||||||
|
if (( len > MAX_LEN )); then
|
||||||
|
echo "✗ filename too long ($len > $MAX_LEN chars): $f" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
# count distinct churn tokens in the basename ( || true: grep exits 1 on no match)
|
||||||
|
n=$(printf '%s' "$base" | grep -oE "$CHURN_RE" | sort -u | wc -l || true)
|
||||||
|
if (( n >= 2 )); then
|
||||||
|
echo "✗ runaway naming pattern ($n churn tokens) — refactor in place, don't add a layer: $f" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( fail )); then
|
||||||
|
echo "" >&2
|
||||||
|
echo "Source hygiene check failed. See docs in scripts/check-source-hygiene.sh." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "source hygiene OK (${#files[@]} files checked)"
|
||||||
35
scripts/gen-lite-checkpoints.sh
Executable file
35
scripts/gen-lite-checkpoints.sh
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generate SDXL lite-wallet mainnet checkpoint entries from a fully-synced dragonxd.
|
||||||
|
# Each entry is (height,"blockhash","serialized_sapling_tree") in checkpoints.rs format.
|
||||||
|
# Fills the 1,770,000 -> tip gap so wallets reseed close to their birthday on rescan,
|
||||||
|
# bounding the (divergence-prone) compact-block replay span. Usage:
|
||||||
|
# scripts/gen-lite-checkpoints.sh [start] [step] > /tmp/new_checkpoints.txt
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CLI=${DRAGONX_CLI:-/home/d/dragonx/src/dragonx-cli}
|
||||||
|
START=${1:-1770000}
|
||||||
|
STEP=${2:-10000}
|
||||||
|
|
||||||
|
tip=$("$CLI" getblockcount)
|
||||||
|
end=$(( (tip / STEP) * STEP ))
|
||||||
|
|
||||||
|
# Sanity: confirm the method reproduces a KNOWN checkpoint tree before trusting it.
|
||||||
|
ref_hash=$("$CLI" getblockhash 1760000 | tr -d '"[:space:]')
|
||||||
|
ref_tree=$("$CLI" getblockmerkletree 1760000 | tr -d '"[:space:]')
|
||||||
|
expect_hash="0000545a45b8d4ee4e4b423cb1ea74d67e3a04c320c6ea2f59ee06c08f91a117"
|
||||||
|
if [ "$ref_hash" != "$expect_hash" ]; then
|
||||||
|
echo "ABORT: getblockhash 1760000 = $ref_hash != known $expect_hash" >&2; exit 1
|
||||||
|
fi
|
||||||
|
echo "# self-check: 1760000 hash matches; tree len=${#ref_tree}" >&2
|
||||||
|
|
||||||
|
n=0
|
||||||
|
h=$START
|
||||||
|
while [ "$h" -le "$end" ]; do
|
||||||
|
hash=$("$CLI" getblockhash "$h" | tr -d '"[:space:]')
|
||||||
|
tree=$("$CLI" getblockmerkletree "$h" | tr -d '"[:space:]')
|
||||||
|
if [ -z "$hash" ] || [ -z "$tree" ]; then echo "ABORT: empty hash/tree at $h" >&2; exit 1; fi
|
||||||
|
printf '\t(%s,"%s",\n\t\t"%s"\n\t),\n' "$h" "$hash" "$tree"
|
||||||
|
n=$((n+1))
|
||||||
|
h=$((h+STEP))
|
||||||
|
done
|
||||||
|
echo "# generated $n checkpoints from $START to $end (tip=$tip)" >&2
|
||||||
67
scripts/sign-daemon-release.sh
Executable file
67
scripts/sign-daemon-release.sh
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sign dragonx full-node release archives for the wallet's in-app daemon updater (ed25519).
|
||||||
|
#
|
||||||
|
# The wallet verifies a detached ed25519 signature over the EXACT archive bytes against a public
|
||||||
|
# key pinned in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64). Verification is
|
||||||
|
# MANDATORY (kDaemonRequireSignature = true): an in-app update is refused unless a valid signature
|
||||||
|
# is published. For each archive <name>.zip this produces <name>.zip.sig holding the base64 of the
|
||||||
|
# raw 64-byte ed25519 signature — upload that .sig next to the .zip as a release asset.
|
||||||
|
#
|
||||||
|
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl needed. OpenSSL's ed25519 is PureEdDSA (RFC 8032),
|
||||||
|
# the same primitive libsodium's crypto_sign_verify_detached checks, so signatures are compatible
|
||||||
|
# (the same flow the wallet's unit tests verify for the miner updater).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/sign-daemon-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
||||||
|
# scripts/sign-daemon-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||||
|
# scripts/sign-daemon-release.sh sign <secret.key> <file>...# -> <file>.sig per file
|
||||||
|
#
|
||||||
|
# Keep the secret key (.ed25519.key) OFFLINE. Paste the base64 public key into
|
||||||
|
# kDaemonSignaturePublicKeyBase64 in src/util/daemon_updater.h.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
die() { echo "error: $*" >&2; exit 1; }
|
||||||
|
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
|
||||||
|
|
||||||
|
# Raw 32-byte ed25519 public key (base64) from a private key file. The DER SubjectPublicKeyInfo for
|
||||||
|
# ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key.
|
||||||
|
pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; }
|
||||||
|
|
||||||
|
cmd="${1:-}"; shift || true
|
||||||
|
case "$cmd" in
|
||||||
|
keygen)
|
||||||
|
prefix="${1:-dragonx-daemon}"
|
||||||
|
[ -e "$prefix.ed25519.key" ] && die "$prefix.ed25519.key already exists — refusing to overwrite"
|
||||||
|
openssl genpkey -algorithm ed25519 -out "$prefix.ed25519.key"
|
||||||
|
chmod 600 "$prefix.ed25519.key"
|
||||||
|
pub="$(pubkey_b64 "$prefix.ed25519.key")"
|
||||||
|
printf '%s\n' "$pub" > "$prefix.ed25519.pub.b64"
|
||||||
|
echo "secret key : $prefix.ed25519.key (KEEP OFFLINE, mode 600)"
|
||||||
|
echo "public key : $prefix.ed25519.pub.b64"
|
||||||
|
echo
|
||||||
|
echo "Pin this in src/util/daemon_updater.h (kDaemonSignaturePublicKeyBase64):"
|
||||||
|
echo " $pub"
|
||||||
|
;;
|
||||||
|
pubkey)
|
||||||
|
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
|
||||||
|
pubkey_b64 "$1"
|
||||||
|
;;
|
||||||
|
sign)
|
||||||
|
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
|
||||||
|
key="$1"; shift
|
||||||
|
[ -f "$key" ] || die "no such key: $key"
|
||||||
|
for f in "$@"; do
|
||||||
|
[ -f "$f" ] || die "no such file: $f"
|
||||||
|
raw="$(mktemp)"
|
||||||
|
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
|
||||||
|
openssl base64 -A -in "$raw" > "$f.sig"
|
||||||
|
printf '\n' >> "$f.sig"
|
||||||
|
rm -f "$raw"
|
||||||
|
echo "signed: $f -> $f.sig"
|
||||||
|
done
|
||||||
|
echo "Upload each .sig as a release asset next to its archive."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>...}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
66
scripts/sign-xmrig-release.sh
Executable file
66
scripts/sign-xmrig-release.sh
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sign DRG-XMRig release archives for the wallet's in-app updater (opt-in ed25519 signatures).
|
||||||
|
#
|
||||||
|
# The wallet verifies a detached ed25519 signature over the EXACT archive bytes against a public
|
||||||
|
# key pinned in src/util/xmrig_updater.h (kXmrigSignaturePublicKeyBase64). For each archive
|
||||||
|
# <name>.zip this produces <name>.zip.sig holding the base64 of the raw 64-byte ed25519 signature —
|
||||||
|
# upload that .sig next to the .zip as a release asset.
|
||||||
|
#
|
||||||
|
# Uses OpenSSL (>= 1.1.1) only — no Python/PyNaCl needed. OpenSSL's ed25519 is PureEdDSA (RFC 8032),
|
||||||
|
# the same primitive libsodium's crypto_sign_verify_detached checks, so signatures are compatible
|
||||||
|
# (verified by the wallet's unit tests + an interop check).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/sign-xmrig-release.sh keygen [out-prefix] # -> <prefix>.ed25519.{key,pub.b64}
|
||||||
|
# scripts/sign-xmrig-release.sh pubkey <secret.key> # print the base64 public key to pin
|
||||||
|
# scripts/sign-xmrig-release.sh sign <secret.key> <file>...# -> <file>.sig per file
|
||||||
|
#
|
||||||
|
# Keep the secret key (.ed25519.key) OFFLINE. Paste the base64 public key into
|
||||||
|
# kXmrigSignaturePublicKeyBase64 in src/util/xmrig_updater.h.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
die() { echo "error: $*" >&2; exit 1; }
|
||||||
|
command -v openssl >/dev/null || die "openssl not found (need >= 1.1.1 with ed25519)"
|
||||||
|
|
||||||
|
# Raw 32-byte ed25519 public key (base64) from a private key file. The DER SubjectPublicKeyInfo for
|
||||||
|
# ed25519 is a fixed 12-byte prefix + the 32-byte key, so the trailing 32 bytes are the raw key.
|
||||||
|
pubkey_b64() { openssl pkey -in "$1" -pubout -outform DER | tail -c 32 | openssl base64 -A; }
|
||||||
|
|
||||||
|
cmd="${1:-}"; shift || true
|
||||||
|
case "$cmd" in
|
||||||
|
keygen)
|
||||||
|
prefix="${1:-drg-xmrig}"
|
||||||
|
[ -e "$prefix.ed25519.key" ] && die "$prefix.ed25519.key already exists — refusing to overwrite"
|
||||||
|
openssl genpkey -algorithm ed25519 -out "$prefix.ed25519.key"
|
||||||
|
chmod 600 "$prefix.ed25519.key"
|
||||||
|
pub="$(pubkey_b64 "$prefix.ed25519.key")"
|
||||||
|
printf '%s\n' "$pub" > "$prefix.ed25519.pub.b64"
|
||||||
|
echo "secret key : $prefix.ed25519.key (KEEP OFFLINE, mode 600)"
|
||||||
|
echo "public key : $prefix.ed25519.pub.b64"
|
||||||
|
echo
|
||||||
|
echo "Pin this in src/util/xmrig_updater.h (kXmrigSignaturePublicKeyBase64):"
|
||||||
|
echo " $pub"
|
||||||
|
;;
|
||||||
|
pubkey)
|
||||||
|
[ $# -ge 1 ] || die "usage: pubkey <secret.key>"
|
||||||
|
pubkey_b64 "$1"
|
||||||
|
;;
|
||||||
|
sign)
|
||||||
|
[ $# -ge 2 ] || die "usage: sign <secret.key> <file>..."
|
||||||
|
key="$1"; shift
|
||||||
|
[ -f "$key" ] || die "no such key: $key"
|
||||||
|
for f in "$@"; do
|
||||||
|
[ -f "$f" ] || die "no such file: $f"
|
||||||
|
raw="$(mktemp)"
|
||||||
|
openssl pkeyutl -sign -inkey "$key" -rawin -in "$f" -out "$raw"
|
||||||
|
openssl base64 -A -in "$raw" > "$f.sig"
|
||||||
|
printf '\n' >> "$f.sig"
|
||||||
|
rm -f "$raw"
|
||||||
|
echo "signed: $f -> $f.sig"
|
||||||
|
done
|
||||||
|
echo "Upload each .sig as a release asset next to its archive."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "usage: $0 {keygen [prefix] | pubkey <secret.key> | sign <secret.key> <file>...}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
73
setup.sh
73
setup.sh
@@ -120,20 +120,20 @@ pkgs_core_debian="build-essential cmake git pkg-config
|
|||||||
libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev
|
libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev
|
||||||
libxinerama-dev libxi-dev libxkbcommon-dev libwayland-dev
|
libxinerama-dev libxi-dev libxkbcommon-dev libwayland-dev
|
||||||
libsodium-dev libcurl4-openssl-dev
|
libsodium-dev libcurl4-openssl-dev
|
||||||
autoconf automake libtool wget"
|
autoconf automake libtool wget python3 xxd"
|
||||||
|
|
||||||
pkgs_core_fedora="gcc gcc-c++ cmake git pkg-config
|
pkgs_core_fedora="gcc gcc-c++ cmake git pkg-config
|
||||||
mesa-libGL-devel libX11-devel libXcursor-devel libXrandr-devel
|
mesa-libGL-devel libX11-devel libXcursor-devel libXrandr-devel
|
||||||
libXinerama-devel libXi-devel libxkbcommon-devel wayland-devel
|
libXinerama-devel libXi-devel libxkbcommon-devel wayland-devel
|
||||||
libsodium-devel libcurl-devel
|
libsodium-devel libcurl-devel
|
||||||
autoconf automake libtool wget"
|
autoconf automake libtool wget python3 vim-common"
|
||||||
|
|
||||||
pkgs_core_arch="base-devel cmake git pkg-config
|
pkgs_core_arch="base-devel cmake git pkg-config
|
||||||
mesa libx11 libxcursor libxrandr libxinerama libxi
|
mesa libx11 libxcursor libxrandr libxinerama libxi
|
||||||
libxkbcommon wayland libsodium curl
|
libxkbcommon wayland libsodium curl
|
||||||
autoconf automake libtool wget"
|
autoconf automake libtool wget python xxd"
|
||||||
|
|
||||||
pkgs_core_macos="cmake"
|
pkgs_core_macos="cmake python xxd"
|
||||||
|
|
||||||
# Windows cross-compile (from Linux)
|
# Windows cross-compile (from Linux)
|
||||||
pkgs_win_debian="mingw-w64 zip"
|
pkgs_win_debian="mingw-w64 zip"
|
||||||
@@ -245,7 +245,7 @@ if [[ -z "$core_pkgs" ]]; then
|
|||||||
else
|
else
|
||||||
# Check if key tools are already present
|
# Check if key tools are already present
|
||||||
NEED_CORE=false
|
NEED_CORE=false
|
||||||
has_cmd cmake && has_cmd g++ && has_cmd pkg-config || NEED_CORE=true
|
has_cmd cmake && has_cmd g++ && has_cmd pkg-config && has_cmd python3 && has_cmd xxd || NEED_CORE=true
|
||||||
|
|
||||||
if $NEED_CORE; then
|
if $NEED_CORE; then
|
||||||
install_pkgs "$core_pkgs" "core build"
|
install_pkgs "$core_pkgs" "core build"
|
||||||
@@ -258,6 +258,8 @@ check_tool cmake "cmake"
|
|||||||
check_tool g++ "g++ (C++ compiler)"
|
check_tool g++ "g++ (C++ compiler)"
|
||||||
check_tool git "git"
|
check_tool git "git"
|
||||||
check_tool make "make"
|
check_tool make "make"
|
||||||
|
check_tool python3 "python3 (theme expansion)"
|
||||||
|
check_tool xxd "xxd (embedded language headers)"
|
||||||
|
|
||||||
# ── 2. libsodium ────────────────────────────────────────────────────────────
|
# ── 2. libsodium ────────────────────────────────────────────────────────────
|
||||||
header "libsodium"
|
header "libsodium"
|
||||||
@@ -440,6 +442,53 @@ copy_daemon_data() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Stale-daemon guard ───────────────────────────────────────────────────────
|
||||||
|
# A prebuilt daemon binary is only rebuilt on its platform's flag (--win/--mac),
|
||||||
|
# and build.sh merely BUNDLES whatever binary already exists — so a daemon left
|
||||||
|
# over from an old source revision silently ships in the wallet (e.g. the Network
|
||||||
|
# tab once reported v1.0.1 while the source was v1.0.2). These helpers compare the
|
||||||
|
# version baked into a prebuilt binary against the dragonx source and flag drift.
|
||||||
|
STALE_DAEMON=0
|
||||||
|
|
||||||
|
# MAJOR.MINOR.REVISION from the checked-out dragonx source (empty if unavailable).
|
||||||
|
dragonx_source_version() {
|
||||||
|
local hdr="$DRAGONX_SRC/src/clientversion.h"
|
||||||
|
[[ -f "$hdr" ]] || return 1
|
||||||
|
local maj min rev
|
||||||
|
maj=$(awk '/#define[ \t]+CLIENT_VERSION_MAJOR/{print $3; exit}' "$hdr")
|
||||||
|
min=$(awk '/#define[ \t]+CLIENT_VERSION_MINOR/{print $3; exit}' "$hdr")
|
||||||
|
rev=$(awk '/#define[ \t]+CLIENT_VERSION_REVISION/{print $3; exit}' "$hdr")
|
||||||
|
[[ -n "$maj" && -n "$min" && -n "$rev" ]] || return 1
|
||||||
|
printf '%s.%s.%s' "$maj" "$min" "$rev"
|
||||||
|
}
|
||||||
|
|
||||||
|
# vX.Y.Z baked into a built daemon binary (the daemon embeds "vX.Y.Z-<githash>").
|
||||||
|
# Uses grep -a so no `strings`/binutils dependency is required.
|
||||||
|
dragonx_binary_version() {
|
||||||
|
local bin="$1"
|
||||||
|
[[ -f "$bin" ]] || return 1
|
||||||
|
LC_ALL=C grep -aoE 'v[0-9]+\.[0-9]+\.[0-9]+-[0-9a-f]{6,}' "$bin" 2>/dev/null \
|
||||||
|
| head -1 | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compare a prebuilt daemon against the source; warn (and set STALE_DAEMON) on drift.
|
||||||
|
# $1 = label, $2 = binary path, $3 = rebuild flag(s) (e.g. "--win", "" for Linux)
|
||||||
|
daemon_version_guard() {
|
||||||
|
local label="$1" bin="$2" rebuild_hint="$3"
|
||||||
|
[[ -f "$bin" ]] || return 0
|
||||||
|
local src bv
|
||||||
|
src=$(dragonx_source_version) || return 0 # no source checked out → can't compare
|
||||||
|
bv=$(dragonx_binary_version "$bin")
|
||||||
|
[[ -n "$bv" ]] || return 0 # couldn't read the binary's version
|
||||||
|
if [[ "$bv" == "$src" ]]; then
|
||||||
|
ok " $label daemon is v$bv (matches dragonx source)"
|
||||||
|
else
|
||||||
|
warn " $label daemon is v$bv but dragonx source is v$src — STALE"
|
||||||
|
warn " rebuild so the wallet ships the current daemon: ./setup.sh${rebuild_hint:+ $rebuild_hint}"
|
||||||
|
STALE_DAEMON=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ── Linux daemon ─────────────────────────────────────────────────────────────
|
# ── Linux daemon ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Skip Linux daemon build if only cross-compile targets were requested
|
# Skip Linux daemon build if only cross-compile targets were requested
|
||||||
@@ -459,11 +508,13 @@ fi
|
|||||||
if $CHECK_ONLY; then
|
if $CHECK_ONLY; then
|
||||||
if [[ -f "$DRAGONXD_LINUX/dragonxd" ]] || [[ -f "$DRAGONXD_LINUX/hushd" ]]; then
|
if [[ -f "$DRAGONXD_LINUX/dragonxd" ]] || [[ -f "$DRAGONXD_LINUX/hushd" ]]; then
|
||||||
ok "dragonxd daemon (Linux) present"
|
ok "dragonxd daemon (Linux) present"
|
||||||
|
daemon_version_guard "Linux" "$DRAGONXD_LINUX/dragonxd" ""
|
||||||
else
|
else
|
||||||
miss "dragonxd daemon (Linux) not built"
|
miss "dragonxd daemon (Linux) not built"
|
||||||
fi
|
fi
|
||||||
elif $SKIP_LINUX_DAEMON; then
|
elif $SKIP_LINUX_DAEMON; then
|
||||||
skip "dragonxd (Linux) — skipped, binaries already present (cross-compile only)"
|
skip "dragonxd (Linux) — skipped, binaries already present (cross-compile only)"
|
||||||
|
daemon_version_guard "Linux" "$DRAGONXD_LINUX/dragonxd" ""
|
||||||
else
|
else
|
||||||
clone_dragonx_if_needed
|
clone_dragonx_if_needed
|
||||||
|
|
||||||
@@ -498,9 +549,11 @@ fi
|
|||||||
|
|
||||||
if ! $SETUP_WIN; then
|
if ! $SETUP_WIN; then
|
||||||
skip "dragonxd (Windows) — use --win to cross-compile"
|
skip "dragonxd (Windows) — use --win to cross-compile"
|
||||||
|
daemon_version_guard "Windows" "$DRAGONXD_WIN/dragonxd.exe" "--win"
|
||||||
elif $CHECK_ONLY; then
|
elif $CHECK_ONLY; then
|
||||||
if [[ -f "$DRAGONXD_WIN/dragonxd.exe" ]] || [[ -f "$DRAGONXD_WIN/hushd.exe" ]]; then
|
if [[ -f "$DRAGONXD_WIN/dragonxd.exe" ]] || [[ -f "$DRAGONXD_WIN/hushd.exe" ]]; then
|
||||||
ok "dragonxd daemon (Windows) present"
|
ok "dragonxd daemon (Windows) present"
|
||||||
|
daemon_version_guard "Windows" "$DRAGONXD_WIN/dragonxd.exe" "--win"
|
||||||
else
|
else
|
||||||
miss "dragonxd daemon (Windows) not built"
|
miss "dragonxd daemon (Windows) not built"
|
||||||
fi
|
fi
|
||||||
@@ -556,9 +609,11 @@ fi
|
|||||||
|
|
||||||
if ! $SETUP_MAC; then
|
if ! $SETUP_MAC; then
|
||||||
skip "dragonxd (macOS) — use --mac to cross-compile"
|
skip "dragonxd (macOS) — use --mac to cross-compile"
|
||||||
|
daemon_version_guard "macOS" "$DRAGONXD_MAC/dragonxd" "--mac"
|
||||||
elif $CHECK_ONLY; then
|
elif $CHECK_ONLY; then
|
||||||
if [[ -f "$DRAGONXD_MAC/dragonxd" ]] || [[ -f "$DRAGONXD_MAC/hushd" ]]; then
|
if [[ -f "$DRAGONXD_MAC/dragonxd" ]] || [[ -f "$DRAGONXD_MAC/hushd" ]]; then
|
||||||
ok "dragonxd daemon (macOS) present"
|
ok "dragonxd daemon (macOS) present"
|
||||||
|
daemon_version_guard "macOS" "$DRAGONXD_MAC/dragonxd" "--mac"
|
||||||
else
|
else
|
||||||
miss "dragonxd daemon (macOS) not built"
|
miss "dragonxd daemon (macOS) not built"
|
||||||
fi
|
fi
|
||||||
@@ -621,6 +676,14 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Prominent reminder if any prebuilt daemon drifted from the source — these are bundled verbatim
|
||||||
|
# by build.sh, so a stale binary ships in the wallet (and shows an old version in the Network tab).
|
||||||
|
if [[ "$STALE_DAEMON" -eq 1 ]]; then
|
||||||
|
warn "One or more prebuilt daemons are OLDER than the dragonx source (see above)."
|
||||||
|
warn "build.sh bundles them as-is, so rebuild the stale platform(s) before releasing:"
|
||||||
|
warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
|
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
|
||||||
header "xmrig-hac Mining Binary"
|
header "xmrig-hac Mining Binary"
|
||||||
|
|
||||||
|
|||||||
2113
src/app.cpp
2113
src/app.cpp
File diff suppressed because it is too large
Load Diff
465
src/app.h
465
src/app.h
@@ -12,8 +12,19 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
#include <nlohmann/json_fwd.hpp>
|
||||||
|
#include "data/transaction_history_cache.h"
|
||||||
|
#include "data/address_book.h"
|
||||||
#include "data/wallet_state.h"
|
#include "data/wallet_state.h"
|
||||||
#include "rpc/connection.h"
|
#include "rpc/connection.h"
|
||||||
|
#include "services/network_refresh_service.h"
|
||||||
|
#include "services/wallet_security_controller.h"
|
||||||
|
#include "services/wallet_security_workflow.h"
|
||||||
|
#include "util/async_task_manager.h"
|
||||||
|
#include "util/pool_stats_service.h"
|
||||||
|
#include "wallet/wallet_capabilities.h"
|
||||||
|
#include "chat/chat_service.h"
|
||||||
|
#include "chat/chat_database.h"
|
||||||
#include "ui/sidebar.h"
|
#include "ui/sidebar.h"
|
||||||
#include "ui/windows/console_tab.h"
|
#include "ui/windows/console_tab.h"
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
@@ -25,8 +36,9 @@ namespace dragonx {
|
|||||||
class RPCWorker;
|
class RPCWorker;
|
||||||
}
|
}
|
||||||
namespace config { class Settings; }
|
namespace config { class Settings; }
|
||||||
namespace daemon { class EmbeddedDaemon; class XmrigManager; }
|
namespace daemon { class DaemonController; class EmbeddedDaemon; class XmrigManager; }
|
||||||
namespace util { class Bootstrap; class SecureVault; }
|
namespace util { class Bootstrap; class SecureVault; }
|
||||||
|
namespace wallet { class LiteWalletController; struct LiteWalletAppRefreshModel; }
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
@@ -125,6 +137,13 @@ public:
|
|||||||
* @brief Whether we are in the shutdown phase
|
* @brief Whether we are in the shutdown phase
|
||||||
*/
|
*/
|
||||||
bool isShuttingDown() const { return shutting_down_; }
|
bool isShuttingDown() const { return shutting_down_; }
|
||||||
|
wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); }
|
||||||
|
bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); }
|
||||||
|
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
|
||||||
|
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
|
||||||
|
bool supportsSoloMining() const { return wallet::supportsSoloMining(walletCapabilities()); }
|
||||||
|
bool supportsPoolMining() const { return wallet::supportsPoolMining(walletCapabilities()); }
|
||||||
|
bool supportsLiteBackend() const { return wallet::supportsLiteBackend(walletCapabilities()); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Render the shutdown overlay (called instead of normal UI during shutdown)
|
* @brief Render the shutdown overlay (called instead of normal UI during shutdown)
|
||||||
@@ -140,10 +159,42 @@ public:
|
|||||||
// Accessors for subsystems
|
// Accessors for subsystems
|
||||||
rpc::RPCClient* rpc() { return rpc_.get(); }
|
rpc::RPCClient* rpc() { return rpc_.get(); }
|
||||||
rpc::RPCWorker* worker() { return worker_.get(); }
|
rpc::RPCWorker* worker() { return worker_.get(); }
|
||||||
|
// Console backend accessors (fast-lane-preferring, defined in app.cpp where the
|
||||||
|
// subsystem types are complete) used by the shared console executor.
|
||||||
|
rpc::RPCClient* consoleRpc();
|
||||||
|
rpc::RPCWorker* consoleWorker();
|
||||||
|
daemon::EmbeddedDaemon* consoleDaemon();
|
||||||
|
daemon::XmrigManager* consoleXmrig();
|
||||||
config::Settings* settings() { return settings_.get(); }
|
config::Settings* settings() { return settings_.get(); }
|
||||||
|
// Lite wallet controller (non-null only in lite builds with a linked backend).
|
||||||
|
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
|
||||||
|
// HushChat service (identity + in-memory message store); the Chat tab reads its store.
|
||||||
|
chat::ChatService& chatService() { return chat_service_; }
|
||||||
|
// HushChat composing: construct, broadcast (broadcastChatMemos), and locally echo an outgoing
|
||||||
|
// message (to a conversation whose peer key we know) / a new-conversation contact request.
|
||||||
|
void sendChatMessage(const std::string& conversationId, const std::string& text);
|
||||||
|
void startChatConversation(const std::string& peerZaddr, const std::string& text);
|
||||||
|
// Debug/sweep convenience: give the Chat tab a demo identity + a few sample conversations
|
||||||
|
// (in-memory only, not persisted) so the screenshot sweep captures the populated UI. No-op when
|
||||||
|
// the chat feature is off.
|
||||||
|
void seedChatDemoData();
|
||||||
|
// Reason the lite wallet failed to auto-open this session (empty if none / opened OK).
|
||||||
|
const std::string& liteOpenError() const { return lite_open_error_; }
|
||||||
|
// Show the lite send-time unlock modal (called when a spend is attempted on a locked wallet).
|
||||||
|
void requestLiteUnlock() { lite_unlock_prompt_ = true; }
|
||||||
|
// Lock the lite wallet AND immediately tear down the chat session (the lite backend `lock`
|
||||||
|
// doesn't update state_.locked until the next poll, so chat secrets would otherwise linger).
|
||||||
|
bool lockLiteWallet();
|
||||||
|
// (Re)build the lite controller from current settings so a changed lite-server selection
|
||||||
|
// takes effect. No-op on non-lite/unlinked builds; preserves a live wallet (see app.cpp).
|
||||||
|
void rebuildLiteWallet(bool force = false);
|
||||||
WalletState& state() { return state_; }
|
WalletState& state() { return state_; }
|
||||||
const WalletState& state() const { return state_; }
|
const WalletState& state() const { return state_; }
|
||||||
const WalletState& getWalletState() const { return state_; }
|
const WalletState& getWalletState() const { return state_; }
|
||||||
|
// Shared contact store (Contacts tab / Send picker / future Chat roster). App-owned so
|
||||||
|
// every surface reads one source of truth instead of a per-dialog singleton.
|
||||||
|
data::AddressBook& addressBook() { return address_book_; }
|
||||||
|
const data::AddressBook& addressBook() const { return address_book_; }
|
||||||
|
|
||||||
// Connection state (convenience wrappers)
|
// Connection state (convenience wrappers)
|
||||||
bool isConnected() const { return state_.connected; }
|
bool isConnected() const { return state_.connected; }
|
||||||
@@ -176,6 +227,21 @@ public:
|
|||||||
int getXmrigRequestedThreads() const {
|
int getXmrigRequestedThreads() const {
|
||||||
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
|
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
|
||||||
}
|
}
|
||||||
|
// True while the pool miner process is live — used to refuse replacing the binary under it.
|
||||||
|
bool isPoolMinerRunning() const {
|
||||||
|
return xmrig_manager_ && xmrig_manager_->isRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-balance: latest per-pool hashrate snapshot (for the mining tab pool list),
|
||||||
|
// and a request to refresh it now (Refresh button / switching into Auto mode).
|
||||||
|
util::PoolStatsService::Snapshot poolStatsSnapshot() const {
|
||||||
|
return pool_stats_service_.snapshot();
|
||||||
|
}
|
||||||
|
void requestPoolBalanceRefresh() { balance_refresh_pending_ = true; }
|
||||||
|
|
||||||
|
// Installed miner version (detected from `xmrig --version`, cached; kicks the one-shot
|
||||||
|
// detection on first call) so the mining tab can show it before mining starts.
|
||||||
|
std::string poolMiningInstalledVersion();
|
||||||
|
|
||||||
// Mine-when-idle state query
|
// Mine-when-idle state query
|
||||||
bool isIdleMiningActive() const { return idle_mining_active_; }
|
bool isIdleMiningActive() const { return idle_mining_active_; }
|
||||||
@@ -183,7 +249,9 @@ public:
|
|||||||
// Peers
|
// Peers
|
||||||
const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
|
const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
|
||||||
const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; }
|
const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; }
|
||||||
bool isPeerRefreshInProgress() const { return peer_refresh_in_progress_.load(std::memory_order_relaxed); }
|
bool isPeerRefreshInProgress() const {
|
||||||
|
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Peers);
|
||||||
|
}
|
||||||
void banPeer(const std::string& ip, int duration_seconds = 86400);
|
void banPeer(const std::string& ip, int duration_seconds = 86400);
|
||||||
void unbanPeer(const std::string& ip);
|
void unbanPeer(const std::string& ip);
|
||||||
void clearBans();
|
void clearBans();
|
||||||
@@ -211,10 +279,18 @@ public:
|
|||||||
void setAddressSortOrder(const std::string& addr, int order);
|
void setAddressSortOrder(const std::string& addr, int order);
|
||||||
int getNextSortOrder() const;
|
int getNextSortOrder() const;
|
||||||
void swapAddressOrder(const std::string& a, const std::string& b);
|
void swapAddressOrder(const std::string& a, const std::string& b);
|
||||||
|
// Assign dense sort orders (0..N-1) to the given addresses in the given order and
|
||||||
|
// persist once. Used by drag-reorder so a drop always takes effect (even from the
|
||||||
|
// default un-ordered state, where a pairwise swap would be a no-op).
|
||||||
|
void reorderAddresses(const std::vector<std::string>& orderedAddrs);
|
||||||
|
bool isMiningAddress(const std::string& addr) const;
|
||||||
|
void setMiningAddress(const std::string& addr, bool mining);
|
||||||
|
void invalidateAddressValidationCache();
|
||||||
|
|
||||||
// Key export/import
|
// Key export/import
|
||||||
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
|
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
|
||||||
void exportAllKeys(std::function<void(const std::string&)> callback);
|
// callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export.
|
||||||
|
void exportAllKeys(std::function<void(const std::string&, int, int)> callback);
|
||||||
void importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback);
|
void importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback);
|
||||||
|
|
||||||
// Wallet backup
|
// Wallet backup
|
||||||
@@ -225,19 +301,27 @@ public:
|
|||||||
double amount, double fee, const std::string& memo,
|
double amount, double fee, const std::string& memo,
|
||||||
std::function<void(bool success, const std::string& result)> callback);
|
std::function<void(bool success, const std::string& result)> callback);
|
||||||
|
|
||||||
|
// Register a daemon async operation id (z_shieldcoinbase / z_mergetoaddress /
|
||||||
|
// auto-shield) with the shared opid poller so its eventual success/failure is
|
||||||
|
// surfaced and balances/transactions refresh on completion. z_sendmany uses the
|
||||||
|
// richer pending-send path internally; this is for operations with no optimistic
|
||||||
|
// transaction row of their own.
|
||||||
|
void trackOperation(const std::string& opid);
|
||||||
|
|
||||||
// Force refresh
|
// Force refresh
|
||||||
void refreshNow();
|
void refreshNow();
|
||||||
void refreshMiningInfo();
|
void refreshMiningInfo();
|
||||||
void refreshPeerInfo();
|
void refreshPeerInfo();
|
||||||
void refreshMarketData();
|
void refreshMarketData();
|
||||||
|
// Fetch the live exchange/pair list from CoinGecko once per session (venues are
|
||||||
|
// near-static); populates state.market.exchanges. Safe to call every frame.
|
||||||
|
void refreshExchanges();
|
||||||
|
// Fetch historical USD price series (CoinGecko market_chart) that back the portfolio
|
||||||
|
// sparkline intervals; self-throttled to ~30 min. Safe to call every frame.
|
||||||
|
void refreshMarketChart();
|
||||||
|
|
||||||
/// @brief Per-category refresh intervals, adjusted by active tab
|
/// @brief Per-category refresh intervals, adjusted by active tab
|
||||||
struct RefreshIntervals {
|
using RefreshIntervals = services::NetworkRefreshService::Intervals;
|
||||||
float core; // balance + sync status
|
|
||||||
float transactions; // tx list + enrichment
|
|
||||||
float addresses; // address lists + balances
|
|
||||||
float peers; // peer info (0 = disabled)
|
|
||||||
};
|
|
||||||
|
|
||||||
/// @brief Get recommended refresh intervals for a given page
|
/// @brief Get recommended refresh intervals for a given page
|
||||||
static RefreshIntervals getIntervalsForPage(ui::NavPage page);
|
static RefreshIntervals getIntervalsForPage(ui::NavPage page);
|
||||||
@@ -246,6 +330,16 @@ public:
|
|||||||
void setCurrentPage(ui::NavPage page);
|
void setCurrentPage(ui::NavPage page);
|
||||||
ui::NavPage getCurrentPage() const { return current_page_; }
|
ui::NavPage getCurrentPage() const { return current_page_; }
|
||||||
|
|
||||||
|
// Debug: screenshot sweep — cycles every skin x every enabled tab, one PNG each, into a
|
||||||
|
// timestamped folder under the config dir. Driven from App::render(); main.cpp polls
|
||||||
|
// wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls
|
||||||
|
// onScreenshotCaptured() to advance. Transient — restores the original skin/page when done.
|
||||||
|
void startScreenshotSweep();
|
||||||
|
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
|
||||||
|
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
|
||||||
|
void onScreenshotCaptured();
|
||||||
|
std::string screenshotDir() const; // <config>/screenshots (fixed; sweeps overwrite in place)
|
||||||
|
|
||||||
// Dialog triggers (used by settings page to open modal dialogs)
|
// Dialog triggers (used by settings page to open modal dialogs)
|
||||||
void showImportKeyDialog() { show_import_key_ = true; }
|
void showImportKeyDialog() { show_import_key_ = true; }
|
||||||
void showExportKeyDialog() { show_export_key_ = true; }
|
void showExportKeyDialog() { show_export_key_ = true; }
|
||||||
@@ -269,9 +363,23 @@ public:
|
|||||||
bool startEmbeddedDaemon();
|
bool startEmbeddedDaemon();
|
||||||
void stopEmbeddedDaemon();
|
void stopEmbeddedDaemon();
|
||||||
bool isEmbeddedDaemonRunning() const;
|
bool isEmbeddedDaemonRunning() const;
|
||||||
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
|
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
|
||||||
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
|
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
|
||||||
void rescanBlockchain(); // restart daemon with -rescan flag
|
void rescanBlockchain(); // restart daemon with -rescan flag (full-history nodes)
|
||||||
|
// Runtime rescanblockchain RPC starting at a snapshot-available height. Unlike the
|
||||||
|
// -rescan restart, this works on bootstrapped/pruned nodes (which lack pre-snapshot
|
||||||
|
// block data), reconciling the wallet's stale spent-state without a daemon restart.
|
||||||
|
void runtimeRescan(int startHeight);
|
||||||
|
// Async binary-search probe for the lowest block height the node still has on disk.
|
||||||
|
// cb(ok, lowestHeight, fullHistory): fullHistory==true when genesis is present (a normal,
|
||||||
|
// non-bootstrapped node). Runs on the UI thread via the RPC worker callbacks.
|
||||||
|
void detectLowestAvailableBlockHeight(std::function<void(bool ok, int lowestHeight, bool fullHistory)> cb);
|
||||||
|
// Flag that a bootstrap just finished so the wallet auto-reconciles spent-state once the
|
||||||
|
// daemon is back up (consumed in update()).
|
||||||
|
void markPostBootstrapRescanPending() { post_bootstrap_rescan_pending_ = true; }
|
||||||
|
bool runtimeRescanActive() const { return runtime_rescan_active_; }
|
||||||
|
void repairWallet(); // restart daemon with -zapwallettxes=2 (wipe & rebuild wallet tx records)
|
||||||
|
void reinstallBundledDaemon(); // stop daemon, overwrite installed binary with the bundled one, restart
|
||||||
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
|
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
|
||||||
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
|
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
|
||||||
bool isBootstrapDownloading() const { return bootstrap_downloading_; }
|
bool isBootstrapDownloading() const { return bootstrap_downloading_; }
|
||||||
@@ -321,6 +429,12 @@ public:
|
|||||||
|
|
||||||
// Wallet encryption helpers
|
// Wallet encryption helpers
|
||||||
void encryptWalletWithPassphrase(const std::string& passphrase);
|
void encryptWalletWithPassphrase(const std::string& passphrase);
|
||||||
|
// Post-encrypt daemon restart: the daemon shuts itself down after
|
||||||
|
// encryptwallet, so restart it off the main thread. Shared by the
|
||||||
|
// immediate and deferred encryption continuations. When
|
||||||
|
// announceRestartStatus is true, connection_status_ is updated first so
|
||||||
|
// the loading overlay explains the restart.
|
||||||
|
void restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus);
|
||||||
void unlockWallet(const std::string& passphrase, int timeout);
|
void unlockWallet(const std::string& passphrase, int timeout);
|
||||||
void lockWallet();
|
void lockWallet();
|
||||||
void changePassphrase(const std::string& oldPass, const std::string& newPass);
|
void changePassphrase(const std::string& oldPass, const std::string& newPass);
|
||||||
@@ -340,10 +454,7 @@ public:
|
|||||||
void showChangePassphraseDialog() { show_change_passphrase_ = true; }
|
void showChangePassphraseDialog() { show_change_passphrase_ = true; }
|
||||||
void showDecryptDialog() {
|
void showDecryptDialog() {
|
||||||
show_decrypt_dialog_ = true;
|
show_decrypt_dialog_ = true;
|
||||||
decrypt_phase_ = 0; // passphrase entry
|
wallet_security_workflow_.reset();
|
||||||
decrypt_step_ = 0;
|
|
||||||
decrypt_status_.clear();
|
|
||||||
decrypt_in_progress_ = false;
|
|
||||||
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,8 +466,100 @@ public:
|
|||||||
|
|
||||||
/// @brief Check if RPC worker has queued results waiting to be processed
|
/// @brief Check if RPC worker has queued results waiting to be processed
|
||||||
bool hasPendingRPCResults() const;
|
bool hasPendingRPCResults() const;
|
||||||
|
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
|
||||||
|
std::string transactionSendProgressText() const;
|
||||||
|
std::string transactionRefreshProgressText() const;
|
||||||
|
|
||||||
|
// Copy a SECRET (seed phrase, private key) to the clipboard and arm an auto-clear: after a
|
||||||
|
// short delay the clipboard is wiped IF it still holds this secret (so we don't clobber
|
||||||
|
// something the user copied afterwards). Only a hash of the secret is retained, never the
|
||||||
|
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
|
||||||
|
void copySecretToClipboard(const std::string& secret);
|
||||||
|
void pumpSecretClipboardClear();
|
||||||
|
bool isTransactionRefreshInProgress() const {
|
||||||
|
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
friend class AppDaemonLifecycleRuntime;
|
||||||
|
friend class AppDaemonLifecycleTaskContext;
|
||||||
|
|
||||||
|
// Global keyboard-shortcut handling, dispatched once per frame from update().
|
||||||
|
void handleGlobalShortcuts();
|
||||||
|
|
||||||
|
bool sendStopCommandSafely(rpc::RPCClient& client, const char* context);
|
||||||
|
void maybeFinishTransactionSendProgress();
|
||||||
|
// Shared body of createNewZAddress/createNewTAddress (which are thin public forwarders).
|
||||||
|
// `shielded` selects the z_getnewaddress/getnewaddress RPC, the "shielded"/"transparent" type
|
||||||
|
// string, and the z_addresses/t_addresses target (the new AddressInfo is pushed into both that
|
||||||
|
// list and state_.addresses). Lite builds derive locally via the controller and early-return.
|
||||||
|
void createNewAddress(bool shielded, std::function<void(const std::string&)> callback);
|
||||||
|
void upsertPendingSendTransaction(const std::string& opid,
|
||||||
|
const std::string& from,
|
||||||
|
const std::string& to,
|
||||||
|
double amount,
|
||||||
|
const std::string& memo,
|
||||||
|
double fee = 0.0);
|
||||||
|
// Work around a dragonxd note-selection bug: its z_sendmany picks notes to cover the recipient
|
||||||
|
// total but not the miner fee, so a shielded send whose largest notes sum exactly to the amount
|
||||||
|
// fails with "Insufficient shielded funds, have H, need H+fee" despite ample balance. When a
|
||||||
|
// failed opid matches that (H >= the requested amount), re-issue the send once with a tiny
|
||||||
|
// self-output that lifts the daemon's selection target past the boundary so it grabs another
|
||||||
|
// note; the recipient still receives the exact amount. Returns true if a retry was issued.
|
||||||
|
bool maybeRetrySendForFeeGap(const std::string& opid, const std::string& rawMsg);
|
||||||
|
void resendWithFeeGapWorkaround(const std::string& from, const std::string& to,
|
||||||
|
double amount, double fee, const std::string& memo,
|
||||||
|
std::function<void(bool, const std::string&)> callback);
|
||||||
|
// Shared z_sendmany submit path for sendTransaction (single recipient, markFeeGapRetry=false)
|
||||||
|
// and resendWithFeeGapWorkaround (recipient + self-output, markFeeGapRetry=true). Owns the
|
||||||
|
// in-flight increment, the worker post + call, and the result closure (dirty flags, opid
|
||||||
|
// tracking, pending-send bookkeeping, callback delivery). Callers build `recipients` (and pass
|
||||||
|
// the recipient `to`/`amount`/`memo`/`fee` used to record the optimistic pending-send row).
|
||||||
|
// When markFeeGapRetry is set, the returned opid is recorded in send_feegap_retried_opids_ so a
|
||||||
|
// retry of a retry is reported as a real error.
|
||||||
|
void submitZSendMany(const std::string& from, const std::string& to, double amount, double fee,
|
||||||
|
const std::string& memo, const nlohmann::json& recipients,
|
||||||
|
const char* traceLabel, bool markFeeGapRetry,
|
||||||
|
std::function<void(bool, const std::string&)> callback);
|
||||||
|
void markPendingSendTransactionSucceeded(const std::string& opid,
|
||||||
|
const std::string& txid);
|
||||||
|
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
||||||
|
bool restoreBalances);
|
||||||
|
// Apply a signed per-address balance delta for a pending send: walk z_addresses then
|
||||||
|
// t_addresses for `fromAddress` and clamp its balance at >=0. A positive `signedAmount`
|
||||||
|
// restores a debit; a negative one applies it. When `includeAggregates` is set, adjust the
|
||||||
|
// private/transparent bucket (chosen by the address's leading 'z') and totalBalance with the
|
||||||
|
// same clamp. Shared by the three pending-send delta sites so they can't drift.
|
||||||
|
void applyPendingSendDelta(const std::string& fromAddress, double signedAmount,
|
||||||
|
bool includeAggregates);
|
||||||
|
// Deliver a deferred z_sendmany result to its waiting UI callback once the opid
|
||||||
|
// reaches a terminal status. Returns true if a callback was registered (and fired).
|
||||||
|
bool invokeSendResultCallback(const std::string& opid, bool ok,
|
||||||
|
const std::string& result);
|
||||||
|
void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
|
||||||
|
std::string transactionHistoryCacheWalletIdentity() const;
|
||||||
|
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
|
||||||
|
void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
|
||||||
|
// Shared main-thread continuations for a wallet unlock attempt, so the passphrase
|
||||||
|
// and PIN paths cannot drift. applyUnlockFailure applies the escalating lockout
|
||||||
|
// curve on every failed path (a PIN RPC failure used to skip it).
|
||||||
|
void applyUnlockSuccess(const std::string& passphrase, int timeout);
|
||||||
|
void applyUnlockFailure(const std::string& errorMessage);
|
||||||
|
// Clear all rescan + witness-rebuild progress/accumulator state. Shared by the four
|
||||||
|
// rescan-completion sites so a new witness field can't be forgotten in one copy.
|
||||||
|
void resetWitnessRescanProgress();
|
||||||
|
void loadTransactionHistoryCacheIfAvailable();
|
||||||
|
void storeTransactionHistoryCacheIfAvailable();
|
||||||
|
void wipePendingTransactionHistoryCachePassphrase();
|
||||||
|
void resetTransactionHistoryCacheSession();
|
||||||
|
void pruneShieldedHistoryScanProgress();
|
||||||
|
void invalidateShieldedHistoryScanProgress(bool persistCache);
|
||||||
|
|
||||||
|
// Auto-balance pool selection: drive the periodic hashrate refresh and apply a
|
||||||
|
// freshly-completed snapshot (weighted-random pick + optional miner restart).
|
||||||
|
void updatePoolAutoBalance();
|
||||||
|
void applyPoolAutoBalance(const util::PoolStatsService::Snapshot& snap);
|
||||||
|
|
||||||
// Subsystems
|
// Subsystems
|
||||||
std::unique_ptr<rpc::RPCClient> rpc_;
|
std::unique_ptr<rpc::RPCClient> rpc_;
|
||||||
std::unique_ptr<rpc::RPCWorker> worker_;
|
std::unique_ptr<rpc::RPCWorker> worker_;
|
||||||
@@ -371,8 +574,55 @@ private:
|
|||||||
rpc::ConnectionConfig saved_config_;
|
rpc::ConnectionConfig saved_config_;
|
||||||
|
|
||||||
std::unique_ptr<config::Settings> settings_;
|
std::unique_ptr<config::Settings> settings_;
|
||||||
std::unique_ptr<daemon::EmbeddedDaemon> embedded_daemon_;
|
std::unique_ptr<wallet::LiteWalletController> lite_wallet_; // lite builds w/ linked backend
|
||||||
|
// Pending send_tab callback for an in-flight lite send (delivered in update() once the
|
||||||
|
// controller's async broadcast result arrives). Only one lite send runs at a time.
|
||||||
|
std::function<void(bool, const std::string&)> lite_send_callback_;
|
||||||
|
// One-shot guard: auto-open an existing lite wallet on the first update() tick (kept off
|
||||||
|
// init() so a slow initialize_existing network call doesn't freeze startup before the window).
|
||||||
|
bool lite_autoopen_done_ = false;
|
||||||
|
double lite_open_last_attempt_ = 0.0; // ImGui time of the last async open attempt (retry timer)
|
||||||
|
// Reason an existing lite wallet failed to auto-open (e.g. server unreachable). Surfaced in
|
||||||
|
// the UI so a stuck "disconnected" state isn't silent; cleared once a wallet opens.
|
||||||
|
std::string lite_open_error_;
|
||||||
|
|
||||||
|
// HushChat (experimental; gated by DRAGONX_ENABLE_CHAT — inert when OFF). App owns the chat
|
||||||
|
// service so the transaction-refresh harvest can decrypt incoming memos into threaded messages.
|
||||||
|
// The identity is derived from the wallet's OWN SDXLite-compatible seed phrase (full-node
|
||||||
|
// z_exportmnemonic / lite exportSeed → the same KDF), so it is portable across both variants.
|
||||||
|
chat::ChatService chat_service_;
|
||||||
|
chat::ChatDatabase chat_db_; // persistent backing (seed-derived encryption at rest)
|
||||||
|
bool chat_identity_provisioned_ = false; // identity set on the service this session
|
||||||
|
bool chat_identity_fetch_in_flight_ = false; // a z_exportmnemonic worker job is pending
|
||||||
|
bool chat_identity_unavailable_ = false; // provisioning failed definitively (e.g. non-mnemonic wallet)
|
||||||
|
// Provision the chat identity once the wallet seed is reachable+unlocked (per-tick, both
|
||||||
|
// variants); derives via deriveChatIdentityFromSecret and wipes the secret. No-op when the
|
||||||
|
// feature is off, already provisioned, in flight, or unavailable.
|
||||||
|
void maybeProvisionChatIdentity();
|
||||||
|
void provisionChatIdentityFromSecret(std::string secret);
|
||||||
|
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr for chat
|
||||||
|
std::string generateChatLocalId(const char* prefix, int numBytes) const; // unique echo id / cid
|
||||||
|
bool broadcastChatMemos(const chat::OutgoingChatMemos& memos); // returns true if submitted
|
||||||
|
bool broadcastChatMemosLite(const chat::OutgoingChatMemos& memos); // lite two-recipient send
|
||||||
|
void ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model); // lite chat receive harvest
|
||||||
|
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
|
||||||
|
bool lite_firstrun_dismissed_ = false;
|
||||||
|
// Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
|
||||||
|
bool lite_unlock_prompt_ = false;
|
||||||
|
// One-shot: prompt to unlock on startup once we learn the auto-opened wallet is encrypted+locked.
|
||||||
|
bool lite_startup_lock_checked_ = false;
|
||||||
|
std::unique_ptr<daemon::DaemonController> daemon_controller_;
|
||||||
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
|
||||||
|
// Auto-balance runtime state (pool mining, full-node only). The service fetches
|
||||||
|
// pool hashrates off-thread; the RNG drives the weighted-random pick.
|
||||||
|
util::PoolStatsService pool_stats_service_;
|
||||||
|
std::mt19937 balance_rng_;
|
||||||
|
long long last_balance_eval_ms_ = 0; // steady-clock ms of the last refresh kick
|
||||||
|
bool balance_refresh_pending_ = false; // UI asked for an immediate refresh
|
||||||
|
bool balance_snapshot_seen_ = false; // the current in-flight snapshot was applied
|
||||||
|
bool exchanges_fetch_started_ = false; // once-per-session CoinGecko tickers fetch
|
||||||
|
bool chart_fetch_in_flight_ = false; // a market_chart history fetch is on the worker
|
||||||
|
util::AsyncTaskManager async_tasks_;
|
||||||
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
|
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
|
||||||
|
|
||||||
// Wallet state
|
// Wallet state
|
||||||
@@ -390,7 +640,10 @@ private:
|
|||||||
|
|
||||||
// Daemon restart (e.g. after changing debug log categories)
|
// Daemon restart (e.g. after changing debug log categories)
|
||||||
std::atomic<bool> daemon_restarting_{false};
|
std::atomic<bool> daemon_restarting_{false};
|
||||||
std::thread daemon_restart_thread_;
|
|
||||||
|
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
|
||||||
|
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
|
||||||
|
std::atomic<int> pending_delete_result_{-1};
|
||||||
|
|
||||||
// Encryption state check timeout
|
// Encryption state check timeout
|
||||||
float encryption_check_timer_ = 0.0f;
|
float encryption_check_timer_ = 0.0f;
|
||||||
@@ -403,10 +656,9 @@ private:
|
|||||||
bool show_import_key_ = false;
|
bool show_import_key_ = false;
|
||||||
bool show_export_key_ = false;
|
bool show_export_key_ = false;
|
||||||
bool show_backup_ = false;
|
bool show_backup_ = false;
|
||||||
bool show_address_book_ = false;
|
|
||||||
|
|
||||||
// Embedded daemon state
|
// Embedded daemon state
|
||||||
bool use_embedded_daemon_ = true;
|
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
|
||||||
std::string daemon_status_;
|
std::string daemon_status_;
|
||||||
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
|
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
|
||||||
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
|
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
|
||||||
@@ -423,6 +675,16 @@ private:
|
|||||||
// Connection
|
// Connection
|
||||||
std::string connection_status_ = "Disconnected";
|
std::string connection_status_ = "Disconnected";
|
||||||
bool connection_in_progress_ = false;
|
bool connection_in_progress_ = false;
|
||||||
|
bool remote_rpc_plaintext_warning_shown_ = false;
|
||||||
|
// Startup daemon-launch diagnostics: bound the "RPC port busy, no config" wait before warning,
|
||||||
|
// and show the embedded-daemon start failure (binary/params/spawn) only once. Reset on connect.
|
||||||
|
int daemon_wait_attempts_ = 0;
|
||||||
|
bool daemon_start_error_shown_ = false;
|
||||||
|
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once
|
||||||
|
bool refresh_policy_syncing_ = false; // whether the sync-throttle refresh profile is active
|
||||||
|
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
|
||||||
|
std::uint64_t clipboard_secret_hash_ = 0;
|
||||||
|
double clipboard_clear_deadline_ = 0.0;
|
||||||
float loading_timer_ = 0.0f; // spinner animation for loading overlay
|
float loading_timer_ = 0.0f; // spinner animation for loading overlay
|
||||||
|
|
||||||
// Current page (sidebar navigation)
|
// Current page (sidebar navigation)
|
||||||
@@ -430,6 +692,21 @@ private:
|
|||||||
ui::NavPage prev_page_ = ui::NavPage::Overview;
|
ui::NavPage prev_page_ = ui::NavPage::Overview;
|
||||||
float page_alpha_ = 1.0f; // 0→1 fade on page switch
|
float page_alpha_ = 1.0f; // 0→1 fade on page switch
|
||||||
bool sidebar_collapsed_ = false; // true = icon-only mode
|
bool sidebar_collapsed_ = false; // true = icon-only mode
|
||||||
|
|
||||||
|
// Debug screenshot sweep state.
|
||||||
|
bool screenshot_sweep_active_ = false;
|
||||||
|
bool sweep_capture_this_frame_ = false;
|
||||||
|
int sweep_skin_idx_ = 0;
|
||||||
|
int sweep_page_idx_ = 0;
|
||||||
|
int sweep_settle_frames_ = 0; // frames to let a new skin/page settle before capturing
|
||||||
|
std::vector<std::string> sweep_skins_; // skin ids to cycle
|
||||||
|
std::vector<ui::NavPage> sweep_pages_; // enabled pages to cycle
|
||||||
|
std::string sweep_dir_; // output folder for this sweep
|
||||||
|
std::string sweep_current_path_; // PNG path for the frame about to be captured
|
||||||
|
std::string sweep_saved_skin_; // restore on completion
|
||||||
|
ui::NavPage sweep_saved_page_ = ui::NavPage::Overview;
|
||||||
|
void updateScreenshotSweep(); // called at the top of render() while active
|
||||||
|
void applySweepTarget(); // apply current (skin,page), compute path, arm settle
|
||||||
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
|
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
|
||||||
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
|
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
|
||||||
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
|
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
|
||||||
@@ -450,8 +727,9 @@ private:
|
|||||||
int coin_logo_h_ = 0;
|
int coin_logo_h_ = 0;
|
||||||
bool coin_logo_loaded_ = false;
|
bool coin_logo_loaded_ = false;
|
||||||
|
|
||||||
// Console tab
|
// Console tab + its backend executor (full-node RPC or lite backend), created lazily.
|
||||||
ui::ConsoleTab console_tab_;
|
ui::ConsoleTab console_tab_;
|
||||||
|
std::unique_ptr<ui::ConsoleCommandExecutor> console_exec_;
|
||||||
|
|
||||||
// Pending payment from URI
|
// Pending payment from URI
|
||||||
bool pending_payment_valid_ = false;
|
bool pending_payment_valid_ = false;
|
||||||
@@ -460,64 +738,35 @@ private:
|
|||||||
std::string pending_memo_;
|
std::string pending_memo_;
|
||||||
std::string pending_label_;
|
std::string pending_label_;
|
||||||
|
|
||||||
// Per-category timers (in seconds since last refresh)
|
// Per-category refresh timers, policy, and worker queue guards.
|
||||||
float core_timer_ = 0.0f; // balance + sync status
|
services::NetworkRefreshService network_refresh_;
|
||||||
float address_timer_ = 0.0f; // address lists
|
|
||||||
float transaction_timer_ = 0.0f; // transaction list
|
|
||||||
float peer_timer_ = 0.0f; // peer info
|
|
||||||
float price_timer_ = 0.0f;
|
|
||||||
float fast_refresh_timer_ = 0.0f; // For mining stats
|
|
||||||
|
|
||||||
// Default refresh intervals (seconds)
|
|
||||||
static constexpr float CORE_INTERVAL_DEFAULT = 5.0f;
|
|
||||||
static constexpr float ADDRESS_INTERVAL_DEFAULT = 15.0f;
|
|
||||||
static constexpr float TX_INTERVAL_DEFAULT = 10.0f;
|
|
||||||
static constexpr float PEER_INTERVAL_DEFAULT = 10.0f;
|
|
||||||
static constexpr float PRICE_INTERVAL = 60.0f;
|
|
||||||
static constexpr float FAST_REFRESH_INTERVAL = 1.0f;
|
|
||||||
|
|
||||||
// Active intervals — adjusted by tab priority via applyRefreshPolicy()
|
|
||||||
float active_core_interval_ = CORE_INTERVAL_DEFAULT;
|
|
||||||
float active_tx_interval_ = TX_INTERVAL_DEFAULT;
|
|
||||||
float active_addr_interval_ = ADDRESS_INTERVAL_DEFAULT;
|
|
||||||
float active_peer_interval_ = PEER_INTERVAL_DEFAULT;
|
|
||||||
|
|
||||||
// Per-category refresh guards (prevent worker queue pileup)
|
|
||||||
std::atomic<bool> core_refresh_in_progress_{false};
|
|
||||||
std::atomic<bool> address_refresh_in_progress_{false};
|
|
||||||
std::atomic<bool> tx_refresh_in_progress_{false};
|
|
||||||
|
|
||||||
// Mining refresh guard (prevents worker queue pileup)
|
|
||||||
std::atomic<bool> mining_refresh_in_progress_{false};
|
|
||||||
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
|
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
|
||||||
|
|
||||||
// Mining toggle guard (prevents concurrent setgenerate calls)
|
// Mining toggle guard (prevents concurrent setgenerate calls)
|
||||||
std::atomic<bool> mining_toggle_in_progress_{false};
|
std::atomic<bool> mining_toggle_in_progress_{false};
|
||||||
|
|
||||||
// Peer refresh guard (visual feedback for refresh button)
|
// True from a successful startPoolMining() until the miner is confirmed connected/hashing in the
|
||||||
std::atomic<bool> peer_refresh_in_progress_{false};
|
// poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay).
|
||||||
|
std::atomic<bool> pool_starting_{false};
|
||||||
|
|
||||||
// Auto-shield guard (prevents concurrent auto-shield operations)
|
// Auto-shield guard (prevents concurrent auto-shield operations)
|
||||||
std::atomic<bool> auto_shield_pending_{false};
|
std::atomic<bool> auto_shield_pending_{false};
|
||||||
|
|
||||||
// P4: Incremental transaction cache
|
// P4: Incremental transaction cache
|
||||||
int last_tx_block_height_ = -1; // block height at last full tx fetch
|
int last_tx_block_height_ = -1; // block height at last full tx fetch
|
||||||
float tx_age_timer_ = 0.0f; // seconds since last tx fetch
|
|
||||||
static constexpr float TX_MAX_AGE = 15.0f; // force tx refresh every N seconds even without new blocks
|
|
||||||
static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh
|
static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh
|
||||||
|
std::size_t shielded_history_scan_cursor_ = 0;
|
||||||
|
bool shielded_history_scan_pending_ = false;
|
||||||
|
// False until the first full shielded-history scan finishes. Drives the History tab's
|
||||||
|
// "Loading older history…" progress so the user knows transactions are still streaming in
|
||||||
|
// after the first batch appears; goes quiet for the routine per-block re-scans afterward.
|
||||||
|
bool initial_history_scan_complete_ = false;
|
||||||
|
std::unordered_map<std::string, int> shielded_history_scan_heights_;
|
||||||
|
|
||||||
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for
|
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for
|
||||||
// txids we've already enriched. Keyed by txid.
|
// txids we've already enriched. Keyed by txid.
|
||||||
struct ViewTxCacheEntry {
|
using ViewTxCacheEntry = services::NetworkRefreshService::TransactionViewCacheEntry;
|
||||||
std::string from_address; // first spend address
|
services::NetworkRefreshService::TransactionViewCache viewtx_cache_;
|
||||||
struct Output {
|
|
||||||
std::string address;
|
|
||||||
double value = 0.0;
|
|
||||||
std::string memo;
|
|
||||||
};
|
|
||||||
std::vector<Output> outgoing_outputs;
|
|
||||||
};
|
|
||||||
std::unordered_map<std::string, ViewTxCacheEntry> viewtx_cache_;
|
|
||||||
|
|
||||||
// P4c: Confirmed transaction cache — deeply-confirmed txns (>= 10 confs)
|
// P4c: Confirmed transaction cache — deeply-confirmed txns (>= 10 confs)
|
||||||
// are accumulated here and reused across refresh cycles. Only
|
// are accumulated here and reused across refresh cycles. Only
|
||||||
@@ -528,14 +777,58 @@ private:
|
|||||||
|
|
||||||
// Dirty flags for demand-driven refresh
|
// Dirty flags for demand-driven refresh
|
||||||
bool addresses_dirty_ = true; // true → refreshAddresses() will run
|
bool addresses_dirty_ = true; // true → refreshAddresses() will run
|
||||||
|
bool address_validation_cache_dirty_ = true;
|
||||||
bool transactions_dirty_ = false; // true → force tx refresh regardless of block height
|
bool transactions_dirty_ = false; // true → force tx refresh regardless of block height
|
||||||
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
|
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
|
||||||
|
bool rescan_status_poll_in_progress_ = false;
|
||||||
|
// True once we've actually observed the rescan running (daemon restarted into -rescan warmup).
|
||||||
|
// Gates the "rescan complete" detection so a getrescaninfo poll that hits the still-running
|
||||||
|
// pre-restart daemon (which reports rescanning=false) can't fire a false "complete" instantly.
|
||||||
|
bool rescan_confirmed_active_ = false;
|
||||||
|
// A runtime rescanblockchain RPC is in flight (vs the -rescan daemon restart). While set,
|
||||||
|
// the per-second mining/rescan-status pollers are suppressed (the daemon holds cs_main for
|
||||||
|
// the whole scan and would block them); completion is signalled by the rescan RPC callback.
|
||||||
|
bool runtime_rescan_active_ = false;
|
||||||
|
// Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan
|
||||||
|
// that reconciles the preserved wallet.dat against the freshly-imported chain.
|
||||||
|
bool post_bootstrap_rescan_pending_ = false;
|
||||||
|
// Largest "blocks remaining" seen during the current witness-rebuild phase. The daemon's
|
||||||
|
// "Building Witnesses for block" fraction resets every call (it's re-invoked per connected
|
||||||
|
// block, each walking from its own start height to the tip), so we derive a stable, monotonic
|
||||||
|
// overall percentage from how far "remaining" has fallen below this peak. Reset per phase.
|
||||||
|
int witness_rebuild_total_blocks_ = 0;
|
||||||
|
// The daemon's primary witness signal is "Setting Initial Sapling Witness for tx <hash>, <i>
|
||||||
|
// of <N>", logged once per wallet tx as its initial witness is set. The <i> is the tx's slot in
|
||||||
|
// an UNORDERED map, so it bounces wildly (was the cause of the resetting progress). The honest
|
||||||
|
// monotonic metric is how many DISTINCT txs have been witnessed (the set only grows; it also
|
||||||
|
// dedups the daemon's occasional double-prints) over the reported total N.
|
||||||
|
std::unordered_set<std::string> witness_seen_txids_;
|
||||||
|
int witness_total_txs_ = 0;
|
||||||
|
bool opid_poll_in_progress_ = false;
|
||||||
|
// Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
|
||||||
|
// connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.
|
||||||
|
int consecutive_core_failures_ = 0;
|
||||||
|
|
||||||
// Pending z_sendmany operation tracking
|
// Pending z_sendmany operation tracking
|
||||||
|
bool send_progress_active_ = false;
|
||||||
|
int send_submissions_in_flight_ = 0;
|
||||||
std::vector<std::string> pending_opids_; // opids to poll for completion
|
std::vector<std::string> pending_opids_; // opids to poll for completion
|
||||||
float opid_poll_timer_ = 0.0f;
|
struct PendingSendInfo {
|
||||||
static constexpr float OPID_POLL_INTERVAL = 2.0f;
|
std::string from;
|
||||||
|
std::string to;
|
||||||
|
std::string memo;
|
||||||
|
double amount = 0.0;
|
||||||
|
double fee = 0.0;
|
||||||
|
std::int64_t timestamp = 0;
|
||||||
|
};
|
||||||
|
std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
|
||||||
|
// Opids issued as a fee-gap auto-retry (see maybeRetrySendForFeeGap). Tracked so a retry that
|
||||||
|
// fails again is reported to the user instead of looping.
|
||||||
|
std::unordered_set<std::string> send_feegap_retried_opids_;
|
||||||
|
// z_sendmany UI callbacks held until the opid reaches a terminal status, so the
|
||||||
|
// user isn't told "sent successfully" before the tx is actually built/broadcast.
|
||||||
|
std::unordered_map<std::string, std::function<void(bool, const std::string&)>>
|
||||||
|
pending_send_callbacks_;
|
||||||
// Txids from completed z_sendmany operations.
|
// Txids from completed z_sendmany operations.
|
||||||
// Ensures shielded sends are discoverable by z_viewtransaction
|
// Ensures shielded sends are discoverable by z_viewtransaction
|
||||||
// even when they don't appear in listtransactions or
|
// even when they don't appear in listtransactions or
|
||||||
@@ -549,18 +842,20 @@ private:
|
|||||||
std::string wizard_pending_passphrase_; // held until daemon connects
|
std::string wizard_pending_passphrase_; // held until daemon connects
|
||||||
std::string wizard_saved_passphrase_; // held until PinSetup completes/skipped
|
std::string wizard_saved_passphrase_; // held until PinSetup completes/skipped
|
||||||
|
|
||||||
// Deferred encryption (wizard background task)
|
// Wallet security flow state shared by wizard/settings encryption paths.
|
||||||
std::string deferred_encrypt_passphrase_;
|
services::WalletSecurityController wallet_security_;
|
||||||
std::string deferred_encrypt_pin_;
|
services::WalletSecurityWorkflow wallet_security_workflow_;
|
||||||
bool deferred_encrypt_pending_ = false;
|
|
||||||
|
|
||||||
// Wizard: stopping an external daemon before bootstrap
|
// Wizard: stopping an external daemon before bootstrap
|
||||||
bool wizard_stopping_external_ = false;
|
bool wizard_stopping_external_ = false;
|
||||||
std::string wizard_stop_status_;
|
std::string wizard_stop_status_;
|
||||||
std::thread wizard_stop_thread_;
|
|
||||||
|
|
||||||
// PIN vault
|
// PIN vault
|
||||||
std::unique_ptr<util::SecureVault> vault_;
|
std::unique_ptr<util::SecureVault> vault_;
|
||||||
|
data::TransactionHistoryCache transaction_history_cache_;
|
||||||
|
data::AddressBook address_book_; // shared contact store; loaded once in init(), self-saves on mutation
|
||||||
|
std::string pending_transaction_history_cache_passphrase_;
|
||||||
|
bool transaction_history_cache_loaded_ = false;
|
||||||
|
|
||||||
// Lock screen state
|
// Lock screen state
|
||||||
bool lock_screen_was_visible_ = false; // tracks lock→unlock transitions for auto-focus
|
bool lock_screen_was_visible_ = false; // tracks lock→unlock transitions for auto-focus
|
||||||
@@ -602,14 +897,7 @@ private:
|
|||||||
|
|
||||||
// Decrypt wallet dialog state
|
// Decrypt wallet dialog state
|
||||||
bool show_decrypt_dialog_ = false;
|
bool show_decrypt_dialog_ = false;
|
||||||
int decrypt_phase_ = 0; // 0=passphrase, 1=working, 2=done, 3=error
|
|
||||||
int decrypt_step_ = 0; // 0=unlock, 1=export, 2=stop, 3=rename, 4=restart, 5=import
|
|
||||||
char decrypt_pass_buf_[256] = {};
|
char decrypt_pass_buf_[256] = {};
|
||||||
std::string decrypt_status_;
|
|
||||||
bool decrypt_in_progress_ = false;
|
|
||||||
std::chrono::steady_clock::time_point decrypt_step_start_time_{};
|
|
||||||
std::chrono::steady_clock::time_point decrypt_overall_start_time_{};
|
|
||||||
std::atomic<bool> decrypt_import_active_{false}; // background z_importwallet running
|
|
||||||
|
|
||||||
// Wizard PIN setup state
|
// Wizard PIN setup state
|
||||||
char wizard_pin_buf_[16] = {};
|
char wizard_pin_buf_[16] = {};
|
||||||
@@ -625,7 +913,8 @@ private:
|
|||||||
|
|
||||||
// Private methods - rendering
|
// Private methods - rendering
|
||||||
void renderStatusBar();
|
void renderStatusBar();
|
||||||
void renderAboutDialog();
|
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
|
||||||
|
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
|
||||||
void renderImportKeyDialog();
|
void renderImportKeyDialog();
|
||||||
void renderExportKeyDialog();
|
void renderExportKeyDialog();
|
||||||
void renderBackupDialog();
|
void renderBackupDialog();
|
||||||
@@ -641,6 +930,16 @@ private:
|
|||||||
void tryConnect();
|
void tryConnect();
|
||||||
void onConnected();
|
void onConnected();
|
||||||
void onDisconnected(const std::string& reason);
|
void onDisconnected(const std::string& reason);
|
||||||
|
// Set the "node is initializing" UI state (status line + overlay description) from the
|
||||||
|
// embedded/external daemon's launch state and its own console output (current phase + block
|
||||||
|
// height), so a connect probe that times out while the daemon loads shows WHAT it's doing.
|
||||||
|
// `reachableButBusy` is true when the probe connected but got no RPC reply (a timeout),
|
||||||
|
// false when the daemon is merely launching (not bound yet). Returns the status title.
|
||||||
|
std::string applyDaemonInitStatus(bool reachableButBusy);
|
||||||
|
// Tear down a connection that died mid-session (daemon crash / restart / dropped
|
||||||
|
// socket) so update()'s reconnect branch re-enters tryConnect(). Unlike onDisconnected
|
||||||
|
// alone, this also rpc_->disconnect()s so rpc_->isConnected() actually flips to false.
|
||||||
|
void handleLostConnection(const std::string& reason);
|
||||||
void applyDefaultBanlist();
|
void applyDefaultBanlist();
|
||||||
|
|
||||||
// Private methods - data refresh
|
// Private methods - data refresh
|
||||||
@@ -648,13 +947,17 @@ private:
|
|||||||
void refreshCoreData(); // Balance + blockchain info (can use fast_worker_)
|
void refreshCoreData(); // Balance + blockchain info (can use fast_worker_)
|
||||||
void refreshAddressData(); // Address lists + balances
|
void refreshAddressData(); // Address lists + balances
|
||||||
void refreshTransactionData(); // Transaction list + z_viewtransaction enrichment
|
void refreshTransactionData(); // Transaction list + z_viewtransaction enrichment
|
||||||
void refreshEncryptionState(); // Wallet encryption/lock state
|
void refreshRecentTransactionData(); // Lightweight recent/unconfirmed tx poll
|
||||||
|
bool refreshEncryptionState(); // Wallet encryption/lock state
|
||||||
void refreshBalance(); // Legacy: balance-only refresh (used by specific callers)
|
void refreshBalance(); // Legacy: balance-only refresh (used by specific callers)
|
||||||
void refreshAddresses(); // Legacy: standalone address refresh
|
void refreshAddresses(); // Legacy: standalone address refresh
|
||||||
void refreshPrice();
|
void refreshPrice();
|
||||||
void refreshWalletEncryptionState();
|
void refreshWalletEncryptionState();
|
||||||
void applyRefreshPolicy(ui::NavPage page);
|
void applyRefreshPolicy(ui::NavPage page);
|
||||||
|
bool currentPageNeedsWalletDataRefresh() const;
|
||||||
|
bool shouldRunWalletTransactionRefresh() const;
|
||||||
bool shouldRefreshTransactions() const;
|
bool shouldRefreshTransactions() const;
|
||||||
|
bool shouldRefreshRecentTransactions() const;
|
||||||
void checkAutoLock();
|
void checkAutoLock();
|
||||||
void checkIdleMining();
|
void checkIdleMining();
|
||||||
};
|
};
|
||||||
|
|||||||
2986
src/app_network.cpp
2986
src/app_network.cpp
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
|||||||
#include "rpc/rpc_worker.h"
|
#include "rpc/rpc_worker.h"
|
||||||
#include "rpc/connection.h"
|
#include "rpc/connection.h"
|
||||||
#include "config/settings.h"
|
#include "config/settings.h"
|
||||||
|
#include "daemon/daemon_controller.h"
|
||||||
#include "daemon/embedded_daemon.h"
|
#include "daemon/embedded_daemon.h"
|
||||||
#include "ui/notifications.h"
|
#include "ui/notifications.h"
|
||||||
#include "ui/material/color_theme.h"
|
#include "ui/material/color_theme.h"
|
||||||
@@ -39,13 +40,48 @@ namespace dragonx {
|
|||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct WizardLowSpecSnapshot {
|
||||||
|
bool valid = false;
|
||||||
|
float blur = 0.0f;
|
||||||
|
float uiOp = 0.0f;
|
||||||
|
bool fx = false;
|
||||||
|
bool scanline = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WizardUiState {
|
||||||
|
float blur_amount = 1.5f;
|
||||||
|
bool theme_effects = true;
|
||||||
|
float ui_opacity = 1.0f;
|
||||||
|
bool low_spec = false;
|
||||||
|
bool scanline = true;
|
||||||
|
std::string balance_layout = "classic";
|
||||||
|
int language_index = 0;
|
||||||
|
bool appearance_init = false;
|
||||||
|
WizardLowSpecSnapshot low_spec_snapshot;
|
||||||
|
float card0_max_h = 0.0f;
|
||||||
|
float card1_max_h = 0.0f;
|
||||||
|
double external_last_check = -10.0;
|
||||||
|
bool daemon_prestarted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
WizardUiState s_wizardUi;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void App::restartWizard()
|
void App::restartWizard()
|
||||||
{
|
{
|
||||||
|
if (!supportsFullNodeLifecycleActions()) {
|
||||||
|
ui::Notifications::instance().warning("Lite wallet lifecycle requests are available from Settings as dry-run readiness checks");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
|
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
|
||||||
|
|
||||||
// Reset crash counter for fresh wizard attempt
|
// Reset crash counter for fresh wizard attempt
|
||||||
if (embedded_daemon_) {
|
if (daemon_controller_) {
|
||||||
embedded_daemon_->resetCrashCount();
|
daemon_controller_->resetCrashCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect RPC
|
// Disconnect RPC
|
||||||
@@ -56,10 +92,11 @@ void App::restartWizard()
|
|||||||
|
|
||||||
// Stop the embedded daemon in a background thread to avoid
|
// Stop the embedded daemon in a background thread to avoid
|
||||||
// blocking the UI for up to 32 seconds (RPC stop + process wait).
|
// blocking the UI for up to 32 seconds (RPC stop + process wait).
|
||||||
if (embedded_daemon_ && isEmbeddedDaemonRunning()) {
|
if (daemon_controller_ && isEmbeddedDaemonRunning()) {
|
||||||
std::thread([this]() {
|
async_tasks_.submit("wizard-restart-stop-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||||
|
if (token.cancelled()) return;
|
||||||
stopEmbeddedDaemon();
|
stopEmbeddedDaemon();
|
||||||
}).detach();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter wizard — the wizard completion handler already calls
|
// Enter wizard — the wizard completion handler already calls
|
||||||
@@ -73,6 +110,7 @@ void App::restartWizard()
|
|||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
||||||
void App::renderFirstRunWizard() {
|
void App::renderFirstRunWizard() {
|
||||||
|
auto& wizardUi = s_wizardUi;
|
||||||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||||
ImGui::SetNextWindowPos(viewport->WorkPos);
|
ImGui::SetNextWindowPos(viewport->WorkPos);
|
||||||
ImGui::SetNextWindowSize(viewport->WorkSize);
|
ImGui::SetNextWindowSize(viewport->WorkSize);
|
||||||
@@ -243,15 +281,14 @@ void App::renderFirstRunWizard() {
|
|||||||
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
|
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
|
||||||
cy += 14.0f * dp;
|
cy += 14.0f * dp;
|
||||||
|
|
||||||
// Statics for appearance settings
|
float& wiz_blur_amount = wizardUi.blur_amount;
|
||||||
static float wiz_blur_amount = 1.5f;
|
bool& wiz_theme_effects = wizardUi.theme_effects;
|
||||||
static bool wiz_theme_effects = true;
|
float& wiz_ui_opacity = wizardUi.ui_opacity;
|
||||||
static float wiz_ui_opacity = 1.0f;
|
bool& wiz_low_spec = wizardUi.low_spec;
|
||||||
static bool wiz_low_spec = false;
|
bool& wiz_scanline = wizardUi.scanline;
|
||||||
static bool wiz_scanline = true;
|
std::string& wiz_balance_layout = wizardUi.balance_layout;
|
||||||
static std::string wiz_balance_layout = "classic";
|
int& wiz_language_index = wizardUi.language_index;
|
||||||
static int wiz_language_index = 0;
|
bool& wiz_appearance_init = wizardUi.appearance_init;
|
||||||
static bool wiz_appearance_init = false;
|
|
||||||
if (!wiz_appearance_init) {
|
if (!wiz_appearance_init) {
|
||||||
wiz_blur_amount = settings_->getBlurMultiplier();
|
wiz_blur_amount = settings_->getBlurMultiplier();
|
||||||
wiz_theme_effects = settings_->getThemeEffectsEnabled();
|
wiz_theme_effects = settings_->getThemeEffectsEnabled();
|
||||||
@@ -398,7 +435,7 @@ void App::renderFirstRunWizard() {
|
|||||||
|
|
||||||
// --- Low-spec mode checkbox ---
|
// --- Low-spec mode checkbox ---
|
||||||
// Snapshot for restoring settings when low-spec is turned off
|
// Snapshot for restoring settings when low-spec is turned off
|
||||||
static struct { bool valid; float blur; float uiOp; bool fx; bool scanline; } wiz_lsSnap = {};
|
WizardLowSpecSnapshot& wiz_lsSnap = wizardUi.low_spec_snapshot;
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
|
||||||
@@ -574,7 +611,7 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Continue##app", ImVec2(btnW, btnH))) {
|
if (ui::material::TactileButton("Continue##app", ImVec2(btnW, btnH))) {
|
||||||
// Save appearance choices, advance to Bootstrap
|
// Save appearance choices, advance to Bootstrap
|
||||||
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
|
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
|
||||||
settings_->setAcrylicQuality(wiz_blur_amount > 0.001f
|
settings_->setAcrylicQuality(wiz_blur_amount > 0.001f
|
||||||
@@ -596,7 +633,7 @@ void App::renderFirstRunWizard() {
|
|||||||
|
|
||||||
cy += cardPad;
|
cy += cardPad;
|
||||||
// Lock card height to the tallest content ever seen
|
// Lock card height to the tallest content ever seen
|
||||||
static float card0MaxH = 0.0f;
|
float& card0MaxH = wizardUi.card0_max_h;
|
||||||
card0MaxH = std::max(card0MaxH, cy - card0Top);
|
card0MaxH = std::max(card0MaxH, cy - card0Top);
|
||||||
card0Bot = card0Top + card0MaxH;
|
card0Bot = card0Top + card0MaxH;
|
||||||
|
|
||||||
@@ -726,7 +763,7 @@ void App::renderFirstRunWizard() {
|
|||||||
float cancelBX = rightX + (colW - cancelW) * 0.5f;
|
float cancelBX = rightX + (colW - cancelW) * 0.5f;
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy));
|
ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Cancel##bs", ImVec2(cancelW, cancelH))) {
|
if (ui::material::TactileButton("Cancel##bs", ImVec2(cancelW, cancelH))) {
|
||||||
bootstrap_->cancel();
|
bootstrap_->cancel();
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
@@ -737,6 +774,8 @@ void App::renderFirstRunWizard() {
|
|||||||
auto finalProg = bootstrap_->getProgress();
|
auto finalProg = bootstrap_->getProgress();
|
||||||
if (finalProg.state == util::Bootstrap::State::Completed) {
|
if (finalProg.state == util::Bootstrap::State::Completed) {
|
||||||
bootstrap_.reset();
|
bootstrap_.reset();
|
||||||
|
// Reconcile the preserved wallet.dat against the new chain once the daemon is up.
|
||||||
|
markPostBootstrapRescanPending();
|
||||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||||
} else {
|
} else {
|
||||||
wizard_phase_ = WizardPhase::BootstrapFailed;
|
wizard_phase_ = WizardPhase::BootstrapFailed;
|
||||||
@@ -772,27 +811,22 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
|
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||||
|
if (ui::material::TactileButton("Retry##bs", ImVec2(btnW2, btnH2))) {
|
||||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||||
if (isEmbeddedDaemonRunning()) {
|
stopDaemonForBootstrap();
|
||||||
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap retry...\n");
|
|
||||||
if (rpc_ && rpc_->isConnected()) {
|
|
||||||
try { rpc_->call("stop"); } catch (...) {}
|
|
||||||
rpc_->disconnect();
|
|
||||||
}
|
|
||||||
onDisconnected("Bootstrap retry");
|
|
||||||
}
|
|
||||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||||
bootstrap_->start(dataDir);
|
bootstrap_->start(dataDir);
|
||||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||||
}
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
|
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Skip##bsfail", ImVec2(btnW2, btnH2))) {
|
if (ui::material::TactileButton("Skip##bsfail", ImVec2(btnW2, btnH2))) {
|
||||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
@@ -808,17 +842,23 @@ void App::renderFirstRunWizard() {
|
|||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
static std::atomic<bool> s_extCached{false};
|
static std::atomic<bool> s_extCached{false};
|
||||||
static std::atomic<bool> s_checkInFlight{false};
|
static std::atomic<bool> s_checkInFlight{false};
|
||||||
static double s_extLastCheck = -10.0;
|
double& s_extLastCheck = wizardUi.external_last_check;
|
||||||
double now = ImGui::GetTime();
|
double now = ImGui::GetTime();
|
||||||
if (now - s_extLastCheck >= 2.0 && !s_checkInFlight.load()) {
|
if (now - s_extLastCheck >= 2.0 && !s_checkInFlight.load()) {
|
||||||
s_extLastCheck = now;
|
s_extLastCheck = now;
|
||||||
bool embeddedRunning = isEmbeddedDaemonRunning();
|
bool embeddedRunning = isEmbeddedDaemonRunning();
|
||||||
s_checkInFlight.store(true);
|
s_checkInFlight.store(true);
|
||||||
std::thread([embeddedRunning]() {
|
async_tasks_.submit("wizard-external-daemon-check", [embeddedRunning](const util::AsyncTaskManager::Token& token) {
|
||||||
|
if (token.cancelled()) {
|
||||||
|
s_checkInFlight.store(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
bool inUse = daemon::EmbeddedDaemon::isRpcPortInUse();
|
bool inUse = daemon::EmbeddedDaemon::isRpcPortInUse();
|
||||||
s_extCached.store(inUse && !embeddedRunning);
|
if (!token.cancelled()) {
|
||||||
|
s_extCached.store(inUse && !embeddedRunning);
|
||||||
|
}
|
||||||
s_checkInFlight.store(false);
|
s_checkInFlight.store(false);
|
||||||
}).detach();
|
});
|
||||||
}
|
}
|
||||||
externalRunning = s_extCached.load();
|
externalRunning = s_extCached.load();
|
||||||
}
|
}
|
||||||
@@ -856,22 +896,22 @@ void App::renderFirstRunWizard() {
|
|||||||
IM_COL32(220, 60, 60, 255)));
|
IM_COL32(220, 60, 60, 255)));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 255));
|
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 255));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
|
if (ui::material::TactileButton("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
|
||||||
wizard_stopping_external_ = true;
|
wizard_stopping_external_ = true;
|
||||||
wizard_stop_status_ = "Sending stop command...";
|
wizard_stop_status_ = "Sending stop command...";
|
||||||
if (wizard_stop_thread_.joinable()) wizard_stop_thread_.join();
|
async_tasks_.submit("wizard-stop-external-daemon", [this](const util::AsyncTaskManager::Token& token) {
|
||||||
wizard_stop_thread_ = std::thread([this]() {
|
|
||||||
auto config = rpc::Connection::autoDetectConfig();
|
auto config = rpc::Connection::autoDetectConfig();
|
||||||
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
|
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
|
||||||
auto tmp_rpc = std::make_unique<rpc::RPCClient>();
|
auto tmp_rpc = std::make_unique<rpc::RPCClient>();
|
||||||
if (tmp_rpc->connect(config.host, config.port,
|
if (tmp_rpc->connect(config.host, config.port,
|
||||||
config.rpcuser, config.rpcpassword)) {
|
config.rpcuser, config.rpcpassword,
|
||||||
try { tmp_rpc->call("stop"); } catch (...) {}
|
config.use_tls)) {
|
||||||
|
sendStopCommandSafely(*tmp_rpc, "Wizard external daemon stop");
|
||||||
tmp_rpc->disconnect();
|
tmp_rpc->disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wizard_stop_status_ = "Waiting for daemon to shut down...";
|
wizard_stop_status_ = "Waiting for daemon to shut down...";
|
||||||
for (int i = 0; i < 60; i++) {
|
for (int i = 0; i < 60 && !token.cancelled(); i++) {
|
||||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||||
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
|
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
|
||||||
wizard_stop_status_ = "Daemon stopped.";
|
wizard_stop_status_ = "Daemon stopped.";
|
||||||
@@ -879,6 +919,7 @@ void App::renderFirstRunWizard() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (token.cancelled()) return;
|
||||||
wizard_stop_status_ = "Daemon did not stop — try manually.";
|
wizard_stop_status_ = "Daemon did not stop — try manually.";
|
||||||
wizard_stopping_external_ = false;
|
wizard_stopping_external_ = false;
|
||||||
});
|
});
|
||||||
@@ -888,7 +929,7 @@ void App::renderFirstRunWizard() {
|
|||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bx + stopW + 12.0f * dp, cy));
|
ImGui::SetCursorScreenPos(ImVec2(bx + stopW + 12.0f * dp, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Skip##extd", ImVec2(skipW2, btnH2))) {
|
if (ui::material::TactileButton("Skip##extd", ImVec2(skipW2, btnH2))) {
|
||||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
@@ -953,21 +994,16 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
|
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||||
|
if (ui::material::TactileButton("Download##bs", ImVec2(dlBtnW, btnH2))) {
|
||||||
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
// Stop embedded daemon before bootstrap to avoid chain data corruption
|
||||||
if (isEmbeddedDaemonRunning()) {
|
stopDaemonForBootstrap();
|
||||||
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap...\n");
|
|
||||||
if (rpc_ && rpc_->isConnected()) {
|
|
||||||
try { rpc_->call("stop"); } catch (...) {}
|
|
||||||
rpc_->disconnect();
|
|
||||||
}
|
|
||||||
onDisconnected("Bootstrap");
|
|
||||||
}
|
|
||||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||||
bootstrap_->start(dataDir);
|
bootstrap_->start(dataDir);
|
||||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||||
}
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
|
|
||||||
@@ -977,23 +1013,18 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
|
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
|
||||||
if (isEmbeddedDaemonRunning()) {
|
if (ui::material::TactileButton("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
|
||||||
DEBUG_LOGF("[Wizard] Stopping embedded daemon before bootstrap (mirror)...\n");
|
stopDaemonForBootstrap();
|
||||||
if (rpc_ && rpc_->isConnected()) {
|
|
||||||
try { rpc_->call("stop"); } catch (...) {}
|
|
||||||
rpc_->disconnect();
|
|
||||||
}
|
|
||||||
onDisconnected("Bootstrap");
|
|
||||||
}
|
|
||||||
bootstrap_ = std::make_unique<util::Bootstrap>();
|
bootstrap_ = std::make_unique<util::Bootstrap>();
|
||||||
std::string dataDir = util::Platform::getDragonXDataDir();
|
std::string dataDir = util::Platform::getDragonXDataDir();
|
||||||
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
|
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
|
||||||
bootstrap_->start(dataDir, mirrorUrl);
|
bootstrap_->start(dataDir, mirrorUrl);
|
||||||
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
wizard_phase_ = WizardPhase::BootstrapInProgress;
|
||||||
}
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
if (ImGui::IsItemHovered()) {
|
if (ImGui::IsItemHovered()) {
|
||||||
ImGui::SetTooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
|
ui::material::Tooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
ImGui::PopStyleColor(3);
|
ImGui::PopStyleColor(3);
|
||||||
@@ -1001,7 +1032,7 @@ void App::renderFirstRunWizard() {
|
|||||||
// --- Skip button ---
|
// --- Skip button ---
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
|
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
|
if (ui::material::TactileButton("Skip##bs", ImVec2(skipW2, btnH2))) {
|
||||||
wizard_phase_ = WizardPhase::EncryptOffer;
|
wizard_phase_ = WizardPhase::EncryptOffer;
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
@@ -1012,7 +1043,7 @@ void App::renderFirstRunWizard() {
|
|||||||
|
|
||||||
cy += cardPad;
|
cy += cardPad;
|
||||||
// Lock card height to the tallest content ever seen (but not when collapsed)
|
// Lock card height to the tallest content ever seen (but not when collapsed)
|
||||||
static float card1MaxH = 0.0f;
|
float& card1MaxH = wizardUi.card1_max_h;
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
card1Bot = card1Top + (cy - card1Top);
|
card1Bot = card1Top + (cy - card1Top);
|
||||||
} else {
|
} else {
|
||||||
@@ -1037,7 +1068,7 @@ void App::renderFirstRunWizard() {
|
|||||||
// Pre-start daemon when encrypt card becomes focused so it's ready
|
// Pre-start daemon when encrypt card becomes focused so it's ready
|
||||||
// by the time the user finishes typing their passphrase
|
// by the time the user finishes typing their passphrase
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
static bool wiz_daemon_prestarted = false;
|
bool& wiz_daemon_prestarted = wizardUi.daemon_prestarted;
|
||||||
if (!wiz_daemon_prestarted) {
|
if (!wiz_daemon_prestarted) {
|
||||||
wiz_daemon_prestarted = true;
|
wiz_daemon_prestarted = true;
|
||||||
if (!state_.connected && isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
|
if (!state_.connected && isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
|
||||||
@@ -1096,7 +1127,7 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Continue##encok", ImVec2(btnW2, btnH2))) {
|
if (ui::material::TactileButton("Continue##encok", ImVec2(btnW2, btnH2))) {
|
||||||
wizard_phase_ = WizardPhase::Done;
|
wizard_phase_ = WizardPhase::Done;
|
||||||
settings_->setWizardCompleted(true);
|
settings_->setWizardCompleted(true);
|
||||||
settings_->save();
|
settings_->save();
|
||||||
@@ -1254,10 +1285,24 @@ void App::renderFirstRunWizard() {
|
|||||||
cy += captionFont->LegacySize + 6.0f * dp;
|
cy += captionFont->LegacySize + 6.0f * dp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Warn + block if the passphrase has leading/trailing whitespace. Silently trimming it would
|
||||||
|
// change the passphrase the user believes they set and lock them out on the next unlock.
|
||||||
|
bool passEdgeSpace = false;
|
||||||
|
if (size_t pl = strlen(encrypt_pass_buf_)) {
|
||||||
|
char a = encrypt_pass_buf_[0], b = encrypt_pass_buf_[pl - 1];
|
||||||
|
passEdgeSpace = (a == ' ' || a == '\t' || b == ' ' || b == '\t');
|
||||||
|
}
|
||||||
|
if (passEdgeSpace) {
|
||||||
|
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
|
||||||
|
ui::material::Error(), "Passphrase has leading/trailing spaces — remove them");
|
||||||
|
cy += captionFont->LegacySize + 6.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
{
|
{
|
||||||
bool passValid = strlen(encrypt_pass_buf_) >= 8 &&
|
bool passValid = strlen(encrypt_pass_buf_) >= 8 &&
|
||||||
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0;
|
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0 &&
|
||||||
|
!passEdgeSpace;
|
||||||
// PIN is optional: if entered, must be valid + confirmed
|
// PIN is optional: if entered, must be valid + confirmed
|
||||||
std::string pinStr(wizard_pin_buf_);
|
std::string pinStr(wizard_pin_buf_);
|
||||||
bool pinEntered = !pinStr.empty();
|
bool pinEntered = !pinStr.empty();
|
||||||
@@ -1279,12 +1324,11 @@ void App::renderFirstRunWizard() {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
ImGui::BeginDisabled(!canEncrypt);
|
ImGui::BeginDisabled(!canEncrypt);
|
||||||
if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
|
if (ui::material::TactileButton("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
|
||||||
// Save passphrase + optional PIN for background processing
|
// Save passphrase + optional PIN for background processing
|
||||||
deferred_encrypt_passphrase_ = std::string(encrypt_pass_buf_);
|
wallet_security_.beginDeferredEncryption(
|
||||||
if (pinEntered && pinOk)
|
std::string(encrypt_pass_buf_),
|
||||||
deferred_encrypt_pin_ = pinStr;
|
(pinEntered && pinOk) ? pinStr : std::string());
|
||||||
deferred_encrypt_pending_ = true;
|
|
||||||
|
|
||||||
// Clear sensitive buffers
|
// Clear sensitive buffers
|
||||||
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
|
||||||
@@ -1311,17 +1355,25 @@ void App::renderFirstRunWizard() {
|
|||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
|
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
|
||||||
if (ImGui::Button("Skip##enc", ImVec2(skipW2, btnH2))) {
|
if (ui::material::TactileButton("Skip##enc", ImVec2(skipW2, btnH2))) {
|
||||||
wizard_phase_ = WizardPhase::Done;
|
static bool s_skipEncConfirm = false;
|
||||||
settings_->setWizardCompleted(true);
|
if (!s_skipEncConfirm) {
|
||||||
settings_->save();
|
// Skipping stores private keys UNENCRYPTED — require a confirming second click.
|
||||||
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
s_skipEncConfirm = true;
|
||||||
if (!startEmbeddedDaemon()) {
|
encrypt_status_ = "Continue WITHOUT encryption? Keys will be stored unencrypted — click Skip again to confirm.";
|
||||||
ui::Notifications::instance().warning(
|
} else {
|
||||||
TR("wizard_daemon_start_failed"));
|
s_skipEncConfirm = false;
|
||||||
|
wizard_phase_ = WizardPhase::Done;
|
||||||
|
settings_->setWizardCompleted(true);
|
||||||
|
settings_->save();
|
||||||
|
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
|
||||||
|
if (!startEmbeddedDaemon()) {
|
||||||
|
ui::Notifications::instance().warning(
|
||||||
|
TR("wizard_daemon_start_failed"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
tryConnect();
|
||||||
}
|
}
|
||||||
tryConnect();
|
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
cy += btnH2;
|
cy += btnH2;
|
||||||
|
|||||||
173
src/chat/chat_crypto.cpp
Normal file
173
src/chat/chat_crypto.cpp
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// DragonX Wallet - HushChat crypto primitives (implementation).
|
||||||
|
|
||||||
|
#include "chat_crypto.h"
|
||||||
|
|
||||||
|
#include <sodium.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Local constants tied to the libsodium primitive. (The dev-only chat_fixture_tooling.h
|
||||||
|
// declares equivalents, but that header is not linked into the app.)
|
||||||
|
constexpr std::size_t kStreamHeaderBytes = 24; // crypto_secretstream_xchacha20poly1305_HEADERBYTES
|
||||||
|
constexpr std::size_t kStreamABytes = 17; // crypto_secretstream_xchacha20poly1305_ABYTES
|
||||||
|
|
||||||
|
static_assert(kChatKeyBytes == crypto_kx_PUBLICKEYBYTES, "kx public key size mismatch");
|
||||||
|
static_assert(kChatKeyBytes == crypto_kx_SECRETKEYBYTES, "kx secret key size mismatch");
|
||||||
|
|
||||||
|
// Decode exactly outLen bytes from a lowercase/uppercase hex string; reject any other length.
|
||||||
|
bool hexToFixed(const std::string& hex, unsigned char* out, std::size_t outLen) {
|
||||||
|
if (hex.size() != outLen * 2) return false;
|
||||||
|
std::size_t binLen = 0;
|
||||||
|
if (sodium_hex2bin(out, outLen, hex.data(), hex.size(), nullptr, &binLen, nullptr) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return binLen == outLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode a variable-length hex string into bytes.
|
||||||
|
bool hexToBytes(const std::string& hex, std::vector<unsigned char>& out) {
|
||||||
|
if (hex.empty() || (hex.size() % 2) != 0) return false;
|
||||||
|
out.resize(hex.size() / 2);
|
||||||
|
std::size_t binLen = 0;
|
||||||
|
if (sodium_hex2bin(out.data(), out.size(), hex.data(), hex.size(), nullptr, &binLen, nullptr) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.resize(binLen);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string bytesToHex(const unsigned char* bytes, std::size_t n) {
|
||||||
|
std::string hex(n * 2 + 1, '\0');
|
||||||
|
sodium_bin2hex(&hex[0], hex.size(), bytes, n);
|
||||||
|
hex.resize(n * 2); // drop the NUL sodium_bin2hex appends
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const char* chatCryptoStatusName(ChatCryptoStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case ChatCryptoStatus::Ok: return "Ok";
|
||||||
|
case ChatCryptoStatus::SodiumInitFailed: return "SodiumInitFailed";
|
||||||
|
case ChatCryptoStatus::BadPeerKey: return "BadPeerKey";
|
||||||
|
case ChatCryptoStatus::BadHeaderHex: return "BadHeaderHex";
|
||||||
|
case ChatCryptoStatus::BadCiphertextHex: return "BadCiphertextHex";
|
||||||
|
case ChatCryptoStatus::CiphertextTooShort: return "CiphertextTooShort";
|
||||||
|
case ChatCryptoStatus::SessionKeyFailed: return "SessionKeyFailed";
|
||||||
|
case ChatCryptoStatus::EncryptFailed: return "EncryptFailed";
|
||||||
|
case ChatCryptoStatus::DecryptFailed: return "DecryptFailed";
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
void wipeChatKeyPair(ChatKeyPair& keys) {
|
||||||
|
sodium_memzero(keys.public_key.data(), keys.public_key.size());
|
||||||
|
sodium_memzero(keys.secret_key.data(), keys.secret_key.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatCryptoStatus encryptOutgoing(const ChatKeyPair& mine,
|
||||||
|
const std::string& peerPublicKeyHex,
|
||||||
|
const std::string& plaintext,
|
||||||
|
std::string& outStreamHeaderHex,
|
||||||
|
std::string& outCiphertextHex) {
|
||||||
|
static_assert(kStreamHeaderBytes == crypto_secretstream_xchacha20poly1305_HEADERBYTES, "");
|
||||||
|
static_assert(kStreamABytes == crypto_secretstream_xchacha20poly1305_ABYTES, "");
|
||||||
|
|
||||||
|
if (sodium_init() < 0) return ChatCryptoStatus::SodiumInitFailed;
|
||||||
|
|
||||||
|
unsigned char peerPk[crypto_kx_PUBLICKEYBYTES];
|
||||||
|
if (!hexToFixed(peerPublicKeyHex, peerPk, sizeof peerPk)) return ChatCryptoStatus::BadPeerKey;
|
||||||
|
|
||||||
|
unsigned char rx[crypto_kx_SESSIONKEYBYTES];
|
||||||
|
unsigned char tx[crypto_kx_SESSIONKEYBYTES];
|
||||||
|
if (crypto_kx_server_session_keys(rx, tx, mine.public_key.data(), mine.secret_key.data(), peerPk) != 0) {
|
||||||
|
sodium_memzero(rx, sizeof rx);
|
||||||
|
sodium_memzero(tx, sizeof tx);
|
||||||
|
return ChatCryptoStatus::SessionKeyFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
crypto_secretstream_xchacha20poly1305_state state;
|
||||||
|
unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES];
|
||||||
|
ChatCryptoStatus result = ChatCryptoStatus::EncryptFailed;
|
||||||
|
|
||||||
|
if (crypto_secretstream_xchacha20poly1305_init_push(&state, header, tx) == 0) {
|
||||||
|
std::vector<unsigned char> ciphertext(plaintext.size() + crypto_secretstream_xchacha20poly1305_ABYTES);
|
||||||
|
unsigned long long ctLen = 0;
|
||||||
|
// Only report Ok if the push actually succeeded — otherwise ctLen stays 0 and we would
|
||||||
|
// ship a valid header with an empty ciphertext.
|
||||||
|
if (crypto_secretstream_xchacha20poly1305_push(
|
||||||
|
&state, ciphertext.data(), &ctLen,
|
||||||
|
reinterpret_cast<const unsigned char*>(plaintext.data()), plaintext.size(),
|
||||||
|
nullptr, 0, crypto_secretstream_xchacha20poly1305_TAG_FINAL) == 0) {
|
||||||
|
outStreamHeaderHex = bytesToHex(header, sizeof header);
|
||||||
|
outCiphertextHex = bytesToHex(ciphertext.data(), static_cast<std::size_t>(ctLen));
|
||||||
|
result = ChatCryptoStatus::Ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sodium_memzero(rx, sizeof rx);
|
||||||
|
sodium_memzero(tx, sizeof tx);
|
||||||
|
sodium_memzero(&state, sizeof state);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatCryptoStatus decryptIncoming(const ChatKeyPair& mine,
|
||||||
|
const std::string& peerPublicKeyHex,
|
||||||
|
const std::string& streamHeaderHex,
|
||||||
|
const std::string& ciphertextHex,
|
||||||
|
std::string& outPlaintext) {
|
||||||
|
if (sodium_init() < 0) return ChatCryptoStatus::SodiumInitFailed;
|
||||||
|
|
||||||
|
unsigned char peerPk[crypto_kx_PUBLICKEYBYTES];
|
||||||
|
if (!hexToFixed(peerPublicKeyHex, peerPk, sizeof peerPk)) return ChatCryptoStatus::BadPeerKey;
|
||||||
|
|
||||||
|
unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES];
|
||||||
|
if (!hexToFixed(streamHeaderHex, header, sizeof header)) return ChatCryptoStatus::BadHeaderHex;
|
||||||
|
|
||||||
|
std::vector<unsigned char> ciphertext;
|
||||||
|
if (!hexToBytes(ciphertextHex, ciphertext)) return ChatCryptoStatus::BadCiphertextHex;
|
||||||
|
// Guard the size_t subtraction below (a ciphertext shorter than the auth tag can't be
|
||||||
|
// authentic). Exactly ABYTES is the valid empty-plaintext case, so it round-trips
|
||||||
|
// symmetrically with encryptOutgoing (message-content policy belongs to the caller).
|
||||||
|
if (ciphertext.size() < crypto_secretstream_xchacha20poly1305_ABYTES) {
|
||||||
|
return ChatCryptoStatus::CiphertextTooShort;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char rx[crypto_kx_SESSIONKEYBYTES];
|
||||||
|
unsigned char tx[crypto_kx_SESSIONKEYBYTES];
|
||||||
|
if (crypto_kx_client_session_keys(rx, tx, mine.public_key.data(), mine.secret_key.data(), peerPk) != 0) {
|
||||||
|
sodium_memzero(rx, sizeof rx);
|
||||||
|
sodium_memzero(tx, sizeof tx);
|
||||||
|
return ChatCryptoStatus::SessionKeyFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
crypto_secretstream_xchacha20poly1305_state state;
|
||||||
|
ChatCryptoStatus result = ChatCryptoStatus::DecryptFailed;
|
||||||
|
|
||||||
|
if (crypto_secretstream_xchacha20poly1305_init_pull(&state, header, rx) == 0) {
|
||||||
|
std::vector<unsigned char> plain(ciphertext.size() - crypto_secretstream_xchacha20poly1305_ABYTES);
|
||||||
|
unsigned long long plainLen = 0;
|
||||||
|
unsigned char tag = 0;
|
||||||
|
if (crypto_secretstream_xchacha20poly1305_pull(
|
||||||
|
&state, plain.data(), &plainLen, &tag,
|
||||||
|
ciphertext.data(), ciphertext.size(), nullptr, 0) == 0 &&
|
||||||
|
tag == crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
|
||||||
|
outPlaintext.assign(reinterpret_cast<const char*>(plain.data()),
|
||||||
|
static_cast<std::size_t>(plainLen));
|
||||||
|
result = ChatCryptoStatus::Ok;
|
||||||
|
}
|
||||||
|
sodium_memzero(plain.data(), plain.size()); // wipe the decrypted scratch
|
||||||
|
}
|
||||||
|
|
||||||
|
sodium_memzero(rx, sizeof rx);
|
||||||
|
sodium_memzero(tx, sizeof tx);
|
||||||
|
sodium_memzero(&state, sizeof state);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
65
src/chat/chat_crypto.h
Normal file
65
src/chat/chat_crypto.h
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// DragonX Wallet - HushChat crypto primitives.
|
||||||
|
//
|
||||||
|
// crypto_kx (X25519) session-key agreement + crypto_secretstream_xchacha20poly1305
|
||||||
|
// message encryption, byte-exact per the HushChat wire format so DragonX interoperates
|
||||||
|
// with SilentDragonXLite. See docs/_archive/contacts-chat-phase1-detail-2026-07-05.md
|
||||||
|
// (Appendix A.3/A.4). Pure crypto — no gating, no I/O; the feature gate lives at the
|
||||||
|
// service layer. NEVER logs plaintext, ciphertext, keys, or session material.
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
// crypto_kx key sizes (== crypto_kx_PUBLICKEYBYTES / SECRETKEYBYTES == 32).
|
||||||
|
constexpr std::size_t kChatKeyBytes = 32;
|
||||||
|
|
||||||
|
using ChatPublicKey = std::array<unsigned char, kChatKeyBytes>;
|
||||||
|
using ChatSecretKey = std::array<unsigned char, kChatKeyBytes>;
|
||||||
|
|
||||||
|
struct ChatKeyPair {
|
||||||
|
ChatPublicKey public_key{};
|
||||||
|
ChatSecretKey secret_key{};
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ChatCryptoStatus {
|
||||||
|
Ok,
|
||||||
|
SodiumInitFailed,
|
||||||
|
BadPeerKey, // peer public-key hex missing / wrong length / not hex
|
||||||
|
BadHeaderHex, // secretstream header hex missing / wrong length / not hex
|
||||||
|
BadCiphertextHex, // ciphertext hex malformed
|
||||||
|
CiphertextTooShort, // ciphertext shorter than the auth tag
|
||||||
|
SessionKeyFailed, // crypto_kx_*_session_keys rejected the peer key
|
||||||
|
EncryptFailed,
|
||||||
|
DecryptFailed // init_pull / pull / tag mismatch — the single neutral auth failure
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* chatCryptoStatusName(ChatCryptoStatus status);
|
||||||
|
|
||||||
|
// Encrypt `plaintext` addressed to peer `peerPublicKeyHex` (64 lowercase hex chars).
|
||||||
|
// Sender takes the crypto_kx "server" role (server_tx), matching SDXL's send path.
|
||||||
|
// Outputs the secretstream header hex (the memo "e" field) and the ciphertext hex
|
||||||
|
// (the payload memo). Returns Ok on success.
|
||||||
|
ChatCryptoStatus encryptOutgoing(const ChatKeyPair& mine,
|
||||||
|
const std::string& peerPublicKeyHex,
|
||||||
|
const std::string& plaintext,
|
||||||
|
std::string& outStreamHeaderHex,
|
||||||
|
std::string& outCiphertextHex);
|
||||||
|
|
||||||
|
// Decrypt an incoming message addressed to us from peer `peerPublicKeyHex`.
|
||||||
|
// Receiver takes the crypto_kx "client" role (client_rx), matching SDXL's receive path.
|
||||||
|
// Requires the memo "e" (streamHeaderHex) and the payload ciphertext hex. Enforces the
|
||||||
|
// Poly1305 auth tag AND that the stream tag is TAG_FINAL. Returns Ok + fills outPlaintext.
|
||||||
|
ChatCryptoStatus decryptIncoming(const ChatKeyPair& mine,
|
||||||
|
const std::string& peerPublicKeyHex,
|
||||||
|
const std::string& streamHeaderHex,
|
||||||
|
const std::string& ciphertextHex,
|
||||||
|
std::string& outPlaintext);
|
||||||
|
|
||||||
|
// Zero both key arrays (call when discarding an identity's keys).
|
||||||
|
void wipeChatKeyPair(ChatKeyPair& keys);
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
330
src/chat/chat_database.cpp
Normal file
330
src/chat/chat_database.cpp
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
// DragonX Wallet - HushChat persistent message store (implementation).
|
||||||
|
|
||||||
|
#include "chat_database.h"
|
||||||
|
|
||||||
|
#include "../util/logger.h"
|
||||||
|
#include "../util/platform.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <sodium.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Domain-separated KDF contexts (used as the keyed-BLAKE2b key, like chat_identity). Both lengths
|
||||||
|
// sit inside crypto_generichash's key-length bounds. Bumping a context rotates that derivation.
|
||||||
|
constexpr char kStorageKeyContext[] = "DragonX-HushChat-Storage-v1";
|
||||||
|
constexpr char kWalletTagContext[] = "DragonX-HushChat-WalletId-v1";
|
||||||
|
constexpr std::size_t kStorageKeyContextLen = sizeof(kStorageKeyContext) - 1;
|
||||||
|
constexpr std::size_t kWalletTagContextLen = sizeof(kWalletTagContext) - 1;
|
||||||
|
|
||||||
|
std::string toHex(const unsigned char* data, std::size_t len)
|
||||||
|
{
|
||||||
|
static const char* kHex = "0123456789abcdef";
|
||||||
|
std::string out;
|
||||||
|
out.reserve(len * 2);
|
||||||
|
for (std::size_t i = 0; i < len; ++i) {
|
||||||
|
out.push_back(kHex[data[i] >> 4]);
|
||||||
|
out.push_back(kHex[data[i] & 0x0F]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyed-BLAKE2b: out = generichash(in=secret, key=context). Deterministic, so the same seed always
|
||||||
|
// derives the same storage key + wallet tag across sessions.
|
||||||
|
bool deriveKeyed(const std::string& secret, const char* context, std::size_t contextLen,
|
||||||
|
unsigned char* out, std::size_t outLen)
|
||||||
|
{
|
||||||
|
return crypto_generichash(out, outLen,
|
||||||
|
reinterpret_cast<const unsigned char*>(secret.data()), secret.size(),
|
||||||
|
reinterpret_cast<const unsigned char*>(context), contextLen) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string associatedData(const std::string& walletTag)
|
||||||
|
{
|
||||||
|
return std::string("obsidian-dragon-hushchat-v1:") + walletTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ChatDatabase::ChatDatabase() : database_path_(defaultDatabasePath()) {}
|
||||||
|
ChatDatabase::ChatDatabase(std::string databasePath) : database_path_(std::move(databasePath)) {}
|
||||||
|
|
||||||
|
ChatDatabase::~ChatDatabase()
|
||||||
|
{
|
||||||
|
lock();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ChatDatabase::defaultDatabasePath()
|
||||||
|
{
|
||||||
|
return (fs::path(util::Platform::getConfigDir()) / "chat_messages.sqlite").string();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDatabase::unlockWithSecret(const std::string& secret)
|
||||||
|
{
|
||||||
|
if (sodium_init() < 0) return false;
|
||||||
|
|
||||||
|
if (!deriveKeyed(secret, kStorageKeyContext, kStorageKeyContextLen, key_.data(), key_.size()))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
unsigned char tag[32];
|
||||||
|
if (!deriveKeyed(secret, kWalletTagContext, kWalletTagContextLen, tag, sizeof(tag))) {
|
||||||
|
sodium_memzero(key_.data(), key_.size());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
wallet_tag_ = toHex(tag, sizeof(tag));
|
||||||
|
sodium_memzero(tag, sizeof(tag));
|
||||||
|
key_ready_ = true;
|
||||||
|
|
||||||
|
if (!ensureOpen()) {
|
||||||
|
lock();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDatabase::lock()
|
||||||
|
{
|
||||||
|
sodium_memzero(key_.data(), key_.size());
|
||||||
|
key_ready_ = false;
|
||||||
|
wallet_tag_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDatabase::append(const ChatMessage& message)
|
||||||
|
{
|
||||||
|
if (!key_ready_ || !ensureOpen()) return false;
|
||||||
|
|
||||||
|
std::vector<unsigned char> nonce;
|
||||||
|
std::vector<unsigned char> cipher;
|
||||||
|
std::string plain = serialize(message); // full plaintext (decrypted body + metadata)
|
||||||
|
const bool encrypted = encrypt(plain, nonce, cipher);
|
||||||
|
if (!plain.empty()) sodium_memzero(&plain[0], plain.size()); // don't leave it on the heap
|
||||||
|
if (!encrypted) return false;
|
||||||
|
|
||||||
|
const std::string dedup = dedupHash(message.txid, message.payload_position);
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt = nullptr;
|
||||||
|
if (sqlite3_prepare_v2(db_,
|
||||||
|
"INSERT OR IGNORE INTO chat_messages (wallet_tag, dedup_hash, nonce, payload) "
|
||||||
|
"VALUES (?, ?, ?, ?)",
|
||||||
|
-1, &stmt, nullptr) != SQLITE_OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_text(stmt, 2, dedup.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_blob(stmt, 3, nonce.data(), static_cast<int>(nonce.size()), SQLITE_TRANSIENT);
|
||||||
|
sqlite3_bind_blob(stmt, 4, cipher.data(), static_cast<int>(cipher.size()), SQLITE_TRANSIENT);
|
||||||
|
const bool done = sqlite3_step(stmt) == SQLITE_DONE;
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
if (!done) return false;
|
||||||
|
return sqlite3_changes(db_) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ChatMessage> ChatDatabase::load()
|
||||||
|
{
|
||||||
|
std::vector<ChatMessage> out;
|
||||||
|
if (!key_ready_ || !ensureOpen()) return out;
|
||||||
|
|
||||||
|
sqlite3_stmt* stmt = nullptr;
|
||||||
|
if (sqlite3_prepare_v2(db_,
|
||||||
|
"SELECT nonce, payload FROM chat_messages WHERE wallet_tag = ? ORDER BY rowid",
|
||||||
|
-1, &stmt, nullptr) != SQLITE_OK) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
const auto* noncePtr = static_cast<const unsigned char*>(sqlite3_column_blob(stmt, 0));
|
||||||
|
const int nonceLen = sqlite3_column_bytes(stmt, 0);
|
||||||
|
const auto* cipherPtr = static_cast<const unsigned char*>(sqlite3_column_blob(stmt, 1));
|
||||||
|
const int cipherLen = sqlite3_column_bytes(stmt, 1);
|
||||||
|
if (!noncePtr || !cipherPtr) continue;
|
||||||
|
|
||||||
|
std::vector<unsigned char> nonce(noncePtr, noncePtr + nonceLen);
|
||||||
|
std::vector<unsigned char> cipher(cipherPtr, cipherPtr + cipherLen);
|
||||||
|
std::string plain;
|
||||||
|
if (!decrypt(nonce, cipher, plain)) continue; // wrong wallet / tampered — skip
|
||||||
|
|
||||||
|
ChatMessage message;
|
||||||
|
if (deserialize(plain, message)) out.push_back(std::move(message));
|
||||||
|
sodium_memzero(&plain[0], plain.size());
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDatabase::clearWallet()
|
||||||
|
{
|
||||||
|
if (wallet_tag_.empty() || !ensureOpen()) return;
|
||||||
|
sqlite3_stmt* stmt = nullptr;
|
||||||
|
if (sqlite3_prepare_v2(db_, "DELETE FROM chat_messages WHERE wallet_tag = ?", -1, &stmt, nullptr)
|
||||||
|
!= SQLITE_OK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sqlite3_bind_text(stmt, 1, wallet_tag_.c_str(), -1, SQLITE_TRANSIENT);
|
||||||
|
sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDatabase::ensureOpen()
|
||||||
|
{
|
||||||
|
if (db_) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs::path path(database_path_);
|
||||||
|
if (!path.parent_path().empty()) fs::create_directories(path.parent_path());
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
DEBUG_LOGF("Failed to create chat database directory: %s\n", exception.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3* openedDb = nullptr;
|
||||||
|
if (sqlite3_open(database_path_.c_str(), &openedDb) != SQLITE_OK) {
|
||||||
|
DEBUG_LOGF("Failed to open chat database: %s\n",
|
||||||
|
openedDb ? sqlite3_errmsg(openedDb) : "unknown error");
|
||||||
|
if (openedDb) sqlite3_close(openedDb);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
db_ = openedDb;
|
||||||
|
sqlite3_busy_timeout(db_, 2000);
|
||||||
|
exec("PRAGMA journal_mode=WAL");
|
||||||
|
exec("PRAGMA synchronous=NORMAL");
|
||||||
|
|
||||||
|
if (!createSchema()) {
|
||||||
|
close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDatabase::exec(const char* sql)
|
||||||
|
{
|
||||||
|
if (!db_) return false;
|
||||||
|
char* error = nullptr;
|
||||||
|
if (sqlite3_exec(db_, sql, nullptr, nullptr, &error) != SQLITE_OK) {
|
||||||
|
DEBUG_LOGF("Chat database SQL error: %s\n", error ? error : sqlite3_errmsg(db_));
|
||||||
|
if (error) sqlite3_free(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDatabase::createSchema()
|
||||||
|
{
|
||||||
|
return exec("CREATE TABLE IF NOT EXISTS chat_messages ("
|
||||||
|
"wallet_tag TEXT NOT NULL, "
|
||||||
|
"dedup_hash TEXT NOT NULL, "
|
||||||
|
"nonce BLOB NOT NULL, "
|
||||||
|
"payload BLOB NOT NULL, "
|
||||||
|
"PRIMARY KEY (wallet_tag, dedup_hash))");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ChatDatabase::dedupHash(const std::string& txid, std::size_t position) const
|
||||||
|
{
|
||||||
|
const std::string input = txid + ":" + std::to_string(position);
|
||||||
|
unsigned char hash[32];
|
||||||
|
crypto_generichash(hash, sizeof(hash),
|
||||||
|
reinterpret_cast<const unsigned char*>(input.data()), input.size(),
|
||||||
|
key_.data(), key_.size()); // keyed by the storage key → txid stays private
|
||||||
|
return toHex(hash, sizeof(hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ChatDatabase::serialize(const ChatMessage& message) const
|
||||||
|
{
|
||||||
|
nlohmann::json json;
|
||||||
|
json["d"] = static_cast<int>(message.direction);
|
||||||
|
json["k"] = static_cast<int>(message.kind);
|
||||||
|
json["txid"] = message.txid;
|
||||||
|
json["cid"] = message.conversation_id;
|
||||||
|
json["z"] = message.peer_zaddr;
|
||||||
|
json["p"] = message.peer_public_key_hex;
|
||||||
|
json["b"] = message.body;
|
||||||
|
json["ts"] = message.timestamp;
|
||||||
|
json["pos"] = static_cast<std::uint64_t>(message.payload_position);
|
||||||
|
json["dl"] = static_cast<int>(message.delivery);
|
||||||
|
return json.dump();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDatabase::deserialize(const std::string& json, ChatMessage& out) const
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
const auto parsed = nlohmann::json::parse(json);
|
||||||
|
out.direction = static_cast<ChatDirection>(parsed.value("d", 0));
|
||||||
|
out.kind = static_cast<ChatMessageKind>(parsed.value("k", 0));
|
||||||
|
out.txid = parsed.value("txid", std::string());
|
||||||
|
out.conversation_id = parsed.value("cid", std::string());
|
||||||
|
out.peer_zaddr = parsed.value("z", std::string());
|
||||||
|
out.peer_public_key_hex = parsed.value("p", std::string());
|
||||||
|
out.body = parsed.value("b", std::string());
|
||||||
|
out.timestamp = parsed.value("ts", static_cast<std::int64_t>(0));
|
||||||
|
out.payload_position = static_cast<std::size_t>(parsed.value("pos", static_cast<std::uint64_t>(0)));
|
||||||
|
out.delivery = static_cast<ChatDelivery>(parsed.value("dl", 0)); // old rows → Sent
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDatabase::encrypt(const std::string& plain,
|
||||||
|
std::vector<unsigned char>& nonce,
|
||||||
|
std::vector<unsigned char>& cipher) const
|
||||||
|
{
|
||||||
|
if (!key_ready_) return false;
|
||||||
|
nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
||||||
|
randombytes_buf(nonce.data(), nonce.size());
|
||||||
|
|
||||||
|
const std::string ad = associatedData(wallet_tag_);
|
||||||
|
cipher.resize(plain.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES);
|
||||||
|
unsigned long long cipherLen = 0;
|
||||||
|
if (crypto_aead_xchacha20poly1305_ietf_encrypt(
|
||||||
|
cipher.data(), &cipherLen,
|
||||||
|
reinterpret_cast<const unsigned char*>(plain.data()), plain.size(),
|
||||||
|
reinterpret_cast<const unsigned char*>(ad.data()), ad.size(),
|
||||||
|
nullptr, nonce.data(), key_.data()) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cipher.resize(static_cast<std::size_t>(cipherLen));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatDatabase::decrypt(const std::vector<unsigned char>& nonce,
|
||||||
|
const std::vector<unsigned char>& cipher,
|
||||||
|
std::string& plain) const
|
||||||
|
{
|
||||||
|
if (!key_ready_) return false;
|
||||||
|
if (nonce.size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES) return false;
|
||||||
|
if (cipher.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) return false;
|
||||||
|
|
||||||
|
const std::string ad = associatedData(wallet_tag_);
|
||||||
|
std::vector<unsigned char> out(cipher.size());
|
||||||
|
unsigned long long outLen = 0;
|
||||||
|
if (crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||||
|
out.data(), &outLen, nullptr,
|
||||||
|
cipher.data(), cipher.size(),
|
||||||
|
reinterpret_cast<const unsigned char*>(ad.data()), ad.size(),
|
||||||
|
nonce.data(), key_.data()) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
plain.assign(reinterpret_cast<const char*>(out.data()), static_cast<std::size_t>(outLen));
|
||||||
|
sodium_memzero(out.data(), out.size());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatDatabase::close()
|
||||||
|
{
|
||||||
|
if (db_) {
|
||||||
|
sqlite3_close(db_);
|
||||||
|
db_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
74
src/chat/chat_database.h
Normal file
74
src/chat/chat_database.h
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// DragonX Wallet - HushChat persistent message store (Phase 2).
|
||||||
|
//
|
||||||
|
// Sqlite-backed, encrypted at rest with a SEED-DERIVED key (no wallet passphrase). Every record —
|
||||||
|
// message bodies, peer z-addresses, threading (conversation id), and timestamps — is AEAD-encrypted
|
||||||
|
// under a key derived from the wallet's own seed secret (the same secret used for the chat
|
||||||
|
// identity), and even the per-message dedup key is a KEYED hash of the txid — so the database
|
||||||
|
// reveals nothing about your conversations to disk-level access without the seed. Rows are
|
||||||
|
// partitioned by a seed-derived wallet tag so one file can hold several wallets, each readable only
|
||||||
|
// with its own seed. Not thread-safe — drive from the main thread. Mirrors the lifecycle of
|
||||||
|
// data::TransactionHistoryCache.
|
||||||
|
|
||||||
|
#include "chat_message.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct sqlite3;
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
class ChatDatabase {
|
||||||
|
public:
|
||||||
|
ChatDatabase();
|
||||||
|
explicit ChatDatabase(std::string databasePath);
|
||||||
|
~ChatDatabase();
|
||||||
|
|
||||||
|
ChatDatabase(const ChatDatabase&) = delete;
|
||||||
|
ChatDatabase& operator=(const ChatDatabase&) = delete;
|
||||||
|
|
||||||
|
static std::string defaultDatabasePath();
|
||||||
|
|
||||||
|
// Derive the storage key + wallet tag from the wallet's seed secret and open the DB. The caller
|
||||||
|
// still owns and must wipe `secret`. Returns false on sodium/db failure (DB then stays locked).
|
||||||
|
bool unlockWithSecret(const std::string& secret);
|
||||||
|
void lock(); // wipe the key material (DB handle stays open); load()/append() then no-op
|
||||||
|
bool hasKey() const { return key_ready_; }
|
||||||
|
|
||||||
|
// Persist one message (INSERT OR IGNORE, deduped by a keyed hash of txid+payload_position).
|
||||||
|
// Returns true if newly inserted; false on duplicate or while locked.
|
||||||
|
bool append(const ChatMessage& message);
|
||||||
|
|
||||||
|
// Decrypt and return every stored message for the unlocked wallet, in insertion order. Empty
|
||||||
|
// while locked or if none. Rows that fail to decrypt/parse are skipped.
|
||||||
|
std::vector<ChatMessage> load();
|
||||||
|
|
||||||
|
void clearWallet(); // delete the unlocked wallet's rows
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool ensureOpen();
|
||||||
|
bool exec(const char* sql);
|
||||||
|
bool createSchema();
|
||||||
|
std::string dedupHash(const std::string& txid, std::size_t position) const;
|
||||||
|
std::string serialize(const ChatMessage& message) const;
|
||||||
|
bool deserialize(const std::string& json, ChatMessage& out) const;
|
||||||
|
bool encrypt(const std::string& plain,
|
||||||
|
std::vector<unsigned char>& nonce,
|
||||||
|
std::vector<unsigned char>& cipher) const;
|
||||||
|
bool decrypt(const std::vector<unsigned char>& nonce,
|
||||||
|
const std::vector<unsigned char>& cipher,
|
||||||
|
std::string& plain) const;
|
||||||
|
void close();
|
||||||
|
|
||||||
|
sqlite3* db_ = nullptr;
|
||||||
|
std::string database_path_;
|
||||||
|
std::array<unsigned char, 32> key_{}; // AEAD storage key (seed-derived)
|
||||||
|
std::string wallet_tag_; // seed-derived row partition (a keyed hash, hex)
|
||||||
|
bool key_ready_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
1594
src/chat/chat_fixture_tooling.cpp
Normal file
1594
src/chat/chat_fixture_tooling.cpp
Normal file
File diff suppressed because it is too large
Load Diff
505
src/chat/chat_fixture_tooling.h
Normal file
505
src/chat/chat_fixture_tooling.h
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "chat_protocol.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// HushChat compatibility-fixture / capture-manifest / seed-projection tooling.
|
||||||
|
//
|
||||||
|
// Everything in this header is DEAD AT RUNTIME in the shipping app and test
|
||||||
|
// binaries — its only caller is the standalone dev CLI
|
||||||
|
// tools/hushchat_fixture_check.cpp. It is deliberately compiled ONLY into the
|
||||||
|
// HushChatFixtureCheck target so this validation scaffolding (and its libsodium
|
||||||
|
// seed-projection path) never reaches the wallet binary. Keep it that way: do
|
||||||
|
// not add chat_fixture_tooling.cpp to APP_SOURCES or the test target.
|
||||||
|
//
|
||||||
|
// It builds on the runtime types declared in chat_protocol.h.
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
enum class HushChatDecryptPreflightError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
NonMessageHeader,
|
||||||
|
InvalidHeaderNumber,
|
||||||
|
UnsupportedVersion,
|
||||||
|
MissingReplyAddress,
|
||||||
|
MissingConversationId,
|
||||||
|
InvalidSecretstreamHeader,
|
||||||
|
InvalidPublicKey,
|
||||||
|
EmptyCiphertext,
|
||||||
|
OversizedCiphertext,
|
||||||
|
OddLengthCiphertext,
|
||||||
|
InvalidCiphertextHex,
|
||||||
|
TruncatedCiphertext
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatDecryptPreflightInput {
|
||||||
|
HushChatHeader header;
|
||||||
|
std::string ciphertext_hex;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatDecryptPreflightResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
HushChatDecryptPreflightError error = HushChatDecryptPreflightError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
std::size_t ciphertext_size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatHexDecodeError {
|
||||||
|
None,
|
||||||
|
Empty,
|
||||||
|
OddLength,
|
||||||
|
InvalidHex,
|
||||||
|
UnexpectedByteLength
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatHexDecodeResult {
|
||||||
|
bool ok = false;
|
||||||
|
HushChatHexDecodeError error = HushChatHexDecodeError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
std::vector<unsigned char> bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatDecryptDirection {
|
||||||
|
Incoming,
|
||||||
|
Outgoing
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatSessionKeySelection {
|
||||||
|
ClientRx,
|
||||||
|
ServerTx
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatDecryptInputError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
InvalidStoredChatKey,
|
||||||
|
DecryptPreflightFailed,
|
||||||
|
InvalidPeerPublicKey,
|
||||||
|
InvalidStreamHeader,
|
||||||
|
InvalidCiphertext
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatDecryptInputMaterial {
|
||||||
|
std::string stored_chat_key_hex;
|
||||||
|
HushChatHeader header;
|
||||||
|
std::string ciphertext_hex;
|
||||||
|
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
|
||||||
|
std::string peer_public_key_hex;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatPreparedDecryptInput {
|
||||||
|
std::vector<unsigned char> stored_chat_key_bytes;
|
||||||
|
std::vector<unsigned char> seed_bytes;
|
||||||
|
std::vector<unsigned char> peer_public_key_bytes;
|
||||||
|
std::vector<unsigned char> stream_header_bytes;
|
||||||
|
std::vector<unsigned char> ciphertext_bytes;
|
||||||
|
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
|
||||||
|
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
|
||||||
|
std::size_t plaintext_capacity = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatDecryptInputPreparationResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
HushChatDecryptInputError error = HushChatDecryptInputError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
|
||||||
|
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
|
||||||
|
HushChatPreparedDecryptInput prepared;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatDecryptFixtureReadinessResult {
|
||||||
|
bool ready = false;
|
||||||
|
std::size_t stored_chat_key_size = 0;
|
||||||
|
std::size_t seed_size = 0;
|
||||||
|
std::size_t peer_public_key_size = 0;
|
||||||
|
std::size_t stream_header_size = 0;
|
||||||
|
std::size_t ciphertext_size = 0;
|
||||||
|
std::size_t plaintext_capacity = 0;
|
||||||
|
HushChatSessionKeySelection session_key_selection = HushChatSessionKeySelection::ClientRx;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCompatibilityFixtureError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
MissingFixtureId,
|
||||||
|
InvalidLocalPublicKey,
|
||||||
|
InvalidPeerPublicKey,
|
||||||
|
InvalidHeaderMemo,
|
||||||
|
InvalidMemoPair,
|
||||||
|
NonMemoHeader,
|
||||||
|
HeaderPublicKeyMismatch,
|
||||||
|
DecryptInputFailed,
|
||||||
|
NotFixtureReady,
|
||||||
|
ExpectedStoredChatKeyLengthMismatch,
|
||||||
|
ExpectedSeedLengthMismatch,
|
||||||
|
ExpectedLocalPublicKeyLengthMismatch,
|
||||||
|
ExpectedPeerPublicKeyLengthMismatch,
|
||||||
|
ExpectedStreamHeaderLengthMismatch,
|
||||||
|
ExpectedCiphertextLengthMismatch,
|
||||||
|
ExpectedPlaintextLengthMismatch,
|
||||||
|
ExpectedRoleMismatch,
|
||||||
|
InvalidPlaintextHash
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixture {
|
||||||
|
std::string fixture_id;
|
||||||
|
std::string stored_chat_key_hex;
|
||||||
|
std::string local_public_key_hex;
|
||||||
|
std::string peer_public_key_hex;
|
||||||
|
std::string header_memo;
|
||||||
|
std::string ciphertext_memo;
|
||||||
|
HushChatDecryptDirection direction = HushChatDecryptDirection::Incoming;
|
||||||
|
HushChatSessionKeySelection expected_session_key_selection = HushChatSessionKeySelection::ClientRx;
|
||||||
|
std::size_t expected_stored_chat_key_size = 32;
|
||||||
|
std::size_t expected_seed_size = 32;
|
||||||
|
std::size_t expected_local_public_key_size = 32;
|
||||||
|
std::size_t expected_peer_public_key_size = 32;
|
||||||
|
std::size_t expected_stream_header_size = 24;
|
||||||
|
std::size_t expected_ciphertext_size = 0;
|
||||||
|
std::size_t expected_plaintext_size = 0;
|
||||||
|
std::string expected_plaintext_hash_hex;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureVerificationResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
HushChatCompatibilityFixtureError error = HushChatCompatibilityFixtureError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
|
||||||
|
HushChatDecryptInputError decrypt_input_error = HushChatDecryptInputError::None;
|
||||||
|
HushChatDecryptPreflightError preflight_error = HushChatDecryptPreflightError::None;
|
||||||
|
HushChatHeader header;
|
||||||
|
HushChatDecryptInputPreparationResult preparation;
|
||||||
|
HushChatDecryptFixtureReadinessResult readiness;
|
||||||
|
std::size_t local_public_key_size = 0;
|
||||||
|
std::size_t peer_public_key_size = 0;
|
||||||
|
std::size_t plaintext_hash_size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCompatibilityFixtureKind {
|
||||||
|
IncomingMemo,
|
||||||
|
OutgoingMemo,
|
||||||
|
SeedPublicKeyProjection,
|
||||||
|
CorruptedAuthFailure,
|
||||||
|
ContactExclusion
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCompatibilityFixtureFileStatus {
|
||||||
|
Pending,
|
||||||
|
Ready
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCompatibilityFixtureFileError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
InvalidJson,
|
||||||
|
JsonNotObject,
|
||||||
|
InvalidSchema,
|
||||||
|
MissingKind,
|
||||||
|
UnknownKind,
|
||||||
|
MissingStatus,
|
||||||
|
UnknownStatus,
|
||||||
|
MissingFixtureId,
|
||||||
|
MissingPendingReason,
|
||||||
|
MissingFixtureObject,
|
||||||
|
InvalidFixtureField,
|
||||||
|
FixtureVerificationFailed,
|
||||||
|
ContactFixtureNotExcluded,
|
||||||
|
FileReadFailed
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureFile {
|
||||||
|
std::string schema;
|
||||||
|
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
HushChatCompatibilityFixtureFileStatus status = HushChatCompatibilityFixtureFileStatus::Pending;
|
||||||
|
std::string fixture_id;
|
||||||
|
std::string pending_reason;
|
||||||
|
HushChatCompatibilityFixture fixture;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureFileParseResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
bool pending = false;
|
||||||
|
bool verified = false;
|
||||||
|
bool excluded_from_decrypt = false;
|
||||||
|
HushChatCompatibilityFixtureFileError error = HushChatCompatibilityFixtureFileError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
HushChatCompatibilityFixtureFile file;
|
||||||
|
HushChatCompatibilityFixtureVerificationResult verification;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatSeedPublicKeyProjectionError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
MissingFixtureId,
|
||||||
|
InvalidStoredChatKey,
|
||||||
|
InvalidLocalPublicKey,
|
||||||
|
ExpectedStoredChatKeyLengthMismatch,
|
||||||
|
ExpectedSeedLengthMismatch,
|
||||||
|
ExpectedLocalPublicKeyLengthMismatch,
|
||||||
|
SodiumInitializationFailed,
|
||||||
|
KeypairProjectionFailed,
|
||||||
|
ProjectedPublicKeyMismatch
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatSeedPublicKeyProjectionResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
HushChatSeedPublicKeyProjectionError error = HushChatSeedPublicKeyProjectionError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
HushChatHexDecodeError hex_error = HushChatHexDecodeError::None;
|
||||||
|
std::size_t stored_chat_key_size = 0;
|
||||||
|
std::size_t seed_size = 0;
|
||||||
|
std::size_t local_public_key_size = 0;
|
||||||
|
std::size_t projected_public_key_size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCorruptedAuthFailureReadinessError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
FixturePending,
|
||||||
|
WrongFixtureKind,
|
||||||
|
FixtureNotVerified,
|
||||||
|
SeedProjectionNotVerified
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCorruptedAuthFailureReadinessResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
bool structurally_ready_for_future_auth_check = false;
|
||||||
|
bool requires_future_secretstream_auth_failure = false;
|
||||||
|
bool decrypted = false;
|
||||||
|
bool authenticated = false;
|
||||||
|
HushChatCorruptedAuthFailureReadinessError error = HushChatCorruptedAuthFailureReadinessError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCompatibilityFixtureImportError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
MissingRequiredKind,
|
||||||
|
DuplicateKind,
|
||||||
|
FixtureLoadFailed,
|
||||||
|
FixtureKindMismatch,
|
||||||
|
FixturePending,
|
||||||
|
FixtureInvalid,
|
||||||
|
FixtureNotVerified,
|
||||||
|
SeedProjectionFailed,
|
||||||
|
AuthFailureScaffoldFailed,
|
||||||
|
ContactFixtureNotExcluded
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureImportCandidate {
|
||||||
|
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureImportItem {
|
||||||
|
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
std::string path;
|
||||||
|
bool supplied = false;
|
||||||
|
bool pending = false;
|
||||||
|
bool replacement_eligible = false;
|
||||||
|
bool seed_projection_verified = false;
|
||||||
|
bool future_auth_failure_required = false;
|
||||||
|
bool structurally_ready_for_future_auth_check = false;
|
||||||
|
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
HushChatCompatibilityFixtureFileParseResult parsed;
|
||||||
|
HushChatSeedPublicKeyProjectionResult seed_projection;
|
||||||
|
HushChatCorruptedAuthFailureReadinessResult auth_failure_readiness;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureImportChecklistResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
bool replacement_ready = false;
|
||||||
|
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
std::size_t required_count = 0;
|
||||||
|
std::size_t supplied_count = 0;
|
||||||
|
std::size_t missing_count = 0;
|
||||||
|
std::size_t pending_count = 0;
|
||||||
|
std::size_t verified_count = 0;
|
||||||
|
std::size_t seed_projection_verified_count = 0;
|
||||||
|
std::size_t future_auth_failure_required_count = 0;
|
||||||
|
std::size_t auth_failure_structural_ready_count = 0;
|
||||||
|
std::size_t excluded_count = 0;
|
||||||
|
std::size_t rejected_count = 0;
|
||||||
|
std::vector<HushChatCompatibilityFixtureImportItem> items;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureReplacementReportItem {
|
||||||
|
HushChatCompatibilityFixtureKind expected_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
HushChatCompatibilityFixtureKind loaded_kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
std::string path;
|
||||||
|
bool supplied = false;
|
||||||
|
bool pending = false;
|
||||||
|
bool replacement_eligible = false;
|
||||||
|
bool refused = true;
|
||||||
|
bool seed_projection_verified = false;
|
||||||
|
bool future_auth_failure_required = false;
|
||||||
|
bool structurally_ready_for_future_auth_check = false;
|
||||||
|
bool cont_excluded = false;
|
||||||
|
bool decrypted = false;
|
||||||
|
bool authenticated = false;
|
||||||
|
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCompatibilityFixtureReplacementDryRunResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
bool dry_run_only = true;
|
||||||
|
bool redacted_report = true;
|
||||||
|
bool would_replace = false;
|
||||||
|
bool replacement_refused = true;
|
||||||
|
HushChatCompatibilityFixtureImportError error = HushChatCompatibilityFixtureImportError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
std::size_t required_count = 0;
|
||||||
|
std::size_t supplied_count = 0;
|
||||||
|
std::size_t missing_count = 0;
|
||||||
|
std::size_t pending_count = 0;
|
||||||
|
std::size_t verified_count = 0;
|
||||||
|
std::size_t seed_projection_verified_count = 0;
|
||||||
|
std::size_t future_auth_failure_required_count = 0;
|
||||||
|
std::size_t auth_failure_structural_ready_count = 0;
|
||||||
|
std::size_t excluded_count = 0;
|
||||||
|
std::size_t rejected_count = 0;
|
||||||
|
std::vector<HushChatCompatibilityFixtureReplacementReportItem> report_items;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCaptureManifestError {
|
||||||
|
None,
|
||||||
|
FeatureDisabled,
|
||||||
|
FileReadFailed,
|
||||||
|
InvalidJson,
|
||||||
|
JsonNotObject,
|
||||||
|
InvalidSchema,
|
||||||
|
MissingManifestId,
|
||||||
|
MissingStatus,
|
||||||
|
UnknownStatus,
|
||||||
|
MissingFixtureDirectory,
|
||||||
|
MissingDryRunCommand,
|
||||||
|
InvalidDryRunCommand,
|
||||||
|
MissingProvenance,
|
||||||
|
MissingSourceClient,
|
||||||
|
InvalidSourceClient,
|
||||||
|
MissingSourceClientVersion,
|
||||||
|
MissingCaptureDate,
|
||||||
|
MissingNetwork,
|
||||||
|
MissingCaptureMethod,
|
||||||
|
MissingHandling,
|
||||||
|
MissingHandlingFlag,
|
||||||
|
HandlingFlagNotTrue,
|
||||||
|
MissingCategories,
|
||||||
|
InvalidCategoryEntry,
|
||||||
|
UnknownCategory,
|
||||||
|
DuplicateCategory,
|
||||||
|
MissingRequiredCategory,
|
||||||
|
ProhibitedFieldPresent
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatCaptureManifestStatus {
|
||||||
|
Staged
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCaptureManifestCategoryReport {
|
||||||
|
HushChatCompatibilityFixtureKind kind = HushChatCompatibilityFixtureKind::IncomingMemo;
|
||||||
|
std::string staged_filename;
|
||||||
|
bool declared = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatCaptureManifestValidationResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool feature_enabled = false;
|
||||||
|
bool redacted_report = true;
|
||||||
|
bool validates_provenance_only = true;
|
||||||
|
bool no_sensitive_material_declared = false;
|
||||||
|
bool has_dry_run_command = false;
|
||||||
|
HushChatCaptureManifestError error = HushChatCaptureManifestError::None;
|
||||||
|
const char* error_name = "None";
|
||||||
|
HushChatCaptureManifestStatus status = HushChatCaptureManifestStatus::Staged;
|
||||||
|
std::string manifest_path;
|
||||||
|
std::string fixture_directory;
|
||||||
|
std::size_t required_count = 0;
|
||||||
|
std::size_t declared_count = 0;
|
||||||
|
std::size_t missing_count = 0;
|
||||||
|
std::size_t duplicate_count = 0;
|
||||||
|
std::size_t prohibited_field_count = 0;
|
||||||
|
std::size_t handling_flag_count = 0;
|
||||||
|
std::vector<HushChatCaptureManifestCategoryReport> categories;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr std::size_t kHushChatSecretstreamABytes = 17;
|
||||||
|
constexpr std::size_t kHushChatStoredChatKeyByteLength = 32;
|
||||||
|
constexpr std::size_t kHushChatStoredChatKeyHexLength = kHushChatStoredChatKeyByteLength * 2;
|
||||||
|
constexpr std::size_t kHushChatSeedByteLength = 32;
|
||||||
|
constexpr std::size_t kHushChatPublicKeyByteLength = kHushChatPublicKeyHexLength / 2;
|
||||||
|
constexpr std::size_t kHushChatSecretstreamHeaderByteLength = kHushChatSecretstreamHeaderHexLength / 2;
|
||||||
|
constexpr const char* kHushChatCompatibilityFixtureSchema = "dragonx.hushchat.compat-fixture.v1";
|
||||||
|
constexpr const char* kHushChatCaptureManifestSchema = "dragonx.hushchat.capture-manifest.v1";
|
||||||
|
|
||||||
|
HushChatDecryptPreflightResult validateHushChatMemoDecryptPreflight(
|
||||||
|
const HushChatDecryptPreflightInput& input,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatHexDecodeResult decodeHushChatHexBytes(const std::string& hex,
|
||||||
|
std::size_t expectedByteLength);
|
||||||
|
HushChatDecryptInputPreparationResult prepareHushChatDecryptInput(
|
||||||
|
const HushChatDecryptInputMaterial& material,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatDecryptFixtureReadinessResult inspectHushChatDecryptFixtureReadiness(
|
||||||
|
const HushChatPreparedDecryptInput& prepared);
|
||||||
|
HushChatSessionKeySelection hushChatSessionKeySelectionForDirection(HushChatDecryptDirection direction);
|
||||||
|
HushChatCompatibilityFixtureVerificationResult verifyHushChatCompatibilityFixture(
|
||||||
|
const HushChatCompatibilityFixture& fixture,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatCompatibilityFixtureFileParseResult parseHushChatCompatibilityFixtureFile(
|
||||||
|
const std::string& jsonText,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatCompatibilityFixtureFileParseResult loadHushChatCompatibilityFixtureFile(
|
||||||
|
const std::string& path,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatSeedPublicKeyProjectionResult verifyHushChatSeedPublicKeyProjection(
|
||||||
|
const HushChatCompatibilityFixture& fixture,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatCorruptedAuthFailureReadinessResult inspectHushChatCorruptedAuthFailureReadiness(
|
||||||
|
const HushChatCompatibilityFixtureFileParseResult& parsed,
|
||||||
|
const HushChatSeedPublicKeyProjectionResult& seedProjection,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
std::vector<HushChatCompatibilityFixtureKind> hushChatRequiredCompatibilityFixtureKinds();
|
||||||
|
HushChatCompatibilityFixtureImportChecklistResult inspectHushChatCompatibilityFixtureImportChecklist(
|
||||||
|
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatCompatibilityFixtureReplacementDryRunResult inspectHushChatCompatibilityFixtureReplacementDryRun(
|
||||||
|
const std::vector<HushChatCompatibilityFixtureImportCandidate>& candidates,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatCaptureManifestValidationResult validateHushChatCaptureManifest(
|
||||||
|
const std::string& jsonText,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
HushChatCaptureManifestValidationResult loadHushChatCaptureManifestFile(
|
||||||
|
const std::string& path,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
const char* hushChatDecryptPreflightErrorName(HushChatDecryptPreflightError error);
|
||||||
|
const char* hushChatHexDecodeErrorName(HushChatHexDecodeError error);
|
||||||
|
const char* hushChatDecryptDirectionName(HushChatDecryptDirection direction);
|
||||||
|
const char* hushChatSessionKeySelectionName(HushChatSessionKeySelection selection);
|
||||||
|
const char* hushChatDecryptInputErrorName(HushChatDecryptInputError error);
|
||||||
|
const char* hushChatCompatibilityFixtureErrorName(HushChatCompatibilityFixtureError error);
|
||||||
|
const char* hushChatCompatibilityFixtureKindName(HushChatCompatibilityFixtureKind kind);
|
||||||
|
const char* hushChatCompatibilityFixtureFileStatusName(HushChatCompatibilityFixtureFileStatus status);
|
||||||
|
const char* hushChatCompatibilityFixtureFileErrorName(HushChatCompatibilityFixtureFileError error);
|
||||||
|
const char* hushChatSeedPublicKeyProjectionErrorName(HushChatSeedPublicKeyProjectionError error);
|
||||||
|
const char* hushChatCorruptedAuthFailureReadinessErrorName(HushChatCorruptedAuthFailureReadinessError error);
|
||||||
|
const char* hushChatCompatibilityFixtureImportErrorName(HushChatCompatibilityFixtureImportError error);
|
||||||
|
const char* hushChatCaptureManifestErrorName(HushChatCaptureManifestError error);
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
70
src/chat/chat_identity.cpp
Normal file
70
src/chat/chat_identity.cpp
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// DragonX Wallet - HushChat identity derivation (implementation).
|
||||||
|
|
||||||
|
#include "chat_identity.h"
|
||||||
|
|
||||||
|
#include <sodium.h>
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
// The KDF context is used as the BLAKE2b key, so its length must sit within the primitive's
|
||||||
|
// key bounds.
|
||||||
|
static_assert(kChatIdentityKdfContextLen >= crypto_generichash_KEYBYTES_MIN,
|
||||||
|
"chat identity KDF context is shorter than BLAKE2b's minimum key length");
|
||||||
|
static_assert(kChatIdentityKdfContextLen <= crypto_generichash_KEYBYTES_MAX,
|
||||||
|
"chat identity KDF context is longer than BLAKE2b's maximum key length");
|
||||||
|
|
||||||
|
const char* chatIdentityStatusName(ChatIdentityStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case ChatIdentityStatus::Ready: return "Ready";
|
||||||
|
case ChatIdentityStatus::FeatureDisabled: return "FeatureDisabled";
|
||||||
|
case ChatIdentityStatus::SecretUnavailable: return "SecretUnavailable";
|
||||||
|
case ChatIdentityStatus::DerivationFailed: return "DerivationFailed";
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string chatIdentityPublicKeyHex(const ChatKeyPair& keys) {
|
||||||
|
char hex[crypto_kx_PUBLICKEYBYTES * 2 + 1];
|
||||||
|
sodium_bin2hex(hex, sizeof hex, keys.public_key.data(), keys.public_key.size());
|
||||||
|
return std::string(hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatIdentityResult deriveChatIdentityFromSecret(const std::string& stableSecret,
|
||||||
|
ChatKeyPair& outKeys,
|
||||||
|
bool featureEnabled) {
|
||||||
|
ChatIdentityResult result;
|
||||||
|
auto finish = [&result](ChatIdentityStatus status) -> ChatIdentityResult {
|
||||||
|
result.status = status;
|
||||||
|
result.error_name = chatIdentityStatusName(status);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!featureEnabled) return finish(ChatIdentityStatus::FeatureDisabled);
|
||||||
|
if (stableSecret.empty()) return finish(ChatIdentityStatus::SecretUnavailable);
|
||||||
|
if (sodium_init() < 0) return finish(ChatIdentityStatus::DerivationFailed);
|
||||||
|
|
||||||
|
unsigned char kxSeed[crypto_kx_SEEDBYTES];
|
||||||
|
static_assert(sizeof(kxSeed) == kChatKeyBytes, "kx seed size mismatch");
|
||||||
|
|
||||||
|
const int hashStatus = crypto_generichash(
|
||||||
|
kxSeed, sizeof kxSeed,
|
||||||
|
reinterpret_cast<const unsigned char*>(stableSecret.data()), stableSecret.size(),
|
||||||
|
reinterpret_cast<const unsigned char*>(kChatIdentityKdfContext), kChatIdentityKdfContextLen);
|
||||||
|
if (hashStatus != 0) {
|
||||||
|
sodium_memzero(kxSeed, sizeof kxSeed);
|
||||||
|
return finish(ChatIdentityStatus::DerivationFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int keypairStatus =
|
||||||
|
crypto_kx_seed_keypair(outKeys.public_key.data(), outKeys.secret_key.data(), kxSeed);
|
||||||
|
sodium_memzero(kxSeed, sizeof kxSeed); // wipe the seed immediately, success or failure
|
||||||
|
if (keypairStatus != 0) {
|
||||||
|
wipeChatKeyPair(outKeys);
|
||||||
|
return finish(ChatIdentityStatus::DerivationFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.public_key_hex = chatIdentityPublicKeyHex(outKeys);
|
||||||
|
return finish(ChatIdentityStatus::Ready);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
57
src/chat/chat_identity.h
Normal file
57
src/chat/chat_identity.h
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// DragonX Wallet - HushChat identity derivation.
|
||||||
|
//
|
||||||
|
// The DragonX-native chat identity is an X25519 (crypto_kx) keypair derived from a stable
|
||||||
|
// per-wallet secret via a domain-separated keyed BLAKE2b KDF. This deliberately does NOT
|
||||||
|
// use SDXL's UTF-8-hex-seed quirk (that quirk is only for the Phase-4 "import an existing
|
||||||
|
// SDXL identity" path). Interop is unaffected: identity derivation is local — peers only
|
||||||
|
// exchange public keys. See docs/_archive/contacts-chat-tab-plan-2026-07-05.md §5.6.
|
||||||
|
|
||||||
|
#include "chat_crypto.h" // ChatKeyPair
|
||||||
|
#include "chat_protocol.h" // hushChatFeatureEnabledAtBuild()
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
// Domain-separation label — used as the BLAKE2b key so a different app/version cannot
|
||||||
|
// derive the same identity from the same wallet secret. A char[] (not const char*) so its
|
||||||
|
// length is a compile-time constant for the KEYBYTES-bounds static_assert.
|
||||||
|
inline constexpr char kChatIdentityKdfContext[] = "DragonX-HushChat-Identity-v1";
|
||||||
|
inline constexpr std::size_t kChatIdentityKdfContextLen = sizeof(kChatIdentityKdfContext) - 1;
|
||||||
|
|
||||||
|
enum class ChatIdentityStatus {
|
||||||
|
Ready,
|
||||||
|
FeatureDisabled, // DRAGONX_ENABLE_CHAT off (or caller passed featureEnabled=false)
|
||||||
|
SecretUnavailable, // no stable secret (wallet locked / not open / empty)
|
||||||
|
DerivationFailed // libsodium init or KDF/keypair failure
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* chatIdentityStatusName(ChatIdentityStatus status);
|
||||||
|
|
||||||
|
struct ChatIdentityResult {
|
||||||
|
ChatIdentityStatus status = ChatIdentityStatus::FeatureDisabled;
|
||||||
|
std::string public_key_hex; // 64 lowercase hex chars when Ready
|
||||||
|
const char* error_name = "FeatureDisabled"; // == chatIdentityStatusName(status)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pure, no-I/O derivation: hashes `stableSecret` (variable-length: a mnemonic on lite, a
|
||||||
|
// spending key on full-node) with the KDF context as the BLAKE2b key into a clean 32-byte
|
||||||
|
// crypto_kx seed, then crypto_kx_seed_keypair() into `outKeys`. Deterministic for a given
|
||||||
|
// secret. On any non-Ready result `outKeys` is left wiped. featureEnabled defaults to the
|
||||||
|
// build predicate but tests pass true to exercise the crypto in an OFF build.
|
||||||
|
//
|
||||||
|
// OWNERSHIP: `stableSecret` is BORROWED (const&) and is NOT wiped here — the caller owns it
|
||||||
|
// and MUST sodium_memzero its backing buffer after this returns. The per-variant provider
|
||||||
|
// that fetches the wallet secret (mnemonic / spending key) should hold it in a wipeable
|
||||||
|
// buffer, not a plain std::string literal, in production.
|
||||||
|
ChatIdentityResult deriveChatIdentityFromSecret(const std::string& stableSecret,
|
||||||
|
ChatKeyPair& outKeys,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
|
||||||
|
// 64-char lowercase hex of the public key.
|
||||||
|
std::string chatIdentityPublicKeyHex(const ChatKeyPair& keys);
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
30
src/chat/chat_message.h
Normal file
30
src/chat/chat_message.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// DragonX Wallet - HushChat decrypted message model. Held in memory by ChatStore and persisted at
|
||||||
|
// rest (encrypted under a seed-derived key) by ChatDatabase.
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
enum class ChatDirection { Incoming, Outgoing };
|
||||||
|
enum class ChatMessageKind { Message, ContactRequest };
|
||||||
|
// Outgoing delivery: Sent = the broadcast was submitted; Failed = it wasn't (not connected, no
|
||||||
|
// spendable address, a send already in progress). Always Sent for incoming.
|
||||||
|
enum class ChatDelivery { Sent, Failed };
|
||||||
|
|
||||||
|
struct ChatMessage {
|
||||||
|
ChatDirection direction = ChatDirection::Incoming;
|
||||||
|
ChatMessageKind kind = ChatMessageKind::Message;
|
||||||
|
std::string txid;
|
||||||
|
std::string conversation_id; // cid — the conversation thread key
|
||||||
|
std::string peer_zaddr; // header "z": peer's reply z-address
|
||||||
|
std::string peer_public_key_hex; // header "p": peer's crypto_kx public key
|
||||||
|
std::string body; // decrypted plaintext (Message) or request text (ContactRequest)
|
||||||
|
std::int64_t timestamp = 0; // tx time in seconds; set by the ingesting caller
|
||||||
|
std::size_t payload_position = 0; // together with txid, the dedup key
|
||||||
|
ChatDelivery delivery = ChatDelivery::Sent; // outgoing only
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
108
src/chat/chat_outgoing.cpp
Normal file
108
src/chat/chat_outgoing.cpp
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// DragonX Wallet - HushChat outgoing memo construction (implementation).
|
||||||
|
|
||||||
|
#include "chat_outgoing.h"
|
||||||
|
|
||||||
|
#include "chat_protocol.h" // kHushChat* constants
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Serialize the HushChat header. nlohmann emits object keys in sorted (alphabetical) order —
|
||||||
|
// cid,e,h,p,t,v,z — which is exactly SilentDragonXLite's on-wire key order.
|
||||||
|
std::string buildHeaderMemo(const std::string& replyZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const char* type,
|
||||||
|
const std::string& streamHeaderHex,
|
||||||
|
const std::string& publicKeyHex)
|
||||||
|
{
|
||||||
|
nlohmann::json header;
|
||||||
|
header["h"] = 1; // header number (>= 1)
|
||||||
|
header["v"] = kHushChatSupportedVersion; // 0
|
||||||
|
header["z"] = replyZaddr; // where the peer should reply (my address)
|
||||||
|
header["cid"] = conversationId;
|
||||||
|
header["t"] = type; // "Memo" or "Cont"
|
||||||
|
header["e"] = streamHeaderHex; // 48-hex secretstream header (Memo) / "" (Cont)
|
||||||
|
header["p"] = publicKeyHex; // my 64-hex crypto_kx public key
|
||||||
|
return header.dump();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool present(const std::string& value) { return !value.empty(); }
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::array<ChatSendOutput, 2> chatSendOutputs(const OutgoingChatMemos& memos, bool utf8Prefix)
|
||||||
|
{
|
||||||
|
const std::string prefix = utf8Prefix ? "utf8:" : "";
|
||||||
|
return {{
|
||||||
|
{ memos.recipientZaddr, prefix + memos.headerMemo }, // header — the lower memo position
|
||||||
|
{ memos.recipientZaddr, prefix + memos.payloadMemo },
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine,
|
||||||
|
const std::string& myPublicKeyHex,
|
||||||
|
const std::string& myReplyZaddr,
|
||||||
|
const std::string& peerPublicKeyHex,
|
||||||
|
const std::string& peerZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& plaintext,
|
||||||
|
OutgoingChatMemos& out)
|
||||||
|
{
|
||||||
|
if (plaintext.empty()) return ChatComposeStatus::EmptyBody;
|
||||||
|
if (!present(myPublicKeyHex) || !present(myReplyZaddr) || !present(peerZaddr) ||
|
||||||
|
!present(conversationId)) {
|
||||||
|
return ChatComposeStatus::MissingField;
|
||||||
|
}
|
||||||
|
if (peerPublicKeyHex.size() != kHushChatPublicKeyHexLength) return ChatComposeStatus::BadPeerKey;
|
||||||
|
|
||||||
|
std::string streamHeaderHex;
|
||||||
|
std::string ciphertextHex;
|
||||||
|
if (encryptOutgoing(mine, peerPublicKeyHex, plaintext, streamHeaderHex, ciphertextHex)
|
||||||
|
!= ChatCryptoStatus::Ok) {
|
||||||
|
return ChatComposeStatus::EncryptFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutgoingChatMemos memos;
|
||||||
|
memos.recipientZaddr = peerZaddr;
|
||||||
|
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Memo", streamHeaderHex, myPublicKeyHex);
|
||||||
|
memos.payloadMemo = ciphertextHex;
|
||||||
|
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||||
|
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
||||||
|
return ChatComposeStatus::TooLong;
|
||||||
|
}
|
||||||
|
out = std::move(memos);
|
||||||
|
return ChatComposeStatus::Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
||||||
|
const std::string& myReplyZaddr,
|
||||||
|
const std::string& peerZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& requestText,
|
||||||
|
OutgoingChatMemos& out)
|
||||||
|
{
|
||||||
|
if (requestText.empty()) return ChatComposeStatus::EmptyBody;
|
||||||
|
// The receive parser treats any memo starting with '{' as a header, so a request payload must
|
||||||
|
// not start with one (see isContactPayloadCandidate in chat_protocol.cpp).
|
||||||
|
if (requestText.front() == '{') return ChatComposeStatus::BadRequestText;
|
||||||
|
if (!present(myPublicKeyHex) || !present(myReplyZaddr) || !present(peerZaddr) ||
|
||||||
|
!present(conversationId)) {
|
||||||
|
return ChatComposeStatus::MissingField;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutgoingChatMemos memos;
|
||||||
|
memos.recipientZaddr = peerZaddr;
|
||||||
|
memos.headerMemo = buildHeaderMemo(myReplyZaddr, conversationId, "Cont", "", myPublicKeyHex);
|
||||||
|
memos.payloadMemo = requestText;
|
||||||
|
if (memos.headerMemo.size() > kHushChatMemoByteLimit ||
|
||||||
|
memos.payloadMemo.size() > kHushChatMemoByteLimit) {
|
||||||
|
return ChatComposeStatus::TooLong;
|
||||||
|
}
|
||||||
|
out = std::move(memos);
|
||||||
|
return ChatComposeStatus::Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
66
src/chat/chat_outgoing.h
Normal file
66
src/chat/chat_outgoing.h
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// DragonX Wallet - HushChat outgoing memo construction (the inverse of the receive parser).
|
||||||
|
//
|
||||||
|
// Given the sender's identity and the peer, produce the header memo JSON + payload memo that,
|
||||||
|
// sent as two 0-value memo outputs to the peer's z-address (header at the LOWER memo position),
|
||||||
|
// another HushChat client parses and decrypts. The byte format matches SilentDragonXLite: the
|
||||||
|
// header keys serialize alphabetically (nlohmann default) to cid,e,h,p,t,v,z. Pure — no I/O, no
|
||||||
|
// network; broadcasting the memos is the caller's job (the transport lands in a later phase).
|
||||||
|
|
||||||
|
#include "chat_crypto.h" // ChatKeyPair
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
struct OutgoingChatMemos {
|
||||||
|
std::string recipientZaddr; // the peer's z-address (recipient of both memo outputs)
|
||||||
|
std::string headerMemo; // JSON header — MUST occupy the lower memo position on the wire
|
||||||
|
std::string payloadMemo; // ciphertext hex (Message) or plaintext (ContactRequest)
|
||||||
|
};
|
||||||
|
|
||||||
|
// One of the two 0-value memo outputs a HushChat send produces (amount is always 0).
|
||||||
|
struct ChatSendOutput {
|
||||||
|
std::string address; // the peer's z-address
|
||||||
|
std::string memo; // memo encoded for the target transport (utf8:-prefixed or raw)
|
||||||
|
};
|
||||||
|
|
||||||
|
// The two outputs for a HushChat send, HEADER FIRST (it must occupy the lower memo position).
|
||||||
|
// `utf8Prefix` prepends the daemon's "utf8:" marker required by full-node z_sendmany (which then
|
||||||
|
// UTF-8-encodes the bytes on-chain, byte-identical to SDXLite's Memo::from_str); lite backends take
|
||||||
|
// raw UTF-8, so pass false there.
|
||||||
|
std::array<ChatSendOutput, 2> chatSendOutputs(const OutgoingChatMemos& memos, bool utf8Prefix);
|
||||||
|
|
||||||
|
enum class ChatComposeStatus {
|
||||||
|
Ok,
|
||||||
|
EmptyBody,
|
||||||
|
MissingField,
|
||||||
|
BadPeerKey,
|
||||||
|
BadRequestText, // a contact request text must not start with '{' (parser would read it as a header)
|
||||||
|
EncryptFailed,
|
||||||
|
TooLong // a resulting memo exceeds the HushChat 512-byte memo limit
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build an ENCRYPTED message to a peer whose public key you already learned from a memo they sent
|
||||||
|
// you. `mine` is the sender's identity keypair; `myPublicKeyHex` its public half (goes in header p).
|
||||||
|
ChatComposeStatus buildOutgoingMessage(const ChatKeyPair& mine,
|
||||||
|
const std::string& myPublicKeyHex,
|
||||||
|
const std::string& myReplyZaddr,
|
||||||
|
const std::string& peerPublicKeyHex,
|
||||||
|
const std::string& peerZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& plaintext,
|
||||||
|
OutgoingChatMemos& out);
|
||||||
|
|
||||||
|
// Build a plaintext contact request — no peer public key needed yet; this is how the peer first
|
||||||
|
// learns your public key + reply address. The payload is the (plaintext) request text.
|
||||||
|
ChatComposeStatus buildOutgoingContactRequest(const std::string& myPublicKeyHex,
|
||||||
|
const std::string& myReplyZaddr,
|
||||||
|
const std::string& peerZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& requestText,
|
||||||
|
OutgoingChatMemos& out);
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
312
src/chat/chat_protocol.cpp
Normal file
312
src/chat/chat_protocol.cpp
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
#include "chat_protocol.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool isHexString(const std::string& value)
|
||||||
|
{
|
||||||
|
for (unsigned char ch : value) {
|
||||||
|
if (!std::isxdigit(ch)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isCiphertextPayloadCandidate(const std::string& value)
|
||||||
|
{
|
||||||
|
return !value.empty() && value.size() <= kHushChatMemoByteLimit &&
|
||||||
|
value.size() % 2 == 0 && isHexString(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isContactPayloadCandidate(const std::string& value)
|
||||||
|
{
|
||||||
|
return !value.empty() && value.size() <= kHushChatMemoByteLimit && value.front() != '{';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool readRequiredString(const nlohmann::json& object,
|
||||||
|
const char* key,
|
||||||
|
std::string& value,
|
||||||
|
std::string& error)
|
||||||
|
{
|
||||||
|
auto it = object.find(key);
|
||||||
|
if (it == object.end()) {
|
||||||
|
error = std::string("missing field: ") + key;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!it->is_string()) {
|
||||||
|
error = std::string("field is not a string: ") + key;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
value = it->get<std::string>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool readRequiredInt(const nlohmann::json& object,
|
||||||
|
const char* key,
|
||||||
|
int& value,
|
||||||
|
std::string& error)
|
||||||
|
{
|
||||||
|
auto it = object.find(key);
|
||||||
|
if (it == object.end()) {
|
||||||
|
error = std::string("missing field: ") + key;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!it->is_number_integer()) {
|
||||||
|
error = std::string("field is not an integer: ") + key;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
value = it->get<int>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
HushChatHeaderParseResult fail(std::string error)
|
||||||
|
{
|
||||||
|
HushChatHeaderParseResult result;
|
||||||
|
result.error = std::move(error);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const char* hushChatHeaderTypeName(HushChatHeaderType type)
|
||||||
|
{
|
||||||
|
switch (type) {
|
||||||
|
case HushChatHeaderType::Message:
|
||||||
|
return "Memo";
|
||||||
|
case HushChatHeaderType::ContactRequest:
|
||||||
|
return "Cont";
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue)
|
||||||
|
{
|
||||||
|
switch (issue) {
|
||||||
|
case HushChatMemoGroupingIssue::InvalidHeader:
|
||||||
|
return "InvalidHeader";
|
||||||
|
case HushChatMemoGroupingIssue::MissingPayload:
|
||||||
|
return "MissingPayload";
|
||||||
|
case HushChatMemoGroupingIssue::DuplicateHeader:
|
||||||
|
return "DuplicateHeader";
|
||||||
|
case HushChatMemoGroupingIssue::OversizedMemo:
|
||||||
|
return "OversizedMemo";
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo)
|
||||||
|
{
|
||||||
|
if (memo.empty()) return fail("empty memo");
|
||||||
|
if (memo.size() > kHushChatMemoByteLimit) return fail("memo exceeds HushChat memo byte limit");
|
||||||
|
if (memo.front() != '{') return fail("memo is not a HushChat header JSON object");
|
||||||
|
|
||||||
|
nlohmann::json object;
|
||||||
|
try {
|
||||||
|
object = nlohmann::json::parse(memo);
|
||||||
|
} catch (const nlohmann::json::parse_error& e) {
|
||||||
|
return fail(std::string("invalid JSON: ") + e.what());
|
||||||
|
}
|
||||||
|
if (!object.is_object()) return fail("header memo JSON is not an object");
|
||||||
|
|
||||||
|
HushChatHeader header;
|
||||||
|
std::string type;
|
||||||
|
std::string error;
|
||||||
|
if (!readRequiredInt(object, "h", header.header_number, error)) return fail(error);
|
||||||
|
if (!readRequiredInt(object, "v", header.version, error)) return fail(error);
|
||||||
|
if (!readRequiredString(object, "z", header.reply_zaddr, error)) return fail(error);
|
||||||
|
if (!readRequiredString(object, "cid", header.conversation_id, error)) return fail(error);
|
||||||
|
if (!readRequiredString(object, "t", type, error)) return fail(error);
|
||||||
|
if (!readRequiredString(object, "e", header.secretstream_header_hex, error)) return fail(error);
|
||||||
|
if (!readRequiredString(object, "p", header.public_key_hex, error)) return fail(error);
|
||||||
|
|
||||||
|
if (header.header_number < 1) return fail("header number must be positive");
|
||||||
|
if (header.version != kHushChatSupportedVersion) return fail("unsupported HushChat version");
|
||||||
|
if (header.reply_zaddr.empty()) return fail("reply z-address is empty");
|
||||||
|
if (header.conversation_id.empty()) return fail("conversation id is empty");
|
||||||
|
|
||||||
|
if (type == "Memo") {
|
||||||
|
header.type = HushChatHeaderType::Message;
|
||||||
|
} else if (type == "Cont") {
|
||||||
|
header.type = HushChatHeaderType::ContactRequest;
|
||||||
|
} else {
|
||||||
|
return fail("unknown HushChat header type");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.public_key_hex.size() != kHushChatPublicKeyHexLength || !isHexString(header.public_key_hex)) {
|
||||||
|
return fail("public key must be 32 bytes encoded as hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.type == HushChatHeaderType::Message) {
|
||||||
|
if (header.secretstream_header_hex.size() != kHushChatSecretstreamHeaderHexLength ||
|
||||||
|
!isHexString(header.secretstream_header_hex)) {
|
||||||
|
return fail("message header must include a 24 byte secretstream header encoded as hex");
|
||||||
|
}
|
||||||
|
} else if (!header.secretstream_header_hex.empty()) {
|
||||||
|
return fail("contact request header must not include a secretstream header");
|
||||||
|
}
|
||||||
|
|
||||||
|
HushChatHeaderParseResult result;
|
||||||
|
result.ok = true;
|
||||||
|
result.header = std::move(header);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMemoOutput>& outputs)
|
||||||
|
{
|
||||||
|
struct OrderedOutput {
|
||||||
|
std::size_t input_index = 0;
|
||||||
|
HushChatMemoOutput output;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<OrderedOutput> ordered;
|
||||||
|
ordered.reserve(outputs.size());
|
||||||
|
for (std::size_t index = 0; index < outputs.size(); ++index) {
|
||||||
|
ordered.push_back(OrderedOutput{index, outputs[index]});
|
||||||
|
}
|
||||||
|
std::stable_sort(ordered.begin(), ordered.end(), [](const OrderedOutput& left, const OrderedOutput& right) {
|
||||||
|
if (left.output.position == right.output.position) return left.input_index < right.input_index;
|
||||||
|
return left.output.position < right.output.position;
|
||||||
|
});
|
||||||
|
|
||||||
|
HushChatMemoGroupingResult result;
|
||||||
|
std::optional<HushChatMemoOutput> pending_header_output;
|
||||||
|
std::optional<HushChatHeader> pending_header;
|
||||||
|
// A payload memo seen BEFORE its header. The full-node daemon shuffles the two 0-value memo
|
||||||
|
// outputs of a chat tx (transaction_builder ShuffleOutputs), so the header is NOT guaranteed to
|
||||||
|
// occupy the lower position. We hold an orphan payload until its header arrives and pair them
|
||||||
|
// regardless of on-chain order (a chat tx carries exactly one header + one payload).
|
||||||
|
std::optional<HushChatMemoOutput> pending_payload_output;
|
||||||
|
|
||||||
|
auto addIssue = [&](HushChatMemoGroupingIssue issue, std::size_t position, std::string detail) {
|
||||||
|
result.issues.push_back(HushChatMemoGroupingIssueInfo{issue, position, std::move(detail)});
|
||||||
|
};
|
||||||
|
|
||||||
|
auto payloadMatchesHeader = [&](const std::string& memo) {
|
||||||
|
return pending_header->type == HushChatHeaderType::Message
|
||||||
|
? isCiphertextPayloadCandidate(memo)
|
||||||
|
: isContactPayloadCandidate(memo);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto emitPair = [&](const HushChatMemoOutput& payloadOutput) {
|
||||||
|
HushChatMemoPair pair;
|
||||||
|
pair.header = std::move(*pending_header);
|
||||||
|
pair.header_position = pending_header_output->position;
|
||||||
|
pair.payload_position = payloadOutput.position;
|
||||||
|
pair.payload_memo = payloadOutput.memo;
|
||||||
|
result.pairs.push_back(std::move(pair));
|
||||||
|
pending_header_output.reset();
|
||||||
|
pending_header.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pair a held (early) payload with the now-pending header, if it matches the header's type.
|
||||||
|
auto tryPairHeldPayload = [&]() {
|
||||||
|
if (!pending_header || !pending_payload_output) return;
|
||||||
|
if (payloadMatchesHeader(pending_payload_output->memo)) {
|
||||||
|
emitPair(*pending_payload_output);
|
||||||
|
pending_payload_output.reset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto clearPendingAsMissing = [&]() {
|
||||||
|
if (!pending_header_output) return;
|
||||||
|
addIssue(HushChatMemoGroupingIssue::MissingPayload,
|
||||||
|
pending_header_output->position,
|
||||||
|
"header did not have a matching payload memo");
|
||||||
|
pending_header_output.reset();
|
||||||
|
pending_header.reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& entry : ordered) {
|
||||||
|
const auto& output = entry.output;
|
||||||
|
if (output.memo.size() > kHushChatMemoByteLimit) {
|
||||||
|
addIssue(HushChatMemoGroupingIssue::OversizedMemo,
|
||||||
|
output.position,
|
||||||
|
"memo exceeds HushChat memo byte limit");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output.memo.empty() && output.memo.front() == '{') {
|
||||||
|
auto parsed = parseHushChatHeaderMemo(output.memo);
|
||||||
|
if (!parsed.ok) {
|
||||||
|
addIssue(HushChatMemoGroupingIssue::InvalidHeader, output.position, parsed.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending_header_output) {
|
||||||
|
addIssue(HushChatMemoGroupingIssue::DuplicateHeader,
|
||||||
|
output.position,
|
||||||
|
"encountered another HushChat header before a payload");
|
||||||
|
clearPendingAsMissing();
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_header_output = output;
|
||||||
|
pending_header = std::move(parsed.header);
|
||||||
|
tryPairHeldPayload(); // the payload may have arrived first (shuffled output order)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-header memo.
|
||||||
|
if (pending_header_output && pending_header) {
|
||||||
|
if (payloadMatchesHeader(output.memo)) {
|
||||||
|
emitPair(output);
|
||||||
|
} else {
|
||||||
|
++result.ignored_memo_count;
|
||||||
|
}
|
||||||
|
} else if (!pending_payload_output && !output.memo.empty()) {
|
||||||
|
// No header yet — hold this as a candidate payload for a header still to come.
|
||||||
|
pending_payload_output = output;
|
||||||
|
} else {
|
||||||
|
++result.ignored_memo_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPendingAsMissing();
|
||||||
|
if (pending_payload_output) ++result.ignored_memo_count; // orphan payload, no header arrived
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
|
||||||
|
const HushChatTransactionInput& transaction,
|
||||||
|
bool featureEnabled)
|
||||||
|
{
|
||||||
|
HushChatTransactionExtractionResult result;
|
||||||
|
result.feature_enabled = featureEnabled;
|
||||||
|
if (!featureEnabled || transaction.txid.empty()) return result;
|
||||||
|
|
||||||
|
auto grouped = groupHushChatMemoOutputs(transaction.outputs);
|
||||||
|
result.ignored_memo_count = grouped.ignored_memo_count;
|
||||||
|
result.issues.reserve(grouped.issues.size());
|
||||||
|
for (const auto& issue : grouped.issues) {
|
||||||
|
result.issues.push_back(HushChatMemoGroupingIssueInfo{
|
||||||
|
issue.issue,
|
||||||
|
issue.position,
|
||||||
|
hushChatMemoGroupingIssueName(issue.issue)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
result.metadata.reserve(grouped.pairs.size());
|
||||||
|
for (const auto& pair : grouped.pairs) {
|
||||||
|
HushChatTransactionMetadata metadata;
|
||||||
|
metadata.txid = transaction.txid;
|
||||||
|
metadata.type = pair.header.type;
|
||||||
|
metadata.conversation_id = pair.header.conversation_id;
|
||||||
|
metadata.reply_zaddr = pair.header.reply_zaddr;
|
||||||
|
metadata.header_position = pair.header_position;
|
||||||
|
metadata.payload_position = pair.payload_position;
|
||||||
|
metadata.payload_size = pair.payload_memo.size();
|
||||||
|
metadata.sender_public_key_hex = pair.header.public_key_hex;
|
||||||
|
metadata.secretstream_header_hex = pair.header.secretstream_header_hex;
|
||||||
|
metadata.payload_memo = pair.payload_memo;
|
||||||
|
result.metadata.push_back(std::move(metadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
110
src/chat/chat_protocol.h
Normal file
110
src/chat/chat_protocol.h
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifndef DRAGONX_ENABLE_CHAT
|
||||||
|
#define DRAGONX_ENABLE_CHAT 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
enum class HushChatHeaderType {
|
||||||
|
Message,
|
||||||
|
ContactRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatHeader {
|
||||||
|
int header_number = 0;
|
||||||
|
int version = 0;
|
||||||
|
std::string reply_zaddr;
|
||||||
|
std::string conversation_id;
|
||||||
|
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||||
|
std::string secretstream_header_hex;
|
||||||
|
std::string public_key_hex;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatHeaderParseResult {
|
||||||
|
bool ok = false;
|
||||||
|
HushChatHeader header;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatMemoOutput {
|
||||||
|
std::size_t position = 0;
|
||||||
|
std::string memo;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatMemoPair {
|
||||||
|
HushChatHeader header;
|
||||||
|
std::size_t header_position = 0;
|
||||||
|
std::size_t payload_position = 0;
|
||||||
|
std::string payload_memo;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HushChatMemoGroupingIssue {
|
||||||
|
InvalidHeader,
|
||||||
|
MissingPayload,
|
||||||
|
DuplicateHeader,
|
||||||
|
OversizedMemo
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatMemoGroupingIssueInfo {
|
||||||
|
HushChatMemoGroupingIssue issue = HushChatMemoGroupingIssue::InvalidHeader;
|
||||||
|
std::size_t position = 0;
|
||||||
|
std::string detail;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatMemoGroupingResult {
|
||||||
|
std::vector<HushChatMemoPair> pairs;
|
||||||
|
std::vector<HushChatMemoGroupingIssueInfo> issues;
|
||||||
|
std::size_t ignored_memo_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatTransactionInput {
|
||||||
|
std::string txid;
|
||||||
|
std::vector<HushChatMemoOutput> outputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatTransactionMetadata {
|
||||||
|
std::string txid;
|
||||||
|
HushChatHeaderType type = HushChatHeaderType::Message;
|
||||||
|
std::string conversation_id;
|
||||||
|
std::string reply_zaddr;
|
||||||
|
std::size_t header_position = 0;
|
||||||
|
std::size_t payload_position = 0;
|
||||||
|
std::size_t payload_size = 0;
|
||||||
|
// Decrypt inputs carried through from the paired header + payload memos so the chat
|
||||||
|
// service can actually decrypt (a Message) or read the request (a ContactRequest).
|
||||||
|
std::string sender_public_key_hex; // header "p": peer crypto_kx public key (hex)
|
||||||
|
std::string secretstream_header_hex; // header "e": secretstream header (hex; empty for ContactRequest)
|
||||||
|
std::string payload_memo; // ciphertext hex (Message) or plaintext request text (ContactRequest)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HushChatTransactionExtractionResult {
|
||||||
|
bool feature_enabled = false;
|
||||||
|
std::vector<HushChatTransactionMetadata> metadata;
|
||||||
|
std::vector<HushChatMemoGroupingIssueInfo> issues;
|
||||||
|
std::size_t ignored_memo_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr int kHushChatSupportedVersion = 0;
|
||||||
|
constexpr std::size_t kHushChatMemoByteLimit = 512;
|
||||||
|
constexpr std::size_t kHushChatPublicKeyHexLength = 64;
|
||||||
|
constexpr std::size_t kHushChatSecretstreamHeaderHexLength = 48;
|
||||||
|
|
||||||
|
constexpr bool hushChatFeatureEnabledAtBuild()
|
||||||
|
{
|
||||||
|
return DRAGONX_ENABLE_CHAT != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo);
|
||||||
|
HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMemoOutput>& outputs);
|
||||||
|
HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
|
||||||
|
const HushChatTransactionInput& transaction,
|
||||||
|
bool featureEnabled = hushChatFeatureEnabledAtBuild());
|
||||||
|
const char* hushChatHeaderTypeName(HushChatHeaderType type);
|
||||||
|
const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue);
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
108
src/chat/chat_service.cpp
Normal file
108
src/chat/chat_service.cpp
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// DragonX Wallet - HushChat service (implementation).
|
||||||
|
|
||||||
|
#include "chat_service.h"
|
||||||
|
|
||||||
|
#include "chat_database.h"
|
||||||
|
#include "chat_identity.h" // chatIdentityPublicKeyHex
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
ChatService::~ChatService() {
|
||||||
|
clearIdentity();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatService::setIdentity(const ChatKeyPair& keys) {
|
||||||
|
identity_ = keys;
|
||||||
|
has_identity_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatService::clearIdentity() {
|
||||||
|
wipeChatKeyPair(identity_);
|
||||||
|
has_identity_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
||||||
|
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
|
||||||
|
std::int64_t fallbackTimestamp) {
|
||||||
|
if (!has_identity_) return 0;
|
||||||
|
|
||||||
|
int added = 0;
|
||||||
|
for (const auto& meta : metadata) {
|
||||||
|
ChatMessage message;
|
||||||
|
message.direction = ChatDirection::Incoming;
|
||||||
|
message.txid = meta.txid;
|
||||||
|
message.conversation_id = meta.conversation_id;
|
||||||
|
message.peer_zaddr = meta.reply_zaddr;
|
||||||
|
message.peer_public_key_hex = meta.sender_public_key_hex;
|
||||||
|
const auto timeIt = txTimestamps.find(meta.txid);
|
||||||
|
message.timestamp = timeIt != txTimestamps.end() ? timeIt->second : fallbackTimestamp;
|
||||||
|
message.payload_position = meta.payload_position;
|
||||||
|
|
||||||
|
if (meta.type == HushChatHeaderType::ContactRequest) {
|
||||||
|
message.kind = ChatMessageKind::ContactRequest;
|
||||||
|
message.body = meta.payload_memo; // plaintext request text
|
||||||
|
} else {
|
||||||
|
message.kind = ChatMessageKind::Message;
|
||||||
|
std::string plaintext;
|
||||||
|
const ChatCryptoStatus status = decryptIncoming(
|
||||||
|
identity_, meta.sender_public_key_hex, meta.secretstream_header_hex,
|
||||||
|
meta.payload_memo, plaintext);
|
||||||
|
if (status != ChatCryptoStatus::Ok) continue; // drop undecryptable silently
|
||||||
|
message.body = std::move(plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-memory store dedups (txid+position); only persist the genuinely new ones. On the next
|
||||||
|
// session loadFromDatabase() repopulates the store, so re-scanning the chain re-ingests but
|
||||||
|
// the store dedup prevents a duplicate write.
|
||||||
|
if (store_.append(message)) {
|
||||||
|
if (db_) db_->append(message);
|
||||||
|
++added;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatService::loadFromDatabase() {
|
||||||
|
if (!db_) return;
|
||||||
|
for (const auto& message : db_->load()) {
|
||||||
|
store_.append(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ChatService::identityPublicKeyHex() const {
|
||||||
|
if (!has_identity_) return {};
|
||||||
|
return chatIdentityPublicKeyHex(identity_);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatComposeStatus ChatService::composeMessage(const std::string& myReplyZaddr,
|
||||||
|
const std::string& peerPublicKeyHex,
|
||||||
|
const std::string& peerZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& plaintext,
|
||||||
|
OutgoingChatMemos& out) const {
|
||||||
|
if (!has_identity_) return ChatComposeStatus::MissingField;
|
||||||
|
return buildOutgoingMessage(identity_, chatIdentityPublicKeyHex(identity_), myReplyZaddr,
|
||||||
|
peerPublicKeyHex, peerZaddr, conversationId, plaintext, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatComposeStatus ChatService::composeContactRequest(const std::string& myReplyZaddr,
|
||||||
|
const std::string& peerZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& requestText,
|
||||||
|
OutgoingChatMemos& out) const {
|
||||||
|
if (!has_identity_) return ChatComposeStatus::MissingField;
|
||||||
|
return buildOutgoingContactRequest(chatIdentityPublicKeyHex(identity_), myReplyZaddr,
|
||||||
|
peerZaddr, conversationId, requestText, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatService::recordOutgoing(const ChatMessage& message) {
|
||||||
|
if (store_.append(message)) {
|
||||||
|
if (db_) db_->append(message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
85
src/chat/chat_service.h
Normal file
85
src/chat/chat_service.h
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// DragonX Wallet - HushChat service: turns harvested memo metadata into decrypted, threaded
|
||||||
|
// messages. Owns the long-lived chat identity keypair (a secret) + the in-memory store.
|
||||||
|
// Move-disabled (the secret stays pinned); wipes the secret on destruction/clear.
|
||||||
|
// Not thread-safe — drive from the main thread (where refresh results are applied).
|
||||||
|
|
||||||
|
#include "chat_crypto.h" // ChatKeyPair
|
||||||
|
#include "chat_protocol.h" // HushChatTransactionMetadata
|
||||||
|
#include "chat_outgoing.h" // OutgoingChatMemos, ChatComposeStatus
|
||||||
|
#include "chat_store.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
class ChatDatabase; // optional persistent backing (Phase 2); set via setPersistence
|
||||||
|
|
||||||
|
class ChatService {
|
||||||
|
public:
|
||||||
|
ChatService() = default;
|
||||||
|
~ChatService();
|
||||||
|
ChatService(const ChatService&) = delete;
|
||||||
|
ChatService& operator=(const ChatService&) = delete;
|
||||||
|
ChatService(ChatService&&) = delete;
|
||||||
|
ChatService& operator=(ChatService&&) = delete;
|
||||||
|
|
||||||
|
// Provision (or replace) the chat identity. Copies the keypair — the caller should wipe
|
||||||
|
// its own copy afterwards (see chat_identity.h ownership note).
|
||||||
|
void setIdentity(const ChatKeyPair& keys);
|
||||||
|
bool hasIdentity() const { return has_identity_; }
|
||||||
|
void clearIdentity(); // wipes the held secret key
|
||||||
|
|
||||||
|
// Decrypt/record each metadata entry (a Message is decrypted; a ContactRequest carries its
|
||||||
|
// plaintext through) and thread it into the store. Each message is stamped with its own
|
||||||
|
// transaction time via `txTimestamps` (keyed by txid), falling back to `fallbackTimestamp`
|
||||||
|
// when the txid isn't present. Newly-added messages are also persisted (if a database is
|
||||||
|
// attached). Returns the number of NEW messages added; 0 with no identity. Undecryptable
|
||||||
|
// Messages are dropped silently (no logging of memo/plaintext).
|
||||||
|
int ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
||||||
|
const std::unordered_map<std::string, std::int64_t>& txTimestamps,
|
||||||
|
std::int64_t fallbackTimestamp = 0);
|
||||||
|
|
||||||
|
// Attach a persistent backing store (Phase 2). Not owned. Pass nullptr to detach. New messages
|
||||||
|
// from ingest() are written through; loadFromDatabase() rehydrates the in-memory store from it.
|
||||||
|
void setPersistence(ChatDatabase* db) { db_ = db; }
|
||||||
|
// Load previously-persisted messages (already decrypted at ingest, re-encrypted at rest under
|
||||||
|
// the seed-derived key) into the in-memory store. No-op without an unlocked database.
|
||||||
|
void loadFromDatabase();
|
||||||
|
|
||||||
|
// --- Outgoing (compose) ---
|
||||||
|
// My chat public key (hex), or "" without an identity — goes in an outgoing header's "p".
|
||||||
|
std::string identityPublicKeyHex() const;
|
||||||
|
// Construct the outgoing memos for an ENCRYPTED message, using the held identity to encrypt.
|
||||||
|
ChatComposeStatus composeMessage(const std::string& myReplyZaddr,
|
||||||
|
const std::string& peerPublicKeyHex,
|
||||||
|
const std::string& peerZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& plaintext,
|
||||||
|
OutgoingChatMemos& out) const;
|
||||||
|
// Construct the outgoing memos for a plaintext contact request (no peer key needed yet).
|
||||||
|
ChatComposeStatus composeContactRequest(const std::string& myReplyZaddr,
|
||||||
|
const std::string& peerZaddr,
|
||||||
|
const std::string& conversationId,
|
||||||
|
const std::string& requestText,
|
||||||
|
OutgoingChatMemos& out) const;
|
||||||
|
// Echo a locally-composed outgoing message into the store (and DB). Returns true if new. (We
|
||||||
|
// never harvest our own sent memos — they land on the peer's address — so this echo is the
|
||||||
|
// only local record of what we sent.)
|
||||||
|
bool recordOutgoing(const ChatMessage& message);
|
||||||
|
|
||||||
|
const ChatStore& store() const { return store_; }
|
||||||
|
ChatStore& store() { return store_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
ChatKeyPair identity_{};
|
||||||
|
bool has_identity_ = false;
|
||||||
|
ChatStore store_;
|
||||||
|
ChatDatabase* db_ = nullptr; // optional; not owned
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
39
src/chat/chat_store.cpp
Normal file
39
src/chat/chat_store.cpp
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// DragonX Wallet - HushChat in-memory message store (implementation).
|
||||||
|
|
||||||
|
#include "chat_store.h"
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
std::string ChatStore::dedupKey(const ChatMessage& message) {
|
||||||
|
return message.txid + ":" + std::to_string(message.payload_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatStore::append(const ChatMessage& message) {
|
||||||
|
if (!seen_.insert(dedupKey(message)).second) return false;
|
||||||
|
messages_.push_back(message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ChatMessage> ChatStore::conversation(const std::string& conversationId) const {
|
||||||
|
std::vector<ChatMessage> out;
|
||||||
|
for (const auto& message : messages_) {
|
||||||
|
if (message.conversation_id == conversationId) out.push_back(message);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> ChatStore::conversationIds() const {
|
||||||
|
std::vector<std::string> ids;
|
||||||
|
std::unordered_set<std::string> seenIds;
|
||||||
|
for (const auto& message : messages_) {
|
||||||
|
if (seenIds.insert(message.conversation_id).second) ids.push_back(message.conversation_id);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatStore::clear() {
|
||||||
|
messages_.clear();
|
||||||
|
seen_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
38
src/chat/chat_store.h
Normal file
38
src/chat/chat_store.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// DragonX Wallet - HushChat in-memory message store: the fast read model / dedup view. Durable
|
||||||
|
// persistence lives in ChatDatabase; ChatService rehydrates this store from it on unlock.
|
||||||
|
|
||||||
|
#include "chat_message.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx::chat {
|
||||||
|
|
||||||
|
// Threads messages by conversation_id (cid) and deduplicates by (txid, payload_position) so
|
||||||
|
// re-scanning the chain never double-inserts. Not thread-safe — drive from the main thread.
|
||||||
|
class ChatStore {
|
||||||
|
public:
|
||||||
|
// Returns true if newly inserted, false if a duplicate was ignored.
|
||||||
|
bool append(const ChatMessage& message);
|
||||||
|
|
||||||
|
// Messages in a conversation, in insertion order.
|
||||||
|
std::vector<ChatMessage> conversation(const std::string& conversationId) const;
|
||||||
|
|
||||||
|
// Distinct conversation ids, in first-seen order.
|
||||||
|
std::vector<std::string> conversationIds() const;
|
||||||
|
|
||||||
|
std::size_t size() const { return messages_.size(); }
|
||||||
|
bool empty() const { return messages_.empty(); }
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::string dedupKey(const ChatMessage& message);
|
||||||
|
|
||||||
|
std::vector<ChatMessage> messages_;
|
||||||
|
std::unordered_set<std::string> seen_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace dragonx::chat
|
||||||
@@ -11,8 +11,12 @@
|
|||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <ctime>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
|
#include "../util/platform.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <shlobj.h>
|
#include <shlobj.h>
|
||||||
@@ -30,35 +34,87 @@ namespace config {
|
|||||||
Settings::Settings() = default;
|
Settings::Settings() = default;
|
||||||
Settings::~Settings() = default;
|
Settings::~Settings() = default;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
Settings::LiteServerSelectionPreferenceMode parseLiteServerSelectionPreferenceMode(
|
||||||
|
const json& value)
|
||||||
|
{
|
||||||
|
if (!value.is_string()) return Settings::LiteServerSelectionPreferenceMode::Sticky;
|
||||||
|
const std::string mode = value.get<std::string>();
|
||||||
|
if (mode == "random" || mode == "Random") {
|
||||||
|
return Settings::LiteServerSelectionPreferenceMode::Random;
|
||||||
|
}
|
||||||
|
return Settings::LiteServerSelectionPreferenceMode::Sticky;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* liteServerSelectionPreferenceModeName(
|
||||||
|
Settings::LiteServerSelectionPreferenceMode mode)
|
||||||
|
{
|
||||||
|
switch (mode) {
|
||||||
|
case Settings::LiteServerSelectionPreferenceMode::Sticky:
|
||||||
|
return "sticky";
|
||||||
|
case Settings::LiteServerSelectionPreferenceMode::Random:
|
||||||
|
return "random";
|
||||||
|
}
|
||||||
|
return "sticky";
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings::PoolSelectMode parsePoolSelectMode(const json& value)
|
||||||
|
{
|
||||||
|
if (!value.is_string()) return Settings::PoolSelectMode::Manual;
|
||||||
|
const std::string mode = value.get<std::string>();
|
||||||
|
if (mode == "auto_balance" || mode == "auto") {
|
||||||
|
return Settings::PoolSelectMode::AutoBalance;
|
||||||
|
}
|
||||||
|
return Settings::PoolSelectMode::Manual;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* poolSelectModeName(Settings::PoolSelectMode mode)
|
||||||
|
{
|
||||||
|
switch (mode) {
|
||||||
|
case Settings::PoolSelectMode::Manual: return "manual";
|
||||||
|
case Settings::PoolSelectMode::AutoBalance: return "auto_balance";
|
||||||
|
}
|
||||||
|
return "manual";
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if j[key] exists and holds a JSON value convertible to T. Guards every scalar
|
||||||
|
// read so a malformed value (wrong type / missing key) leaves the field's default
|
||||||
|
// instead of throwing out of the whole load().
|
||||||
|
template <typename T>
|
||||||
|
bool jsonHasType(const json& v)
|
||||||
|
{
|
||||||
|
if constexpr (std::is_same_v<T, bool>) return v.is_boolean();
|
||||||
|
else if constexpr (std::is_same_v<T, std::string>) return v.is_string();
|
||||||
|
else if constexpr (std::is_floating_point_v<T>) return v.is_number();
|
||||||
|
else if constexpr (std::is_integral_v<T>) return v.is_number_integer();
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads j[key] into field only when present AND of the matching type.
|
||||||
|
template <typename T>
|
||||||
|
void loadScalar(const json& j, const char* key, T& field)
|
||||||
|
{
|
||||||
|
if (j.contains(key) && jsonHasType<T>(j[key])) field = j[key].get<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as loadScalar but clamps the read value into [lo, hi].
|
||||||
|
template <typename T>
|
||||||
|
void loadClamped(const json& j, const char* key, T& field, T lo, T hi)
|
||||||
|
{
|
||||||
|
if (j.contains(key) && jsonHasType<T>(j[key]))
|
||||||
|
field = std::max(lo, std::min(hi, j[key].get<T>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
std::string Settings::getDefaultPath()
|
std::string Settings::getDefaultPath()
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
// Single per-platform, per-variant config dir (util::Platform::getConfigDir handles the
|
||||||
char path[MAX_PATH];
|
// _WIN32 / __APPLE__ / XDG split and the DRAGONX_APP_NAME variant suffix in one place).
|
||||||
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
|
const std::string dir = util::Platform::getConfigDir();
|
||||||
std::string dir = std::string(path) + "\\ObsidianDragon";
|
|
||||||
fs::create_directories(dir);
|
|
||||||
return dir + "\\settings.json";
|
|
||||||
}
|
|
||||||
return "settings.json";
|
|
||||||
#elif defined(__APPLE__)
|
|
||||||
const char* home = getenv("HOME");
|
|
||||||
if (!home) {
|
|
||||||
struct passwd* pw = getpwuid(getuid());
|
|
||||||
home = pw->pw_dir;
|
|
||||||
}
|
|
||||||
std::string dir = std::string(home) + "/Library/Application Support/ObsidianDragon";
|
|
||||||
fs::create_directories(dir);
|
fs::create_directories(dir);
|
||||||
return dir + "/settings.json";
|
return (fs::path(dir) / "settings.json").string();
|
||||||
#else
|
|
||||||
const char* home = getenv("HOME");
|
|
||||||
if (!home) {
|
|
||||||
struct passwd* pw = getpwuid(getuid());
|
|
||||||
home = pw->pw_dir;
|
|
||||||
}
|
|
||||||
std::string dir = std::string(home) + "/.config/ObsidianDragon";
|
|
||||||
fs::create_directories(dir);
|
|
||||||
return dir + "/settings.json";
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Settings::load()
|
bool Settings::load()
|
||||||
@@ -79,29 +135,30 @@ bool Settings::load(const std::string& path)
|
|||||||
json j;
|
json j;
|
||||||
file >> j;
|
file >> j;
|
||||||
|
|
||||||
if (j.contains("theme")) theme_ = j["theme"].get<std::string>();
|
loadScalar(j, "theme", theme_);
|
||||||
if (j.contains("save_ztxs")) save_ztxs_ = j["save_ztxs"].get<bool>();
|
loadScalar(j, "save_ztxs", save_ztxs_);
|
||||||
if (j.contains("auto_shield")) auto_shield_ = j["auto_shield"].get<bool>();
|
loadScalar(j, "auto_shield", auto_shield_);
|
||||||
if (j.contains("use_tor")) use_tor_ = j["use_tor"].get<bool>();
|
loadScalar(j, "use_tor", use_tor_);
|
||||||
if (j.contains("allow_custom_fees")) allow_custom_fees_ = j["allow_custom_fees"].get<bool>();
|
loadScalar(j, "allow_custom_fees", allow_custom_fees_);
|
||||||
if (j.contains("default_fee")) default_fee_ = j["default_fee"].get<double>();
|
loadScalar(j, "default_fee", default_fee_);
|
||||||
if (j.contains("fetch_prices")) fetch_prices_ = j["fetch_prices"].get<bool>();
|
loadScalar(j, "fetch_prices", fetch_prices_);
|
||||||
if (j.contains("tx_explorer_url")) tx_explorer_url_ = j["tx_explorer_url"].get<std::string>();
|
loadScalar(j, "tx_explorer_url", tx_explorer_url_);
|
||||||
if (j.contains("address_explorer_url")) address_explorer_url_ = j["address_explorer_url"].get<std::string>();
|
loadScalar(j, "address_explorer_url", address_explorer_url_);
|
||||||
if (j.contains("language")) language_ = j["language"].get<std::string>();
|
loadScalar(j, "language", language_);
|
||||||
if (j.contains("skin_id")) skin_id_ = j["skin_id"].get<std::string>();
|
loadScalar(j, "skin_id", skin_id_);
|
||||||
if (j.contains("acrylic_enabled")) acrylic_enabled_ = j["acrylic_enabled"].get<bool>();
|
loadScalar(j, "chat_reply_zaddr", chat_reply_zaddr_);
|
||||||
if (j.contains("acrylic_quality")) acrylic_quality_ = j["acrylic_quality"].get<int>();
|
loadScalar(j, "acrylic_enabled", acrylic_enabled_);
|
||||||
if (j.contains("blur_multiplier")) blur_multiplier_ = j["blur_multiplier"].get<float>();
|
loadScalar(j, "acrylic_quality", acrylic_quality_);
|
||||||
if (j.contains("noise_opacity")) noise_opacity_ = j["noise_opacity"].get<float>();
|
loadScalar(j, "blur_multiplier", blur_multiplier_);
|
||||||
if (j.contains("gradient_background")) gradient_background_ = j["gradient_background"].get<bool>();
|
loadScalar(j, "noise_opacity", noise_opacity_);
|
||||||
|
loadScalar(j, "gradient_background", gradient_background_);
|
||||||
// Migrate legacy reduced_transparency bool -> ui_opacity float
|
// Migrate legacy reduced_transparency bool -> ui_opacity float
|
||||||
if (j.contains("ui_opacity")) {
|
if (j.contains("ui_opacity")) {
|
||||||
ui_opacity_ = j["ui_opacity"].get<float>();
|
ui_opacity_ = j["ui_opacity"].get<float>();
|
||||||
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) {
|
} else if (j.contains("reduced_transparency") && j["reduced_transparency"].get<bool>()) {
|
||||||
ui_opacity_ = 1.0f; // legacy: reduced = fully opaque
|
ui_opacity_ = 1.0f; // legacy: reduced = fully opaque
|
||||||
}
|
}
|
||||||
if (j.contains("window_opacity")) window_opacity_ = j["window_opacity"].get<float>();
|
loadScalar(j, "window_opacity", window_opacity_);
|
||||||
if (j.contains("balance_layout")) {
|
if (j.contains("balance_layout")) {
|
||||||
if (j["balance_layout"].is_string())
|
if (j["balance_layout"].is_string())
|
||||||
balance_layout_ = j["balance_layout"].get<std::string>();
|
balance_layout_ = j["balance_layout"].get<std::string>();
|
||||||
@@ -115,7 +172,7 @@ bool Settings::load(const std::string& path)
|
|||||||
if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
|
if (idx >= 0 && idx < 9) balance_layout_ = legacyIds[idx];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (j.contains("scanline_enabled")) scanline_enabled_ = j["scanline_enabled"].get<bool>();
|
loadScalar(j, "scanline_enabled", scanline_enabled_);
|
||||||
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
|
if (j.contains("hidden_addresses") && j["hidden_addresses"].is_array()) {
|
||||||
hidden_addresses_.clear();
|
hidden_addresses_.clear();
|
||||||
for (const auto& a : j["hidden_addresses"])
|
for (const auto& a : j["hidden_addresses"])
|
||||||
@@ -136,42 +193,108 @@ bool Settings::load(const std::string& path)
|
|||||||
m.icon = meta["icon"].get<std::string>();
|
m.icon = meta["icon"].get<std::string>();
|
||||||
if (meta.contains("order") && meta["order"].is_number_integer())
|
if (meta.contains("order") && meta["order"].is_number_integer())
|
||||||
m.sortOrder = meta["order"].get<int>();
|
m.sortOrder = meta["order"].get<int>();
|
||||||
|
if (meta.contains("mining") && meta["mining"].is_boolean())
|
||||||
|
m.mining = meta["mining"].get<bool>();
|
||||||
address_meta_[addr] = m;
|
address_meta_[addr] = m;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get<bool>();
|
loadScalar(j, "wizard_completed", wizard_completed_);
|
||||||
if (j.contains("auto_lock_timeout")) auto_lock_timeout_ = j["auto_lock_timeout"].get<int>();
|
loadScalar(j, "auto_lock_timeout", auto_lock_timeout_);
|
||||||
if (j.contains("unlock_duration")) unlock_duration_ = j["unlock_duration"].get<int>();
|
loadScalar(j, "unlock_duration", unlock_duration_);
|
||||||
if (j.contains("pin_enabled")) pin_enabled_ = j["pin_enabled"].get<bool>();
|
loadScalar(j, "pin_enabled", pin_enabled_);
|
||||||
if (j.contains("keep_daemon_running")) keep_daemon_running_ = j["keep_daemon_running"].get<bool>();
|
loadScalar(j, "keep_daemon_running", keep_daemon_running_);
|
||||||
if (j.contains("stop_external_daemon")) stop_external_daemon_ = j["stop_external_daemon"].get<bool>();
|
loadScalar(j, "stop_external_daemon", stop_external_daemon_);
|
||||||
if (j.contains("max_connections")) max_connections_ = j["max_connections"].get<int>();
|
loadScalar(j, "max_connections", max_connections_);
|
||||||
if (j.contains("verbose_logging")) verbose_logging_ = j["verbose_logging"].get<bool>();
|
if (j.contains("lite_wallet") && j["lite_wallet"].is_object()) {
|
||||||
|
const auto& lite = j["lite_wallet"];
|
||||||
|
if (lite.contains("server_selection_mode")) {
|
||||||
|
lite_server_selection_mode_ = parseLiteServerSelectionPreferenceMode(
|
||||||
|
lite["server_selection_mode"]);
|
||||||
|
}
|
||||||
|
if (lite.contains("sticky_server_url") && lite["sticky_server_url"].is_string()) {
|
||||||
|
lite_sticky_server_url_ = lite["sticky_server_url"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (lite.contains("chain_name") && lite["chain_name"].is_string()) {
|
||||||
|
lite_chain_name_ = lite["chain_name"].get<std::string>();
|
||||||
|
}
|
||||||
|
// Migration: the SDXL backend only accepts main/test/regtest and hard-panics
|
||||||
|
// (process abort) on any other chain name. Older builds persisted the "DRAGONX"
|
||||||
|
// ticker here, which crashed the lite backend on launch. Rewrite any invalid
|
||||||
|
// value to "main" and flag a re-save so the corrected setting persists.
|
||||||
|
if (lite_chain_name_ != "main" && lite_chain_name_ != "test" &&
|
||||||
|
lite_chain_name_ != "regtest") {
|
||||||
|
lite_chain_name_ = "main";
|
||||||
|
needs_upgrade_save_ = true;
|
||||||
|
}
|
||||||
|
if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_unsigned()) {
|
||||||
|
lite_random_selection_seed_ = lite["random_selection_seed"].get<std::size_t>();
|
||||||
|
} else if (lite.contains("random_selection_seed") && lite["random_selection_seed"].is_number_integer()) {
|
||||||
|
const auto seed = lite["random_selection_seed"].get<long long>();
|
||||||
|
lite_random_selection_seed_ = seed > 0 ? static_cast<std::size_t>(seed) : 0;
|
||||||
|
}
|
||||||
|
if (lite.contains("persist_selected_server") && lite["persist_selected_server"].is_boolean()) {
|
||||||
|
lite_persist_selected_server_ = lite["persist_selected_server"].get<bool>();
|
||||||
|
}
|
||||||
|
if (lite.contains("servers") && lite["servers"].is_array()) {
|
||||||
|
lite_servers_.clear();
|
||||||
|
for (const auto& server : lite["servers"]) {
|
||||||
|
if (!server.is_object()) continue;
|
||||||
|
LiteServerPreference preference;
|
||||||
|
if (server.contains("url") && server["url"].is_string()) {
|
||||||
|
preference.url = server["url"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (server.contains("label") && server["label"].is_string()) {
|
||||||
|
preference.label = server["label"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (server.contains("enabled") && server["enabled"].is_boolean()) {
|
||||||
|
preference.enabled = server["enabled"].get<bool>();
|
||||||
|
}
|
||||||
|
lite_servers_.push_back(preference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lite.contains("rollout_override") && lite["rollout_override"].is_string()) {
|
||||||
|
const auto v = lite["rollout_override"].get<std::string>();
|
||||||
|
lite_rollout_override_ = (v == "force_on" || v == "force_off") ? v : "auto";
|
||||||
|
}
|
||||||
|
if (lite.contains("install_id") && lite["install_id"].is_string()) {
|
||||||
|
lite_install_id_ = lite["install_id"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (lite.contains("hidden_servers") && lite["hidden_servers"].is_array()) {
|
||||||
|
lite_hidden_servers_.clear();
|
||||||
|
for (const auto& u : lite["hidden_servers"])
|
||||||
|
if (u.is_string()) lite_hidden_servers_.insert(u.get<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadScalar(j, "verbose_logging", verbose_logging_);
|
||||||
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
|
if (j.contains("debug_categories") && j["debug_categories"].is_array()) {
|
||||||
debug_categories_.clear();
|
debug_categories_.clear();
|
||||||
for (const auto& c : j["debug_categories"])
|
for (const auto& c : j["debug_categories"])
|
||||||
if (c.is_string()) debug_categories_.insert(c.get<std::string>());
|
if (c.is_string()) debug_categories_.insert(c.get<std::string>());
|
||||||
}
|
}
|
||||||
if (j.contains("theme_effects_enabled")) theme_effects_enabled_ = j["theme_effects_enabled"].get<bool>();
|
loadScalar(j, "theme_effects_enabled", theme_effects_enabled_);
|
||||||
if (j.contains("low_spec_mode")) low_spec_mode_ = j["low_spec_mode"].get<bool>();
|
loadScalar(j, "low_spec_mode", low_spec_mode_);
|
||||||
if (j.contains("reduce_motion")) reduce_motion_ = j["reduce_motion"].get<bool>();
|
loadScalar(j, "reduce_motion", reduce_motion_);
|
||||||
if (j.contains("selected_exchange")) selected_exchange_ = j["selected_exchange"].get<std::string>();
|
loadScalar(j, "selected_exchange", selected_exchange_);
|
||||||
if (j.contains("selected_pair")) selected_pair_ = j["selected_pair"].get<std::string>();
|
loadScalar(j, "selected_pair", selected_pair_);
|
||||||
if (j.contains("pool_url")) pool_url_ = j["pool_url"].get<std::string>();
|
loadScalar(j, "pool_url", pool_url_);
|
||||||
// Migrate old default pool URL that was missing the stratum port
|
// Migrate old default pool URL that was missing the stratum port
|
||||||
if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
|
if (pool_url_ == "pool.dragonx.is") pool_url_ = "pool.dragonx.is:3433";
|
||||||
if (j.contains("pool_algo")) pool_algo_ = j["pool_algo"].get<std::string>();
|
loadScalar(j, "pool_algo", pool_algo_);
|
||||||
if (j.contains("pool_worker")) pool_worker_ = j["pool_worker"].get<std::string>();
|
loadScalar(j, "pool_worker", pool_worker_);
|
||||||
if (j.contains("pool_threads")) pool_threads_ = j["pool_threads"].get<int>();
|
loadScalar(j, "pool_threads", pool_threads_);
|
||||||
if (j.contains("pool_tls")) pool_tls_ = j["pool_tls"].get<bool>();
|
loadScalar(j, "pool_tls", pool_tls_);
|
||||||
if (j.contains("pool_hugepages")) pool_hugepages_ = j["pool_hugepages"].get<bool>();
|
loadScalar(j, "pool_hugepages", pool_hugepages_);
|
||||||
if (j.contains("pool_mode")) pool_mode_ = j["pool_mode"].get<bool>();
|
loadScalar(j, "pool_mode", pool_mode_);
|
||||||
if (j.contains("mine_when_idle")) mine_when_idle_ = j["mine_when_idle"].get<bool>();
|
if (j.contains("pool_select_mode")) pool_select_mode_ = parsePoolSelectMode(j["pool_select_mode"]);
|
||||||
if (j.contains("mine_idle_delay")) mine_idle_delay_= std::max(30, j["mine_idle_delay"].get<int>());
|
loadScalar(j, "mine_when_idle", mine_when_idle_);
|
||||||
if (j.contains("idle_thread_scaling")) idle_thread_scaling_ = j["idle_thread_scaling"].get<bool>();
|
loadScalar(j, "xmrig_version", xmrig_version_);
|
||||||
if (j.contains("idle_threads_active")) idle_threads_active_ = j["idle_threads_active"].get<int>();
|
// Lower-bounded only (min 30s), matching setMineIdleDelay; now type-guarded too.
|
||||||
if (j.contains("idle_threads_idle")) idle_threads_idle_ = j["idle_threads_idle"].get<int>();
|
if (j.contains("mine_idle_delay") && j["mine_idle_delay"].is_number_integer())
|
||||||
if (j.contains("idle_gpu_aware")) idle_gpu_aware_ = j["idle_gpu_aware"].get<bool>();
|
mine_idle_delay_ = std::max(30, j["mine_idle_delay"].get<int>());
|
||||||
|
loadScalar(j, "idle_thread_scaling", idle_thread_scaling_);
|
||||||
|
loadScalar(j, "idle_threads_active", idle_threads_active_);
|
||||||
|
loadScalar(j, "idle_threads_idle", idle_threads_idle_);
|
||||||
|
loadScalar(j, "idle_gpu_aware", idle_gpu_aware_);
|
||||||
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
|
if (j.contains("saved_pool_urls") && j["saved_pool_urls"].is_array()) {
|
||||||
saved_pool_urls_.clear();
|
saved_pool_urls_.clear();
|
||||||
for (const auto& u : j["saved_pool_urls"])
|
for (const auto& u : j["saved_pool_urls"])
|
||||||
@@ -182,15 +305,55 @@ bool Settings::load(const std::string& path)
|
|||||||
for (const auto& w : j["saved_pool_workers"])
|
for (const auto& w : j["saved_pool_workers"])
|
||||||
if (w.is_string()) saved_pool_workers_.push_back(w.get<std::string>());
|
if (w.is_string()) saved_pool_workers_.push_back(w.get<std::string>());
|
||||||
}
|
}
|
||||||
if (j.contains("font_scale") && j["font_scale"].is_number())
|
if (j.contains("portfolio_entries") && j["portfolio_entries"].is_array()) {
|
||||||
font_scale_ = std::max(1.0f, std::min(1.5f, j["font_scale"].get<float>()));
|
portfolio_entries_.clear();
|
||||||
if (j.contains("window_width") && j["window_width"].is_number_integer())
|
for (const auto& e : j["portfolio_entries"]) {
|
||||||
window_width_ = j["window_width"].get<int>();
|
if (!e.is_object()) continue;
|
||||||
if (j.contains("window_height") && j["window_height"].is_number_integer())
|
PortfolioEntry entry;
|
||||||
window_height_ = j["window_height"].get<int>();
|
if (e.contains("label") && e["label"].is_string())
|
||||||
|
entry.label = e["label"].get<std::string>();
|
||||||
|
if (e.contains("addresses") && e["addresses"].is_array())
|
||||||
|
for (const auto& a : e["addresses"])
|
||||||
|
if (a.is_string()) entry.addresses.push_back(a.get<std::string>());
|
||||||
|
if (e.contains("icon") && e["icon"].is_string())
|
||||||
|
entry.icon = e["icon"].get<std::string>();
|
||||||
|
if (e.contains("color") && e["color"].is_number())
|
||||||
|
entry.color = e["color"].get<unsigned int>();
|
||||||
|
if (e.contains("outline_opacity") && e["outline_opacity"].is_number_integer())
|
||||||
|
entry.outlineOpacity = e["outline_opacity"].get<int>();
|
||||||
|
if (e.contains("price_basis") && e["price_basis"].is_number_integer())
|
||||||
|
entry.priceBasis = e["price_basis"].get<int>();
|
||||||
|
if (e.contains("manual_price") && e["manual_price"].is_number())
|
||||||
|
entry.manualPrice = e["manual_price"].get<double>();
|
||||||
|
if (e.contains("manual_currency") && e["manual_currency"].is_string())
|
||||||
|
entry.manualCurrency = e["manual_currency"].get<std::string>();
|
||||||
|
if (e.contains("show_drgx") && e["show_drgx"].is_boolean())
|
||||||
|
entry.showDrgx = e["show_drgx"].get<bool>();
|
||||||
|
if (e.contains("show_value") && e["show_value"].is_boolean())
|
||||||
|
entry.showValue = e["show_value"].get<bool>();
|
||||||
|
if (e.contains("show_24h") && e["show_24h"].is_boolean())
|
||||||
|
entry.show24h = e["show_24h"].get<bool>();
|
||||||
|
if (e.contains("show_sparkline") && e["show_sparkline"].is_boolean())
|
||||||
|
entry.showSparkline = e["show_sparkline"].get<bool>();
|
||||||
|
if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer())
|
||||||
|
entry.sparklineInterval = e["sparkline_interval"].get<int>();
|
||||||
|
if (e.contains("grid_col") && e["grid_col"].is_number_integer())
|
||||||
|
entry.gridCol = e["grid_col"].get<int>();
|
||||||
|
if (e.contains("grid_row") && e["grid_row"].is_number_integer())
|
||||||
|
entry.gridRow = e["grid_row"].get<int>();
|
||||||
|
if (e.contains("grid_w") && e["grid_w"].is_number_integer())
|
||||||
|
entry.gridW = e["grid_w"].get<int>();
|
||||||
|
if (e.contains("grid_h") && e["grid_h"].is_number_integer())
|
||||||
|
entry.gridH = e["grid_h"].get<int>();
|
||||||
|
if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadClamped(j, "font_scale", font_scale_, 1.0f, 1.5f);
|
||||||
|
loadScalar(j, "window_width", window_width_);
|
||||||
|
loadScalar(j, "window_height", window_height_);
|
||||||
|
|
||||||
// Version tracking — detect upgrades so we can re-save with new defaults
|
// Version tracking — detect upgrades so we can re-save with new defaults
|
||||||
if (j.contains("settings_version")) settings_version_ = j["settings_version"].get<std::string>();
|
loadScalar(j, "settings_version", settings_version_);
|
||||||
if (settings_version_ != DRAGONX_VERSION) {
|
if (settings_version_ != DRAGONX_VERSION) {
|
||||||
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
|
DEBUG_LOGF("Settings version %s differs from wallet %s — will re-save\n",
|
||||||
settings_version_.empty() ? "(none)" : settings_version_.c_str(),
|
settings_version_.empty() ? "(none)" : settings_version_.c_str(),
|
||||||
@@ -201,6 +364,17 @@ bool Settings::load(const std::string& path)
|
|||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
|
DEBUG_LOGF("Failed to parse settings: %s\n", e.what());
|
||||||
|
// The file exists but is unparseable (truncated/corrupt). Quarantine it so the
|
||||||
|
// next save() doesn't silently overwrite it with defaults — the user's data stays
|
||||||
|
// recoverable. Proceed with in-memory defaults.
|
||||||
|
file.close();
|
||||||
|
std::error_code ec;
|
||||||
|
const std::string quarantine =
|
||||||
|
path + ".corrupt-" + std::to_string(static_cast<long long>(std::time(nullptr)));
|
||||||
|
fs::rename(path, quarantine, ec);
|
||||||
|
if (!ec) {
|
||||||
|
DEBUG_LOGF("Quarantined corrupt settings to %s\n", quarantine.c_str());
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,6 +402,7 @@ bool Settings::save(const std::string& path)
|
|||||||
j["address_explorer_url"] = address_explorer_url_;
|
j["address_explorer_url"] = address_explorer_url_;
|
||||||
j["language"] = language_;
|
j["language"] = language_;
|
||||||
j["skin_id"] = skin_id_;
|
j["skin_id"] = skin_id_;
|
||||||
|
j["chat_reply_zaddr"] = chat_reply_zaddr_;
|
||||||
j["acrylic_enabled"] = acrylic_enabled_;
|
j["acrylic_enabled"] = acrylic_enabled_;
|
||||||
j["acrylic_quality"] = acrylic_quality_;
|
j["acrylic_quality"] = acrylic_quality_;
|
||||||
j["blur_multiplier"] = blur_multiplier_;
|
j["blur_multiplier"] = blur_multiplier_;
|
||||||
@@ -246,11 +421,12 @@ bool Settings::save(const std::string& path)
|
|||||||
{
|
{
|
||||||
json meta_obj = json::object();
|
json meta_obj = json::object();
|
||||||
for (const auto& [addr, m] : address_meta_) {
|
for (const auto& [addr, m] : address_meta_) {
|
||||||
if (m.label.empty() && m.icon.empty() && m.sortOrder < 0) continue;
|
if (m.label.empty() && m.icon.empty() && m.sortOrder < 0 && !m.mining) continue;
|
||||||
json entry = json::object();
|
json entry = json::object();
|
||||||
if (!m.label.empty()) entry["label"] = m.label;
|
if (!m.label.empty()) entry["label"] = m.label;
|
||||||
if (!m.icon.empty()) entry["icon"] = m.icon;
|
if (!m.icon.empty()) entry["icon"] = m.icon;
|
||||||
if (m.sortOrder >= 0) entry["order"] = m.sortOrder;
|
if (m.sortOrder >= 0) entry["order"] = m.sortOrder;
|
||||||
|
if (m.mining) entry["mining"] = true;
|
||||||
meta_obj[addr] = entry;
|
meta_obj[addr] = entry;
|
||||||
}
|
}
|
||||||
j["address_meta"] = meta_obj;
|
j["address_meta"] = meta_obj;
|
||||||
@@ -262,6 +438,27 @@ bool Settings::save(const std::string& path)
|
|||||||
j["keep_daemon_running"] = keep_daemon_running_;
|
j["keep_daemon_running"] = keep_daemon_running_;
|
||||||
j["stop_external_daemon"] = stop_external_daemon_;
|
j["stop_external_daemon"] = stop_external_daemon_;
|
||||||
j["max_connections"] = max_connections_;
|
j["max_connections"] = max_connections_;
|
||||||
|
{
|
||||||
|
json lite = json::object();
|
||||||
|
lite["server_selection_mode"] = liteServerSelectionPreferenceModeName(lite_server_selection_mode_);
|
||||||
|
lite["sticky_server_url"] = lite_sticky_server_url_;
|
||||||
|
lite["chain_name"] = lite_chain_name_;
|
||||||
|
lite["random_selection_seed"] = lite_random_selection_seed_;
|
||||||
|
lite["persist_selected_server"] = lite_persist_selected_server_;
|
||||||
|
lite["servers"] = json::array();
|
||||||
|
for (const auto& server : lite_servers_) {
|
||||||
|
json entry = json::object();
|
||||||
|
entry["url"] = server.url;
|
||||||
|
entry["label"] = server.label;
|
||||||
|
entry["enabled"] = server.enabled;
|
||||||
|
lite["servers"].push_back(entry);
|
||||||
|
}
|
||||||
|
lite["rollout_override"] = lite_rollout_override_;
|
||||||
|
lite["install_id"] = lite_install_id_;
|
||||||
|
lite["hidden_servers"] = json::array();
|
||||||
|
for (const auto& u : lite_hidden_servers_) lite["hidden_servers"].push_back(u);
|
||||||
|
j["lite_wallet"] = lite;
|
||||||
|
}
|
||||||
j["verbose_logging"] = verbose_logging_;
|
j["verbose_logging"] = verbose_logging_;
|
||||||
j["debug_categories"] = json::array();
|
j["debug_categories"] = json::array();
|
||||||
for (const auto& cat : debug_categories_)
|
for (const auto& cat : debug_categories_)
|
||||||
@@ -278,7 +475,9 @@ bool Settings::save(const std::string& path)
|
|||||||
j["pool_tls"] = pool_tls_;
|
j["pool_tls"] = pool_tls_;
|
||||||
j["pool_hugepages"] = pool_hugepages_;
|
j["pool_hugepages"] = pool_hugepages_;
|
||||||
j["pool_mode"] = pool_mode_;
|
j["pool_mode"] = pool_mode_;
|
||||||
|
j["pool_select_mode"] = poolSelectModeName(pool_select_mode_);
|
||||||
j["mine_when_idle"] = mine_when_idle_;
|
j["mine_when_idle"] = mine_when_idle_;
|
||||||
|
j["xmrig_version"] = xmrig_version_;
|
||||||
j["mine_idle_delay"]= mine_idle_delay_;
|
j["mine_idle_delay"]= mine_idle_delay_;
|
||||||
j["idle_thread_scaling"] = idle_thread_scaling_;
|
j["idle_thread_scaling"] = idle_thread_scaling_;
|
||||||
j["idle_threads_active"] = idle_threads_active_;
|
j["idle_threads_active"] = idle_threads_active_;
|
||||||
@@ -290,6 +489,29 @@ bool Settings::save(const std::string& path)
|
|||||||
j["saved_pool_workers"] = json::array();
|
j["saved_pool_workers"] = json::array();
|
||||||
for (const auto& w : saved_pool_workers_)
|
for (const auto& w : saved_pool_workers_)
|
||||||
j["saved_pool_workers"].push_back(w);
|
j["saved_pool_workers"].push_back(w);
|
||||||
|
j["portfolio_entries"] = json::array();
|
||||||
|
for (const auto& e : portfolio_entries_) {
|
||||||
|
json entry;
|
||||||
|
entry["label"] = e.label;
|
||||||
|
entry["addresses"] = json::array();
|
||||||
|
for (const auto& a : e.addresses) entry["addresses"].push_back(a);
|
||||||
|
entry["icon"] = e.icon;
|
||||||
|
entry["color"] = e.color;
|
||||||
|
entry["outline_opacity"] = e.outlineOpacity;
|
||||||
|
entry["price_basis"] = e.priceBasis;
|
||||||
|
entry["manual_price"] = e.manualPrice;
|
||||||
|
entry["manual_currency"] = e.manualCurrency;
|
||||||
|
entry["show_drgx"] = e.showDrgx;
|
||||||
|
entry["show_value"] = e.showValue;
|
||||||
|
entry["show_24h"] = e.show24h;
|
||||||
|
entry["show_sparkline"] = e.showSparkline;
|
||||||
|
entry["sparkline_interval"] = e.sparklineInterval;
|
||||||
|
entry["grid_col"] = e.gridCol;
|
||||||
|
entry["grid_row"] = e.gridRow;
|
||||||
|
entry["grid_w"] = e.gridW;
|
||||||
|
entry["grid_h"] = e.gridH;
|
||||||
|
j["portfolio_entries"].push_back(std::move(entry));
|
||||||
|
}
|
||||||
j["font_scale"] = font_scale_;
|
j["font_scale"] = font_scale_;
|
||||||
j["settings_version"] = std::string(DRAGONX_VERSION);
|
j["settings_version"] = std::string(DRAGONX_VERSION);
|
||||||
if (window_width_ > 0 && window_height_ > 0) {
|
if (window_width_ > 0 && window_height_ > 0) {
|
||||||
@@ -298,17 +520,11 @@ bool Settings::save(const std::string& path)
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure directory exists
|
// Atomic + durable: write to a temp file, fsync, then rename over the real file.
|
||||||
fs::path p(path);
|
// A crash mid-write can no longer truncate settings.json (which would silently
|
||||||
fs::create_directories(p.parent_path());
|
// reset every preference on the next launch). Owner-only (0600) — it carries the
|
||||||
|
// lite-server list and address metadata.
|
||||||
std::ofstream file(path);
|
return util::Platform::writeFileAtomically(path, j.dump(4), /*restrictPermissions=*/true);
|
||||||
if (!file.is_open()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
file << j.dump(4);
|
|
||||||
return true;
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
DEBUG_LOGF("Failed to save settings: %s\n", e.what());
|
DEBUG_LOGF("Failed to save settings: %s\n", e.what());
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <set>
|
#include <set>
|
||||||
@@ -54,6 +55,51 @@ public:
|
|||||||
*/
|
*/
|
||||||
static std::string getDefaultPath();
|
static std::string getDefaultPath();
|
||||||
|
|
||||||
|
enum class LiteServerSelectionPreferenceMode {
|
||||||
|
Sticky,
|
||||||
|
Random
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pool selection mode for the mining tab: Manual (user picks the pool) or
|
||||||
|
// AutoBalance (the wallet spreads miners across the official pools by hashrate).
|
||||||
|
enum class PoolSelectMode {
|
||||||
|
Manual,
|
||||||
|
AutoBalance
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LiteServerPreference {
|
||||||
|
std::string url;
|
||||||
|
std::string label;
|
||||||
|
bool enabled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A user-defined portfolio entry: a custom label tied to a group of wallet addresses.
|
||||||
|
// The Market tab's portfolio card sums these addresses' balances under the label.
|
||||||
|
struct PortfolioEntry {
|
||||||
|
std::string label;
|
||||||
|
std::vector<std::string> addresses;
|
||||||
|
std::string icon; // project_icons wallet-icon name; empty = no icon
|
||||||
|
unsigned int color = 0; // packed IM_COL32 accent; 0 = theme default
|
||||||
|
int outlineOpacity = 25; // accent-outline opacity, percent (0-100)
|
||||||
|
// Per-group price data. priceBasis: 0 = live market (USD), 1 = live market (BTC),
|
||||||
|
// 2 = DRGX only (no fiat value), 3 = manual price. Defaults preserve prior behavior
|
||||||
|
// (show DRGX + USD value).
|
||||||
|
int priceBasis = 0;
|
||||||
|
double manualPrice = 0.0; // price per DRGX for the Manual basis
|
||||||
|
std::string manualCurrency = "USD";
|
||||||
|
bool showDrgx = true; // show the DRGX amount on the card
|
||||||
|
bool showValue = true; // show the converted/fiat value on the card
|
||||||
|
bool show24h = false; // show the 24h % change (live-market bases only)
|
||||||
|
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
|
||||||
|
int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history)
|
||||||
|
// Dashboard grid placement on the Market portfolio. col/row in fine ~32px square cells
|
||||||
|
// (-1 = auto-place), w/h in cells (span). Set when the user drags/resizes a card.
|
||||||
|
int gridCol = -1;
|
||||||
|
int gridRow = -1;
|
||||||
|
int gridW = 8; // default group width (cells); minimum is 8
|
||||||
|
int gridH = 3; // default group height (cells); minimum is 2, card design adapts
|
||||||
|
};
|
||||||
|
|
||||||
// Theme
|
// Theme
|
||||||
std::string getTheme() const { return theme_; }
|
std::string getTheme() const { return theme_; }
|
||||||
void setTheme(const std::string& theme) { theme_ = theme; }
|
void setTheme(const std::string& theme) { theme_ = theme; }
|
||||||
@@ -62,6 +108,12 @@ public:
|
|||||||
std::string getSkinId() const { return skin_id_; }
|
std::string getSkinId() const { return skin_id_; }
|
||||||
void setSkinId(const std::string& id) { skin_id_ = id; }
|
void setSkinId(const std::string& id) { skin_id_ = id; }
|
||||||
|
|
||||||
|
// Stable z-address chosen for HushChat: the reply-to address in outgoing headers, and (for
|
||||||
|
// legacy/non-mnemonic wallets) the source of the seed-derived chat identity. Persisted so the
|
||||||
|
// identity + reply address don't shift when new addresses are generated.
|
||||||
|
std::string getChatReplyZaddr() const { return chat_reply_zaddr_; }
|
||||||
|
void setChatReplyZaddr(const std::string& z) { chat_reply_zaddr_ = z; }
|
||||||
|
|
||||||
// Privacy
|
// Privacy
|
||||||
bool getSaveZtxs() const { return save_ztxs_; }
|
bool getSaveZtxs() const { return save_ztxs_; }
|
||||||
void setSaveZtxs(bool save) { save_ztxs_ = save; }
|
void setSaveZtxs(bool save) { save_ztxs_ = save; }
|
||||||
@@ -147,6 +199,7 @@ public:
|
|||||||
std::string label;
|
std::string label;
|
||||||
std::string icon; // material icon name, e.g. "savings"
|
std::string icon; // material icon name, e.g. "savings"
|
||||||
int sortOrder = -1; // -1 = auto (use default sort)
|
int sortOrder = -1; // -1 = auto (use default sort)
|
||||||
|
bool mining = false;
|
||||||
};
|
};
|
||||||
const AddressMeta& getAddressMeta(const std::string& addr) const {
|
const AddressMeta& getAddressMeta(const std::string& addr) const {
|
||||||
static const AddressMeta empty{};
|
static const AddressMeta empty{};
|
||||||
@@ -162,6 +215,20 @@ public:
|
|||||||
void setAddressSortOrder(const std::string& addr, int order) {
|
void setAddressSortOrder(const std::string& addr, int order) {
|
||||||
address_meta_[addr].sortOrder = order;
|
address_meta_[addr].sortOrder = order;
|
||||||
}
|
}
|
||||||
|
bool isMiningAddress(const std::string& addr) const {
|
||||||
|
auto it = address_meta_.find(addr);
|
||||||
|
return it != address_meta_.end() && it->second.mining;
|
||||||
|
}
|
||||||
|
void setMiningAddress(const std::string& addr, bool mining) {
|
||||||
|
address_meta_[addr].mining = mining;
|
||||||
|
}
|
||||||
|
std::set<std::string> getMiningAddresses() const {
|
||||||
|
std::set<std::string> addresses;
|
||||||
|
for (const auto& [addr, meta] : address_meta_) {
|
||||||
|
if (meta.mining) addresses.insert(addr);
|
||||||
|
}
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
int getNextSortOrder() const {
|
int getNextSortOrder() const {
|
||||||
int mx = -1;
|
int mx = -1;
|
||||||
for (const auto& [k, v] : address_meta_)
|
for (const auto& [k, v] : address_meta_)
|
||||||
@@ -203,6 +270,39 @@ public:
|
|||||||
int getMaxConnections() const { return max_connections_; }
|
int getMaxConnections() const { return max_connections_; }
|
||||||
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
|
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
|
||||||
|
|
||||||
|
// Lite wallet server selection
|
||||||
|
LiteServerSelectionPreferenceMode getLiteServerSelectionMode() const { return lite_server_selection_mode_; }
|
||||||
|
void setLiteServerSelectionMode(LiteServerSelectionPreferenceMode mode) { lite_server_selection_mode_ = mode; }
|
||||||
|
std::string getLiteStickyServerUrl() const { return lite_sticky_server_url_; }
|
||||||
|
void setLiteStickyServerUrl(const std::string& url) { lite_sticky_server_url_ = url; }
|
||||||
|
std::string getLiteChainName() const { return lite_chain_name_; }
|
||||||
|
void setLiteChainName(const std::string& chainName) { lite_chain_name_ = chainName; }
|
||||||
|
std::size_t getLiteRandomSelectionSeed() const { return lite_random_selection_seed_; }
|
||||||
|
void setLiteRandomSelectionSeed(std::size_t seed) { lite_random_selection_seed_ = seed; }
|
||||||
|
bool getLitePersistSelectedServer() const { return lite_persist_selected_server_; }
|
||||||
|
void setLitePersistSelectedServer(bool persist) { lite_persist_selected_server_ = persist; }
|
||||||
|
const std::vector<LiteServerPreference>& getLiteServers() const { return lite_servers_; }
|
||||||
|
void setLiteServers(const std::vector<LiteServerPreference>& servers) { lite_servers_ = servers; }
|
||||||
|
|
||||||
|
// User-defined portfolio entries (Market tab). "All funds" is implicit, not stored here.
|
||||||
|
const std::vector<PortfolioEntry>& getPortfolioEntries() const { return portfolio_entries_; }
|
||||||
|
void setPortfolioEntries(const std::vector<PortfolioEntry>& entries) { portfolio_entries_ = entries; }
|
||||||
|
|
||||||
|
// Lite servers the user has hidden from the Network tab (kept by URL, shown via a toggle).
|
||||||
|
const std::set<std::string>& getLiteHiddenServers() const { return lite_hidden_servers_; }
|
||||||
|
bool isLiteServerHidden(const std::string& url) const { return lite_hidden_servers_.count(url) > 0; }
|
||||||
|
void hideLiteServer(const std::string& url) { lite_hidden_servers_.insert(url); }
|
||||||
|
void unhideLiteServer(const std::string& url) { lite_hidden_servers_.erase(url); }
|
||||||
|
|
||||||
|
// Lite wallet rollout / kill-switch (see wallet/lite_rollout_policy.h).
|
||||||
|
// Override: "auto" (honor rollout manifest), "force_on", or "force_off".
|
||||||
|
std::string getLiteRolloutOverride() const { return lite_rollout_override_; }
|
||||||
|
void setLiteRolloutOverride(const std::string& v) { lite_rollout_override_ = v; }
|
||||||
|
// Stable, locally-generated install id used only to derive the staged-rollout bucket.
|
||||||
|
// Never transmitted; carries no PII. Generated on first use if empty.
|
||||||
|
std::string getLiteInstallId() const { return lite_install_id_; }
|
||||||
|
void setLiteInstallId(const std::string& v) { lite_install_id_ = v; }
|
||||||
|
|
||||||
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
|
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
|
||||||
bool getVerboseLogging() const { return verbose_logging_; }
|
bool getVerboseLogging() const { return verbose_logging_; }
|
||||||
void setVerboseLogging(bool v) { verbose_logging_ = v; }
|
void setVerboseLogging(bool v) { verbose_logging_ = v; }
|
||||||
@@ -249,6 +349,12 @@ public:
|
|||||||
void setPoolHugepages(bool v) { pool_hugepages_ = v; }
|
void setPoolHugepages(bool v) { pool_hugepages_ = v; }
|
||||||
bool getPoolMode() const { return pool_mode_; }
|
bool getPoolMode() const { return pool_mode_; }
|
||||||
void setPoolMode(bool v) { pool_mode_ = v; }
|
void setPoolMode(bool v) { pool_mode_ = v; }
|
||||||
|
PoolSelectMode getPoolSelectMode() const { return pool_select_mode_; }
|
||||||
|
void setPoolSelectMode(PoolSelectMode v) { pool_select_mode_ = v; }
|
||||||
|
|
||||||
|
// Installed DRG-XMRig release tag (for in-app miner update detection); empty if unknown/bundled.
|
||||||
|
std::string getXmrigVersion() const { return xmrig_version_; }
|
||||||
|
void setXmrigVersion(const std::string& v) { xmrig_version_ = v; }
|
||||||
|
|
||||||
// Mine when idle (auto-start mining when system is idle)
|
// Mine when idle (auto-start mining when system is idle)
|
||||||
bool getMineWhenIdle() const { return mine_when_idle_; }
|
bool getMineWhenIdle() const { return mine_when_idle_; }
|
||||||
@@ -310,6 +416,7 @@ private:
|
|||||||
// Settings values
|
// Settings values
|
||||||
std::string theme_ = "dragonx";
|
std::string theme_ = "dragonx";
|
||||||
std::string skin_id_ = "dragonx";
|
std::string skin_id_ = "dragonx";
|
||||||
|
std::string chat_reply_zaddr_;
|
||||||
bool save_ztxs_ = true;
|
bool save_ztxs_ = true;
|
||||||
bool auto_shield_ = true;
|
bool auto_shield_ = true;
|
||||||
bool use_tor_ = false;
|
bool use_tor_ = false;
|
||||||
@@ -343,13 +450,35 @@ private:
|
|||||||
bool keep_daemon_running_ = false;
|
bool keep_daemon_running_ = false;
|
||||||
bool stop_external_daemon_ = false;
|
bool stop_external_daemon_ = false;
|
||||||
int max_connections_ = 0; // 0 = daemon default
|
int max_connections_ = 0; // 0 = daemon default
|
||||||
|
|
||||||
|
// Lite wallet server preferences. These are user/server settings only;
|
||||||
|
// wallet secrets, wallet files, and lifecycle state are never stored here.
|
||||||
|
LiteServerSelectionPreferenceMode lite_server_selection_mode_ = LiteServerSelectionPreferenceMode::Sticky;
|
||||||
|
std::string lite_sticky_server_url_ = "https://lite.dragonx.is";
|
||||||
|
std::string lite_chain_name_ = "main"; // SDXL backend chain id; must be main/test/regtest
|
||||||
|
std::size_t lite_random_selection_seed_ = 0;
|
||||||
|
bool lite_persist_selected_server_ = true;
|
||||||
|
std::string lite_rollout_override_ = "auto"; // auto|force_on|force_off
|
||||||
|
std::string lite_install_id_; // random local-only id; rollout-bucket source
|
||||||
|
std::vector<LiteServerPreference> lite_servers_ = {
|
||||||
|
{"https://lite.dragonx.is", "DragonX Lite", true},
|
||||||
|
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
|
||||||
|
{"https://lite2.dragonx.is", "DragonX Lite 2", true},
|
||||||
|
{"https://lite3.dragonx.is", "DragonX Lite 3", true},
|
||||||
|
{"https://lite4.dragonx.is", "DragonX Lite 4", true},
|
||||||
|
{"https://lite5.dragonx.is", "DragonX Lite 5", true}
|
||||||
|
};
|
||||||
|
std::set<std::string> lite_hidden_servers_; // server URLs hidden from the Network tab
|
||||||
|
|
||||||
|
std::vector<PortfolioEntry> portfolio_entries_; // Market tab custom portfolio groups
|
||||||
|
|
||||||
bool verbose_logging_ = false;
|
bool verbose_logging_ = false;
|
||||||
std::set<std::string> debug_categories_;
|
std::set<std::string> debug_categories_;
|
||||||
bool theme_effects_enabled_ = true;
|
bool theme_effects_enabled_ = true;
|
||||||
bool low_spec_mode_ = false;
|
bool low_spec_mode_ = false;
|
||||||
bool reduce_motion_ = false;
|
bool reduce_motion_ = false;
|
||||||
std::string selected_exchange_ = "TradeOgre";
|
std::string selected_exchange_ = "Nonkyc.io";
|
||||||
std::string selected_pair_ = "DRGX/BTC";
|
std::string selected_pair_ = "DRGX/USDT";
|
||||||
|
|
||||||
// Pool mining
|
// Pool mining
|
||||||
std::string pool_url_ = "pool.dragonx.is:3433";
|
std::string pool_url_ = "pool.dragonx.is:3433";
|
||||||
@@ -359,6 +488,8 @@ private:
|
|||||||
bool pool_tls_ = false;
|
bool pool_tls_ = false;
|
||||||
bool pool_hugepages_ = true;
|
bool pool_hugepages_ = true;
|
||||||
bool pool_mode_ = false; // false=solo, true=pool
|
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
|
bool mine_when_idle_ = false; // auto-start mining when system idle
|
||||||
int mine_idle_delay_= 120; // seconds of idle before mining starts
|
int mine_idle_delay_= 120; // seconds of idle before mining starts
|
||||||
bool idle_thread_scaling_ = false; // scale threads instead of start/stop
|
bool idle_thread_scaling_ = false; // scale threads instead of start/stop
|
||||||
|
|||||||
@@ -1,31 +1,3 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
#include "dragonx_generated_version.h"
|
||||||
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
|
|
||||||
|
|
||||||
#define DRAGONX_VERSION "1.2.0-rc1"
|
|
||||||
#define DRAGONX_VERSION_MAJOR 1
|
|
||||||
#define DRAGONX_VERSION_MINOR 2
|
|
||||||
#define DRAGONX_VERSION_PATCH 0
|
|
||||||
|
|
||||||
#define DRAGONX_APP_NAME "ObsidianDragon"
|
|
||||||
#define DRAGONX_ORG_NAME "Hush"
|
|
||||||
|
|
||||||
// Default RPC settings
|
|
||||||
#define DRAGONX_DEFAULT_RPC_HOST "127.0.0.1"
|
|
||||||
#define DRAGONX_DEFAULT_RPC_PORT "21769"
|
|
||||||
|
|
||||||
// Coin parameters
|
|
||||||
#define DRAGONX_TICKER "DRGX"
|
|
||||||
#define DRAGONX_COIN_NAME "DragonX"
|
|
||||||
#define DRAGONX_URI_SCHEME "drgx"
|
|
||||||
#define DRAGONX_ZATOSHI_PER_COIN 100000000
|
|
||||||
#define DRAGONX_DEFAULT_FEE 0.0001
|
|
||||||
|
|
||||||
// Config file names
|
|
||||||
#define DRAGONX_CONF_FILENAME "DRAGONX.conf"
|
|
||||||
#define DRAGONX_WALLET_FILENAME "wallet.dat"
|
|
||||||
|
|||||||
@@ -4,15 +4,16 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// !! DO NOT EDIT version.h — it is generated from version.h.in by CMake.
|
// !! DO NOT EDIT generated version output — it is generated from version.h.in by CMake.
|
||||||
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...)
|
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...) for the full-node app,
|
||||||
|
// !! or DRAGONX_LITE_VERSION for ObsidianDragonLite. DRAGONX_APP_VERSION is the active variant.
|
||||||
|
|
||||||
#define DRAGONX_VERSION "@PROJECT_VERSION@@DRAGONX_VERSION_SUFFIX@"
|
#define DRAGONX_VERSION "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@"
|
||||||
#define DRAGONX_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
|
#define DRAGONX_VERSION_MAJOR @DRAGONX_APP_VERSION_MAJOR@
|
||||||
#define DRAGONX_VERSION_MINOR @PROJECT_VERSION_MINOR@
|
#define DRAGONX_VERSION_MINOR @DRAGONX_APP_VERSION_MINOR@
|
||||||
#define DRAGONX_VERSION_PATCH @PROJECT_VERSION_PATCH@
|
#define DRAGONX_VERSION_PATCH @DRAGONX_APP_VERSION_PATCH@
|
||||||
|
|
||||||
#define DRAGONX_APP_NAME "ObsidianDragon"
|
#define DRAGONX_APP_NAME "@DRAGONX_APP_NAME@"
|
||||||
#define DRAGONX_ORG_NAME "Hush"
|
#define DRAGONX_ORG_NAME "Hush"
|
||||||
|
|
||||||
// Default RPC settings
|
// Default RPC settings
|
||||||
|
|||||||
128
src/daemon/daemon_controller.cpp
Normal file
128
src/daemon/daemon_controller.cpp
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
#include "daemon_controller.h"
|
||||||
|
#include "../config/settings.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace daemon {
|
||||||
|
|
||||||
|
DaemonController::DaemonController()
|
||||||
|
: daemon_(std::make_unique<EmbeddedDaemon>())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
DaemonController::~DaemonController() = default;
|
||||||
|
|
||||||
|
void DaemonController::setStateCallback(StateCallback callback)
|
||||||
|
{
|
||||||
|
daemon_->setStateCallback(std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonController::syncSettings(const config::Settings* settings)
|
||||||
|
{
|
||||||
|
if (!settings) return;
|
||||||
|
daemon_->setDebugCategories(settings->getDebugCategories());
|
||||||
|
daemon_->setMaxConnections(settings->getMaxConnections());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DaemonController::start(const config::Settings* settings)
|
||||||
|
{
|
||||||
|
syncSettings(settings);
|
||||||
|
return daemon_->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonController::stop(int waitMs)
|
||||||
|
{
|
||||||
|
daemon_->stop(waitMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DaemonController::isRunning() const
|
||||||
|
{
|
||||||
|
return daemon_->isRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DaemonController::externalDaemonDetected() const
|
||||||
|
{
|
||||||
|
return daemon_->externalDaemonDetected();
|
||||||
|
}
|
||||||
|
|
||||||
|
DaemonController::State DaemonController::state() const
|
||||||
|
{
|
||||||
|
return daemon_->getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& DaemonController::lastError() const
|
||||||
|
{
|
||||||
|
return daemon_->getLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
int DaemonController::crashCount() const
|
||||||
|
{
|
||||||
|
return daemon_->getCrashCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
int DaemonController::lastBlockHeight() const
|
||||||
|
{
|
||||||
|
return daemon_ ? daemon_->getLastBlockHeight() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double DaemonController::memoryUsageMB() const
|
||||||
|
{
|
||||||
|
return daemon_ ? daemon_->getMemoryUsageMB() : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> DaemonController::recentLines(std::size_t count) const
|
||||||
|
{
|
||||||
|
return daemon_ ? daemon_->getRecentLines(count) : std::vector<std::string>{};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DaemonController::outputSince(std::size_t& offset) const
|
||||||
|
{
|
||||||
|
return daemon_ ? daemon_->getOutputSince(offset) : std::string{};
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonController::resetCrashCount()
|
||||||
|
{
|
||||||
|
daemon_->resetCrashCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonController::setRescanOnNextStart(bool enabled)
|
||||||
|
{
|
||||||
|
daemon_->setRescanOnNextStart(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DaemonController::rescanOnNextStart() const
|
||||||
|
{
|
||||||
|
return daemon_->rescanOnNextStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonController::setZapOnNextStart(bool enabled)
|
||||||
|
{
|
||||||
|
daemon_->setZapOnNextStart(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DaemonController::zapOnNextStart() const
|
||||||
|
{
|
||||||
|
return daemon_->zapOnNextStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DaemonController::prepareLifecycleOperation(const LifecycleDecision& decision,
|
||||||
|
const config::Settings* settings)
|
||||||
|
{
|
||||||
|
if (settings) syncSettings(settings);
|
||||||
|
if (decision.resetCrashCount) resetCrashCount();
|
||||||
|
if (decision.setRescanOnNextStart) setRescanOnNextStart(true);
|
||||||
|
if (decision.setZapOnNextStart) setZapOnNextStart(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
DaemonController::ShutdownDecision DaemonController::shutdownDecision(
|
||||||
|
bool keepDaemonRunning, bool stopExternalDaemon) const
|
||||||
|
{
|
||||||
|
return evaluateShutdownPolicy(static_cast<bool>(daemon_),
|
||||||
|
daemon_ && daemon_->externalDaemonDetected(),
|
||||||
|
keepDaemonRunning,
|
||||||
|
stopExternalDaemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace daemon
|
||||||
|
} // namespace dragonx
|
||||||
238
src/daemon/daemon_controller.h
Normal file
238
src/daemon/daemon_controller.h
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "embedded_daemon.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace config { class Settings; }
|
||||||
|
namespace daemon {
|
||||||
|
|
||||||
|
class DaemonController {
|
||||||
|
public:
|
||||||
|
using State = EmbeddedDaemon::State;
|
||||||
|
using StateCallback = EmbeddedDaemon::StateCallback;
|
||||||
|
|
||||||
|
enum class ShutdownAction {
|
||||||
|
DisconnectOnly,
|
||||||
|
StopDaemon
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ShutdownDecision {
|
||||||
|
ShutdownAction action = ShutdownAction::DisconnectOnly;
|
||||||
|
const char* logReason = "no embedded daemon";
|
||||||
|
const char* status = "Disconnecting...";
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class LifecycleOperation {
|
||||||
|
ManualRestart,
|
||||||
|
Rescan,
|
||||||
|
RepairWallet, // restart with -zapwallettxes=2 (wipe & rebuild wallet tx records)
|
||||||
|
DeleteBlockchainData,
|
||||||
|
BootstrapStop
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LifecycleDecision {
|
||||||
|
LifecycleOperation operation = LifecycleOperation::ManualRestart;
|
||||||
|
bool allowed = false;
|
||||||
|
bool wasRunning = false;
|
||||||
|
const char* taskName = "";
|
||||||
|
const char* status = "";
|
||||||
|
const char* warning = "";
|
||||||
|
bool resetCrashCount = false;
|
||||||
|
bool setRescanOnNextStart = false;
|
||||||
|
bool disconnectRpc = false;
|
||||||
|
int restartDelayMs = 0;
|
||||||
|
bool setZapOnNextStart = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LifecycleTaskContext {
|
||||||
|
public:
|
||||||
|
virtual ~LifecycleTaskContext() = default;
|
||||||
|
virtual bool cancelled() const = 0;
|
||||||
|
virtual bool shuttingDown() const = 0;
|
||||||
|
virtual void sleepForMs(int milliseconds) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LifecycleRuntime {
|
||||||
|
public:
|
||||||
|
virtual ~LifecycleRuntime() = default;
|
||||||
|
virtual void stopDaemonWithPolicy() = 0;
|
||||||
|
virtual bool startDaemon() = 0;
|
||||||
|
virtual int deleteBlockchainData() = 0;
|
||||||
|
virtual void resetOutputOffset() = 0;
|
||||||
|
virtual void requestRpcStopAndDisconnect(const char* context, const char* reason) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LifecycleExecutionResult {
|
||||||
|
bool completed = false;
|
||||||
|
bool cancelled = false;
|
||||||
|
bool stopped = false;
|
||||||
|
bool started = false;
|
||||||
|
int deletedItems = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
DaemonController();
|
||||||
|
~DaemonController();
|
||||||
|
|
||||||
|
DaemonController(const DaemonController&) = delete;
|
||||||
|
DaemonController& operator=(const DaemonController&) = delete;
|
||||||
|
|
||||||
|
EmbeddedDaemon* daemon() { return daemon_.get(); }
|
||||||
|
const EmbeddedDaemon* daemon() const { return daemon_.get(); }
|
||||||
|
|
||||||
|
void setStateCallback(StateCallback callback);
|
||||||
|
void syncSettings(const config::Settings* settings);
|
||||||
|
|
||||||
|
bool start(const config::Settings* settings);
|
||||||
|
void stop(int waitMs);
|
||||||
|
|
||||||
|
bool isRunning() const;
|
||||||
|
bool externalDaemonDetected() const;
|
||||||
|
State state() const;
|
||||||
|
const std::string& lastError() const;
|
||||||
|
int crashCount() const;
|
||||||
|
int lastBlockHeight() const;
|
||||||
|
double memoryUsageMB() const;
|
||||||
|
std::vector<std::string> recentLines(std::size_t count) const;
|
||||||
|
std::string outputSince(std::size_t& offset) const;
|
||||||
|
|
||||||
|
void resetCrashCount();
|
||||||
|
void setRescanOnNextStart(bool enabled);
|
||||||
|
bool rescanOnNextStart() const;
|
||||||
|
void setZapOnNextStart(bool enabled);
|
||||||
|
bool zapOnNextStart() const;
|
||||||
|
|
||||||
|
static ShutdownDecision evaluateShutdownPolicy(bool hasDaemon,
|
||||||
|
bool externalDaemonDetected,
|
||||||
|
bool keepDaemonRunning,
|
||||||
|
bool stopExternalDaemon) {
|
||||||
|
if (!hasDaemon) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (keepDaemonRunning) {
|
||||||
|
return {ShutdownAction::DisconnectOnly,
|
||||||
|
"keep_daemon_running enabled",
|
||||||
|
"Disconnecting (daemon stays running)..."};
|
||||||
|
}
|
||||||
|
if (externalDaemonDetected && !stopExternalDaemon) {
|
||||||
|
return {ShutdownAction::DisconnectOnly,
|
||||||
|
"external daemon (not ours to stop)",
|
||||||
|
"Disconnecting (daemon stays running)..."};
|
||||||
|
}
|
||||||
|
return {ShutdownAction::StopDaemon,
|
||||||
|
"stopping managed daemon",
|
||||||
|
"Sending stop command to daemon..."};
|
||||||
|
}
|
||||||
|
static LifecycleDecision evaluateLifecycleOperation(LifecycleOperation operation,
|
||||||
|
bool usingEmbeddedDaemon,
|
||||||
|
bool hasDaemon,
|
||||||
|
bool daemonRunning,
|
||||||
|
bool restartInProgress = false) {
|
||||||
|
switch (operation) {
|
||||||
|
case LifecycleOperation::ManualRestart:
|
||||||
|
if (!usingEmbeddedDaemon || restartInProgress) return {};
|
||||||
|
return {operation, true, daemonRunning, "daemon-restart", "Restarting daemon...", "",
|
||||||
|
true, false, true, 500};
|
||||||
|
case LifecycleOperation::Rescan:
|
||||||
|
if (!usingEmbeddedDaemon || !hasDaemon) {
|
||||||
|
return {operation, false, daemonRunning, "", "",
|
||||||
|
"Rescan requires embedded daemon. Restart your daemon with -rescan manually."};
|
||||||
|
}
|
||||||
|
return {operation, true, daemonRunning, "rescan-blockchain", "Starting rescan...", "",
|
||||||
|
false, true, false, 3000};
|
||||||
|
case LifecycleOperation::RepairWallet:
|
||||||
|
if (!usingEmbeddedDaemon || !hasDaemon) {
|
||||||
|
return {operation, false, daemonRunning, "", "",
|
||||||
|
"Wallet repair requires embedded daemon. Restart your daemon with -zapwallettxes=2 manually."};
|
||||||
|
}
|
||||||
|
return {operation, true, daemonRunning, "repair-wallet", "Repairing wallet...", "",
|
||||||
|
false, false, false, 3000, true};
|
||||||
|
case LifecycleOperation::DeleteBlockchainData:
|
||||||
|
if (!usingEmbeddedDaemon || !hasDaemon) {
|
||||||
|
return {operation, false, daemonRunning, "", "",
|
||||||
|
"Delete blockchain requires embedded daemon. Stop your daemon manually and delete the data directory."};
|
||||||
|
}
|
||||||
|
return {operation, true, daemonRunning, "delete-blockchain-data", "Deleting blockchain data...", "",
|
||||||
|
false, false, false, 3000};
|
||||||
|
case LifecycleOperation::BootstrapStop:
|
||||||
|
return {operation, true, daemonRunning, "bootstrap-stop-daemon", "Stopping daemon for bootstrap...", "",
|
||||||
|
false, false, true, 0};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
void prepareLifecycleOperation(const LifecycleDecision& decision,
|
||||||
|
const config::Settings* settings = nullptr);
|
||||||
|
static inline LifecycleExecutionResult executeLifecycleOperation(const LifecycleDecision& decision,
|
||||||
|
LifecycleRuntime& runtime,
|
||||||
|
LifecycleTaskContext& task)
|
||||||
|
{
|
||||||
|
LifecycleExecutionResult result;
|
||||||
|
if (!decision.allowed) return result;
|
||||||
|
|
||||||
|
auto waitForDelay = [&]() {
|
||||||
|
int waitTicks = std::max(0, decision.restartDelayMs / 100);
|
||||||
|
for (int i = 0; i < waitTicks && !task.cancelled() && !task.shuttingDown(); ++i) {
|
||||||
|
task.sleepForMs(100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto cancelled = [&]() {
|
||||||
|
result.cancelled = task.cancelled() || task.shuttingDown();
|
||||||
|
return result.cancelled;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (decision.operation) {
|
||||||
|
case LifecycleOperation::BootstrapStop:
|
||||||
|
if (decision.wasRunning) {
|
||||||
|
runtime.requestRpcStopAndDisconnect("Bootstrap daemon stop", "Bootstrap");
|
||||||
|
result.stopped = true;
|
||||||
|
}
|
||||||
|
result.completed = true;
|
||||||
|
return result;
|
||||||
|
case LifecycleOperation::ManualRestart:
|
||||||
|
if (decision.wasRunning) {
|
||||||
|
runtime.stopDaemonWithPolicy();
|
||||||
|
result.stopped = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LifecycleOperation::Rescan:
|
||||||
|
case LifecycleOperation::RepairWallet:
|
||||||
|
case LifecycleOperation::DeleteBlockchainData:
|
||||||
|
runtime.stopDaemonWithPolicy();
|
||||||
|
result.stopped = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelled()) return result;
|
||||||
|
waitForDelay();
|
||||||
|
if (cancelled()) return result;
|
||||||
|
|
||||||
|
if (decision.operation == LifecycleOperation::DeleteBlockchainData) {
|
||||||
|
result.deletedItems = runtime.deleteBlockchainData();
|
||||||
|
if (cancelled()) return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decision.operation == LifecycleOperation::Rescan ||
|
||||||
|
decision.operation == LifecycleOperation::RepairWallet ||
|
||||||
|
decision.operation == LifecycleOperation::DeleteBlockchainData) {
|
||||||
|
runtime.resetOutputOffset();
|
||||||
|
}
|
||||||
|
|
||||||
|
result.started = runtime.startDaemon();
|
||||||
|
result.completed = !cancelled();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
ShutdownDecision shutdownDecision(bool keepDaemonRunning,
|
||||||
|
bool stopExternalDaemon) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<EmbeddedDaemon> daemon_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace daemon
|
||||||
|
} // namespace dragonx
|
||||||
@@ -482,8 +482,14 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
|
|||||||
args.push_back("-maxconnections=" + std::to_string(max_connections_));
|
args.push_back("-maxconnections=" + std::to_string(max_connections_));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add -rescan flag if requested (one-shot)
|
// Add wallet-repair flag if requested (one-shot). -zapwallettxes=2 wipes all wallet tx/note
|
||||||
if (rescan_on_next_start_.exchange(false)) {
|
// records and rebuilds them from the chain; it implies -rescan, so don't also pass -rescan.
|
||||||
|
if (zap_on_next_start_.exchange(false)) {
|
||||||
|
DEBUG_LOGF("[INFO] Adding -zapwallettxes=2 flag for wallet repair (zap & rebuild)\n");
|
||||||
|
args.push_back("-zapwallettxes=2");
|
||||||
|
rescan_on_next_start_.store(false); // implied by zap; avoid redundant -rescan
|
||||||
|
} else if (rescan_on_next_start_.exchange(false)) {
|
||||||
|
// Add -rescan flag if requested (one-shot)
|
||||||
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
|
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
|
||||||
args.push_back("-rescan");
|
args.push_back("-rescan");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,6 +183,14 @@ public:
|
|||||||
void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; }
|
void setRescanOnNextStart(bool v) { rescan_on_next_start_ = v; }
|
||||||
bool rescanOnNextStart() const { return rescan_on_next_start_.load(); }
|
bool rescanOnNextStart() const { return rescan_on_next_start_.load(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Request a wallet repair (-zapwallettxes=2) on the next daemon start. This deletes all
|
||||||
|
* wallet transaction/note records and rebuilds them from the chain (keys are kept); the
|
||||||
|
* daemon implicitly rescans afterwards. One-shot, like the rescan flag.
|
||||||
|
*/
|
||||||
|
void setZapOnNextStart(bool v) { zap_on_next_start_ = v; }
|
||||||
|
bool zapOnNextStart() const { return zap_on_next_start_.load(); }
|
||||||
|
|
||||||
/** Get number of consecutive daemon crashes (resets on successful start or manual reset) */
|
/** Get number of consecutive daemon crashes (resets on successful start or manual reset) */
|
||||||
int getCrashCount() const { return crash_count_.load(); }
|
int getCrashCount() const { return crash_count_.load(); }
|
||||||
/** Reset crash counter (call on successful connection or manual restart) */
|
/** Reset crash counter (call on successful connection or manual restart) */
|
||||||
@@ -222,6 +230,7 @@ private:
|
|||||||
int max_connections_ = 0; // 0 = daemon default
|
int max_connections_ = 0; // 0 = daemon default
|
||||||
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
std::atomic<int> crash_count_{0}; // consecutive crash counter
|
||||||
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
std::atomic<bool> rescan_on_next_start_{false}; // -rescan flag for next start
|
||||||
|
std::atomic<bool> zap_on_next_start_{false}; // -zapwallettxes=2 flag for next start
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace daemon
|
} // namespace daemon
|
||||||
|
|||||||
93
src/daemon/lifecycle_adapters.cpp
Normal file
93
src/daemon/lifecycle_adapters.cpp
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#include "lifecycle_adapters.h"
|
||||||
|
#include "../util/logger.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace daemon {
|
||||||
|
|
||||||
|
AsyncLifecycleTaskContext::AsyncLifecycleTaskContext(
|
||||||
|
const util::AsyncTaskManager::Token& token,
|
||||||
|
const std::atomic<bool>& shuttingDown)
|
||||||
|
: token_(token), shuttingDown_(shuttingDown)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AsyncLifecycleTaskContext::cancelled() const
|
||||||
|
{
|
||||||
|
return token_.cancelled();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AsyncLifecycleTaskContext::shuttingDown() const
|
||||||
|
{
|
||||||
|
return shuttingDown_.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsyncLifecycleTaskContext::sleepForMs(int milliseconds)
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(std::max(0, milliseconds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImmediateLifecycleTaskContext::cancelled() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImmediateLifecycleTaskContext::shuttingDown() const
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImmediateLifecycleTaskContext::sleepForMs(int)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
int BlockchainDataCleaner::removeBlockchainData(const std::filesystem::path& dataDir)
|
||||||
|
{
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
static constexpr std::array<const char*, 4> directories = {
|
||||||
|
"blocks", "chainstate", "database", "notarizations"
|
||||||
|
};
|
||||||
|
static constexpr std::array<const char*, 5> files = {
|
||||||
|
"peers.dat", "fee_estimates.dat", "banlist.dat", "db.log", ".lock"
|
||||||
|
};
|
||||||
|
|
||||||
|
int removed = 0;
|
||||||
|
for (const char* directoryName : directories) {
|
||||||
|
fs::path path = dataDir / directoryName;
|
||||||
|
std::error_code existsError;
|
||||||
|
if (!fs::exists(path, existsError)) continue;
|
||||||
|
|
||||||
|
std::error_code removeError;
|
||||||
|
auto count = fs::remove_all(path, removeError);
|
||||||
|
if (!removeError) {
|
||||||
|
removed += static_cast<int>(count);
|
||||||
|
DEBUG_LOGF("[DaemonLifecycle] Removed %s (%d entries)\n",
|
||||||
|
directoryName, static_cast<int>(count));
|
||||||
|
} else {
|
||||||
|
DEBUG_LOGF("[DaemonLifecycle] Failed to remove %s: %s\n",
|
||||||
|
directoryName, removeError.message().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const char* fileName : files) {
|
||||||
|
fs::path path = dataDir / fileName;
|
||||||
|
std::error_code removeError;
|
||||||
|
if (fs::remove(path, removeError)) {
|
||||||
|
++removed;
|
||||||
|
DEBUG_LOGF("[DaemonLifecycle] Removed %s\n", fileName);
|
||||||
|
} else if (removeError) {
|
||||||
|
DEBUG_LOGF("[DaemonLifecycle] Failed to remove %s: %s\n",
|
||||||
|
fileName, removeError.message().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace daemon
|
||||||
|
} // namespace dragonx
|
||||||
39
src/daemon/lifecycle_adapters.h
Normal file
39
src/daemon/lifecycle_adapters.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "daemon_controller.h"
|
||||||
|
#include "../util/async_task_manager.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace daemon {
|
||||||
|
|
||||||
|
class AsyncLifecycleTaskContext final : public DaemonController::LifecycleTaskContext {
|
||||||
|
public:
|
||||||
|
AsyncLifecycleTaskContext(const util::AsyncTaskManager::Token& token,
|
||||||
|
const std::atomic<bool>& shuttingDown);
|
||||||
|
|
||||||
|
bool cancelled() const override;
|
||||||
|
bool shuttingDown() const override;
|
||||||
|
void sleepForMs(int milliseconds) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const util::AsyncTaskManager::Token& token_;
|
||||||
|
const std::atomic<bool>& shuttingDown_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImmediateLifecycleTaskContext final : public DaemonController::LifecycleTaskContext {
|
||||||
|
public:
|
||||||
|
bool cancelled() const override;
|
||||||
|
bool shuttingDown() const override;
|
||||||
|
void sleepForMs(int milliseconds) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BlockchainDataCleaner final {
|
||||||
|
public:
|
||||||
|
static int removeBlockchainData(const std::filesystem::path& dataDir);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace daemon
|
||||||
|
} // namespace dragonx
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "xmrig_manager.h"
|
#include "xmrig_manager.h"
|
||||||
#include "../resources/embedded_resources.h"
|
#include "../resources/embedded_resources.h"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
|
|
||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
|
#include "../util/pool_registry.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <winsock2.h>
|
#include <winsock2.h>
|
||||||
@@ -718,6 +720,11 @@ void XmrigManager::fetchStatsHttp() {
|
|||||||
|
|
||||||
std::lock_guard<std::mutex> lk(stats_mutex_);
|
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")) {
|
if (resp.contains("hashrate") && resp["hashrate"].contains("total")) {
|
||||||
auto& total = resp["hashrate"]["total"];
|
auto& total = resp["hashrate"]["total"];
|
||||||
if (total.is_array() && total.size() >= 3) {
|
if (total.is_array() && total.size() >= 3) {
|
||||||
@@ -768,10 +775,14 @@ void XmrigManager::fetchStatsHttp() {
|
|||||||
void XmrigManager::fetchPoolApiStats() {
|
void XmrigManager::fetchPoolApiStats() {
|
||||||
if (state_ != State::Running || pool_host_.empty()) return;
|
if (state_ != State::Running || pool_host_.empty()) return;
|
||||||
|
|
||||||
// Query the pool's public stats API
|
// Resolve the stats endpoint + JSON schema for this pool. Known pools carry their
|
||||||
std::string url = "https://" + pool_host_ + "/api/stats";
|
// own API shape (pool.dragonx.is = custom /api/stats; pool.dragonx.cc = Miningcore
|
||||||
std::string responseData;
|
// /api/pools); unknown/custom hosts fall back to the .is convention.
|
||||||
|
const util::KnownPool* known = util::findKnownPoolByUrl(pool_host_);
|
||||||
|
const std::string url = known ? known->statsUrl
|
||||||
|
: ("https://" + pool_host_ + "/api/stats");
|
||||||
|
|
||||||
|
std::string responseData;
|
||||||
CURL* curl = curl_easy_init();
|
CURL* curl = curl_easy_init();
|
||||||
if (!curl) return;
|
if (!curl) return;
|
||||||
|
|
||||||
@@ -782,31 +793,99 @@ void XmrigManager::fetchPoolApiStats() {
|
|||||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
|
||||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
||||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
|
// pool.dragonx.cc sits behind Cloudflare and 403s odd User-Agents.
|
||||||
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; ObsidianDragon)");
|
||||||
|
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "");
|
||||||
|
|
||||||
CURLcode res = curl_easy_perform(curl);
|
CURLcode res = curl_easy_perform(curl);
|
||||||
curl_easy_cleanup(curl);
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
if (res != CURLE_OK) return;
|
if (res != CURLE_OK) return;
|
||||||
|
|
||||||
try {
|
bool ok = false;
|
||||||
json resp = json::parse(responseData);
|
const double poolHR = util::parsePoolHashrate(
|
||||||
|
known ? known->schema : util::PoolStatsSchema::DragonXIs,
|
||||||
|
responseData, known ? known->miningcorePoolId : std::string{}, ok);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
// Pool stats API format: { "pools": { "<name>": { "hashrate": ... } } }
|
std::lock_guard<std::mutex> lk(stats_mutex_);
|
||||||
double poolHR = 0;
|
stats_.pool_hashrate = poolHR;
|
||||||
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>();
|
// Installed-miner version detection (`<binary> --version`, cached)
|
||||||
break; // Use the first pool entry
|
// ============================================================================
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::mutex g_installed_ver_mutex;
|
||||||
|
std::string g_installed_ver;
|
||||||
|
std::atomic<bool> g_ver_detect_started{false};
|
||||||
|
|
||||||
|
// Extract the first "D.D[.D...]" version token from `--version` output (skips the
|
||||||
|
// build date, which uses '-' separators). Returns e.g. "6.21.0", or "" if none.
|
||||||
|
std::string parseMinerVersion(const std::string& out)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < out.size(); ++i) {
|
||||||
|
if (std::isdigit(static_cast<unsigned char>(out[i]))) {
|
||||||
|
size_t j = i;
|
||||||
|
int dots = 0;
|
||||||
|
while (j < out.size() &&
|
||||||
|
(std::isdigit(static_cast<unsigned char>(out[j])) || out[j] == '.')) {
|
||||||
|
if (out[j] == '.') ++dots;
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
if (dots >= 1 && (j - i) >= 3) {
|
||||||
|
// Include a trailing build suffix like "-hac" / "-drg1" (e.g. "6.25.1-hac"),
|
||||||
|
// matching what the running miner's API reports.
|
||||||
|
size_t end = j;
|
||||||
|
if (end < out.size() && out[end] == '-') {
|
||||||
|
size_t k = end + 1;
|
||||||
|
while (k < out.size() && std::isalnum(static_cast<unsigned char>(out[k]))) ++k;
|
||||||
|
if (k > end + 1) end = k;
|
||||||
}
|
}
|
||||||
|
return out.substr(i, end - i);
|
||||||
|
}
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void XmrigManager::startVersionDetection()
|
||||||
|
{
|
||||||
|
if (g_ver_detect_started.exchange(true)) return; // one-shot
|
||||||
|
std::thread([]() {
|
||||||
|
const std::string bin = findXmrigBinary();
|
||||||
|
std::string ver;
|
||||||
|
if (!bin.empty()) {
|
||||||
|
const std::string cmd = "\"" + bin + "\" --version 2>&1";
|
||||||
|
#ifdef _WIN32
|
||||||
|
FILE* fp = _popen(cmd.c_str(), "r");
|
||||||
|
#else
|
||||||
|
FILE* fp = popen(cmd.c_str(), "r");
|
||||||
|
#endif
|
||||||
|
if (fp) {
|
||||||
|
std::string out;
|
||||||
|
char buf[256];
|
||||||
|
size_t n;
|
||||||
|
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) out.append(buf, n);
|
||||||
|
#ifdef _WIN32
|
||||||
|
_pclose(fp);
|
||||||
|
#else
|
||||||
|
pclose(fp);
|
||||||
|
#endif
|
||||||
|
ver = parseMinerVersion(out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
|
||||||
|
g_installed_ver = ver;
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lk(stats_mutex_);
|
std::string XmrigManager::installedVersion()
|
||||||
stats_.pool_hashrate = poolHR;
|
{
|
||||||
} catch (...) {
|
std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
|
||||||
// Malformed response — ignore
|
return g_installed_ver;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace daemon
|
} // namespace daemon
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ public:
|
|||||||
double pool_diff = 0;
|
double pool_diff = 0;
|
||||||
std::string pool_url;
|
std::string pool_url;
|
||||||
std::string algo;
|
std::string algo;
|
||||||
|
std::string version; // miner version reported by the running xmrig API
|
||||||
bool connected = false;
|
bool connected = false;
|
||||||
// Memory usage
|
// Memory usage
|
||||||
int64_t memory_free = 0; // bytes
|
int64_t memory_free = 0; // bytes
|
||||||
@@ -137,6 +138,18 @@ public:
|
|||||||
*/
|
*/
|
||||||
static std::string findXmrigBinary();
|
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:
|
private:
|
||||||
bool generateConfig(const Config& cfg, const std::string& outPath);
|
bool generateConfig(const Config& cfg, const std::string& outPath);
|
||||||
bool startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads);
|
bool startProcess(const std::string& xmrigPath, const std::string& cfgPath, int threads);
|
||||||
|
|||||||
@@ -9,13 +9,7 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
|
#include "../util/platform.h"
|
||||||
#ifdef _WIN32
|
|
||||||
#include <shlobj.h>
|
|
||||||
#else
|
|
||||||
#include <pwd.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
@@ -28,33 +22,11 @@ AddressBook::~AddressBook() = default;
|
|||||||
|
|
||||||
std::string AddressBook::getDefaultPath()
|
std::string AddressBook::getDefaultPath()
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
// Co-locate with settings.json in the per-variant config dir (Lite -> ObsidianDragonLite/).
|
||||||
char path[MAX_PATH];
|
// util::Platform::getConfigDir() owns the per-platform + per-variant path in one place.
|
||||||
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, path))) {
|
const std::string dir = util::Platform::getConfigDir();
|
||||||
std::string dir = std::string(path) + "\\ObsidianDragon";
|
|
||||||
fs::create_directories(dir);
|
|
||||||
return dir + "\\addressbook.json";
|
|
||||||
}
|
|
||||||
return "addressbook.json";
|
|
||||||
#elif defined(__APPLE__)
|
|
||||||
const char* home = getenv("HOME");
|
|
||||||
if (!home) {
|
|
||||||
struct passwd* pw = getpwuid(getuid());
|
|
||||||
home = pw->pw_dir;
|
|
||||||
}
|
|
||||||
std::string dir = std::string(home) + "/Library/Application Support/ObsidianDragon";
|
|
||||||
fs::create_directories(dir);
|
fs::create_directories(dir);
|
||||||
return dir + "/addressbook.json";
|
return (fs::path(dir) / "addressbook.json").string();
|
||||||
#else
|
|
||||||
const char* home = getenv("HOME");
|
|
||||||
if (!home) {
|
|
||||||
struct passwd* pw = getpwuid(getuid());
|
|
||||||
home = pw->pw_dir;
|
|
||||||
}
|
|
||||||
std::string dir = std::string(home) + "/.config/ObsidianDragon";
|
|
||||||
fs::create_directories(dir);
|
|
||||||
return dir + "/addressbook.json";
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AddressBook::load()
|
bool AddressBook::load()
|
||||||
@@ -113,17 +85,13 @@ bool AddressBook::save()
|
|||||||
j["entries"].push_back(e);
|
j["entries"].push_back(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure directory exists
|
// Atomic + durable: temp file + fsync + rename, so a crash mid-write can't
|
||||||
fs::path p(file_path_);
|
// truncate addressbook.json (which is fully rewritten on every entry change).
|
||||||
fs::create_directories(p.parent_path());
|
// Owner-only (0600) — it holds the user's saved contacts.
|
||||||
|
if (!util::Platform::writeFileAtomically(file_path_, j.dump(2), /*restrictPermissions=*/true)) {
|
||||||
std::ofstream file(file_path_);
|
DEBUG_LOGF("Could not write address book: %s\n", file_path_.c_str());
|
||||||
if (!file.is_open()) {
|
|
||||||
DEBUG_LOGF("Could not open address book for writing: %s\n", file_path_.c_str());
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
file << j.dump(2);
|
|
||||||
DEBUG_LOGF("Address book saved: %zu entries\n", entries_.size());
|
DEBUG_LOGF("Address book saved: %zu entries\n", entries_.size());
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,19 @@
|
|||||||
|
|
||||||
#include "exchange_info.h"
|
#include "exchange_info.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace data {
|
namespace data {
|
||||||
|
|
||||||
const std::vector<ExchangeInfo>& getExchangeRegistry()
|
const std::vector<ExchangeInfo>& getExchangeRegistry()
|
||||||
{
|
{
|
||||||
|
// Offline seed / fallback. The live list from CoinGecko (parseCoinGeckoTickers)
|
||||||
|
// supersedes this when available; keep the known venues here so the tab is never
|
||||||
|
// empty without network.
|
||||||
static const std::vector<ExchangeInfo> registry = {
|
static const std::vector<ExchangeInfo> registry = {
|
||||||
{
|
{
|
||||||
"Nonkyc.io",
|
"Nonkyc.io",
|
||||||
@@ -17,9 +25,68 @@ const std::vector<ExchangeInfo>& getExchangeRegistry()
|
|||||||
{"DRGX", "USDT", "DRGX/USDT", "https://nonkyc.io/market/DRGX_USDT"},
|
{"DRGX", "USDT", "DRGX/USDT", "https://nonkyc.io/market/DRGX_USDT"},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"OurBit",
|
||||||
|
"https://www.ourbit.com",
|
||||||
|
{
|
||||||
|
{"DRGX", "USDT", "DRGX/USDT", "https://www.ourbit.com/exchange/DRGX_USDT"},
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Extract the scheme://host origin from a URL (for ExchangeInfo.baseUrl).
|
||||||
|
std::string originOf(const std::string& url)
|
||||||
|
{
|
||||||
|
const auto scheme = url.find("://");
|
||||||
|
if (scheme == std::string::npos) return url;
|
||||||
|
const auto host = url.find('/', scheme + 3);
|
||||||
|
return (host == std::string::npos) ? url : url.substr(0, host);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<ExchangeInfo> parseCoinGeckoTickers(const std::string& body)
|
||||||
|
{
|
||||||
|
std::vector<ExchangeInfo> exchanges;
|
||||||
|
try {
|
||||||
|
const nlohmann::json j = nlohmann::json::parse(body);
|
||||||
|
if (!j.contains("tickers") || !j["tickers"].is_array()) return {};
|
||||||
|
|
||||||
|
std::unordered_map<std::string, size_t> byName; // exchange name -> index
|
||||||
|
for (const auto& t : j["tickers"]) {
|
||||||
|
if (!t.is_object()) continue;
|
||||||
|
const std::string base = t.value("base", std::string{});
|
||||||
|
const std::string target = t.value("target", std::string{});
|
||||||
|
std::string market;
|
||||||
|
if (t.contains("market") && t["market"].is_object())
|
||||||
|
market = t["market"].value("name", std::string{});
|
||||||
|
const std::string tradeUrl = t.value("trade_url", std::string{});
|
||||||
|
if (base.empty() || target.empty() || market.empty()) continue;
|
||||||
|
|
||||||
|
auto it = byName.find(market);
|
||||||
|
if (it == byName.end()) {
|
||||||
|
byName[market] = exchanges.size();
|
||||||
|
exchanges.push_back(ExchangeInfo{market, originOf(tradeUrl), {}});
|
||||||
|
it = byName.find(market);
|
||||||
|
}
|
||||||
|
ExchangePair pair{base, target, base + "/" + target, tradeUrl};
|
||||||
|
// Skip duplicate pairs on the same exchange.
|
||||||
|
bool dup = false;
|
||||||
|
for (const auto& p : exchanges[it->second].pairs)
|
||||||
|
if (p.displayName == pair.displayName) { dup = true; break; }
|
||||||
|
if (!dup) exchanges[it->second].pairs.push_back(std::move(pair));
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// Drop exchanges that ended up with no pairs (defensive).
|
||||||
|
exchanges.erase(std::remove_if(exchanges.begin(), exchanges.end(),
|
||||||
|
[](const ExchangeInfo& e) { return e.pairs.empty(); }),
|
||||||
|
exchanges.end());
|
||||||
|
return exchanges;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace data
|
} // namespace data
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
@@ -30,9 +30,19 @@ struct ExchangeInfo {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Returns the static registry of supported exchanges + pairs
|
* @brief Returns the static registry of supported exchanges + pairs.
|
||||||
|
* Used as the offline fallback / seed when the live CoinGecko list is unavailable.
|
||||||
*/
|
*/
|
||||||
const std::vector<ExchangeInfo>& getExchangeRegistry();
|
const std::vector<ExchangeInfo>& getExchangeRegistry();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parse a CoinGecko /coins/{id}/tickers JSON body into the exchange registry.
|
||||||
|
*
|
||||||
|
* Groups tickers by exchange (market.name), preserving first-seen order, into
|
||||||
|
* ExchangeInfo entries (each pair carries base/target/displayName/trade_url). Returns
|
||||||
|
* an empty vector on parse failure or when no usable tickers are present. Pure (no I/O).
|
||||||
|
*/
|
||||||
|
std::vector<ExchangeInfo> parseCoinGeckoTickers(const std::string& json);
|
||||||
|
|
||||||
} // namespace data
|
} // namespace data
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
99
src/data/market_series.h
Normal file
99
src/data/market_series.h
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <utility>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
#include "wallet_state.h" // MarketInfo
|
||||||
|
|
||||||
|
// Pure (no-I/O, no-ImGui) price-series math backing the market chart and the portfolio-group
|
||||||
|
// sparklines. Kept header-only + inline so it can be unit-tested without the GUI (see
|
||||||
|
// tests/test_phase4.cpp), mirroring data/portfolio.h.
|
||||||
|
namespace dragonx {
|
||||||
|
namespace data {
|
||||||
|
|
||||||
|
// Resample a ~1-sample/minute price history to the given interval by averaging each block of K
|
||||||
|
// minute-samples into one point. interval: 0=min 1=hour 2=day 3=week 4=month.
|
||||||
|
inline std::vector<double> resampleHistory(const std::vector<double>& hist, int interval)
|
||||||
|
{
|
||||||
|
static const int kMins[5] = {1, 60, 1440, 10080, 43200};
|
||||||
|
int k = kMins[(interval >= 0 && interval < 5) ? interval : 0];
|
||||||
|
if (k <= 1) return hist;
|
||||||
|
std::vector<double> out;
|
||||||
|
for (size_t i = 0; i < hist.size(); i += (size_t)k) {
|
||||||
|
double sum = 0.0; size_t cnt = 0;
|
||||||
|
for (size_t j = i; j < hist.size() && j < i + (size_t)k; ++j) { sum += hist[j]; ++cnt; }
|
||||||
|
if (cnt) out.push_back(sum / (double)cnt);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bucket a timestamped (unix-seconds, price) series into fixed-width time windows, averaging the
|
||||||
|
// samples in each window into one point. Input is oldest->newest; output preserves that order.
|
||||||
|
inline std::vector<double> bucketBySeconds(
|
||||||
|
const std::vector<std::pair<std::time_t, double>>& series, long windowSec)
|
||||||
|
{
|
||||||
|
std::vector<double> out;
|
||||||
|
if (series.empty() || windowSec <= 0) return out;
|
||||||
|
long curBucket = 0; double sum = 0.0; int cnt = 0;
|
||||||
|
for (const auto& sample : series) {
|
||||||
|
long b = (long)(sample.first / windowSec);
|
||||||
|
if (cnt > 0 && b != curBucket) { out.push_back(sum / cnt); sum = 0.0; cnt = 0; }
|
||||||
|
curBucket = b;
|
||||||
|
sum += sample.second; ++cnt;
|
||||||
|
}
|
||||||
|
if (cnt > 0) out.push_back(sum / cnt);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the price series backing a group's sparkline for the chosen interval:
|
||||||
|
// 0=minute -> the live in-session buffer; 1=hour -> intraday (5-min) data bucketed to hours;
|
||||||
|
// 2=day / 3=week / 4=month -> the ~1yr daily series bucketed accordingly.
|
||||||
|
// Falls back to the in-session buffer when the historical fetch hasn't populated yet.
|
||||||
|
inline std::vector<double> sparklineSeries(const MarketInfo& m, int interval)
|
||||||
|
{
|
||||||
|
const long kDay = 86400;
|
||||||
|
switch (interval) {
|
||||||
|
case 1: { auto v = bucketBySeconds(m.price_chart_intraday, 3600); if (v.size() >= 2) return v; break; }
|
||||||
|
case 2: { auto v = bucketBySeconds(m.price_chart_daily, kDay); if (v.size() >= 2) return v; break; }
|
||||||
|
case 3: { auto v = bucketBySeconds(m.price_chart_daily, 7 * kDay); if (v.size() >= 2) return v; break; }
|
||||||
|
case 4: { auto v = bucketBySeconds(m.price_chart_daily, 30 * kDay); if (v.size() >= 2) return v; break; }
|
||||||
|
default: break; // minute interval, or no historical data yet -> live buffer below
|
||||||
|
}
|
||||||
|
return resampleHistory(m.price_history, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamped price series backing the main chart for the selected RANGE (Live/1H/1D/1W/1M),
|
||||||
|
// ending at `now`. The interval buttons select a time window ending at `now`: 1H = last hour,
|
||||||
|
// 1D = last 24h, 1W = last 7 days, 1M = last 30 days. 1H/1D come from the 5-minute intraday
|
||||||
|
// series; 1W/1M from the daily series. Live (or an un-fetched/empty range) uses the minute buffer.
|
||||||
|
inline std::vector<std::pair<std::time_t, double>> chartSeries(const MarketInfo& m, int interval,
|
||||||
|
std::time_t now)
|
||||||
|
{
|
||||||
|
const long kDay = 86400;
|
||||||
|
auto lastWindow = [now](const std::vector<std::pair<std::time_t, double>>& src, long rangeSec) {
|
||||||
|
std::vector<std::pair<std::time_t, double>> out;
|
||||||
|
std::time_t cutoff = now - (std::time_t)rangeSec;
|
||||||
|
for (const auto& s : src) if (s.first >= cutoff) out.push_back(s);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
switch (interval) {
|
||||||
|
case 1: { auto v = lastWindow(m.price_chart_intraday, 3600); if (v.size() >= 2) return v; break; } // 1H
|
||||||
|
case 2: { auto v = lastWindow(m.price_chart_intraday, kDay); if (v.size() >= 2) return v; break; } // 1D
|
||||||
|
case 3: { auto v = lastWindow(m.price_chart_daily, 7 * kDay); if (v.size() >= 2) return v; break; } // 1W
|
||||||
|
case 4: { auto v = lastWindow(m.price_chart_daily, 30 * kDay); if (v.size() >= 2) return v; break; } // 1M
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
std::vector<std::pair<std::time_t, double>> out;
|
||||||
|
const auto& h = m.price_history;
|
||||||
|
for (size_t i = 0; i < h.size(); i++)
|
||||||
|
out.push_back({ now - (std::time_t)((h.size() - 1 - i) * 60), h[i] });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace data
|
||||||
|
} // namespace dragonx
|
||||||
61
src/data/portfolio.h
Normal file
61
src/data/portfolio.h
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
//
|
||||||
|
// Portfolio helpers — pure balance math for the Market tab's configurable portfolio.
|
||||||
|
// A portfolio entry is a user label tied to a group of wallet addresses (persisted in
|
||||||
|
// Settings as PortfolioEntry); these helpers sum/query that group against the live
|
||||||
|
// per-address balance list. No I/O, no ImGui — unit-testable.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "wallet_state.h" // AddressInfo
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace data {
|
||||||
|
|
||||||
|
// Sum the DRGX balance of `entryAddresses` by looking them up in the wallet's per-address
|
||||||
|
// list. Addresses not present in the wallet contribute 0 (e.g. removed/rescanned). Pure.
|
||||||
|
inline double SumPortfolioBalance(const std::vector<std::string>& entryAddresses,
|
||||||
|
const std::vector<AddressInfo>& walletAddresses)
|
||||||
|
{
|
||||||
|
if (entryAddresses.empty()) return 0.0;
|
||||||
|
std::unordered_set<std::string> want(entryAddresses.begin(), entryAddresses.end());
|
||||||
|
double sum = 0.0;
|
||||||
|
for (const auto& a : walletAddresses)
|
||||||
|
if (want.count(a.address)) sum += a.balance;
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether `address` is part of the entry's address group.
|
||||||
|
inline bool PortfolioEntryContains(const std::vector<std::string>& entryAddresses,
|
||||||
|
const std::string& address)
|
||||||
|
{
|
||||||
|
for (const auto& a : entryAddresses)
|
||||||
|
if (a == address) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add `address` to the group if absent (returns true if it was added).
|
||||||
|
inline bool PortfolioEntryAdd(std::vector<std::string>& entryAddresses, const std::string& address)
|
||||||
|
{
|
||||||
|
if (address.empty() || PortfolioEntryContains(entryAddresses, address)) return false;
|
||||||
|
entryAddresses.push_back(address);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove `address` from the group if present (returns true if it was removed).
|
||||||
|
inline bool PortfolioEntryRemove(std::vector<std::string>& entryAddresses, const std::string& address)
|
||||||
|
{
|
||||||
|
for (auto it = entryAddresses.begin(); it != entryAddresses.end(); ++it) {
|
||||||
|
if (*it == address) { entryAddresses.erase(it); return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace data
|
||||||
|
} // namespace dragonx
|
||||||
538
src/data/transaction_history_cache.cpp
Normal file
538
src/data/transaction_history_cache.cpp
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
#include "transaction_history_cache.h"
|
||||||
|
|
||||||
|
#include "../util/logger.h"
|
||||||
|
#include "../util/platform.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <sodium.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace data {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kSchemaVersion = 1;
|
||||||
|
constexpr std::size_t kKeyBytes = 32;
|
||||||
|
|
||||||
|
struct Statement {
|
||||||
|
sqlite3_stmt* handle = nullptr;
|
||||||
|
|
||||||
|
Statement(sqlite3* db, const char* sql)
|
||||||
|
{
|
||||||
|
if (sqlite3_prepare_v2(db, sql, -1, &handle, nullptr) != SQLITE_OK) {
|
||||||
|
handle = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~Statement()
|
||||||
|
{
|
||||||
|
if (handle) sqlite3_finalize(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
Statement(const Statement&) = delete;
|
||||||
|
Statement& operator=(const Statement&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool bindText(sqlite3_stmt* statement, int index, const std::string& value)
|
||||||
|
{
|
||||||
|
return sqlite3_bind_text(statement, index, value.c_str(), -1, SQLITE_TRANSIENT) == SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bindBlob(sqlite3_stmt* statement, int index, const std::vector<unsigned char>& value)
|
||||||
|
{
|
||||||
|
return sqlite3_bind_blob(statement, index, value.data(), static_cast<int>(value.size()), SQLITE_TRANSIENT) == SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> readBlob(sqlite3_stmt* statement, int index)
|
||||||
|
{
|
||||||
|
const void* data = sqlite3_column_blob(statement, index);
|
||||||
|
int bytes = sqlite3_column_bytes(statement, index);
|
||||||
|
if (!data || bytes <= 0) return {};
|
||||||
|
const auto* begin = static_cast<const unsigned char*>(data);
|
||||||
|
return std::vector<unsigned char>(begin, begin + bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string hexEncode(const unsigned char* bytes, std::size_t length)
|
||||||
|
{
|
||||||
|
static constexpr char kHex[] = "0123456789abcdef";
|
||||||
|
std::string output;
|
||||||
|
output.resize(length * 2);
|
||||||
|
for (std::size_t index = 0; index < length; ++index) {
|
||||||
|
output[index * 2] = kHex[(bytes[index] >> 4) & 0x0F];
|
||||||
|
output[index * 2 + 1] = kHex[bytes[index] & 0x0F];
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
json transactionToJson(const TransactionInfo& transaction)
|
||||||
|
{
|
||||||
|
return json{
|
||||||
|
{"txid", transaction.txid},
|
||||||
|
{"type", transaction.type},
|
||||||
|
{"amount", transaction.amount},
|
||||||
|
{"timestamp", transaction.timestamp},
|
||||||
|
{"confirmations", transaction.confirmations},
|
||||||
|
{"address", transaction.address},
|
||||||
|
{"from_address", transaction.from_address},
|
||||||
|
{"memo", transaction.memo}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionInfo transactionFromJson(const json& source)
|
||||||
|
{
|
||||||
|
TransactionInfo transaction;
|
||||||
|
transaction.txid = source.value("txid", std::string());
|
||||||
|
transaction.type = source.value("type", std::string());
|
||||||
|
transaction.amount = source.value("amount", 0.0);
|
||||||
|
transaction.timestamp = source.value("timestamp", static_cast<std::int64_t>(0));
|
||||||
|
transaction.confirmations = source.value("confirmations", 0);
|
||||||
|
transaction.address = source.value("address", std::string());
|
||||||
|
transaction.from_address = source.value("from_address", std::string());
|
||||||
|
transaction.memo = source.value("memo", std::string());
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string associatedDataForWallet(const std::string& walletHash)
|
||||||
|
{
|
||||||
|
return std::string("obsidian-dragon-tx-history-v1:") + walletHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TransactionHistoryCache::TransactionHistoryCache()
|
||||||
|
: TransactionHistoryCache(defaultDatabasePath())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionHistoryCache::TransactionHistoryCache(std::string databasePath)
|
||||||
|
: database_path_(std::move(databasePath))
|
||||||
|
{
|
||||||
|
if (sodium_init() < 0) {
|
||||||
|
DEBUG_LOGF("Failed to initialize libsodium for transaction history cache\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionHistoryCache::~TransactionHistoryCache()
|
||||||
|
{
|
||||||
|
lockKey();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string TransactionHistoryCache::defaultDatabasePath()
|
||||||
|
{
|
||||||
|
return (fs::path(util::Platform::getConfigDir()) / "transaction_history.sqlite").string();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string TransactionHistoryCache::walletIdentityFromAddresses(
|
||||||
|
const std::vector<std::string>& shieldedAddresses,
|
||||||
|
const std::vector<std::string>& transparentAddresses)
|
||||||
|
{
|
||||||
|
std::vector<std::string> addresses;
|
||||||
|
addresses.reserve(shieldedAddresses.size() + transparentAddresses.size());
|
||||||
|
for (const auto& address : shieldedAddresses) {
|
||||||
|
if (!address.empty()) addresses.push_back("z:" + address);
|
||||||
|
}
|
||||||
|
for (const auto& address : transparentAddresses) {
|
||||||
|
if (!address.empty()) addresses.push_back("t:" + address);
|
||||||
|
}
|
||||||
|
if (addresses.empty()) return {};
|
||||||
|
|
||||||
|
std::sort(addresses.begin(), addresses.end());
|
||||||
|
std::string identity = "wallet-addresses-v1\n";
|
||||||
|
for (const auto& address : addresses) {
|
||||||
|
identity += address;
|
||||||
|
identity += '\n';
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string TransactionHistoryCache::walletIdentityHash(const std::string& walletIdentity)
|
||||||
|
{
|
||||||
|
unsigned char digest[crypto_generichash_BYTES];
|
||||||
|
crypto_generichash(digest, sizeof(digest),
|
||||||
|
reinterpret_cast<const unsigned char*>(walletIdentity.data()),
|
||||||
|
walletIdentity.size(), nullptr, 0);
|
||||||
|
return hexEncode(digest, sizeof(digest));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::ensureOpen()
|
||||||
|
{
|
||||||
|
if (db_) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs::path path(database_path_);
|
||||||
|
if (!path.parent_path().empty()) fs::create_directories(path.parent_path());
|
||||||
|
} catch (const std::exception& exception) {
|
||||||
|
DEBUG_LOGF("Failed to create transaction history cache directory: %s\n", exception.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3* openedDb = nullptr;
|
||||||
|
if (sqlite3_open(database_path_.c_str(), &openedDb) != SQLITE_OK) {
|
||||||
|
DEBUG_LOGF("Failed to open transaction history cache: %s\n",
|
||||||
|
openedDb ? sqlite3_errmsg(openedDb) : "unknown error");
|
||||||
|
if (openedDb) sqlite3_close(openedDb);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
db_ = openedDb;
|
||||||
|
sqlite3_busy_timeout(db_, 2000);
|
||||||
|
exec("PRAGMA journal_mode=WAL");
|
||||||
|
exec("PRAGMA synchronous=NORMAL");
|
||||||
|
|
||||||
|
if (!createSchema()) {
|
||||||
|
close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
// Owner-only (0600): although the payload is encrypted, don't leave the cache (or its
|
||||||
|
// WAL/SHM sidecars) world-readable. Best-effort; sidecars may not exist until first write.
|
||||||
|
::chmod(database_path_.c_str(), 0600);
|
||||||
|
::chmod((database_path_ + "-wal").c_str(), 0600);
|
||||||
|
::chmod((database_path_ + "-shm").c_str(), 0600);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::unlockWithPassphrase(const std::string& walletIdentity,
|
||||||
|
const std::string& passphrase)
|
||||||
|
{
|
||||||
|
if (walletIdentity.empty() || passphrase.empty() || !ensureOpen()) return false;
|
||||||
|
|
||||||
|
std::string walletHash = walletIdentityHash(walletIdentity);
|
||||||
|
std::vector<unsigned char> salt = getOrCreateSalt(walletHash);
|
||||||
|
if (salt.empty()) return false;
|
||||||
|
|
||||||
|
if (!deriveKey(passphrase, salt)) return false;
|
||||||
|
unlocked_wallet_hash_ = std::move(walletHash);
|
||||||
|
key_ready_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TransactionHistoryCache::lockKey()
|
||||||
|
{
|
||||||
|
if (key_ready_) sodium_memzero(key_.data(), key_.size());
|
||||||
|
key_ready_ = false;
|
||||||
|
unlocked_wallet_hash_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::isUnlockedFor(const std::string& walletIdentity) const
|
||||||
|
{
|
||||||
|
return key_ready_ && !walletIdentity.empty() &&
|
||||||
|
unlocked_wallet_hash_ == walletIdentityHash(walletIdentity);
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionHistoryCache::LoadResult TransactionHistoryCache::load(
|
||||||
|
const std::string& walletIdentity,
|
||||||
|
int currentTipHeight,
|
||||||
|
const std::string& currentTipHash)
|
||||||
|
{
|
||||||
|
LoadResult result;
|
||||||
|
if (!isUnlockedFor(walletIdentity) || !ensureOpen()) return result;
|
||||||
|
|
||||||
|
std::string walletHash = walletIdentityHash(walletIdentity);
|
||||||
|
int tipHeight = 0;
|
||||||
|
std::string tipHash;
|
||||||
|
std::time_t updatedAt = 0;
|
||||||
|
std::vector<unsigned char> nonce;
|
||||||
|
std::vector<unsigned char> cipherText;
|
||||||
|
if (!readSnapshot(walletHash, tipHeight, tipHash, updatedAt, nonce, cipherText)) return result;
|
||||||
|
|
||||||
|
if ((currentTipHeight > 0 && tipHeight > currentTipHeight) ||
|
||||||
|
(currentTipHeight > 0 && tipHeight == currentTipHeight &&
|
||||||
|
!currentTipHash.empty() && !tipHash.empty() && tipHash != currentTipHash)) {
|
||||||
|
clearWalletByHash(walletHash);
|
||||||
|
result.invalidated = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string plainText;
|
||||||
|
if (!decryptPayload(walletHash, nonce, cipherText, plainText)) return result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
json payload = json::parse(plainText);
|
||||||
|
if (payload.value("schema_version", 0) != kSchemaVersion) return result;
|
||||||
|
if (payload.value("wallet_hash", std::string()) != walletHash) return result;
|
||||||
|
if (!payload.contains("transactions") || !payload["transactions"].is_array()) return result;
|
||||||
|
|
||||||
|
result.transactions.reserve(payload["transactions"].size());
|
||||||
|
for (const auto& transactionJson : payload["transactions"]) {
|
||||||
|
if (transactionJson.is_object()) {
|
||||||
|
result.transactions.push_back(transactionFromJson(transactionJson));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payload.contains("shielded_scan_heights") && payload["shielded_scan_heights"].is_object()) {
|
||||||
|
for (auto it = payload["shielded_scan_heights"].begin();
|
||||||
|
it != payload["shielded_scan_heights"].end(); ++it) {
|
||||||
|
if (!it.key().empty() && it.value().is_number_integer()) {
|
||||||
|
result.shieldedScanHeights[it.key()] = it.value().get<int>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.tipHeight = tipHeight;
|
||||||
|
result.tipHash = tipHash;
|
||||||
|
result.updatedAt = updatedAt;
|
||||||
|
result.loaded = true;
|
||||||
|
} catch (...) {
|
||||||
|
result.transactions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
sodium_memzero(plainText.data(), plainText.size());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::replace(const std::string& walletIdentity,
|
||||||
|
int tipHeight,
|
||||||
|
const std::string& tipHash,
|
||||||
|
const std::vector<TransactionInfo>& transactions,
|
||||||
|
std::time_t updatedAt,
|
||||||
|
const std::unordered_map<std::string, int>& shieldedScanHeights)
|
||||||
|
{
|
||||||
|
if (!isUnlockedFor(walletIdentity) || !ensureOpen()) return false;
|
||||||
|
|
||||||
|
std::string walletHash = walletIdentityHash(walletIdentity);
|
||||||
|
json payload;
|
||||||
|
payload["schema_version"] = kSchemaVersion;
|
||||||
|
payload["wallet_hash"] = walletHash;
|
||||||
|
payload["tip_height"] = tipHeight;
|
||||||
|
payload["tip_hash"] = tipHash;
|
||||||
|
payload["updated_at"] = static_cast<std::int64_t>(updatedAt);
|
||||||
|
payload["transactions"] = json::array();
|
||||||
|
for (const auto& transaction : transactions) {
|
||||||
|
payload["transactions"].push_back(transactionToJson(transaction));
|
||||||
|
}
|
||||||
|
payload["shielded_scan_heights"] = json::object();
|
||||||
|
for (const auto& [address, height] : shieldedScanHeights) {
|
||||||
|
if (!address.empty() && height >= 0) {
|
||||||
|
payload["shielded_scan_heights"][address] = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string plainText = payload.dump();
|
||||||
|
std::vector<unsigned char> nonce;
|
||||||
|
std::vector<unsigned char> cipherText;
|
||||||
|
bool encrypted = encryptPayload(walletHash, plainText, nonce, cipherText);
|
||||||
|
sodium_memzero(plainText.data(), plainText.size());
|
||||||
|
if (!encrypted) return false;
|
||||||
|
|
||||||
|
Statement statement(db_,
|
||||||
|
"INSERT OR REPLACE INTO transaction_history_snapshots "
|
||||||
|
"(wallet_hash, schema_version, tip_height, tip_hash, updated_at, nonce, ciphertext) "
|
||||||
|
"VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
if (!statement.handle) return false;
|
||||||
|
|
||||||
|
if (!bindText(statement.handle, 1, walletHash)) return false;
|
||||||
|
sqlite3_bind_int(statement.handle, 2, kSchemaVersion);
|
||||||
|
sqlite3_bind_int(statement.handle, 3, std::max(0, tipHeight));
|
||||||
|
if (!bindText(statement.handle, 4, tipHash)) return false;
|
||||||
|
sqlite3_bind_int64(statement.handle, 5, static_cast<sqlite3_int64>(updatedAt));
|
||||||
|
if (!bindBlob(statement.handle, 6, nonce)) return false;
|
||||||
|
if (!bindBlob(statement.handle, 7, cipherText)) return false;
|
||||||
|
|
||||||
|
if (sqlite3_step(statement.handle) != SQLITE_DONE) return false;
|
||||||
|
pruneOtherWallets(walletHash); // bound DB growth — drop stale-hash snapshots/salts
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TransactionHistoryCache::clearWallet(const std::string& walletIdentity)
|
||||||
|
{
|
||||||
|
if (walletIdentity.empty()) return;
|
||||||
|
clearWalletByHash(walletIdentityHash(walletIdentity));
|
||||||
|
}
|
||||||
|
|
||||||
|
int TransactionHistoryCache::snapshotCount()
|
||||||
|
{
|
||||||
|
if (!ensureOpen()) return 0;
|
||||||
|
Statement statement(db_, "SELECT COUNT(*) FROM transaction_history_snapshots");
|
||||||
|
if (!statement.handle || sqlite3_step(statement.handle) != SQLITE_ROW) return 0;
|
||||||
|
return sqlite3_column_int(statement.handle, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::exec(const char* sql)
|
||||||
|
{
|
||||||
|
if (!db_) return false;
|
||||||
|
char* error = nullptr;
|
||||||
|
int result = sqlite3_exec(db_, sql, nullptr, nullptr, &error);
|
||||||
|
if (result != SQLITE_OK) {
|
||||||
|
DEBUG_LOGF("Transaction history cache SQL error: %s\n", error ? error : sqlite3_errmsg(db_));
|
||||||
|
if (error) sqlite3_free(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::createSchema()
|
||||||
|
{
|
||||||
|
return exec("CREATE TABLE IF NOT EXISTS transaction_history_keys ("
|
||||||
|
"wallet_hash TEXT PRIMARY KEY,"
|
||||||
|
"salt BLOB NOT NULL)") &&
|
||||||
|
exec("CREATE TABLE IF NOT EXISTS transaction_history_snapshots ("
|
||||||
|
"wallet_hash TEXT PRIMARY KEY,"
|
||||||
|
"schema_version INTEGER NOT NULL,"
|
||||||
|
"tip_height INTEGER NOT NULL,"
|
||||||
|
"tip_hash TEXT NOT NULL,"
|
||||||
|
"updated_at INTEGER NOT NULL,"
|
||||||
|
"nonce BLOB NOT NULL,"
|
||||||
|
"ciphertext BLOB NOT NULL)");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> TransactionHistoryCache::getOrCreateSalt(const std::string& walletHash)
|
||||||
|
{
|
||||||
|
if (!ensureOpen()) return {};
|
||||||
|
|
||||||
|
{
|
||||||
|
Statement statement(db_, "SELECT salt FROM transaction_history_keys WHERE wallet_hash = ?");
|
||||||
|
if (!statement.handle) return {};
|
||||||
|
if (!bindText(statement.handle, 1, walletHash)) return {};
|
||||||
|
if (sqlite3_step(statement.handle) == SQLITE_ROW) {
|
||||||
|
auto salt = readBlob(statement.handle, 0);
|
||||||
|
if (salt.size() == crypto_pwhash_SALTBYTES) return salt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> salt(crypto_pwhash_SALTBYTES);
|
||||||
|
randombytes_buf(salt.data(), salt.size());
|
||||||
|
|
||||||
|
Statement insert(db_,
|
||||||
|
"INSERT OR REPLACE INTO transaction_history_keys (wallet_hash, salt) VALUES (?, ?)");
|
||||||
|
if (!insert.handle) return {};
|
||||||
|
if (!bindText(insert.handle, 1, walletHash)) return {};
|
||||||
|
if (!bindBlob(insert.handle, 2, salt)) return {};
|
||||||
|
if (sqlite3_step(insert.handle) != SQLITE_DONE) return {};
|
||||||
|
return salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::deriveKey(const std::string& passphrase,
|
||||||
|
const std::vector<unsigned char>& salt)
|
||||||
|
{
|
||||||
|
if (salt.size() != crypto_pwhash_SALTBYTES) return false;
|
||||||
|
unsigned char derived[kKeyBytes];
|
||||||
|
int result = crypto_pwhash(derived, sizeof(derived),
|
||||||
|
passphrase.c_str(), passphrase.size(),
|
||||||
|
salt.data(),
|
||||||
|
crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||||
|
crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||||
|
crypto_pwhash_ALG_ARGON2ID13);
|
||||||
|
if (result != 0) return false;
|
||||||
|
std::copy(derived, derived + sizeof(derived), key_.begin());
|
||||||
|
sodium_memzero(derived, sizeof(derived));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::encryptPayload(const std::string& walletHash,
|
||||||
|
const std::string& plainText,
|
||||||
|
std::vector<unsigned char>& nonce,
|
||||||
|
std::vector<unsigned char>& cipherText) const
|
||||||
|
{
|
||||||
|
if (!key_ready_) return false;
|
||||||
|
nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
|
||||||
|
randombytes_buf(nonce.data(), nonce.size());
|
||||||
|
|
||||||
|
std::string associatedData = associatedDataForWallet(walletHash);
|
||||||
|
cipherText.resize(plainText.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES);
|
||||||
|
unsigned long long cipherLength = 0;
|
||||||
|
int result = crypto_aead_xchacha20poly1305_ietf_encrypt(
|
||||||
|
cipherText.data(), &cipherLength,
|
||||||
|
reinterpret_cast<const unsigned char*>(plainText.data()), plainText.size(),
|
||||||
|
reinterpret_cast<const unsigned char*>(associatedData.data()), associatedData.size(),
|
||||||
|
nullptr, nonce.data(), key_.data());
|
||||||
|
if (result != 0) return false;
|
||||||
|
cipherText.resize(static_cast<std::size_t>(cipherLength));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::decryptPayload(const std::string& walletHash,
|
||||||
|
const std::vector<unsigned char>& nonce,
|
||||||
|
const std::vector<unsigned char>& cipherText,
|
||||||
|
std::string& plainText) const
|
||||||
|
{
|
||||||
|
if (!key_ready_ || nonce.size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES ||
|
||||||
|
cipherText.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string associatedData = associatedDataForWallet(walletHash);
|
||||||
|
std::vector<unsigned char> plain(cipherText.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES);
|
||||||
|
unsigned long long plainLength = 0;
|
||||||
|
int result = crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||||
|
plain.data(), &plainLength, nullptr,
|
||||||
|
cipherText.data(), cipherText.size(),
|
||||||
|
reinterpret_cast<const unsigned char*>(associatedData.data()), associatedData.size(),
|
||||||
|
nonce.data(), key_.data());
|
||||||
|
if (result != 0) return false;
|
||||||
|
|
||||||
|
plainText.assign(reinterpret_cast<const char*>(plain.data()), static_cast<std::size_t>(plainLength));
|
||||||
|
sodium_memzero(plain.data(), plain.size());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransactionHistoryCache::readSnapshot(const std::string& walletHash,
|
||||||
|
int& tipHeight,
|
||||||
|
std::string& tipHash,
|
||||||
|
std::time_t& updatedAt,
|
||||||
|
std::vector<unsigned char>& nonce,
|
||||||
|
std::vector<unsigned char>& cipherText)
|
||||||
|
{
|
||||||
|
Statement statement(db_,
|
||||||
|
"SELECT tip_height, tip_hash, updated_at, nonce, ciphertext "
|
||||||
|
"FROM transaction_history_snapshots WHERE wallet_hash = ?");
|
||||||
|
if (!statement.handle) return false;
|
||||||
|
if (!bindText(statement.handle, 1, walletHash)) return false;
|
||||||
|
if (sqlite3_step(statement.handle) != SQLITE_ROW) return false;
|
||||||
|
|
||||||
|
tipHeight = sqlite3_column_int(statement.handle, 0);
|
||||||
|
const unsigned char* tipHashText = sqlite3_column_text(statement.handle, 1);
|
||||||
|
tipHash = tipHashText ? reinterpret_cast<const char*>(tipHashText) : std::string();
|
||||||
|
updatedAt = static_cast<std::time_t>(sqlite3_column_int64(statement.handle, 2));
|
||||||
|
nonce = readBlob(statement.handle, 3);
|
||||||
|
cipherText = readBlob(statement.handle, 4);
|
||||||
|
return !nonce.empty() && !cipherText.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TransactionHistoryCache::clearWalletByHash(const std::string& walletHash)
|
||||||
|
{
|
||||||
|
if (!ensureOpen()) return;
|
||||||
|
Statement statement(db_, "DELETE FROM transaction_history_snapshots WHERE wallet_hash = ?");
|
||||||
|
if (!statement.handle) return;
|
||||||
|
if (!bindText(statement.handle, 1, walletHash)) return;
|
||||||
|
sqlite3_step(statement.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TransactionHistoryCache::pruneOtherWallets(const std::string& keepWalletHash)
|
||||||
|
{
|
||||||
|
if (!ensureOpen() || keepWalletHash.empty()) return;
|
||||||
|
// Table names are hardcoded literals (no injection surface). Prune both the snapshot
|
||||||
|
// blobs and the now-orphaned salt rows so a stale salt can't outlive its ciphertext.
|
||||||
|
for (const char* table : {"transaction_history_snapshots", "transaction_history_keys"}) {
|
||||||
|
const std::string sql = std::string("DELETE FROM ") + table + " WHERE wallet_hash <> ?";
|
||||||
|
Statement statement(db_, sql.c_str());
|
||||||
|
if (!statement.handle) continue;
|
||||||
|
if (!bindText(statement.handle, 1, keepWalletHash)) continue;
|
||||||
|
sqlite3_step(statement.handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TransactionHistoryCache::close()
|
||||||
|
{
|
||||||
|
if (!db_) return;
|
||||||
|
sqlite3_close(db_);
|
||||||
|
db_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace data
|
||||||
|
} // namespace dragonx
|
||||||
93
src/data/transaction_history_cache.h
Normal file
93
src/data/transaction_history_cache.h
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "wallet_state.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <ctime>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct sqlite3;
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace data {
|
||||||
|
|
||||||
|
class TransactionHistoryCache {
|
||||||
|
public:
|
||||||
|
struct LoadResult {
|
||||||
|
bool loaded = false;
|
||||||
|
bool invalidated = false;
|
||||||
|
int tipHeight = 0;
|
||||||
|
std::string tipHash;
|
||||||
|
std::time_t updatedAt = 0;
|
||||||
|
std::vector<TransactionInfo> transactions;
|
||||||
|
std::unordered_map<std::string, int> shieldedScanHeights;
|
||||||
|
};
|
||||||
|
|
||||||
|
TransactionHistoryCache();
|
||||||
|
explicit TransactionHistoryCache(std::string databasePath);
|
||||||
|
~TransactionHistoryCache();
|
||||||
|
|
||||||
|
TransactionHistoryCache(const TransactionHistoryCache&) = delete;
|
||||||
|
TransactionHistoryCache& operator=(const TransactionHistoryCache&) = delete;
|
||||||
|
|
||||||
|
static std::string defaultDatabasePath();
|
||||||
|
static std::string walletIdentityFromAddresses(const std::vector<std::string>& shieldedAddresses,
|
||||||
|
const std::vector<std::string>& transparentAddresses);
|
||||||
|
static std::string walletIdentityHash(const std::string& walletIdentity);
|
||||||
|
|
||||||
|
bool ensureOpen();
|
||||||
|
bool unlockWithPassphrase(const std::string& walletIdentity, const std::string& passphrase);
|
||||||
|
void lockKey();
|
||||||
|
bool hasKey() const { return key_ready_; }
|
||||||
|
bool isUnlockedFor(const std::string& walletIdentity) const;
|
||||||
|
|
||||||
|
LoadResult load(const std::string& walletIdentity,
|
||||||
|
int currentTipHeight,
|
||||||
|
const std::string& currentTipHash);
|
||||||
|
bool replace(const std::string& walletIdentity,
|
||||||
|
int tipHeight,
|
||||||
|
const std::string& tipHash,
|
||||||
|
const std::vector<TransactionInfo>& transactions,
|
||||||
|
std::time_t updatedAt,
|
||||||
|
const std::unordered_map<std::string, int>& shieldedScanHeights = {});
|
||||||
|
void clearWallet(const std::string& walletIdentity);
|
||||||
|
int snapshotCount();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool exec(const char* sql);
|
||||||
|
bool createSchema();
|
||||||
|
std::vector<unsigned char> getOrCreateSalt(const std::string& walletHash);
|
||||||
|
bool deriveKey(const std::string& passphrase,
|
||||||
|
const std::vector<unsigned char>& salt);
|
||||||
|
bool encryptPayload(const std::string& walletHash,
|
||||||
|
const std::string& plainText,
|
||||||
|
std::vector<unsigned char>& nonce,
|
||||||
|
std::vector<unsigned char>& cipherText) const;
|
||||||
|
bool decryptPayload(const std::string& walletHash,
|
||||||
|
const std::vector<unsigned char>& nonce,
|
||||||
|
const std::vector<unsigned char>& cipherText,
|
||||||
|
std::string& plainText) const;
|
||||||
|
bool readSnapshot(const std::string& walletHash,
|
||||||
|
int& tipHeight,
|
||||||
|
std::string& tipHash,
|
||||||
|
std::time_t& updatedAt,
|
||||||
|
std::vector<unsigned char>& nonce,
|
||||||
|
std::vector<unsigned char>& cipherText);
|
||||||
|
void clearWalletByHash(const std::string& walletHash);
|
||||||
|
// Delete snapshot + salt rows for every wallet hash except the live one, bounding the
|
||||||
|
// DB so generating a new address (which changes the hash) doesn't orphan history forever.
|
||||||
|
void pruneOtherWallets(const std::string& keepWalletHash);
|
||||||
|
void close();
|
||||||
|
|
||||||
|
sqlite3* db_ = nullptr;
|
||||||
|
std::string database_path_;
|
||||||
|
std::array<unsigned char, 32> key_{};
|
||||||
|
bool key_ready_ = false;
|
||||||
|
std::string unlocked_wallet_hash_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace data
|
||||||
|
} // namespace dragonx
|
||||||
@@ -3,12 +3,44 @@
|
|||||||
// Released under the GPLv3
|
// Released under the GPLv3
|
||||||
|
|
||||||
#include "wallet_state.h"
|
#include "wallet_state.h"
|
||||||
|
#include <algorithm>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
|
|
||||||
|
std::vector<size_t> sortedSpendableAddressIndices(const std::vector<AddressInfo>& addresses,
|
||||||
|
bool requirePositiveBalance)
|
||||||
|
{
|
||||||
|
std::vector<size_t> indices;
|
||||||
|
indices.reserve(addresses.size());
|
||||||
|
for (size_t i = 0; i < addresses.size(); ++i) {
|
||||||
|
const auto& address = addresses[i];
|
||||||
|
if (!address.isSpendable()) continue;
|
||||||
|
if (requirePositiveBalance && address.balance <= 0.0) continue;
|
||||||
|
indices.push_back(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(indices.begin(), indices.end(), [&](size_t lhs, size_t rhs) {
|
||||||
|
return addresses[lhs].balance > addresses[rhs].balance;
|
||||||
|
});
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bestSpendableAddressIndex(const std::vector<AddressInfo>& addresses)
|
||||||
|
{
|
||||||
|
int bestIndex = -1;
|
||||||
|
double bestBalance = 0.0;
|
||||||
|
for (size_t i = 0; i < addresses.size(); ++i) {
|
||||||
|
if (addresses[i].isSpendable() && addresses[i].balance > bestBalance) {
|
||||||
|
bestBalance = addresses[i].balance;
|
||||||
|
bestIndex = static_cast<int>(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestIndex;
|
||||||
|
}
|
||||||
|
|
||||||
std::string TransactionInfo::getTimeString() const
|
std::string TransactionInfo::getTimeString() const
|
||||||
{
|
{
|
||||||
if (timestamp == 0) return "Unknown";
|
if (timestamp == 0) return "Unknown";
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <ctime>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "exchange_info.h"
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
|
|
||||||
@@ -18,6 +22,7 @@ struct AddressInfo {
|
|||||||
std::string address;
|
std::string address;
|
||||||
double balance = 0.0;
|
double balance = 0.0;
|
||||||
std::string type; // "shielded" or "transparent"
|
std::string type; // "shielded" or "transparent"
|
||||||
|
bool has_spending_key = true; // false for view-only (imported via z_importviewingkey)
|
||||||
|
|
||||||
// For display
|
// For display
|
||||||
std::string label;
|
std::string label;
|
||||||
@@ -25,8 +30,13 @@ struct AddressInfo {
|
|||||||
// Derived
|
// Derived
|
||||||
bool isZAddr() const { return !address.empty() && address[0] == 'z'; }
|
bool isZAddr() const { return !address.empty() && address[0] == 'z'; }
|
||||||
bool isShielded() const { return type == "shielded"; }
|
bool isShielded() const { return type == "shielded"; }
|
||||||
|
bool isSpendable() const { return has_spending_key; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
std::vector<size_t> sortedSpendableAddressIndices(const std::vector<AddressInfo>& addresses,
|
||||||
|
bool requirePositiveBalance = true);
|
||||||
|
int bestSpendableAddressIndex(const std::vector<AddressInfo>& addresses);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Represents a wallet transaction
|
* @brief Represents a wallet transaction
|
||||||
*/
|
*/
|
||||||
@@ -121,6 +131,19 @@ struct SyncInfo {
|
|||||||
float rescan_progress = 0.0f; // 0.0 - 1.0
|
float rescan_progress = 0.0f; // 0.0 - 1.0
|
||||||
std::string rescan_status; // e.g. "Rescanning... 25%"
|
std::string rescan_status; // e.g. "Rescanning... 25%"
|
||||||
|
|
||||||
|
// Sapling note witness rebuild — a distinct, often-long phase after a rescan/zap. The daemon
|
||||||
|
// reports it in TWO sub-phases with different signals, so we track which is active:
|
||||||
|
// 1 = initial pass ("Setting Initial Sapling Witness for tx <hash>, <i> of <N>") — progress
|
||||||
|
// is distinct-txs-witnessed / N (the <i> bounces, so it can't be used directly).
|
||||||
|
// 2 = witness-cache walk ("Building Witnesses for block <h> <frac> complete, <n> remaining")
|
||||||
|
// — progress derived from how far "remaining" has fallen from its per-phase peak.
|
||||||
|
// The two are sequential with different scales, so progress is NOT carried across the boundary
|
||||||
|
// (that would pin the bar at the initial pass's ~100% through the whole cache walk).
|
||||||
|
bool building_witnesses = false;
|
||||||
|
int witness_phase = 0; // 0 none, 1 initial-witness pass, 2 witness-cache walk
|
||||||
|
float witness_progress = 0.0f; // 0.0 - 1.0, within the current sub-phase
|
||||||
|
int witness_remaining = 0; // blocks left in the cache walk (0 if unknown / phase 1)
|
||||||
|
|
||||||
bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; }
|
bool isSynced() const { return !syncing && blocks > 0 && blocks >= headers - 2; }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,10 +158,26 @@ struct MarketInfo {
|
|||||||
double market_cap = 0.0;
|
double market_cap = 0.0;
|
||||||
std::string last_updated;
|
std::string last_updated;
|
||||||
std::chrono::steady_clock::time_point last_fetch_time{};
|
std::chrono::steady_clock::time_point last_fetch_time{};
|
||||||
|
bool price_loading = false;
|
||||||
|
std::string price_error;
|
||||||
|
|
||||||
// Price history for chart
|
// Live in-session price history: ~1 sample/minute, capped at MAX_HISTORY samples.
|
||||||
|
// Backs the main chart and the "minute" portfolio-sparkline interval.
|
||||||
std::vector<double> price_history;
|
std::vector<double> price_history;
|
||||||
static constexpr int MAX_HISTORY = 24; // 24 hours
|
static constexpr int MAX_HISTORY = 24; // 24 samples (~24 minutes at the 60s refresh)
|
||||||
|
|
||||||
|
// Historical USD price series fetched from CoinGecko market_chart, so the portfolio-group
|
||||||
|
// sparklines can show real hour/day/week/month trends instead of resampling the ~24-minute
|
||||||
|
// in-session buffer. Timestamped (unix seconds), oldest->newest; empty until the first fetch.
|
||||||
|
// Refreshed on a slow cadence (~30 min) since historical data moves slowly.
|
||||||
|
std::vector<std::pair<std::time_t, double>> price_chart_intraday; // ~24h @ 5-minute granularity
|
||||||
|
std::vector<std::pair<std::time_t, double>> price_chart_daily; // ~1yr @ daily granularity
|
||||||
|
std::chrono::steady_clock::time_point chart_last_fetch_time{};
|
||||||
|
bool chart_loaded = false;
|
||||||
|
|
||||||
|
// Exchanges/pairs fetched live from CoinGecko (empty until fetched; the Market tab
|
||||||
|
// falls back to data::getExchangeRegistry() while empty).
|
||||||
|
std::vector<data::ExchangeInfo> exchanges;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,6 +188,7 @@ struct PoolMiningState {
|
|||||||
bool xmrig_running = false;
|
bool xmrig_running = false;
|
||||||
std::string pool_url;
|
std::string pool_url;
|
||||||
std::string algo;
|
std::string algo;
|
||||||
|
std::string version; // running miner's version (from its API)
|
||||||
|
|
||||||
double hashrate_10s = 0;
|
double hashrate_10s = 0;
|
||||||
double hashrate_60s = 0;
|
double hashrate_60s = 0;
|
||||||
@@ -181,6 +221,12 @@ struct WalletState {
|
|||||||
// Connection
|
// Connection
|
||||||
bool connected = false;
|
bool connected = false;
|
||||||
bool warming_up = false; // daemon reachable but in RPC warmup (error -28)
|
bool warming_up = false; // daemon reachable but in RPC warmup (error -28)
|
||||||
|
// True when the daemon is up/launching but not yet answering RPC (e.g. the connect probe
|
||||||
|
// times out because the node is loading the block index). Distinct from warming_up, which
|
||||||
|
// needs a JSON-RPC -28 reply; here getinfo never returns, so we infer the state from the
|
||||||
|
// daemon's launch state + its own console output. Drives the same loading overlay so the
|
||||||
|
// user sees WHAT the node is doing instead of a bare "Connection failed".
|
||||||
|
bool daemon_initializing = false;
|
||||||
std::string warmup_status; // user-friendly title, e.g. "Processing blocks..."
|
std::string warmup_status; // user-friendly title, e.g. "Processing blocks..."
|
||||||
std::string warmup_description; // subtitle explaining the stage
|
std::string warmup_description; // subtitle explaining the stage
|
||||||
int daemon_version = 0;
|
int daemon_version = 0;
|
||||||
@@ -254,6 +300,7 @@ struct WalletState {
|
|||||||
void clear() {
|
void clear() {
|
||||||
connected = false;
|
connected = false;
|
||||||
warming_up = false;
|
warming_up = false;
|
||||||
|
daemon_initializing = false;
|
||||||
warmup_status.clear();
|
warmup_status.clear();
|
||||||
warmup_description.clear();
|
warmup_description.clear();
|
||||||
daemon_version = 0;
|
daemon_version = 0;
|
||||||
|
|||||||
@@ -11,5 +11,7 @@
|
|||||||
INCBIN(ubuntu_regular, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-R.ttf");
|
INCBIN(ubuntu_regular, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-R.ttf");
|
||||||
INCBIN(ubuntu_light, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Light.ttf");
|
INCBIN(ubuntu_light, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Light.ttf");
|
||||||
INCBIN(ubuntu_medium, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Medium.ttf");
|
INCBIN(ubuntu_medium, "@CMAKE_SOURCE_DIR@/res/fonts/Ubuntu-Medium.ttf");
|
||||||
|
INCBIN(ubuntu_mono, "@CMAKE_SOURCE_DIR@/res/fonts/UbuntuMono-R.ttf");
|
||||||
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf");
|
INCBIN(material_icons, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialIcons-Regular.ttf");
|
||||||
|
INCBIN(mdi_pickaxe_subset, "@CMAKE_SOURCE_DIR@/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf");
|
||||||
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf");
|
INCBIN(noto_cjk_subset, "@CMAKE_SOURCE_DIR@/res/fonts/NotoSansCJK-Subset.ttf");
|
||||||
|
|||||||
@@ -24,9 +24,15 @@ extern "C" {
|
|||||||
extern const unsigned char g_ubuntu_medium_data[];
|
extern const unsigned char g_ubuntu_medium_data[];
|
||||||
extern const unsigned int g_ubuntu_medium_size;
|
extern const unsigned int g_ubuntu_medium_size;
|
||||||
|
|
||||||
|
extern const unsigned char g_ubuntu_mono_data[];
|
||||||
|
extern const unsigned int g_ubuntu_mono_size;
|
||||||
|
|
||||||
extern const unsigned char g_material_icons_data[];
|
extern const unsigned char g_material_icons_data[];
|
||||||
extern const unsigned int g_material_icons_size;
|
extern const unsigned int g_material_icons_size;
|
||||||
|
|
||||||
|
extern const unsigned char g_mdi_pickaxe_subset_data[];
|
||||||
|
extern const unsigned int g_mdi_pickaxe_subset_size;
|
||||||
|
|
||||||
extern const unsigned char g_noto_cjk_subset_data[];
|
extern const unsigned char g_noto_cjk_subset_data[];
|
||||||
extern const unsigned int g_noto_cjk_subset_size;
|
extern const unsigned int g_noto_cjk_subset_size;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,65 +0,0 @@
|
|||||||
// DragonX Wallet - ImGui Edition
|
|
||||||
// Copyright 2024-2026 The Hush Developers
|
|
||||||
// Released under the GPLv3
|
|
||||||
|
|
||||||
// Embedded Resources Header
|
|
||||||
// This provides access to resources embedded in the binary
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
namespace dragonx {
|
|
||||||
namespace embedded {
|
|
||||||
|
|
||||||
// Forward declarations for embedded data (generated at build time)
|
|
||||||
struct EmbeddedResource {
|
|
||||||
const unsigned char* data;
|
|
||||||
size_t size;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resource registry
|
|
||||||
class Resources {
|
|
||||||
public:
|
|
||||||
static Resources& instance() {
|
|
||||||
static Resources inst;
|
|
||||||
return inst;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get embedded resource by name
|
|
||||||
// Returns nullptr if not found
|
|
||||||
const EmbeddedResource* get(const std::string& name) const {
|
|
||||||
auto it = resources_.find(name);
|
|
||||||
if (it != resources_.end()) {
|
|
||||||
return &it->second;
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if resource exists
|
|
||||||
bool has(const std::string& name) const {
|
|
||||||
return resources_.find(name) != resources_.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register a resource (called during static init)
|
|
||||||
void registerResource(const std::string& name, const unsigned char* data, size_t size) {
|
|
||||||
resources_[name] = {data, size};
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
Resources() = default;
|
|
||||||
std::unordered_map<std::string, EmbeddedResource> resources_;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper macro for registering resources
|
|
||||||
#define REGISTER_EMBEDDED_RESOURCE(name, data, size) \
|
|
||||||
static struct _EmbeddedResourceRegister_##name { \
|
|
||||||
_EmbeddedResourceRegister_##name() { \
|
|
||||||
dragonx::embedded::Resources::instance().registerResource(#name, data, size); \
|
|
||||||
} \
|
|
||||||
} _embedded_resource_register_##name
|
|
||||||
|
|
||||||
} // namespace embedded
|
|
||||||
} // namespace dragonx
|
|
||||||
542
src/main.cpp
542
src/main.cpp
@@ -71,11 +71,70 @@ extern "C" HRESULT __stdcall SHGetPropertyStoreForWindow(HWND hwnd, REFIID riid,
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <miniz.h>
|
||||||
#include "util/logger.h"
|
#include "util/logger.h"
|
||||||
|
|
||||||
|
// Read the just-rendered framebuffer and write it to a PNG (used by the debug screenshot sweep).
|
||||||
|
// GL: read FBO 0 (RGBA, bottom-up -> miniz flip). DX11: copy the backbuffer to a staging texture,
|
||||||
|
// map it (BGRA, top-down -> channel-swap, no flip). Alpha forced opaque (the window is transparent).
|
||||||
|
namespace {
|
||||||
|
inline void writePng(const std::string& path, const std::vector<uint8_t>& rgba, int w, int h, bool flip) {
|
||||||
|
size_t len = 0;
|
||||||
|
void* png = tdefl_write_image_to_png_file_in_memory_ex(rgba.data(), w, h, 4, &len, 6, flip ? MZ_TRUE : MZ_FALSE);
|
||||||
|
if (!png) return;
|
||||||
|
std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
|
||||||
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||||
|
if (out) out.write(reinterpret_cast<const char*>(png), static_cast<std::streamsize>(len));
|
||||||
|
mz_free(png);
|
||||||
|
}
|
||||||
|
#ifdef DRAGONX_USE_DX11
|
||||||
|
inline void captureFrameToPng(dragonx::platform::DX11Context& dx, const std::string& path) {
|
||||||
|
IDXGISwapChain1* sc = dx.swapChain(); ID3D11Device* dev = dx.device(); ID3D11DeviceContext* ctx = dx.deviceContext();
|
||||||
|
if (!sc || !dev || !ctx) return;
|
||||||
|
ID3D11Texture2D* back = nullptr;
|
||||||
|
if (FAILED(sc->GetBuffer(0, IID_PPV_ARGS(&back))) || !back) return;
|
||||||
|
D3D11_TEXTURE2D_DESC bd; back->GetDesc(&bd);
|
||||||
|
D3D11_TEXTURE2D_DESC sd = bd; sd.Usage = D3D11_USAGE_STAGING; sd.BindFlags = 0;
|
||||||
|
sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ; sd.MiscFlags = 0;
|
||||||
|
ID3D11Texture2D* staging = nullptr;
|
||||||
|
if (SUCCEEDED(dev->CreateTexture2D(&sd, nullptr, &staging)) && staging) {
|
||||||
|
ctx->CopyResource(staging, back);
|
||||||
|
D3D11_MAPPED_SUBRESOURCE m;
|
||||||
|
if (SUCCEEDED(ctx->Map(staging, 0, D3D11_MAP_READ, 0, &m))) {
|
||||||
|
int w = (int)bd.Width, h = (int)bd.Height;
|
||||||
|
std::vector<uint8_t> px((size_t)w * h * 4);
|
||||||
|
for (int y = 0; y < h; ++y) {
|
||||||
|
const uint8_t* s = (const uint8_t*)m.pData + (size_t)y * m.RowPitch;
|
||||||
|
uint8_t* d = px.data() + (size_t)y * w * 4;
|
||||||
|
for (int x = 0; x < w; ++x) { d[x*4+0]=s[x*4+2]; d[x*4+1]=s[x*4+1]; d[x*4+2]=s[x*4+0]; d[x*4+3]=255; }
|
||||||
|
}
|
||||||
|
ctx->Unmap(staging, 0);
|
||||||
|
writePng(path, px, w, h, /*flip*/false); // DX11 textures are top-down
|
||||||
|
}
|
||||||
|
staging->Release();
|
||||||
|
}
|
||||||
|
back->Release();
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
inline void captureFrameToPng(const std::string& path, int w, int h) {
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
std::vector<uint8_t> px((size_t)w * h * 4);
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||||
|
glReadBuffer(GL_BACK);
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||||
|
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px.data());
|
||||||
|
for (size_t i = 3; i < px.size(); i += 4) px[i] = 255; // opaque
|
||||||
|
writePng(path, px, w, h, /*flip*/true); // GL default framebuffer is bottom-up
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
} // namespace
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
// Windows crash handler — writes a minidump and log entry before exit
|
// Windows crash handler — writes a minidump and log entry before exit
|
||||||
static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep)
|
static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep)
|
||||||
@@ -382,6 +441,184 @@ static std::string findPaymentURI(int argc, char* argv[])
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Shared render-loop helpers (used by both the main loop and the
|
||||||
|
// resize event-watcher). Extracted verbatim to de-duplicate the
|
||||||
|
// per-frame code — same calls, same order, same #ifdef branches.
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// Draw the window backdrop (base gradient / texture tint / acrylic
|
||||||
|
// capture callback / WindowBg alpha). The main loop passes lowSpec =
|
||||||
|
// isLowSpecMode() so low-spec skips texture sampling and the capture
|
||||||
|
// callback; the resize watcher passes lowSpec = false to reproduce its
|
||||||
|
// current always-sample-and-capture behavior.
|
||||||
|
static void drawWindowBackdrop(dragonx::App& app, ImDrawList* bgDL, ImVec2 p0, ImVec2 p1,
|
||||||
|
bool backdrop_active, bool lowSpec)
|
||||||
|
{
|
||||||
|
// Read the current gradient texture from the app each frame
|
||||||
|
// so hot-reloaded / skin-changed images are picked up immediately.
|
||||||
|
ImTextureID curGradTex = app.getGradientTexture();
|
||||||
|
|
||||||
|
// Window opacity: scale only background layer alpha so the
|
||||||
|
// desktop shows through while UI stays fully opaque.
|
||||||
|
float winOpacity = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
|
||||||
|
|
||||||
|
// Scale a single color's alpha channel by window opacity
|
||||||
|
auto scaleAlpha = [&](ImU32 col) -> ImU32 {
|
||||||
|
uint8_t a = (col >> IM_COL32_A_SHIFT) & 0xFF;
|
||||||
|
a = (uint8_t)(a * winOpacity);
|
||||||
|
return (col & ~(0xFFu << IM_COL32_A_SHIFT)) | ((ImU32)a << IM_COL32_A_SHIFT);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (curGradTex != 0) {
|
||||||
|
// Base color gradient underneath the texture — read from ui.toml
|
||||||
|
const auto& S2 = dragonx::ui::schema::UI();
|
||||||
|
ImU32 baseTop = S2.resolveColor(S2.drawElement("backdrop", "base-color-top").color, IM_COL32(18,28,65,200));
|
||||||
|
ImU32 baseBottom = S2.resolveColor(S2.drawElement("backdrop", "base-color-bottom").color, IM_COL32(8,12,35,200));
|
||||||
|
|
||||||
|
if (lowSpec) {
|
||||||
|
// Low-spec: skip texture sampling — just draw the base gradient.
|
||||||
|
bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop),
|
||||||
|
scaleAlpha(baseBottom), scaleAlpha(baseBottom));
|
||||||
|
} else {
|
||||||
|
int tintAlpha = (int)S2.drawElement("backdrop", "texture-tint-alpha").size;
|
||||||
|
if (tintAlpha <= 0) tintAlpha = 140;
|
||||||
|
|
||||||
|
// Scale background gradient + texture tint alpha by
|
||||||
|
// window opacity — only affects the backdrop, not UI.
|
||||||
|
bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop),
|
||||||
|
scaleAlpha(baseBottom), scaleAlpha(baseBottom));
|
||||||
|
int scaledTintAlpha = (int)(tintAlpha * winOpacity);
|
||||||
|
ImU32 tintCol = IM_COL32(255, 255, 255, scaledTintAlpha);
|
||||||
|
bgDL->AddImage(curGradTex, p0, p1, ImVec2(0, 0), ImVec2(1, 1), tintCol);
|
||||||
|
}
|
||||||
|
} else if (backdrop_active) {
|
||||||
|
// Programmatic gradient tint (fallback when texture unavailable)
|
||||||
|
const auto& S = dragonx::ui::schema::UI();
|
||||||
|
auto bde = [&](const char* key, float fb) {
|
||||||
|
float v = S.drawElement("backdrop", key).size;
|
||||||
|
return v >= 0 ? v : fb;
|
||||||
|
};
|
||||||
|
ImU32 colTop = IM_COL32((int)bde("gradient-top-r",8), (int)bde("gradient-top-g",12),
|
||||||
|
(int)bde("gradient-top-b",28), (int)(bde("gradient-top-a",80) * winOpacity));
|
||||||
|
ImU32 colBottom = IM_COL32((int)bde("gradient-bottom-r",6), (int)bde("gradient-bottom-g",8),
|
||||||
|
(int)bde("gradient-bottom-b",18), (int)(bde("gradient-bottom-a",60) * winOpacity));
|
||||||
|
bgDL->AddRectFilledMultiColor(p0, p1, colTop, colTop, colBottom, colBottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert acrylic capture callback BEFORE the noise overlay
|
||||||
|
// so the noise grain is not captured and blurred into the
|
||||||
|
// glass cards (the noise should only be a visual overlay).
|
||||||
|
// Skip in low-spec mode — acrylic is disabled so the FBO
|
||||||
|
// capture/blur callback is unnecessary GPU work.
|
||||||
|
if (!lowSpec) {
|
||||||
|
auto captureCb = dragonx::ui::effects::ImGuiAcrylic::GetBackgroundCaptureCallback();
|
||||||
|
if (captureCb) {
|
||||||
|
bgDL->AddCallback(captureCb, nullptr);
|
||||||
|
bgDL->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowBg alpha stays at its base theme value — NOT scaled
|
||||||
|
// by window opacity. Only the background gradient/texture
|
||||||
|
// layers fade so UI elements remain fully readable.
|
||||||
|
if (backdrop_active || curGradTex != 0) {
|
||||||
|
const auto& S3 = dragonx::ui::schema::UI();
|
||||||
|
float baseBgAlpha = S3.drawElement("backdrop", "background-alpha").sizeOr(0.40f);
|
||||||
|
ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = baseBgAlpha;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: save the old-scale size,
|
||||||
|
// compute + clamp the new physical size, arm the deferred resize retry,
|
||||||
|
// and rebuild fonts / reset ImGui style at the new DPI scale. newScale
|
||||||
|
// is pre-normalized by the caller (<=0 -> 1.0f, and macOS -> 1.0f).
|
||||||
|
static void handleDisplayScaleChange(SDL_Window* window, float newScale,
|
||||||
|
std::map<int,std::pair<int,int>>& savedSizeForScale,
|
||||||
|
int& lastKnownW, int& lastKnownH,
|
||||||
|
int& dpiTargetW, int& dpiTargetH,
|
||||||
|
int& dpiResizeRetries, bool& dpiResizePending)
|
||||||
|
{
|
||||||
|
auto& typo = dragonx::ui::material::Typography::instance();
|
||||||
|
float oldScale = typo.getDpiScale();
|
||||||
|
if (std::abs(newScale - oldScale) > 0.01f) {
|
||||||
|
float ratio = newScale / oldScale;
|
||||||
|
DEBUG_LOGF("Display scale changed: %.2f -> %.2f (ratio %.2f)\n",
|
||||||
|
oldScale, newScale, ratio);
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
|
||||||
|
#ifdef DRAGONX_USE_DX11
|
||||||
|
g_borderlessDpi = newScale;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Save the last known size (before auto-resize) under
|
||||||
|
// the OLD scale. We use lastKnownW/H instead of
|
||||||
|
// SDL_GetWindowSize() because by now WM_DPICHANGED
|
||||||
|
// may have already auto-resized the window.
|
||||||
|
{
|
||||||
|
int oldPct = (int)lroundf(oldScale * 100.0f);
|
||||||
|
savedSizeForScale[oldPct] = {lastKnownW, lastKnownH};
|
||||||
|
DEBUG_LOGF(" Saved %dx%d for scale %d%%\n",
|
||||||
|
lastKnownW, lastKnownH, oldPct);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute new physical size: if we've been to this
|
||||||
|
// scale before, restore that exact size; otherwise
|
||||||
|
// proportionally scale from the current size.
|
||||||
|
int newPct = (int)lroundf(newScale * 100.0f);
|
||||||
|
int newW, newH;
|
||||||
|
auto it = savedSizeForScale.find(newPct);
|
||||||
|
if (it != savedSizeForScale.end()) {
|
||||||
|
newW = it->second.first;
|
||||||
|
newH = it->second.second;
|
||||||
|
} else {
|
||||||
|
// Use lastKnownW/H (pre-auto-resize) instead of
|
||||||
|
// SDL_GetWindowSize() which already reflects the
|
||||||
|
// WM_DPICHANGED auto-resize.
|
||||||
|
newW = (int)lroundf((float)lastKnownW * newScale / oldScale);
|
||||||
|
newH = (int)lroundf((float)lastKnownH * newScale / oldScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp to the target display's work area so the
|
||||||
|
// window doesn't overflow a smaller/higher-density monitor.
|
||||||
|
SDL_DisplayID did = SDL_GetDisplayForWindow(window);
|
||||||
|
if (did) {
|
||||||
|
SDL_Rect usable;
|
||||||
|
if (SDL_GetDisplayUsableBounds(did, &usable)) {
|
||||||
|
newW = std::min(newW, usable.w);
|
||||||
|
newH = std::min(newH, usable.h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dpiResizePending = true;
|
||||||
|
SDL_SetWindowSize(window, newW, newH);
|
||||||
|
lastKnownW = newW;
|
||||||
|
lastKnownH = newH;
|
||||||
|
// Arm deferred retry — the SetWindowSize above may
|
||||||
|
// not stick if the user is mid-drag (modal loop).
|
||||||
|
dpiTargetW = newW;
|
||||||
|
dpiTargetH = newH;
|
||||||
|
dpiResizeRetries = 30; // retry for ~30 frames
|
||||||
|
SDL_SetWindowMinimumSize(window,
|
||||||
|
(int)(1024 * newScale),
|
||||||
|
(int)(720 * newScale));
|
||||||
|
DEBUG_LOGF(" Window resized: %dx%d (scale %.2f, pct %d)\n",
|
||||||
|
newW, newH, newScale, newPct);
|
||||||
|
|
||||||
|
// 2) Rebuild font atlas at the new DPI scale
|
||||||
|
typo.reload(io, newScale);
|
||||||
|
|
||||||
|
// 3) Reset style → reapply base theme → scale by new DPI
|
||||||
|
ImGui::GetStyle() = ImGuiStyle();
|
||||||
|
dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui();
|
||||||
|
if (newScale > 1.01f) {
|
||||||
|
ImGui::GetStyle().ScaleAllSizes(newScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_LOGF(" DPI transition complete\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
// Ensure ObsidianDragon config directory exists early (before any file I/O)
|
// Ensure ObsidianDragon config directory exists early (before any file I/O)
|
||||||
@@ -941,59 +1178,10 @@ int main(int argc, char* argv[])
|
|||||||
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
|
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
|
||||||
ImVec2 p0 = vp->Pos;
|
ImVec2 p0 = vp->Pos;
|
||||||
ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
|
ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
|
||||||
ImTextureID curGradTex2 = ctx->app->getGradientTexture();
|
// Resize path historically always sampled the texture and always
|
||||||
float winOp2 = ctx->app->settings() ? ctx->app->settings()->getWindowOpacity() : 1.0f;
|
// added the capture callback — pass lowSpec = false to preserve
|
||||||
|
// that behavior exactly.
|
||||||
// Scale only background layer alpha by window opacity so the
|
drawWindowBackdrop(*ctx->app, bgDL, p0, p1, ctx->backdrop_active, /*lowSpec=*/false);
|
||||||
// desktop shows through while UI elements stay fully opaque.
|
|
||||||
auto scaleA2 = [&](ImU32 col) -> ImU32 {
|
|
||||||
uint8_t a = (col >> IM_COL32_A_SHIFT) & 0xFF;
|
|
||||||
a = (uint8_t)(a * winOp2);
|
|
||||||
return (col & ~(0xFFu << IM_COL32_A_SHIFT)) | ((ImU32)a << IM_COL32_A_SHIFT);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (curGradTex2 != 0) {
|
|
||||||
const auto& S1 = dragonx::ui::schema::UI();
|
|
||||||
ImU32 baseTop1 = S1.resolveColor(S1.drawElement("backdrop", "base-color-top").color, IM_COL32(18,28,65,200));
|
|
||||||
ImU32 baseBottom1 = S1.resolveColor(S1.drawElement("backdrop", "base-color-bottom").color, IM_COL32(8,12,35,200));
|
|
||||||
int tintAlpha1 = (int)S1.drawElement("backdrop", "texture-tint-alpha").size;
|
|
||||||
if (tintAlpha1 <= 0) tintAlpha1 = 140;
|
|
||||||
bgDL->AddRectFilledMultiColor(p0, p1, scaleA2(baseTop1), scaleA2(baseTop1),
|
|
||||||
scaleA2(baseBottom1), scaleA2(baseBottom1));
|
|
||||||
int scaledTA1 = (int)(tintAlpha1 * winOp2);
|
|
||||||
ImU32 tintCol1 = IM_COL32(255, 255, 255, scaledTA1);
|
|
||||||
bgDL->AddImage(curGradTex2, p0, p1, ImVec2(0, 0), ImVec2(1, 1), tintCol1);
|
|
||||||
} else if (ctx->backdrop_active) {
|
|
||||||
const auto& S = dragonx::ui::schema::UI();
|
|
||||||
auto bde = [&](const char* key, float fb) {
|
|
||||||
float v = S.drawElement("backdrop", key).size;
|
|
||||||
return v >= 0 ? v : fb;
|
|
||||||
};
|
|
||||||
ImU32 colTop = IM_COL32((int)bde("gradient-top-r",8), (int)bde("gradient-top-g",12),
|
|
||||||
(int)bde("gradient-top-b",28), (int)(bde("gradient-top-a",80) * winOp2));
|
|
||||||
ImU32 colBottom = IM_COL32((int)bde("gradient-bottom-r",6), (int)bde("gradient-bottom-g",8),
|
|
||||||
(int)bde("gradient-bottom-b",18), (int)(bde("gradient-bottom-a",60) * winOp2));
|
|
||||||
bgDL->AddRectFilledMultiColor(p0, p1, colTop, colTop, colBottom, colBottom);
|
|
||||||
}
|
|
||||||
// Acrylic capture callback — must fire BEFORE the noise
|
|
||||||
// overlay so the noise grain is not captured and blurred
|
|
||||||
// into the glass cards.
|
|
||||||
auto captureCb2 = dragonx::ui::effects::ImGuiAcrylic::GetBackgroundCaptureCallback();
|
|
||||||
if (captureCb2) {
|
|
||||||
bgDL->AddCallback(captureCb2, nullptr);
|
|
||||||
bgDL->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// WindowBg alpha stays at its base theme value — NOT scaled
|
|
||||||
// by window opacity. Only the background gradient/texture
|
|
||||||
// layers are scaled so the desktop shows through while UI
|
|
||||||
// elements (cards, text, buttons) remain fully readable.
|
|
||||||
if (ctx->backdrop_active || curGradTex2 != 0) {
|
|
||||||
const auto& S4 = dragonx::ui::schema::UI();
|
|
||||||
float baseBgAlpha2 = S4.drawElement("backdrop", "background-alpha").sizeOr(0.40f);
|
|
||||||
ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = baseBgAlpha2;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
@@ -1143,76 +1331,9 @@ int main(int argc, char* argv[])
|
|||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
|
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
|
||||||
#endif
|
#endif
|
||||||
auto& typo = dragonx::ui::material::Typography::instance();
|
handleDisplayScaleChange(window, newScale, savedSizeForScale,
|
||||||
float oldScale = typo.getDpiScale();
|
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
|
||||||
if (std::abs(newScale - oldScale) > 0.01f) {
|
dpiResizeRetries, dpiResizePending);
|
||||||
float ratio = newScale / oldScale;
|
|
||||||
DEBUG_LOGF("Display scale changed (idle): %.2f -> %.2f (ratio %.2f)\n",
|
|
||||||
oldScale, newScale, ratio);
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
|
|
||||||
#ifdef DRAGONX_USE_DX11
|
|
||||||
g_borderlessDpi = newScale;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Save the last known size (before auto-resize) under
|
|
||||||
// the OLD scale. We use lastKnownW/H instead of
|
|
||||||
// SDL_GetWindowSize() because by now WM_DPICHANGED
|
|
||||||
// may have already auto-resized the window.
|
|
||||||
{
|
|
||||||
int oldPct = (int)lroundf(oldScale * 100.0f);
|
|
||||||
savedSizeForScale[oldPct] = {lastKnownW, lastKnownH};
|
|
||||||
DEBUG_LOGF(" Saved %dx%d for scale %d%%\n",
|
|
||||||
lastKnownW, lastKnownH, oldPct);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute new physical size: if we've been to this
|
|
||||||
// scale before, restore that exact size; otherwise
|
|
||||||
// proportionally scale from the current size.
|
|
||||||
int newPct = (int)lroundf(newScale * 100.0f);
|
|
||||||
int newW, newH;
|
|
||||||
auto it = savedSizeForScale.find(newPct);
|
|
||||||
if (it != savedSizeForScale.end()) {
|
|
||||||
newW = it->second.first;
|
|
||||||
newH = it->second.second;
|
|
||||||
} else {
|
|
||||||
// Use lastKnownW/H (pre-auto-resize) instead of
|
|
||||||
// SDL_GetWindowSize() which already reflects the
|
|
||||||
// WM_DPICHANGED auto-resize.
|
|
||||||
newW = (int)lroundf((float)lastKnownW * newScale / oldScale);
|
|
||||||
newH = (int)lroundf((float)lastKnownH * newScale / oldScale);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clamp to the target display's work area so the
|
|
||||||
// window doesn't overflow a smaller/higher-density monitor.
|
|
||||||
SDL_DisplayID did = SDL_GetDisplayForWindow(window);
|
|
||||||
if (did) {
|
|
||||||
SDL_Rect usable;
|
|
||||||
if (SDL_GetDisplayUsableBounds(did, &usable)) {
|
|
||||||
newW = std::min(newW, usable.w);
|
|
||||||
newH = std::min(newH, usable.h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dpiResizePending = true;
|
|
||||||
SDL_SetWindowSize(window, newW, newH);
|
|
||||||
lastKnownW = newW;
|
|
||||||
lastKnownH = newH;
|
|
||||||
// Arm deferred retry — the SetWindowSize above may
|
|
||||||
// not stick if the user is mid-drag (modal loop).
|
|
||||||
dpiTargetW = newW;
|
|
||||||
dpiTargetH = newH;
|
|
||||||
dpiResizeRetries = 30; // retry for ~30 frames
|
|
||||||
SDL_SetWindowMinimumSize(window,
|
|
||||||
(int)(1024 * newScale), (int)(720 * newScale));
|
|
||||||
|
|
||||||
typo.reload(io, newScale);
|
|
||||||
ImGui::GetStyle() = ImGuiStyle();
|
|
||||||
dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui();
|
|
||||||
if (newScale > 1.01f) {
|
|
||||||
ImGui::GetStyle().ScaleAllSizes(newScale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
needsRedraw = true; // Got an event — redraw
|
needsRedraw = true; // Got an event — redraw
|
||||||
}
|
}
|
||||||
@@ -1224,6 +1345,8 @@ int main(int argc, char* argv[])
|
|||||||
|
|
||||||
// Immediate triggers: async RPC results or visible notifications
|
// Immediate triggers: async RPC results or visible notifications
|
||||||
bool hasImmediateWork = app.hasPendingRPCResults()
|
bool hasImmediateWork = app.hasPendingRPCResults()
|
||||||
|
|| app.hasTransactionSendProgress()
|
||||||
|
|| app.isTransactionRefreshInProgress()
|
||||||
|| dragonx::ui::Notifications::instance().hasActive();
|
|| dragonx::ui::Notifications::instance().hasActive();
|
||||||
|
|
||||||
// Periodic maintenance: fire refresh timers in app.update()
|
// Periodic maintenance: fire refresh timers in app.update()
|
||||||
@@ -1321,84 +1444,9 @@ int main(int argc, char* argv[])
|
|||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
|
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
|
||||||
#endif
|
#endif
|
||||||
auto& typo = dragonx::ui::material::Typography::instance();
|
handleDisplayScaleChange(window, newScale, savedSizeForScale,
|
||||||
float oldScale = typo.getDpiScale();
|
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
|
||||||
if (std::abs(newScale - oldScale) > 0.01f) {
|
dpiResizeRetries, dpiResizePending);
|
||||||
float ratio = newScale / oldScale;
|
|
||||||
DEBUG_LOGF("Display scale changed: %.2f -> %.2f (ratio %.2f)\n",
|
|
||||||
oldScale, newScale, ratio);
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
|
|
||||||
#ifdef DRAGONX_USE_DX11
|
|
||||||
g_borderlessDpi = newScale;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Save the last known size (before auto-resize) under
|
|
||||||
// the OLD scale. We use lastKnownW/H instead of
|
|
||||||
// SDL_GetWindowSize() because by now WM_DPICHANGED
|
|
||||||
// may have already auto-resized the window.
|
|
||||||
{
|
|
||||||
int oldPct = (int)lroundf(oldScale * 100.0f);
|
|
||||||
savedSizeForScale[oldPct] = {lastKnownW, lastKnownH};
|
|
||||||
DEBUG_LOGF(" Saved %dx%d for scale %d%%\n",
|
|
||||||
lastKnownW, lastKnownH, oldPct);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute new physical size: if we've been to this
|
|
||||||
// scale before, restore that exact size; otherwise
|
|
||||||
// proportionally scale from the current size.
|
|
||||||
int newPct = (int)lroundf(newScale * 100.0f);
|
|
||||||
int newW, newH;
|
|
||||||
auto it = savedSizeForScale.find(newPct);
|
|
||||||
if (it != savedSizeForScale.end()) {
|
|
||||||
newW = it->second.first;
|
|
||||||
newH = it->second.second;
|
|
||||||
} else {
|
|
||||||
// Use lastKnownW/H (pre-auto-resize) instead of
|
|
||||||
// SDL_GetWindowSize() which already reflects the
|
|
||||||
// WM_DPICHANGED auto-resize.
|
|
||||||
newW = (int)lroundf((float)lastKnownW * newScale / oldScale);
|
|
||||||
newH = (int)lroundf((float)lastKnownH * newScale / oldScale);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clamp to the target display's work area so the
|
|
||||||
// window doesn't overflow a smaller/higher-density monitor.
|
|
||||||
SDL_DisplayID did = SDL_GetDisplayForWindow(window);
|
|
||||||
if (did) {
|
|
||||||
SDL_Rect usable;
|
|
||||||
if (SDL_GetDisplayUsableBounds(did, &usable)) {
|
|
||||||
newW = std::min(newW, usable.w);
|
|
||||||
newH = std::min(newH, usable.h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dpiResizePending = true;
|
|
||||||
SDL_SetWindowSize(window, newW, newH);
|
|
||||||
lastKnownW = newW;
|
|
||||||
lastKnownH = newH;
|
|
||||||
// Arm deferred retry — the SetWindowSize above may
|
|
||||||
// not stick if the user is mid-drag (modal loop).
|
|
||||||
dpiTargetW = newW;
|
|
||||||
dpiTargetH = newH;
|
|
||||||
dpiResizeRetries = 30; // retry for ~30 frames
|
|
||||||
SDL_SetWindowMinimumSize(window,
|
|
||||||
(int)(1024 * newScale),
|
|
||||||
(int)(720 * newScale));
|
|
||||||
DEBUG_LOGF(" Window resized: %dx%d (scale %.2f, pct %d)\n",
|
|
||||||
newW, newH, newScale, newPct);
|
|
||||||
|
|
||||||
// 2) Rebuild font atlas at the new DPI scale
|
|
||||||
typo.reload(io, newScale);
|
|
||||||
|
|
||||||
// 3) Reset style → reapply base theme → scale by new DPI
|
|
||||||
ImGui::GetStyle() = ImGuiStyle();
|
|
||||||
dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui();
|
|
||||||
if (newScale > 1.01f) {
|
|
||||||
ImGui::GetStyle().ScaleAllSizes(newScale);
|
|
||||||
}
|
|
||||||
|
|
||||||
DEBUG_LOGF(" DPI transition complete\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1501,82 +1549,8 @@ int main(int argc, char* argv[])
|
|||||||
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
|
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
|
||||||
ImVec2 p0 = vp->Pos;
|
ImVec2 p0 = vp->Pos;
|
||||||
ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
|
ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
|
||||||
|
drawWindowBackdrop(app, bgDL, p0, p1, backdrop_active,
|
||||||
// Read the current gradient texture from the app each frame
|
dragonx::ui::effects::isLowSpecMode());
|
||||||
// so hot-reloaded / skin-changed images are picked up immediately.
|
|
||||||
ImTextureID curGradTex = app.getGradientTexture();
|
|
||||||
|
|
||||||
// Window opacity: scale only background layer alpha so the
|
|
||||||
// desktop shows through while UI stays fully opaque.
|
|
||||||
float winOpacity = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
|
|
||||||
|
|
||||||
bool lowSpec = dragonx::ui::effects::isLowSpecMode();
|
|
||||||
|
|
||||||
// Scale a single color's alpha channel by window opacity
|
|
||||||
auto scaleAlpha = [&](ImU32 col) -> ImU32 {
|
|
||||||
uint8_t a = (col >> IM_COL32_A_SHIFT) & 0xFF;
|
|
||||||
a = (uint8_t)(a * winOpacity);
|
|
||||||
return (col & ~(0xFFu << IM_COL32_A_SHIFT)) | ((ImU32)a << IM_COL32_A_SHIFT);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (curGradTex != 0) {
|
|
||||||
// Base color gradient underneath the texture — read from ui.toml
|
|
||||||
const auto& S2 = dragonx::ui::schema::UI();
|
|
||||||
ImU32 baseTop = S2.resolveColor(S2.drawElement("backdrop", "base-color-top").color, IM_COL32(18,28,65,200));
|
|
||||||
ImU32 baseBottom = S2.resolveColor(S2.drawElement("backdrop", "base-color-bottom").color, IM_COL32(8,12,35,200));
|
|
||||||
|
|
||||||
if (lowSpec) {
|
|
||||||
// Low-spec: skip texture sampling — just draw the base gradient.
|
|
||||||
bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop),
|
|
||||||
scaleAlpha(baseBottom), scaleAlpha(baseBottom));
|
|
||||||
} else {
|
|
||||||
int tintAlpha = (int)S2.drawElement("backdrop", "texture-tint-alpha").size;
|
|
||||||
if (tintAlpha <= 0) tintAlpha = 140;
|
|
||||||
|
|
||||||
// Scale background gradient + texture tint alpha by
|
|
||||||
// window opacity — only affects the backdrop, not UI.
|
|
||||||
bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop),
|
|
||||||
scaleAlpha(baseBottom), scaleAlpha(baseBottom));
|
|
||||||
int scaledTintAlpha = (int)(tintAlpha * winOpacity);
|
|
||||||
ImU32 tintCol = IM_COL32(255, 255, 255, scaledTintAlpha);
|
|
||||||
bgDL->AddImage(curGradTex, p0, p1, ImVec2(0, 0), ImVec2(1, 1), tintCol);
|
|
||||||
}
|
|
||||||
} else if (backdrop_active) {
|
|
||||||
// Programmatic gradient tint (fallback when texture unavailable)
|
|
||||||
const auto& S = dragonx::ui::schema::UI();
|
|
||||||
auto bde = [&](const char* key, float fb) {
|
|
||||||
float v = S.drawElement("backdrop", key).size;
|
|
||||||
return v >= 0 ? v : fb;
|
|
||||||
};
|
|
||||||
ImU32 colTop = IM_COL32((int)bde("gradient-top-r",8), (int)bde("gradient-top-g",12),
|
|
||||||
(int)bde("gradient-top-b",28), (int)(bde("gradient-top-a",80) * winOpacity));
|
|
||||||
ImU32 colBottom = IM_COL32((int)bde("gradient-bottom-r",6), (int)bde("gradient-bottom-g",8),
|
|
||||||
(int)bde("gradient-bottom-b",18), (int)(bde("gradient-bottom-a",60) * winOpacity));
|
|
||||||
bgDL->AddRectFilledMultiColor(p0, p1, colTop, colTop, colBottom, colBottom);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert acrylic capture callback BEFORE the noise overlay
|
|
||||||
// so the noise grain is not captured and blurred into the
|
|
||||||
// glass cards (the noise should only be a visual overlay).
|
|
||||||
// Skip in low-spec mode — acrylic is disabled so the FBO
|
|
||||||
// capture/blur callback is unnecessary GPU work.
|
|
||||||
if (!lowSpec) {
|
|
||||||
auto captureCb = dragonx::ui::effects::ImGuiAcrylic::GetBackgroundCaptureCallback();
|
|
||||||
if (captureCb) {
|
|
||||||
bgDL->AddCallback(captureCb, nullptr);
|
|
||||||
bgDL->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WindowBg alpha stays at its base theme value — NOT scaled
|
|
||||||
// by window opacity. Only the background gradient/texture
|
|
||||||
// layers fade so UI elements remain fully readable.
|
|
||||||
if (backdrop_active || curGradTex != 0) {
|
|
||||||
const auto& S3 = dragonx::ui::schema::UI();
|
|
||||||
float baseBgAlpha = S3.drawElement("backdrop", "background-alpha").sizeOr(0.40f);
|
|
||||||
ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = baseBgAlpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PERF_END("Backdrop", _perfBackdrop);
|
PERF_END("Backdrop", _perfBackdrop);
|
||||||
@@ -1717,6 +1691,12 @@ int main(int argc, char* argv[])
|
|||||||
}
|
}
|
||||||
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
||||||
|
|
||||||
|
// Debug screenshot sweep — grab the finished frame before present.
|
||||||
|
if (app.wantsScreenshotThisFrame()) {
|
||||||
|
captureFrameToPng(dx, app.screenshotSweepPath());
|
||||||
|
app.onScreenshotCaptured();
|
||||||
|
}
|
||||||
|
|
||||||
// Update and Render additional Platform Windows (multi-viewport)
|
// Update and Render additional Platform Windows (multi-viewport)
|
||||||
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
|
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
|
||||||
{
|
{
|
||||||
@@ -1740,6 +1720,12 @@ int main(int argc, char* argv[])
|
|||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||||
|
|
||||||
|
// Debug screenshot sweep — grab the finished frame before swap.
|
||||||
|
if (app.wantsScreenshotThisFrame()) {
|
||||||
|
captureFrameToPng(app.screenshotSweepPath(), (int)io.DisplaySize.x, (int)io.DisplaySize.y);
|
||||||
|
app.onScreenshotCaptured();
|
||||||
|
}
|
||||||
|
|
||||||
// Update and Render additional Platform Windows (multi-viewport)
|
// Update and Render additional Platform Windows (multi-viewport)
|
||||||
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
|
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
|
||||||
{
|
{
|
||||||
@@ -1801,6 +1787,8 @@ int main(int argc, char* argv[])
|
|||||||
&& !opaqueBackground;
|
&& !opaqueBackground;
|
||||||
bool animating = app.isShuttingDown()
|
bool animating = app.isShuttingDown()
|
||||||
|| backdropNeedsFrames
|
|| backdropNeedsFrames
|
||||||
|
|| app.hasTransactionSendProgress()
|
||||||
|
|| app.isTransactionRefreshInProgress()
|
||||||
|| dragonx::ui::effects::ThemeEffects::instance().hasActiveAnimation()
|
|| dragonx::ui::effects::ThemeEffects::instance().hasActiveAnimation()
|
||||||
|| dragonx::ui::Notifications::instance().hasActive()
|
|| dragonx::ui::Notifications::instance().hasActive()
|
||||||
|| dragonx::ui::material::SmoothScrollAnimating();
|
|| dragonx::ui::material::SmoothScrollAnimating();
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cctype>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -225,11 +227,13 @@ bool needsParamsExtraction()
|
|||||||
if (spendRes && resourceNeedsUpdate(spendRes, spendPath)) return true;
|
if (spendRes && resourceNeedsUpdate(spendRes, spendPath)) return true;
|
||||||
if (outputRes && resourceNeedsUpdate(outputRes, outputPath)) return true;
|
if (outputRes && resourceNeedsUpdate(outputRes, outputPath)) return true;
|
||||||
|
|
||||||
// Also check if daemon binaries need updating
|
// Daemon binaries are only auto-placed when MISSING (never auto-overwritten on a size
|
||||||
|
// mismatch) — the user may be running a specific dragonxd. Replacing the bundled daemon is
|
||||||
|
// an explicit action via Settings → daemon binary. So only trigger extraction if it's absent.
|
||||||
#ifdef HAS_EMBEDDED_DAEMON
|
#ifdef HAS_EMBEDDED_DAEMON
|
||||||
const auto* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
|
const auto* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
|
||||||
std::string daemonPath = daemonDir + pathSep + RESOURCE_DRAGONXD;
|
std::string daemonPath = daemonDir + pathSep + RESOURCE_DRAGONXD;
|
||||||
if (daemonRes && resourceNeedsUpdate(daemonRes, daemonPath)) return true;
|
if (daemonRes && !std::filesystem::exists(daemonPath)) return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAS_EMBEDDED_XMRIG
|
#ifdef HAS_EMBEDDED_XMRIG
|
||||||
@@ -274,8 +278,30 @@ static bool extractResource(const EmbeddedResource* res, const std::string& dest
|
|||||||
// Write file
|
// Write file
|
||||||
std::ofstream file(destPath, std::ios::binary);
|
std::ofstream file(destPath, std::ios::binary);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
DEBUG_LOGF("[ERROR] Failed to open %s for writing\n", destPath.c_str());
|
// The destination may be locked because the previous daemon is still using the binary:
|
||||||
return false;
|
// Windows locks a running .exe against truncation, Linux returns ETXTBSY. Both platforms
|
||||||
|
// DO allow renaming/moving such a file — the running process keeps the moved copy — so move
|
||||||
|
// the stale binary aside and write a fresh one at the original path.
|
||||||
|
std::error_code ec;
|
||||||
|
if (std::filesystem::exists(destPath)) {
|
||||||
|
std::string sidelined = destPath + ".old";
|
||||||
|
std::filesystem::remove(sidelined, ec); // clear any leftover from a prior swap
|
||||||
|
ec.clear();
|
||||||
|
std::filesystem::rename(destPath, sidelined, ec);
|
||||||
|
if (!ec) {
|
||||||
|
file.clear();
|
||||||
|
file.open(destPath, std::ios::binary);
|
||||||
|
if (file)
|
||||||
|
DEBUG_LOGF("[INFO] Replaced in-use %s (old copy moved to .old)\n", destPath.c_str());
|
||||||
|
} else {
|
||||||
|
DEBUG_LOGF("[WARN] Could not move stale %s aside: %s\n",
|
||||||
|
destPath.c_str(), ec.message().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!file) {
|
||||||
|
DEBUG_LOGF("[ERROR] Failed to open %s for writing\n", destPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
file.write(reinterpret_cast<const char*>(res->data), res->size);
|
file.write(reinterpret_cast<const char*>(res->data), res->size);
|
||||||
@@ -347,12 +373,12 @@ bool extractEmbeddedResources()
|
|||||||
#ifdef HAS_EMBEDDED_DAEMON
|
#ifdef HAS_EMBEDDED_DAEMON
|
||||||
DEBUG_LOGF("[INFO] Daemon extraction directory: %s\n", daemonDir.c_str());
|
DEBUG_LOGF("[INFO] Daemon extraction directory: %s\n", daemonDir.c_str());
|
||||||
|
|
||||||
|
// Daemon binaries are placed ONLY when missing — never auto-overwritten on a size mismatch
|
||||||
|
// (the user may run a specific dragonxd; replacing it is an explicit Settings action).
|
||||||
const EmbeddedResource* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
|
const EmbeddedResource* daemonRes = getEmbeddedResource(RESOURCE_DRAGONXD);
|
||||||
if (daemonRes) {
|
if (daemonRes) {
|
||||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONXD;
|
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONXD;
|
||||||
if (resourceNeedsUpdate(daemonRes, dest)) {
|
if (!std::filesystem::exists(dest)) {
|
||||||
if (std::filesystem::exists(dest))
|
|
||||||
DEBUG_LOGF("[INFO] Updating stale dragonxd (size mismatch)...\n");
|
|
||||||
DEBUG_LOGF("[INFO] Extracting dragonxd (%zu MB)...\n", daemonRes->size / (1024*1024));
|
DEBUG_LOGF("[INFO] Extracting dragonxd (%zu MB)...\n", daemonRes->size / (1024*1024));
|
||||||
if (!extractResource(daemonRes, dest)) {
|
if (!extractResource(daemonRes, dest)) {
|
||||||
success = false;
|
success = false;
|
||||||
@@ -366,9 +392,7 @@ bool extractEmbeddedResources()
|
|||||||
const EmbeddedResource* cliRes = getEmbeddedResource(RESOURCE_DRAGONX_CLI);
|
const EmbeddedResource* cliRes = getEmbeddedResource(RESOURCE_DRAGONX_CLI);
|
||||||
if (cliRes) {
|
if (cliRes) {
|
||||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_CLI;
|
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_CLI;
|
||||||
if (resourceNeedsUpdate(cliRes, dest)) {
|
if (!std::filesystem::exists(dest)) {
|
||||||
if (std::filesystem::exists(dest))
|
|
||||||
DEBUG_LOGF("[INFO] Updating stale dragonx-cli (size mismatch)...\n");
|
|
||||||
DEBUG_LOGF("[INFO] Extracting dragonx-cli (%zu MB)...\n", cliRes->size / (1024*1024));
|
DEBUG_LOGF("[INFO] Extracting dragonx-cli (%zu MB)...\n", cliRes->size / (1024*1024));
|
||||||
if (!extractResource(cliRes, dest)) {
|
if (!extractResource(cliRes, dest)) {
|
||||||
success = false;
|
success = false;
|
||||||
@@ -382,9 +406,7 @@ bool extractEmbeddedResources()
|
|||||||
const EmbeddedResource* txRes = getEmbeddedResource(RESOURCE_DRAGONX_TX);
|
const EmbeddedResource* txRes = getEmbeddedResource(RESOURCE_DRAGONX_TX);
|
||||||
if (txRes) {
|
if (txRes) {
|
||||||
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_TX;
|
std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_TX;
|
||||||
if (resourceNeedsUpdate(txRes, dest)) {
|
if (!std::filesystem::exists(dest)) {
|
||||||
if (std::filesystem::exists(dest))
|
|
||||||
DEBUG_LOGF("[INFO] Updating stale dragonx-tx (size mismatch)...\n");
|
|
||||||
DEBUG_LOGF("[INFO] Extracting dragonx-tx (%zu MB)...\n", txRes->size / (1024*1024));
|
DEBUG_LOGF("[INFO] Extracting dragonx-tx (%zu MB)...\n", txRes->size / (1024*1024));
|
||||||
if (!extractResource(txRes, dest)) {
|
if (!extractResource(txRes, dest)) {
|
||||||
success = false;
|
success = false;
|
||||||
@@ -414,6 +436,17 @@ bool extractEmbeddedResources()
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Best-effort cleanup of any ".old" binaries left behind by a previous in-use replacement.
|
||||||
|
// Once the old daemon/xmrig process has exited, the file is no longer locked and removes cleanly;
|
||||||
|
// if it's still running, the remove fails harmlessly and we retry on the next startup.
|
||||||
|
{
|
||||||
|
std::error_code ec;
|
||||||
|
for (const char* name : { RESOURCE_DRAGONXD, RESOURCE_XMRIG }) {
|
||||||
|
std::filesystem::remove(daemonDir + pathSep + name + std::string(".old"), ec);
|
||||||
|
ec.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,6 +590,120 @@ bool forceExtractXmrig()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scan a binary blob for the daemon's version stamp: 'v' <maj>.<min>.<rev> optionally followed by
|
||||||
|
// '-' <commit hash (>=6 hex)>, e.g. "v1.0.2-ddd851dc1". Returns the first match, or "" if none.
|
||||||
|
static std::string scanBinaryVersion(const uint8_t* data, std::size_t size)
|
||||||
|
{
|
||||||
|
if (!data || size < 6) return "";
|
||||||
|
auto isdig = [](uint8_t c) { return std::isdigit(static_cast<unsigned char>(c)) != 0; };
|
||||||
|
auto isxd = [](uint8_t c) { return std::isxdigit(static_cast<unsigned char>(c)) != 0; };
|
||||||
|
for (std::size_t i = 0; i + 5 < size; ++i) {
|
||||||
|
if (data[i] != 'v') continue;
|
||||||
|
std::size_t k = i + 1, s;
|
||||||
|
s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // major
|
||||||
|
if (k >= size || data[k] != '.') continue; ++k;
|
||||||
|
s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // minor
|
||||||
|
if (k >= size || data[k] != '.') continue; ++k;
|
||||||
|
s = k; while (k < size && isdig(data[k])) ++k; if (k == s) continue; // revision
|
||||||
|
std::size_t end = k;
|
||||||
|
if (k < size && data[k] == '-') { // optional -<commit>
|
||||||
|
std::size_t h = k + 1, hs = h;
|
||||||
|
while (h < size && isxd(data[h])) ++h;
|
||||||
|
if (h - hs >= 6) end = h;
|
||||||
|
}
|
||||||
|
return std::string(reinterpret_cast<const char*>(data) + i, end - i);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
DaemonBinaryInfo getInstalledDaemonInfo()
|
||||||
|
{
|
||||||
|
DaemonBinaryInfo info;
|
||||||
|
std::string daemonDir = getDaemonDirectory();
|
||||||
|
#ifdef _WIN32
|
||||||
|
info.path = daemonDir + "\\" + RESOURCE_DRAGONXD;
|
||||||
|
#else
|
||||||
|
info.path = daemonDir + "/" + RESOURCE_DRAGONXD;
|
||||||
|
#endif
|
||||||
|
std::error_code ec;
|
||||||
|
if (!std::filesystem::exists(info.path, ec)) return info; // exists stays false
|
||||||
|
info.exists = true;
|
||||||
|
info.size = std::filesystem::file_size(info.path, ec);
|
||||||
|
if (ec) info.size = 0;
|
||||||
|
|
||||||
|
auto ftime = std::filesystem::last_write_time(info.path, ec);
|
||||||
|
if (!ec) {
|
||||||
|
// Convert filesystem clock → system_clock epoch (pre-C++20 portable approximation).
|
||||||
|
auto sysTime = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
|
||||||
|
ftime - decltype(ftime)::clock::now() + std::chrono::system_clock::now());
|
||||||
|
info.modifiedEpoch =
|
||||||
|
static_cast<std::int64_t>(std::chrono::system_clock::to_time_t(sysTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the binary and scan for its version stamp (one-off; caller caches the result).
|
||||||
|
std::ifstream f(info.path, std::ios::binary);
|
||||||
|
if (f) {
|
||||||
|
f.seekg(0, std::ios::end);
|
||||||
|
std::streamoff len = f.tellg();
|
||||||
|
f.seekg(0, std::ios::beg);
|
||||||
|
if (len > 0) {
|
||||||
|
std::vector<uint8_t> buf(static_cast<std::size_t>(len));
|
||||||
|
f.read(reinterpret_cast<char*>(buf.data()), len);
|
||||||
|
info.version = scanBinaryVersion(buf.data(), static_cast<std::size_t>(f.gcount()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
BundledDaemonInfo getBundledDaemonInfo()
|
||||||
|
{
|
||||||
|
BundledDaemonInfo info;
|
||||||
|
#ifdef HAS_EMBEDDED_DAEMON
|
||||||
|
const EmbeddedResource* res = getEmbeddedResource(RESOURCE_DRAGONXD);
|
||||||
|
if (res && res->data && res->size > 0) {
|
||||||
|
info.available = true;
|
||||||
|
info.size = res->size;
|
||||||
|
// The embedded bytes are constant for this build — scan once.
|
||||||
|
static const std::string cachedVersion = scanBinaryVersion(res->data, res->size);
|
||||||
|
info.version = cachedVersion;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool reextractBundledDaemon()
|
||||||
|
{
|
||||||
|
#ifdef HAS_EMBEDDED_DAEMON
|
||||||
|
std::string daemonDir = getDaemonDirectory();
|
||||||
|
#ifdef _WIN32
|
||||||
|
const char pathSep = '\\';
|
||||||
|
#else
|
||||||
|
const char pathSep = '/';
|
||||||
|
#endif
|
||||||
|
bool ok = true;
|
||||||
|
bool wroteAny = false;
|
||||||
|
const char* names[] = { RESOURCE_DRAGONXD, RESOURCE_DRAGONX_CLI, RESOURCE_DRAGONX_TX };
|
||||||
|
for (const char* name : names) {
|
||||||
|
const EmbeddedResource* res = getEmbeddedResource(name);
|
||||||
|
if (!res) continue;
|
||||||
|
std::string dest = daemonDir + pathSep + name;
|
||||||
|
DEBUG_LOGF("[INFO] reextractBundledDaemon: writing %s (%zu MB)\n", name, res->size / (1024*1024));
|
||||||
|
if (!extractResource(res, dest)) {
|
||||||
|
DEBUG_LOGF("[ERROR] reextractBundledDaemon: failed to write %s\n", name);
|
||||||
|
ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
wroteAny = true;
|
||||||
|
#ifndef _WIN32
|
||||||
|
chmod(dest.c_str(), 0755);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
return ok && wroteAny;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
std::string getXmrigPath()
|
std::string getXmrigPath()
|
||||||
{
|
{
|
||||||
std::string daemonDir = getDaemonDirectory();
|
std::string daemonDir = getDaemonDirectory();
|
||||||
|
|||||||
@@ -30,6 +30,31 @@ bool needsParamsExtraction();
|
|||||||
// Get the params directory path
|
// Get the params directory path
|
||||||
std::string getParamsDirectory();
|
std::string getParamsDirectory();
|
||||||
|
|
||||||
|
// --- Daemon binary management (Settings → daemon binary panel) ------------------------------
|
||||||
|
// Info about the dragonxd binary currently installed in the dragonx/ extraction directory.
|
||||||
|
struct DaemonBinaryInfo {
|
||||||
|
bool exists = false;
|
||||||
|
std::string path;
|
||||||
|
std::uintmax_t size = 0;
|
||||||
|
std::string version; // scanned from the binary ("vX.Y.Z-<commit>"), empty if not found
|
||||||
|
std::int64_t modifiedEpoch = 0; // last-write time as unix epoch seconds, 0 if unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
// Info about the dragonxd binary bundled inside this wallet build.
|
||||||
|
struct BundledDaemonInfo {
|
||||||
|
bool available = false; // a daemon resource is embedded in this build
|
||||||
|
std::uintmax_t size = 0;
|
||||||
|
std::string version;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read + scan the installed dragonxd (reads the file; call off the UI thread or cache the result).
|
||||||
|
DaemonBinaryInfo getInstalledDaemonInfo();
|
||||||
|
// Info about the bundled daemon (scans the embedded bytes once, cached).
|
||||||
|
BundledDaemonInfo getBundledDaemonInfo();
|
||||||
|
// Force-overwrite the installed dragonx binaries (dragonxd/cli/tx) with the bundled ones. The
|
||||||
|
// caller should stop the daemon first. Returns true if all present resources were written.
|
||||||
|
bool reextractBundledDaemon();
|
||||||
|
|
||||||
// Resource names
|
// Resource names
|
||||||
constexpr const char* RESOURCE_SAPLING_SPEND = "sapling-spend.params";
|
constexpr const char* RESOURCE_SAPLING_SPEND = "sapling-spend.params";
|
||||||
constexpr const char* RESOURCE_SAPLING_OUTPUT = "sapling-output.params";
|
constexpr const char* RESOURCE_SAPLING_OUTPUT = "sapling-output.params";
|
||||||
|
|||||||
@@ -6,11 +6,14 @@
|
|||||||
#include "../config/version.h"
|
#include "../config/version.h"
|
||||||
#include "../resources/embedded_resources.h"
|
#include "../resources/embedded_resources.h"
|
||||||
|
|
||||||
|
#include <sodium.h>
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <ctime>
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
|
|
||||||
@@ -26,6 +29,56 @@ namespace fs = std::filesystem;
|
|||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace rpc {
|
namespace rpc {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string generateSecureRandomString(size_t length)
|
||||||
|
{
|
||||||
|
static constexpr char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
static constexpr uint32_t charsetSize = static_cast<uint32_t>(sizeof(charset) - 1);
|
||||||
|
|
||||||
|
if (sodium_init() < 0) {
|
||||||
|
DEBUG_LOGF("Failed to initialize libsodium for RPC credential generation\n");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string result;
|
||||||
|
result.reserve(length);
|
||||||
|
for (size_t i = 0; i < length; ++i) {
|
||||||
|
result.push_back(charset[randombytes_uniform(charsetSize)]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string lowercase(std::string value)
|
||||||
|
{
|
||||||
|
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
|
||||||
|
return static_cast<char>(std::tolower(c));
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseBoolValue(const std::string& value)
|
||||||
|
{
|
||||||
|
std::string lowered = lowercase(value);
|
||||||
|
return lowered == "1" || lowered == "true" || lowered == "yes" || lowered == "on";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool applyCookieAuth(ConnectionConfig& config, const std::string& dataDir)
|
||||||
|
{
|
||||||
|
std::string cookieUser, cookiePass;
|
||||||
|
if (!Connection::readAuthCookie(dataDir, cookieUser, cookiePass)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.rpcuser = cookieUser;
|
||||||
|
config.rpcpassword = cookiePass;
|
||||||
|
config.auth_source = AuthSource::Cookie;
|
||||||
|
if (config.hush_dir.empty()) config.hush_dir = dataDir;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
Connection::Connection() = default;
|
Connection::Connection() = default;
|
||||||
Connection::~Connection() = default;
|
Connection::~Connection() = default;
|
||||||
|
|
||||||
@@ -140,9 +193,15 @@ ConnectionConfig Connection::parseConfFile(const std::string& path)
|
|||||||
config.host = value;
|
config.host = value;
|
||||||
} else if (key == "proxy") {
|
} else if (key == "proxy") {
|
||||||
config.proxy = value;
|
config.proxy = value;
|
||||||
|
} else if (key == "rpctls" || key == "rpcssl" || key == "use_tls" || key == "rpcuse_tls") {
|
||||||
|
config.use_tls = parseBoolValue(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!config.rpcuser.empty() || !config.rpcpassword.empty()) {
|
||||||
|
config.auth_source = AuthSource::ConfigFile;
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,10 +236,7 @@ ConnectionConfig Connection::autoDetectConfig()
|
|||||||
|
|
||||||
// If rpcpassword is empty, the daemon may be using .cookie auth
|
// If rpcpassword is empty, the daemon may be using .cookie auth
|
||||||
if (config.rpcpassword.empty()) {
|
if (config.rpcpassword.empty()) {
|
||||||
std::string cookieUser, cookiePass;
|
if (applyCookieAuth(config, data_dir)) {
|
||||||
if (readAuthCookie(data_dir, cookieUser, cookiePass)) {
|
|
||||||
config.rpcuser = cookieUser;
|
|
||||||
config.rpcpassword = cookiePass;
|
|
||||||
DEBUG_LOGF("Using .cookie authentication (no rpcpassword in config)\n");
|
DEBUG_LOGF("Using .cookie authentication (no rpcpassword in config)\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,23 +252,57 @@ ConnectionConfig Connection::autoDetectConfig()
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Connection::buildCookieAuthConfig(const ConnectionConfig& base, ConnectionConfig& cookieConfig)
|
||||||
|
{
|
||||||
|
if (base.auth_source == AuthSource::Cookie) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string dataDir = base.hush_dir.empty() ? getDefaultDataDir() : base.hush_dir;
|
||||||
|
ConnectionConfig fallback = base;
|
||||||
|
if (!applyCookieAuth(fallback, dataDir)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cookieConfig = std::move(fallback);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Connection::isLocalHost(const std::string& host)
|
||||||
|
{
|
||||||
|
std::string lowered = lowercase(host);
|
||||||
|
if (!lowered.empty() && lowered.front() == '[' && lowered.back() == ']') {
|
||||||
|
lowered = lowered.substr(1, lowered.size() - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lowered == "localhost" || lowered == "localhost." ||
|
||||||
|
lowered == "::1" || lowered == "0:0:0:0:0:0:0:1" ||
|
||||||
|
lowered == "127.0.0.1" || lowered.rfind("127.", 0) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Connection::usesPlaintextRemote(const ConnectionConfig& config)
|
||||||
|
{
|
||||||
|
return !config.use_tls && !isLocalHost(config.host);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* Connection::authSourceName(AuthSource source)
|
||||||
|
{
|
||||||
|
switch (source) {
|
||||||
|
case AuthSource::ConfigFile: return "config";
|
||||||
|
case AuthSource::Cookie: return "cookie";
|
||||||
|
case AuthSource::Missing: return "missing";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
bool Connection::createDefaultConfig(const std::string& path)
|
bool Connection::createDefaultConfig(const std::string& path)
|
||||||
{
|
{
|
||||||
// Generate random rpcuser/rpcpassword
|
std::string rpcuser = generateSecureRandomString(16);
|
||||||
auto generateRandomString = [](int length) -> std::string {
|
std::string rpcpassword = generateSecureRandomString(32);
|
||||||
const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
if (rpcuser.empty() || rpcpassword.empty()) {
|
||||||
std::string result;
|
DEBUG_LOGF("Failed to generate secure RPC credentials for config file: %s\n", path.c_str());
|
||||||
result.reserve(length);
|
return false;
|
||||||
|
}
|
||||||
std::srand(static_cast<unsigned>(std::time(nullptr)));
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
result += charset[std::rand() % (sizeof(charset) - 1)];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string rpcuser = generateRandomString(16);
|
|
||||||
std::string rpcpassword = generateRandomString(32);
|
|
||||||
|
|
||||||
std::ofstream file(path);
|
std::ofstream file(path);
|
||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
@@ -242,6 +332,17 @@ bool Connection::createDefaultConfig(const std::string& path)
|
|||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
|
// The file holds the freshly-generated rpcuser/rpcpassword in plaintext. ofstream creates it
|
||||||
|
// with the process umask (typically world-readable 0644), so restrict it to owner read/write
|
||||||
|
// before another local user can read the credentials.
|
||||||
|
{
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
std::error_code ec;
|
||||||
|
fs::permissions(path, fs::perms::owner_read | fs::perms::owner_write,
|
||||||
|
fs::perm_options::replace, ec);
|
||||||
|
if (ec) DEBUG_LOGF("Could not restrict config permissions on %s: %s\n", path.c_str(), ec.message().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
DEBUG_LOGF("Created default config file: %s\n", path.c_str());
|
DEBUG_LOGF("Created default config file: %s\n", path.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ namespace rpc {
|
|||||||
/**
|
/**
|
||||||
* @brief Connection configuration
|
* @brief Connection configuration
|
||||||
*/
|
*/
|
||||||
|
enum class AuthSource {
|
||||||
|
Missing,
|
||||||
|
ConfigFile,
|
||||||
|
Cookie
|
||||||
|
};
|
||||||
|
|
||||||
struct ConnectionConfig {
|
struct ConnectionConfig {
|
||||||
std::string host = "127.0.0.1";
|
std::string host = "127.0.0.1";
|
||||||
std::string port = "21769";
|
std::string port = "21769";
|
||||||
@@ -20,6 +26,8 @@ struct ConnectionConfig {
|
|||||||
std::string hush_dir;
|
std::string hush_dir;
|
||||||
std::string proxy; // SOCKS5 proxy for Tor
|
std::string proxy; // SOCKS5 proxy for Tor
|
||||||
bool use_embedded = true;
|
bool use_embedded = true;
|
||||||
|
bool use_tls = false;
|
||||||
|
AuthSource auth_source = AuthSource::Missing;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,6 +104,23 @@ public:
|
|||||||
*/
|
*/
|
||||||
static bool readAuthCookie(const std::string& dataDir, std::string& user, std::string& password);
|
static bool readAuthCookie(const std::string& dataDir, std::string& user, std::string& password);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Build a cookie-auth retry config from a failed config-auth attempt
|
||||||
|
*/
|
||||||
|
static bool buildCookieAuthConfig(const ConnectionConfig& base, ConnectionConfig& cookieConfig);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether a host is local enough for plaintext HTTP RPC
|
||||||
|
*/
|
||||||
|
static bool isLocalHost(const std::string& host);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether this config would send RPC credentials over plaintext to a remote host
|
||||||
|
*/
|
||||||
|
static bool usesPlaintextRemote(const ConnectionConfig& config);
|
||||||
|
|
||||||
|
static const char* authSourceName(AuthSource source);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,82 @@
|
|||||||
// All calls are blocking; run on RPCWorker threads, never on main thread.
|
// All calls are blocking; run on RPCWorker threads, never on main thread.
|
||||||
|
|
||||||
#include "rpc_client.h"
|
#include "rpc_client.h"
|
||||||
|
#include "connection.h"
|
||||||
#include "../config/version.h"
|
#include "../config/version.h"
|
||||||
#include "../util/base64.h"
|
#include "../util/base64.h"
|
||||||
|
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
|
#include <sodium.h>
|
||||||
|
#include <atomic>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <utility>
|
||||||
#include "../util/logger.h"
|
#include "../util/logger.h"
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
namespace rpc {
|
namespace rpc {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::mutex g_trace_mutex;
|
||||||
|
RPCClient::TraceCallback g_trace_callback;
|
||||||
|
std::atomic_bool g_trace_enabled{false};
|
||||||
|
thread_local std::string g_trace_source;
|
||||||
|
|
||||||
|
void emitRpcTrace(const std::string& method)
|
||||||
|
{
|
||||||
|
if (!g_trace_enabled.load(std::memory_order_relaxed)) return;
|
||||||
|
|
||||||
|
RPCClient::TraceCallback callback;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_trace_mutex);
|
||||||
|
callback = g_trace_callback;
|
||||||
|
}
|
||||||
|
if (!callback) return;
|
||||||
|
|
||||||
|
std::string source = g_trace_source.empty() ? std::string("App") : g_trace_source;
|
||||||
|
callback(source, method);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
RPCClient::TraceScope::TraceScope(std::string source)
|
||||||
|
: previous_(RPCClient::currentTraceSource())
|
||||||
|
{
|
||||||
|
RPCClient::setTraceSource(std::move(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
RPCClient::TraceScope::~TraceScope()
|
||||||
|
{
|
||||||
|
RPCClient::setTraceSource(std::move(previous_));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RPCClient::setTraceCallback(TraceCallback callback)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_trace_mutex);
|
||||||
|
g_trace_callback = std::move(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RPCClient::setTraceEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
g_trace_enabled.store(enabled, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RPCClient::isTraceEnabled()
|
||||||
|
{
|
||||||
|
return g_trace_enabled.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RPCClient::currentTraceSource()
|
||||||
|
{
|
||||||
|
return g_trace_source;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RPCClient::setTraceSource(std::string source)
|
||||||
|
{
|
||||||
|
g_trace_source = std::move(source);
|
||||||
|
}
|
||||||
|
|
||||||
// Callback for libcurl to write response data
|
// Callback for libcurl to write response data
|
||||||
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
|
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
|
||||||
size_t totalSize = size * nmemb;
|
size_t totalSize = size * nmemb;
|
||||||
@@ -24,6 +89,14 @@ static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::stri
|
|||||||
return totalSize;
|
return totalSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// curl progress callback: a non-zero return aborts the in-flight transfer. This lets a
|
||||||
|
// requestAbort() from another thread (disconnect/shutdown) unblock curl_easy_perform so the
|
||||||
|
// UI thread's worker join() returns promptly instead of waiting out the request timeout.
|
||||||
|
static int xferInfoCallback(void* clientp, curl_off_t, curl_off_t, curl_off_t, curl_off_t) {
|
||||||
|
const auto* self = static_cast<const RPCClient*>(clientp);
|
||||||
|
return (self != nullptr && self->abortRequested()) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Private implementation using libcurl
|
// Private implementation using libcurl
|
||||||
class RPCClient::Impl {
|
class RPCClient::Impl {
|
||||||
public:
|
public:
|
||||||
@@ -60,17 +133,26 @@ RPCClient::~RPCClient() = default;
|
|||||||
|
|
||||||
bool RPCClient::connect(const std::string& host, const std::string& port,
|
bool RPCClient::connect(const std::string& host, const std::string& port,
|
||||||
const std::string& user, const std::string& password)
|
const std::string& user, const std::string& password)
|
||||||
|
{
|
||||||
|
return connect(host, port, user, password, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RPCClient::connect(const std::string& host, const std::string& port,
|
||||||
|
const std::string& user, const std::string& password,
|
||||||
|
bool useTls)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||||
host_ = host;
|
host_ = host;
|
||||||
port_ = port;
|
port_ = port;
|
||||||
|
last_connect_info_ = json();
|
||||||
|
|
||||||
// Create Basic auth header with proper base64 encoding
|
// Create Basic auth header with proper base64 encoding, then wipe the plaintext
|
||||||
|
// "user:password" temporary (std::string does not zero its buffer on destruction).
|
||||||
std::string credentials = user + ":" + password;
|
std::string credentials = user + ":" + password;
|
||||||
auth_ = util::base64_encode(credentials);
|
auth_ = util::base64_encode(credentials);
|
||||||
|
if (!credentials.empty()) sodium_memzero(credentials.data(), credentials.size());
|
||||||
|
|
||||||
// Build URL - use HTTP for localhost RPC (TLS not always enabled)
|
impl_->url = std::string(useTls ? "https://" : "http://") + host + ":" + port + "/";
|
||||||
impl_->url = "http://" + host + ":" + port + "/";
|
|
||||||
VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str());
|
VERBOSE_LOGF("Connecting to dragonxd at %s\n", impl_->url.c_str());
|
||||||
|
|
||||||
// Clean up previous curl handle/headers to avoid leaks on retries
|
// Clean up previous curl handle/headers to avoid leaks on retries
|
||||||
@@ -99,17 +181,31 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
|||||||
curl_easy_setopt(impl_->curl, CURLOPT_URL, impl_->url.c_str());
|
curl_easy_setopt(impl_->curl, CURLOPT_URL, impl_->url.c_str());
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_HTTPHEADER, impl_->headers);
|
curl_easy_setopt(impl_->curl, CURLOPT_HTTPHEADER, impl_->headers);
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
curl_easy_setopt(impl_->curl, CURLOPT_WRITEFUNCTION, WriteCallback);
|
||||||
|
// Progress callback so requestAbort() can unblock an in-flight curl_easy_perform.
|
||||||
|
clearAbort(); // a fresh connection must not start in the aborted state
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_NOPROGRESS, 0L);
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_XFERINFOFUNCTION, xferInfoCallback);
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_XFERINFODATA, this);
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, 30L);
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, 30L);
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, 1L); // localhost — fails fast if not listening
|
// Localhost fails fast if nothing is listening; a remote/TLS daemon needs a larger
|
||||||
|
// budget for the TCP + TLS handshake over real network latency (1s would spuriously fail).
|
||||||
|
const long connectTimeout = Connection::isLocalHost(host) ? 2L : 10L;
|
||||||
|
curl_easy_setopt(impl_->curl, CURLOPT_CONNECTTIMEOUT, connectTimeout);
|
||||||
|
|
||||||
// Test connection with getinfo
|
// Test connection with getinfo. Use a SHORT timeout for the probe on localhost: a healthy
|
||||||
|
// local daemon answers in milliseconds and a warming one returns -28 just as fast, so a long
|
||||||
|
// hang means a wedged/loading occupant — no point blocking the full 30s before we retry and
|
||||||
|
// update the UI. (call(timeoutSec) restores the persistent 30s afterwards, so normal RPC calls
|
||||||
|
// that legitimately take longer are unaffected.) Remote/TLS daemons keep the full budget.
|
||||||
|
const long probeTimeout = Connection::isLocalHost(host) ? 8L : 30L;
|
||||||
try {
|
try {
|
||||||
json result = call("getinfo");
|
json result = call("getinfo", json::array(), probeTimeout);
|
||||||
if (result.contains("version")) {
|
if (result.contains("version")) {
|
||||||
connected_ = true;
|
connected_ = true;
|
||||||
warming_up_ = false;
|
warming_up_ = false;
|
||||||
warmup_status_.clear();
|
warmup_status_.clear();
|
||||||
last_connect_error_.clear();
|
last_connect_error_.clear();
|
||||||
|
last_connect_info_ = result;
|
||||||
DEBUG_LOGF("Connected to dragonxd v%d\n", result["version"].get<int>());
|
DEBUG_LOGF("Connected to dragonxd v%d\n", result["version"].get<int>());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -120,7 +216,12 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
|||||||
// it just hasn't finished initializing yet. Mark as connected+warmup
|
// it just hasn't finished initializing yet. Mark as connected+warmup
|
||||||
// so the wallet can show the UI instead of a blocking overlay.
|
// so the wallet can show the UI instead of a blocking overlay.
|
||||||
std::string msg = e.what();
|
std::string msg = e.what();
|
||||||
bool isWarmup = (msg.find("Loading") != std::string::npos ||
|
// Warmup is JSON-RPC error code -28 (RPC_IN_WARMUP) — the robust signal. Fall back
|
||||||
|
// to message substrings for any path that didn't carry the numeric code.
|
||||||
|
int code = 0;
|
||||||
|
if (const auto* re = dynamic_cast<const RpcError*>(&e)) code = re->code;
|
||||||
|
bool isWarmup = (code == -28) ||
|
||||||
|
(msg.find("Loading") != std::string::npos ||
|
||||||
msg.find("Verifying") != std::string::npos ||
|
msg.find("Verifying") != std::string::npos ||
|
||||||
msg.find("Activating") != std::string::npos ||
|
msg.find("Activating") != std::string::npos ||
|
||||||
msg.find("Rewinding") != std::string::npos ||
|
msg.find("Rewinding") != std::string::npos ||
|
||||||
@@ -140,15 +241,35 @@ bool RPCClient::connect(const std::string& host, const std::string& port,
|
|||||||
connected_ = false;
|
connected_ = false;
|
||||||
warming_up_ = false;
|
warming_up_ = false;
|
||||||
warmup_status_.clear();
|
warmup_status_.clear();
|
||||||
|
last_connect_info_ = json();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json RPCClient::getLastConnectInfo() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||||
|
return last_connect_info_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RPCClient::requestAbort()
|
||||||
|
{
|
||||||
|
// Deliberately NOT taking curl_mutex_ — the whole point is to interrupt a call() that is
|
||||||
|
// currently holding it inside curl_easy_perform. The atomic is read by xferInfoCallback.
|
||||||
|
abort_.store(true, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RPCClient::clearAbort()
|
||||||
|
{
|
||||||
|
abort_.store(false, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
void RPCClient::disconnect()
|
void RPCClient::disconnect()
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||||
connected_ = false;
|
connected_ = false;
|
||||||
warming_up_ = false;
|
warming_up_ = false;
|
||||||
warmup_status_.clear();
|
warmup_status_.clear();
|
||||||
|
last_connect_info_ = json();
|
||||||
if (impl_->curl) {
|
if (impl_->curl) {
|
||||||
curl_easy_cleanup(impl_->curl);
|
curl_easy_cleanup(impl_->curl);
|
||||||
impl_->curl = nullptr;
|
impl_->curl = nullptr;
|
||||||
@@ -169,12 +290,9 @@ json RPCClient::makePayload(const std::string& method, const json& params)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
json RPCClient::call(const std::string& method, const json& params)
|
std::string RPCClient::performCall(const std::string& method, const json& params, long& httpCodeOut)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
emitRpcTrace(method);
|
||||||
if (!impl_->curl) {
|
|
||||||
throw std::runtime_error("Not connected");
|
|
||||||
}
|
|
||||||
|
|
||||||
json payload = makePayload(method, params);
|
json payload = makePayload(method, params);
|
||||||
std::string body = payload.dump();
|
std::string body = payload.dump();
|
||||||
@@ -193,35 +311,65 @@ json RPCClient::call(const std::string& method, const json& params)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check HTTP response code
|
// Check HTTP response code
|
||||||
long http_code = 0;
|
httpCodeOut = 0;
|
||||||
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &http_code);
|
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &httpCodeOut);
|
||||||
|
|
||||||
|
return response_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
json RPCClient::parseRpcResult(long httpCode, const std::string& body)
|
||||||
|
{
|
||||||
// Bitcoin/Hush RPC returns HTTP 500 for application-level errors
|
// Bitcoin/Hush RPC returns HTTP 500 for application-level errors
|
||||||
// (insufficient funds, bad params, etc.) with a valid JSON body.
|
// (insufficient funds, bad params, etc.) with a valid JSON body.
|
||||||
// Parse the body first to extract the real error message.
|
// Parse the body first to extract the real error message.
|
||||||
if (http_code != 200) {
|
if (httpCode != 200) {
|
||||||
|
int errCode = 0;
|
||||||
try {
|
try {
|
||||||
json response = json::parse(response_data);
|
json response = json::parse(body);
|
||||||
if (response.contains("error") && !response["error"].is_null()) {
|
if (response.contains("error") && response["error"].is_object()) {
|
||||||
std::string err_msg = response["error"]["message"].get<std::string>();
|
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
|
||||||
throw std::runtime_error(err_msg);
|
errCode = response["error"]["code"].get<int>();
|
||||||
|
if (response["error"].contains("message") && response["error"]["message"].is_string())
|
||||||
|
throw RpcError(errCode, response["error"]["message"].get<std::string>());
|
||||||
|
// message missing/non-string — keep the detail instead of a bare HTTP code
|
||||||
|
throw RpcError(errCode, "RPC error: " + response["error"].dump());
|
||||||
}
|
}
|
||||||
} catch (const json::exception&) {
|
} catch (const json::exception&) {
|
||||||
// Body wasn't valid JSON — fall through to generic HTTP error
|
// Body wasn't valid JSON — fall through to generic HTTP error
|
||||||
}
|
}
|
||||||
throw std::runtime_error("RPC error: HTTP " + std::to_string(http_code));
|
throw RpcError(errCode, "RPC error: HTTP " + std::to_string(httpCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
json response = json::parse(response_data);
|
json response = json::parse(body);
|
||||||
|
|
||||||
if (response.contains("error") && !response["error"].is_null()) {
|
if (response.contains("error") && !response["error"].is_null()) {
|
||||||
std::string err_msg = response["error"]["message"].get<std::string>();
|
int errCode = 0;
|
||||||
throw std::runtime_error("RPC error: " + err_msg);
|
std::string err_msg;
|
||||||
|
if (response["error"].is_object()) {
|
||||||
|
if (response["error"].contains("code") && response["error"]["code"].is_number_integer())
|
||||||
|
errCode = response["error"]["code"].get<int>();
|
||||||
|
if (response["error"].contains("message") && response["error"]["message"].is_string())
|
||||||
|
err_msg = response["error"]["message"].get<std::string>();
|
||||||
|
}
|
||||||
|
if (err_msg.empty()) err_msg = response["error"].dump();
|
||||||
|
throw RpcError(errCode, "RPC error: " + err_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response["result"];
|
return response["result"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json RPCClient::call(const std::string& method, const json& params)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||||
|
if (!impl_->curl) {
|
||||||
|
throw std::runtime_error("Not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
long http_code = 0;
|
||||||
|
std::string response_data = performCall(method, params, http_code);
|
||||||
|
return parseRpcResult(http_code, response_data);
|
||||||
|
}
|
||||||
|
|
||||||
json RPCClient::call(const std::string& method, const json& params, long timeoutSec)
|
json RPCClient::call(const std::string& method, const json& params, long timeoutSec)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
std::lock_guard<std::recursive_mutex> lk(curl_mutex_);
|
||||||
@@ -234,46 +382,11 @@ json RPCClient::call(const std::string& method, const json& params, long timeout
|
|||||||
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec);
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, timeoutSec);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Unlock before calling to avoid recursive lock issues — but we already hold it,
|
long http_code = 0;
|
||||||
// and call() also locks with recursive_mutex, so just delegate to the body directly.
|
std::string response_data = performCall(method, params, http_code);
|
||||||
json payload = makePayload(method, params);
|
|
||||||
std::string body = payload.dump();
|
|
||||||
std::string response_data;
|
|
||||||
|
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDS, body.c_str());
|
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_POSTFIELDSIZE, (long)body.size());
|
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_WRITEDATA, &response_data);
|
|
||||||
|
|
||||||
CURLcode res = curl_easy_perform(impl_->curl);
|
|
||||||
|
|
||||||
// Restore original timeout
|
// Restore original timeout
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
||||||
|
return parseRpcResult(http_code, response_data);
|
||||||
if (res != CURLE_OK) {
|
|
||||||
throw std::runtime_error("RPC request failed: " + std::string(curl_easy_strerror(res)));
|
|
||||||
}
|
|
||||||
|
|
||||||
long http_code = 0;
|
|
||||||
curl_easy_getinfo(impl_->curl, CURLINFO_RESPONSE_CODE, &http_code);
|
|
||||||
|
|
||||||
if (http_code != 200) {
|
|
||||||
try {
|
|
||||||
json response = json::parse(response_data);
|
|
||||||
if (response.contains("error") && !response["error"].is_null()) {
|
|
||||||
std::string err_msg = response["error"]["message"].get<std::string>();
|
|
||||||
throw std::runtime_error(err_msg);
|
|
||||||
}
|
|
||||||
} catch (const json::exception&) {}
|
|
||||||
throw std::runtime_error("RPC error: HTTP " + std::to_string(http_code));
|
|
||||||
}
|
|
||||||
|
|
||||||
json response = json::parse(response_data);
|
|
||||||
if (response.contains("error") && !response["error"].is_null()) {
|
|
||||||
std::string err_msg = response["error"]["message"].get<std::string>();
|
|
||||||
throw std::runtime_error("RPC error: " + err_msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response["result"];
|
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
// Ensure timeout is always restored
|
// Ensure timeout is always restored
|
||||||
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
curl_easy_setopt(impl_->curl, CURLOPT_TIMEOUT, prevTimeout);
|
||||||
@@ -288,6 +401,8 @@ std::string RPCClient::callRaw(const std::string& method, const json& params)
|
|||||||
throw std::runtime_error("Not connected");
|
throw std::runtime_error("Not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitRpcTrace(method);
|
||||||
|
|
||||||
json payload = makePayload(method, params);
|
json payload = makePayload(method, params);
|
||||||
std::string body = payload.dump();
|
std::string body = payload.dump();
|
||||||
std::string response_data;
|
std::string response_data;
|
||||||
@@ -318,7 +433,14 @@ std::string RPCClient::callRaw(const std::string& method, const json& params)
|
|||||||
// Parse with ordered_json to preserve the daemon's original key order
|
// Parse with ordered_json to preserve the daemon's original key order
|
||||||
nlohmann::ordered_json oj = nlohmann::ordered_json::parse(response_data);
|
nlohmann::ordered_json oj = nlohmann::ordered_json::parse(response_data);
|
||||||
if (oj.contains("error") && !oj["error"].is_null()) {
|
if (oj.contains("error") && !oj["error"].is_null()) {
|
||||||
std::string err_msg = oj["error"]["message"].get<std::string>();
|
// A daemon error object normally has a string "message", but don't assume it — a malformed
|
||||||
|
// error (missing/non-string message) must yield a clean RPC error, not a json type-exception.
|
||||||
|
const auto& err = oj["error"];
|
||||||
|
std::string err_msg;
|
||||||
|
if (err.is_object() && err.contains("message") && err["message"].is_string())
|
||||||
|
err_msg = err["message"].get<std::string>();
|
||||||
|
else
|
||||||
|
err_msg = err.dump();
|
||||||
throw std::runtime_error("RPC error: " + err_msg);
|
throw std::runtime_error("RPC error: " + err_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,7 +631,8 @@ void RPCClient::stop(Callback cb, ErrorCallback err)
|
|||||||
|
|
||||||
void RPCClient::rescanBlockchain(int startHeight, Callback cb, ErrorCallback err)
|
void RPCClient::rescanBlockchain(int startHeight, Callback cb, ErrorCallback err)
|
||||||
{
|
{
|
||||||
doRPC("rescanblockchain", {startHeight}, cb, err);
|
// hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain".
|
||||||
|
doRPC("rescan", {startHeight}, cb, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RPCClient::z_validateAddress(const std::string& address, Callback cb, ErrorCallback err)
|
void RPCClient::z_validateAddress(const std::string& address, Callback cb, ErrorCallback err)
|
||||||
@@ -602,42 +725,40 @@ void RPCClient::z_listReceivedByAddress(const std::string& address, int minconf,
|
|||||||
doRPC("z_listreceivedbyaddress", {address, minconf}, cb, err);
|
doRPC("z_listreceivedbyaddress", {address, minconf}, cb, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unified callback versions
|
// Adapts a UnifiedCallback into the (Callback, ErrorCallback) pair doRPC expects: the
|
||||||
void RPCClient::getInfo(UnifiedCallback cb)
|
// success lambda forwards (result, ""), the error lambda forwards ({}, error). The cb
|
||||||
|
// null-check is preserved on each invocation.
|
||||||
|
std::pair<Callback, ErrorCallback> RPCClient::splitUnified(UnifiedCallback cb)
|
||||||
{
|
{
|
||||||
doRPC("getinfo", {},
|
return {
|
||||||
[cb](const json& result) {
|
[cb](const json& result) {
|
||||||
if (cb) cb(result, "");
|
if (cb) cb(result, "");
|
||||||
},
|
},
|
||||||
[cb](const std::string& error) {
|
[cb](const std::string& error) {
|
||||||
if (cb) cb(json{}, error);
|
if (cb) cb(json{}, error);
|
||||||
}
|
}
|
||||||
);
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unified callback versions
|
||||||
|
void RPCClient::getInfo(UnifiedCallback cb)
|
||||||
|
{
|
||||||
|
auto handlers = splitUnified(std::move(cb));
|
||||||
|
doRPC("getinfo", {}, handlers.first, handlers.second);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RPCClient::rescanBlockchain(int startHeight, UnifiedCallback cb)
|
void RPCClient::rescanBlockchain(int startHeight, UnifiedCallback cb)
|
||||||
{
|
{
|
||||||
doRPC("rescanblockchain", {startHeight},
|
// hush/komodo daemons expose this as "rescan <height>", not bitcoin's "rescanblockchain".
|
||||||
[cb](const json& result) {
|
auto handlers = splitUnified(std::move(cb));
|
||||||
if (cb) cb(result, "");
|
doRPC("rescan", {startHeight}, handlers.first, handlers.second);
|
||||||
},
|
|
||||||
[cb](const std::string& error) {
|
|
||||||
if (cb) cb(json{}, error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RPCClient::z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr,
|
void RPCClient::z_shieldCoinbase(const std::string& fromAddr, const std::string& toAddr,
|
||||||
double fee, int limit, UnifiedCallback cb)
|
double fee, int limit, UnifiedCallback cb)
|
||||||
{
|
{
|
||||||
doRPC("z_shieldcoinbase", {fromAddr, toAddr, fee, limit},
|
auto handlers = splitUnified(std::move(cb));
|
||||||
[cb](const json& result) {
|
doRPC("z_shieldcoinbase", {fromAddr, toAddr, fee, limit}, handlers.first, handlers.second);
|
||||||
if (cb) cb(result, "");
|
|
||||||
},
|
|
||||||
[cb](const std::string& error) {
|
|
||||||
if (cb) cb(json{}, error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RPCClient::z_mergeToAddress(const std::vector<std::string>& fromAddrs, const std::string& toAddr,
|
void RPCClient::z_mergeToAddress(const std::vector<std::string>& fromAddrs, const std::string& toAddr,
|
||||||
@@ -647,14 +768,8 @@ void RPCClient::z_mergeToAddress(const std::vector<std::string>& fromAddrs, cons
|
|||||||
for (const auto& addr : fromAddrs) {
|
for (const auto& addr : fromAddrs) {
|
||||||
addrs.push_back(addr);
|
addrs.push_back(addr);
|
||||||
}
|
}
|
||||||
doRPC("z_mergetoaddress", {addrs, toAddr, fee, 0, limit},
|
auto handlers = splitUnified(std::move(cb));
|
||||||
[cb](const json& result) {
|
doRPC("z_mergetoaddress", {addrs, toAddr, fee, 0, limit}, handlers.first, handlers.second);
|
||||||
if (cb) cb(result, "");
|
|
||||||
},
|
|
||||||
[cb](const std::string& error) {
|
|
||||||
if (cb) cb(json{}, error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, UnifiedCallback cb)
|
void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, UnifiedCallback cb)
|
||||||
@@ -663,14 +778,8 @@ void RPCClient::z_getOperationStatus(const std::vector<std::string>& opids, Unif
|
|||||||
for (const auto& id : opids) {
|
for (const auto& id : opids) {
|
||||||
ids.push_back(id);
|
ids.push_back(id);
|
||||||
}
|
}
|
||||||
doRPC("z_getoperationstatus", {ids},
|
auto handlers = splitUnified(std::move(cb));
|
||||||
[cb](const json& result) {
|
doRPC("z_getoperationstatus", {ids}, handlers.first, handlers.second);
|
||||||
if (cb) cb(result, "");
|
|
||||||
},
|
|
||||||
[cb](const std::string& error) {
|
|
||||||
if (cb) cb(json{}, error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RPCClient::getBlock(int height, UnifiedCallback cb)
|
void RPCClient::getBlock(int height, UnifiedCallback cb)
|
||||||
@@ -679,14 +788,8 @@ void RPCClient::getBlock(int height, UnifiedCallback cb)
|
|||||||
getBlockHash(height,
|
getBlockHash(height,
|
||||||
[this, cb](const json& hashResult) {
|
[this, cb](const json& hashResult) {
|
||||||
std::string hash = hashResult.get<std::string>();
|
std::string hash = hashResult.get<std::string>();
|
||||||
getBlock(hash,
|
auto handlers = splitUnified(cb);
|
||||||
[cb](const json& blockResult) {
|
getBlock(hash, handlers.first, handlers.second);
|
||||||
if (cb) cb(blockResult, "");
|
|
||||||
},
|
|
||||||
[cb](const std::string& error) {
|
|
||||||
if (cb) cb(json{}, error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[cb](const std::string& error) {
|
[cb](const std::string& error) {
|
||||||
if (cb) cb(json{}, error);
|
if (cb) cb(json{}, error);
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "types.h"
|
#include "types.h"
|
||||||
|
#include <atomic>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <stdexcept>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
@@ -18,6 +20,21 @@ using json = nlohmann::json;
|
|||||||
using Callback = std::function<void(const json&)>;
|
using Callback = std::function<void(const json&)>;
|
||||||
using ErrorCallback = std::function<void(const std::string&)>;
|
using ErrorCallback = std::function<void(const std::string&)>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A JSON-RPC error carrying the daemon's numeric error code.
|
||||||
|
*
|
||||||
|
* what() preserves the exact human-readable message (so existing string matching
|
||||||
|
* still works); `code` exposes the JSON-RPC error code — notably -28 (RPC_IN_WARMUP)
|
||||||
|
* for a daemon still starting up. Derives from std::runtime_error, so every existing
|
||||||
|
* `catch (const std::exception&)` continues to handle it unchanged.
|
||||||
|
*/
|
||||||
|
class RpcError : public std::runtime_error {
|
||||||
|
public:
|
||||||
|
RpcError(int errorCode, const std::string& message)
|
||||||
|
: std::runtime_error(message), code(errorCode) {}
|
||||||
|
int code = 0;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief JSON-RPC client for dragonxd
|
* @brief JSON-RPC client for dragonxd
|
||||||
*
|
*
|
||||||
@@ -25,6 +42,20 @@ using ErrorCallback = std::function<void(const std::string&)>;
|
|||||||
*/
|
*/
|
||||||
class RPCClient {
|
class RPCClient {
|
||||||
public:
|
public:
|
||||||
|
using TraceCallback = std::function<void(const std::string& source, const std::string& method)>;
|
||||||
|
|
||||||
|
class TraceScope {
|
||||||
|
public:
|
||||||
|
explicit TraceScope(std::string source);
|
||||||
|
~TraceScope();
|
||||||
|
|
||||||
|
TraceScope(const TraceScope&) = delete;
|
||||||
|
TraceScope& operator=(const TraceScope&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string previous_;
|
||||||
|
};
|
||||||
|
|
||||||
RPCClient();
|
RPCClient();
|
||||||
~RPCClient();
|
~RPCClient();
|
||||||
|
|
||||||
@@ -43,6 +74,10 @@ public:
|
|||||||
bool connect(const std::string& host, const std::string& port,
|
bool connect(const std::string& host, const std::string& port,
|
||||||
const std::string& user, const std::string& password);
|
const std::string& user, const std::string& password);
|
||||||
|
|
||||||
|
bool connect(const std::string& host, const std::string& port,
|
||||||
|
const std::string& user, const std::string& password,
|
||||||
|
bool useTls);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Disconnect from dragonxd
|
* @brief Disconnect from dragonxd
|
||||||
*/
|
*/
|
||||||
@@ -53,6 +88,18 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool isConnected() const { return connected_; }
|
bool isConnected() const { return connected_; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Ask an in-flight call() to abort as soon as possible (thread-safe).
|
||||||
|
*
|
||||||
|
* Set from another thread (e.g. before stop()-ing the worker on disconnect/shutdown):
|
||||||
|
* a curl progress callback aborts the transfer, so a blocked curl_easy_perform returns
|
||||||
|
* promptly instead of freezing the UI thread's join() until the request timeout. Cleared
|
||||||
|
* on the next connect(); abortRequested() is read by the progress callback.
|
||||||
|
*/
|
||||||
|
void requestAbort();
|
||||||
|
void clearAbort();
|
||||||
|
bool abortRequested() const noexcept { return abort_.load(std::memory_order_relaxed); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief True if the last connect() succeeded but daemon returned a warmup error.
|
* @brief True if the last connect() succeeded but daemon returned a warmup error.
|
||||||
* The curl handle is valid and auth succeeded — RPC calls will throw warmup errors
|
* The curl handle is valid and auth succeeded — RPC calls will throw warmup errors
|
||||||
@@ -70,6 +117,13 @@ public:
|
|||||||
* @brief Get the error message from the last failed connect() attempt.
|
* @brief Get the error message from the last failed connect() attempt.
|
||||||
*/
|
*/
|
||||||
const std::string& getLastConnectError() const { return last_connect_error_; }
|
const std::string& getLastConnectError() const { return last_connect_error_; }
|
||||||
|
json getLastConnectInfo() const;
|
||||||
|
|
||||||
|
static void setTraceCallback(TraceCallback callback);
|
||||||
|
static void setTraceEnabled(bool enabled);
|
||||||
|
static bool isTraceEnabled();
|
||||||
|
static std::string currentTraceSource();
|
||||||
|
static void setTraceSource(std::string source);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Make a raw RPC call
|
* @brief Make a raw RPC call
|
||||||
@@ -191,13 +245,26 @@ private:
|
|||||||
json makePayload(const std::string& method, const json& params = json::array());
|
json makePayload(const std::string& method, const json& params = json::array());
|
||||||
void doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err);
|
void doRPC(const std::string& method, const json& params, Callback cb, ErrorCallback err);
|
||||||
|
|
||||||
|
// Shared blocking-call core: dumps the payload, performs the request (throwing on a
|
||||||
|
// curl transport error) and returns the response body plus HTTP status. Caller must
|
||||||
|
// hold curl_mutex_ and have verified impl_->curl.
|
||||||
|
std::string performCall(const std::string& method, const json& params, long& httpCodeOut);
|
||||||
|
// Centralizes the HTTP-code check and JSON error->RpcError extraction, returning
|
||||||
|
// response["result"] on success.
|
||||||
|
static json parseRpcResult(long httpCode, const std::string& body);
|
||||||
|
|
||||||
|
// Splits a UnifiedCallback into the (Callback, ErrorCallback) pair used by doRPC.
|
||||||
|
static std::pair<Callback, ErrorCallback> splitUnified(UnifiedCallback cb);
|
||||||
|
|
||||||
std::string host_;
|
std::string host_;
|
||||||
std::string port_;
|
std::string port_;
|
||||||
std::string auth_; // Base64 encoded "user:password"
|
std::string auth_; // Base64 encoded "user:password"
|
||||||
bool connected_ = false;
|
bool connected_ = false;
|
||||||
|
std::atomic<bool> abort_{false}; // set cross-thread to abort an in-flight transfer
|
||||||
bool warming_up_ = false;
|
bool warming_up_ = false;
|
||||||
std::string warmup_status_;
|
std::string warmup_status_;
|
||||||
std::string last_connect_error_;
|
std::string last_connect_error_;
|
||||||
|
json last_connect_info_;
|
||||||
mutable std::recursive_mutex curl_mutex_; // serializes all curl handle access
|
mutable std::recursive_mutex curl_mutex_; // serializes all curl handle access
|
||||||
|
|
||||||
// HTTP client (implementation hidden)
|
// HTTP client (implementation hidden)
|
||||||
|
|||||||
@@ -97,6 +97,18 @@ bool RPCWorker::hasPendingResults() const
|
|||||||
return !results_.empty();
|
return !results_.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::size_t RPCWorker::pendingTaskCount() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(taskMtx_);
|
||||||
|
return tasks_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t RPCWorker::pendingResultCount() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(resultMtx_);
|
||||||
|
return results_.size();
|
||||||
|
}
|
||||||
|
|
||||||
void RPCWorker::run()
|
void RPCWorker::run()
|
||||||
{
|
{
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
|
#include <cstddef>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -69,6 +70,8 @@ public:
|
|||||||
|
|
||||||
/// True when there are completed results waiting for the main thread.
|
/// True when there are completed results waiting for the main thread.
|
||||||
bool hasPendingResults() const;
|
bool hasPendingResults() const;
|
||||||
|
std::size_t pendingTaskCount() const;
|
||||||
|
std::size_t pendingResultCount() const;
|
||||||
|
|
||||||
/// True when the worker thread is running.
|
/// True when the worker thread is running.
|
||||||
bool isRunning() const { return running_.load(std::memory_order_relaxed); }
|
bool isRunning() const { return running_.load(std::memory_order_relaxed); }
|
||||||
@@ -80,7 +83,7 @@ private:
|
|||||||
std::atomic<bool> running_{false};
|
std::atomic<bool> running_{false};
|
||||||
|
|
||||||
// ---- Task queue (produced by main thread, consumed by worker) ----
|
// ---- Task queue (produced by main thread, consumed by worker) ----
|
||||||
std::mutex taskMtx_;
|
mutable std::mutex taskMtx_;
|
||||||
std::condition_variable taskCv_;
|
std::condition_variable taskCv_;
|
||||||
std::deque<WorkFn> tasks_;
|
std::deque<WorkFn> tasks_;
|
||||||
|
|
||||||
|
|||||||
1645
src/services/network_refresh_service.cpp
Normal file
1645
src/services/network_refresh_service.cpp
Normal file
File diff suppressed because it is too large
Load Diff
418
src/services/network_refresh_service.h
Normal file
418
src/services/network_refresh_service.h
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "chat/chat_protocol.h"
|
||||||
|
#include "data/wallet_state.h"
|
||||||
|
#include "refresh_scheduler.h"
|
||||||
|
#include "rpc/rpc_worker.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <ctime>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace services {
|
||||||
|
|
||||||
|
class NetworkRefreshService {
|
||||||
|
public:
|
||||||
|
using Timer = RefreshScheduler::Timer;
|
||||||
|
using Intervals = RefreshScheduler::Intervals;
|
||||||
|
|
||||||
|
class RefreshRpcGateway {
|
||||||
|
public:
|
||||||
|
virtual ~RefreshRpcGateway() = default;
|
||||||
|
virtual nlohmann::json call(const std::string& method,
|
||||||
|
const nlohmann::json& params) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Job {
|
||||||
|
Core,
|
||||||
|
Addresses,
|
||||||
|
Transactions,
|
||||||
|
Mining,
|
||||||
|
Peers,
|
||||||
|
Price,
|
||||||
|
Encryption,
|
||||||
|
ConnectionInit,
|
||||||
|
Count
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DispatchTicket {
|
||||||
|
Job job = Job::Core;
|
||||||
|
std::uint64_t generation = 0;
|
||||||
|
bool accepted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct JobStats {
|
||||||
|
std::uint64_t started = 0;
|
||||||
|
std::uint64_t finished = 0;
|
||||||
|
std::uint64_t skippedInFlight = 0;
|
||||||
|
std::uint64_t skippedQueuePressure = 0;
|
||||||
|
std::uint64_t staleCallbacks = 0;
|
||||||
|
std::size_t lastQueueDepth = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EnqueueResult {
|
||||||
|
DispatchTicket ticket;
|
||||||
|
bool enqueued = false;
|
||||||
|
std::size_t queueDepth = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ConnectionInfoResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::optional<int> daemonVersion;
|
||||||
|
std::optional<int> protocolVersion;
|
||||||
|
std::optional<int> p2pPort;
|
||||||
|
std::optional<int> longestChain;
|
||||||
|
std::optional<int> notarized;
|
||||||
|
std::optional<int> blocks;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WalletEncryptionResult {
|
||||||
|
bool ok = false;
|
||||||
|
bool encrypted = false;
|
||||||
|
std::int64_t unlockedUntil = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WarmupPollResult {
|
||||||
|
bool ready = false;
|
||||||
|
ConnectionInfoResult info;
|
||||||
|
std::string errorMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ConnectionInitResult {
|
||||||
|
ConnectionInfoResult info;
|
||||||
|
WalletEncryptionResult encryption;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CoreRefreshResult {
|
||||||
|
bool balanceOk = false;
|
||||||
|
std::optional<double> shieldedBalance;
|
||||||
|
std::optional<double> transparentBalance;
|
||||||
|
std::optional<double> totalBalance;
|
||||||
|
bool blockchainOk = false;
|
||||||
|
std::optional<int> blocks;
|
||||||
|
std::optional<int> headers;
|
||||||
|
std::optional<std::string> bestBlockHash;
|
||||||
|
std::optional<double> verificationProgress;
|
||||||
|
std::optional<int> longestChain;
|
||||||
|
std::optional<int> notarized;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MiningRefreshResult {
|
||||||
|
std::optional<double> localHashrate;
|
||||||
|
bool miningOk = false;
|
||||||
|
std::optional<bool> generate;
|
||||||
|
std::optional<int> genproclimit;
|
||||||
|
std::optional<int> blocks;
|
||||||
|
std::optional<double> difficulty;
|
||||||
|
std::optional<double> networkHashrate;
|
||||||
|
std::optional<std::string> chain;
|
||||||
|
double daemonMemoryMb = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PeerRefreshResult {
|
||||||
|
std::vector<PeerInfo> peers;
|
||||||
|
std::vector<BannedPeer> bannedPeers;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PriceRefreshResult {
|
||||||
|
MarketInfo market;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PriceHttpResponse {
|
||||||
|
bool transportOk = false;
|
||||||
|
long httpStatus = 0;
|
||||||
|
std::string body;
|
||||||
|
std::string transportError;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PriceHttpResult {
|
||||||
|
std::optional<PriceRefreshResult> price;
|
||||||
|
std::string errorMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AddressRefreshResult {
|
||||||
|
std::vector<AddressInfo> shieldedAddresses;
|
||||||
|
std::vector<AddressInfo> transparentAddresses;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AddressRefreshSnapshot {
|
||||||
|
std::unordered_map<std::string, bool> shieldedSpendingKeys;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TransactionViewCacheEntry {
|
||||||
|
std::string from_address;
|
||||||
|
std::int64_t timestamp = 0;
|
||||||
|
int confirmations = 0;
|
||||||
|
struct Output {
|
||||||
|
std::string address;
|
||||||
|
double value = 0.0;
|
||||||
|
std::string memo;
|
||||||
|
std::size_t position = 0;
|
||||||
|
};
|
||||||
|
std::vector<Output> outgoing_outputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
using TransactionViewCache = std::unordered_map<std::string, TransactionViewCacheEntry>;
|
||||||
|
|
||||||
|
struct TransactionRefreshSnapshot {
|
||||||
|
std::vector<std::string> shieldedAddresses;
|
||||||
|
std::unordered_set<std::string> fullyEnrichedTxids;
|
||||||
|
TransactionViewCache viewTxCache;
|
||||||
|
std::unordered_set<std::string> sendTxids;
|
||||||
|
std::unordered_set<std::string> pendingOpids;
|
||||||
|
std::vector<TransactionInfo> previousTransactions;
|
||||||
|
std::set<std::string> miningAddresses;
|
||||||
|
std::unordered_map<std::string, int> shieldedScanHeights;
|
||||||
|
std::size_t shieldedScanStartIndex = 0;
|
||||||
|
std::size_t maxShieldedReceiveScans = 0;
|
||||||
|
// How many blocks the tip may advance past an address's last scan before it counts as stale
|
||||||
|
// and needs re-scanning. 0 = strict (must be scanned at the exact current tip). A small
|
||||||
|
// tolerance lets a multi-cycle pass over many shielded addresses COMPLETE even though new
|
||||||
|
// blocks arrive mid-pass — otherwise the "scanned at tip" bar moves every block and the scan
|
||||||
|
// (and the transactions_dirty_ flag it drives) never finishes. It also naturally throttles
|
||||||
|
// full rescans to roughly once per `tolerance` blocks.
|
||||||
|
int shieldedScanTipTolerance = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TransactionRefreshResult {
|
||||||
|
std::vector<TransactionInfo> transactions;
|
||||||
|
std::vector<chat::HushChatTransactionMetadata> hushChatMetadata;
|
||||||
|
int blockHeight = -1;
|
||||||
|
TransactionViewCache newViewTxEntries;
|
||||||
|
std::size_t nextShieldedScanStartIndex = 0;
|
||||||
|
std::size_t shieldedAddressesScanned = 0;
|
||||||
|
std::size_t shieldedAddressCount = 0;
|
||||||
|
std::unordered_map<std::string, int> shieldedScanHeights;
|
||||||
|
bool shieldedScanComplete = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OperationStatusPollResult {
|
||||||
|
std::vector<std::string> doneOpids;
|
||||||
|
std::vector<std::string> staleOpids;
|
||||||
|
std::vector<std::string> successTxids;
|
||||||
|
std::unordered_map<std::string, std::string> successTxidsByOpid;
|
||||||
|
std::vector<std::string> failureMessages;
|
||||||
|
std::unordered_map<std::string, std::string> failureByOpid; // opid -> error message
|
||||||
|
bool anySuccess = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TransactionCacheUpdate {
|
||||||
|
TransactionViewCache& viewTxCache;
|
||||||
|
std::unordered_set<std::string>& sendTxids;
|
||||||
|
std::vector<TransactionInfo>& confirmedTxCache;
|
||||||
|
std::unordered_set<std::string>& confirmedTxIds;
|
||||||
|
int& confirmedCacheBlock;
|
||||||
|
int& lastTxBlockHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
static Intervals intervalsForPage(ui::NavPage page) { return RefreshScheduler::intervalsForPage(page); }
|
||||||
|
|
||||||
|
static ConnectionInfoResult parseConnectionInfoResult(const nlohmann::json& info);
|
||||||
|
static WalletEncryptionResult parseWalletEncryptionResult(const nlohmann::json& walletInfo);
|
||||||
|
static WarmupPollResult collectWarmupPollResult(RefreshRpcGateway& rpc);
|
||||||
|
static ConnectionInitResult collectConnectionInitResult(
|
||||||
|
RefreshRpcGateway& rpc,
|
||||||
|
const std::optional<ConnectionInfoResult>& prefetchedInfo = std::nullopt);
|
||||||
|
static CoreRefreshResult parseCoreRefreshResult(const nlohmann::json& totalBalance,
|
||||||
|
bool balanceOk,
|
||||||
|
const nlohmann::json& blockInfo,
|
||||||
|
bool blockOk);
|
||||||
|
// includeBalance=false skips z_gettotalbalance (which takes the wallet lock + cs_main) and only
|
||||||
|
// fetches getblockchaininfo — used while syncing, where the balance is incomplete anyway and the
|
||||||
|
// wallet should minimise lock contention with block connection.
|
||||||
|
static CoreRefreshResult collectCoreRefreshResult(RefreshRpcGateway& rpc, bool includeBalance = true);
|
||||||
|
static MiningRefreshResult parseMiningRefreshResult(const nlohmann::json& miningInfo,
|
||||||
|
bool miningOk,
|
||||||
|
const nlohmann::json& localHashrate,
|
||||||
|
bool hashrateOk,
|
||||||
|
double daemonMemoryMb);
|
||||||
|
static MiningRefreshResult collectMiningRefreshResult(RefreshRpcGateway& rpc,
|
||||||
|
double daemonMemoryMb,
|
||||||
|
bool includeSlowRefresh,
|
||||||
|
bool includeLocalHashrate = true);
|
||||||
|
static PeerRefreshResult parsePeerRefreshResult(const nlohmann::json& peers,
|
||||||
|
const nlohmann::json& bannedPeers);
|
||||||
|
static PeerRefreshResult collectPeerRefreshResult(RefreshRpcGateway& rpc);
|
||||||
|
static std::optional<PriceRefreshResult> parseCoinGeckoPriceResponse(const std::string& response,
|
||||||
|
std::time_t fetchedAt);
|
||||||
|
static PriceHttpResult parsePriceHttpResponse(const PriceHttpResponse& response,
|
||||||
|
std::time_t fetchedAt);
|
||||||
|
// Parse a CoinGecko /market_chart response ({"prices":[[ms,price],...]}) into a timestamped
|
||||||
|
// (unix seconds, USD price) series, oldest->newest. Malformed rows are skipped; on any error
|
||||||
|
// (or a missing "prices" array) an empty vector is returned. Pure/no-I/O.
|
||||||
|
static std::vector<std::pair<std::time_t, double>> parseCoinGeckoMarketChart(const std::string& response);
|
||||||
|
static AddressInfo buildShieldedAddressInfo(const std::string& address,
|
||||||
|
const nlohmann::json& validation,
|
||||||
|
bool validationSucceeded);
|
||||||
|
static AddressInfo buildTransparentAddressInfo(const std::string& address);
|
||||||
|
static std::vector<AddressInfo> parseTransparentAddressList(const nlohmann::json& addressList);
|
||||||
|
static void applyShieldedBalancesFromUnspent(std::vector<AddressInfo>& addresses,
|
||||||
|
const nlohmann::json& unspent);
|
||||||
|
static void applyTransparentBalancesFromUnspent(std::vector<AddressInfo>& addresses,
|
||||||
|
const nlohmann::json& unspent);
|
||||||
|
static AddressRefreshSnapshot buildAddressRefreshSnapshot(const WalletState& state);
|
||||||
|
static AddressRefreshResult collectAddressRefreshResult(
|
||||||
|
RefreshRpcGateway& rpc,
|
||||||
|
const AddressRefreshSnapshot& snapshot = {});
|
||||||
|
static TransactionRefreshSnapshot buildTransactionRefreshSnapshot(const WalletState& state,
|
||||||
|
const TransactionViewCache& viewTxCache,
|
||||||
|
const std::unordered_set<std::string>& sendTxids);
|
||||||
|
static void appendTransparentTransactions(std::vector<TransactionInfo>& transactions,
|
||||||
|
std::set<std::string>& knownTxids,
|
||||||
|
const nlohmann::json& result,
|
||||||
|
const std::set<std::string>& miningAddresses = {});
|
||||||
|
static void appendShieldedReceivedTransactions(std::vector<TransactionInfo>& transactions,
|
||||||
|
std::set<std::string>& knownTxids,
|
||||||
|
const std::string& address,
|
||||||
|
const nlohmann::json& received,
|
||||||
|
const std::set<std::string>& miningAddresses = {},
|
||||||
|
std::unordered_map<std::string, std::vector<chat::HushChatMemoOutput>>* chatMemoOutputs = nullptr);
|
||||||
|
static TransactionViewCacheEntry parseViewTransactionCacheEntry(const nlohmann::json& viewTransaction);
|
||||||
|
static void appendViewTransactionOutputs(std::vector<TransactionInfo>& transactions,
|
||||||
|
const std::string& txid,
|
||||||
|
const TransactionViewCacheEntry& entry);
|
||||||
|
static void sortTransactionsNewestFirst(std::vector<TransactionInfo>& transactions);
|
||||||
|
static TransactionRefreshResult collectTransactionRefreshResult(RefreshRpcGateway& rpc,
|
||||||
|
const TransactionRefreshSnapshot& snapshot,
|
||||||
|
int currentBlockHeight,
|
||||||
|
int maxViewTransactionsPerCycle);
|
||||||
|
static TransactionRefreshResult collectRecentTransactionRefreshResult(
|
||||||
|
RefreshRpcGateway& rpc,
|
||||||
|
const TransactionRefreshSnapshot& snapshot,
|
||||||
|
int currentBlockHeight,
|
||||||
|
int pageSize = 100);
|
||||||
|
static OperationStatusPollResult parseOperationStatusPoll(const nlohmann::json& result,
|
||||||
|
const std::vector<std::string>& requestedOpids);
|
||||||
|
|
||||||
|
struct DaemonRescanScan {
|
||||||
|
bool foundRescan = false;
|
||||||
|
bool finished = false;
|
||||||
|
float rescanPct = 0.0f;
|
||||||
|
std::string lastStatus;
|
||||||
|
bool foundWitness = false;
|
||||||
|
float witnessPct = -1.0f;
|
||||||
|
int witnessRemaining = -1;
|
||||||
|
int witnessReadTotal = -1;
|
||||||
|
bool witnessRebuilt = false;
|
||||||
|
std::vector<std::string> witnessTxids;
|
||||||
|
int witnessTotalTxs = -1;
|
||||||
|
};
|
||||||
|
// Pure line-by-line scan of newly-appended dragonxd stdout for rescan + Sapling
|
||||||
|
// witness-rebuild progress. No I/O, no state — unit-tested against fixture log snippets.
|
||||||
|
static DaemonRescanScan parseDaemonRescanOutput(const std::string& output);
|
||||||
|
|
||||||
|
static void applyConnectionInfoResult(WalletState& state, const ConnectionInfoResult& result);
|
||||||
|
static void applyWalletEncryptionResult(WalletState& state, const WalletEncryptionResult& result);
|
||||||
|
static void applyConnectionInitResult(WalletState& state, const ConnectionInitResult& result);
|
||||||
|
static void applyCoreRefreshResult(WalletState& state,
|
||||||
|
const CoreRefreshResult& result,
|
||||||
|
std::time_t updatedAt);
|
||||||
|
static void applyMiningRefreshResult(WalletState& state,
|
||||||
|
const MiningRefreshResult& result,
|
||||||
|
std::time_t updatedAt);
|
||||||
|
static void applyPeerRefreshResult(WalletState& state,
|
||||||
|
PeerRefreshResult&& result,
|
||||||
|
std::time_t updatedAt);
|
||||||
|
static void markPriceRefreshStarted(WalletState& state);
|
||||||
|
static void applyPriceRefreshResult(WalletState& state,
|
||||||
|
const PriceRefreshResult& result,
|
||||||
|
std::chrono::steady_clock::time_point fetchedAt);
|
||||||
|
static void applyPriceRefreshFailure(WalletState& state,
|
||||||
|
const std::string& errorMessage);
|
||||||
|
static void applyAddressRefreshResult(WalletState& state,
|
||||||
|
AddressRefreshResult&& result);
|
||||||
|
static void applyTransactionRefreshResult(WalletState& state,
|
||||||
|
TransactionCacheUpdate cacheUpdate,
|
||||||
|
TransactionRefreshResult&& result,
|
||||||
|
std::time_t updatedAt);
|
||||||
|
|
||||||
|
void applyPage(ui::NavPage page) { scheduler_.applyPage(page); }
|
||||||
|
void setIntervals(Intervals intervals) { scheduler_.setIntervals(intervals); }
|
||||||
|
const Intervals& intervals() const { return scheduler_.intervals(); }
|
||||||
|
|
||||||
|
void tick(float deltaSeconds) { scheduler_.tick(deltaSeconds); }
|
||||||
|
bool isDue(Timer timer) const { return scheduler_.isDue(timer); }
|
||||||
|
bool consumeDue(Timer timer) { return scheduler_.consumeDue(timer); }
|
||||||
|
void reset(Timer timer) { scheduler_.reset(timer); }
|
||||||
|
void markDue(Timer timer) { scheduler_.markDue(timer); }
|
||||||
|
void setTimer(Timer timer, float seconds) { scheduler_.setTimer(timer, seconds); }
|
||||||
|
float timer(Timer timer) const { return scheduler_.timer(timer); }
|
||||||
|
float interval(Timer timer) const { return scheduler_.interval(timer); }
|
||||||
|
void markImmediateRefresh() { scheduler_.markImmediateRefresh(); }
|
||||||
|
void markWalletMutationRefresh() { scheduler_.markWalletMutationRefresh(); }
|
||||||
|
void resetTxAge() { scheduler_.resetTxAge(); }
|
||||||
|
bool shouldRefreshTransactions(int lastTxBlockHeight,
|
||||||
|
int currentBlockHeight,
|
||||||
|
bool transactionsDirty) const;
|
||||||
|
|
||||||
|
bool beginJob(Job job);
|
||||||
|
bool beginJob(Job job, std::size_t queuedWork, std::size_t maxQueuedWork);
|
||||||
|
void finishJob(Job job);
|
||||||
|
bool jobInProgress(Job job) const;
|
||||||
|
void resetJobs();
|
||||||
|
DispatchTicket beginDispatch(Job job, std::size_t queuedWork = 0, std::size_t maxQueuedWork = 0);
|
||||||
|
bool completeDispatch(const DispatchTicket& ticket);
|
||||||
|
void cancelDispatch(const DispatchTicket& ticket);
|
||||||
|
JobStats stats(Job job) const;
|
||||||
|
|
||||||
|
template <typename Worker, typename WorkFn>
|
||||||
|
EnqueueResult enqueue(Job job, Worker& worker, WorkFn&& work, std::size_t maxQueuedWork = 0)
|
||||||
|
{
|
||||||
|
std::size_t queueDepth = worker.pendingTaskCount();
|
||||||
|
auto ticket = beginDispatch(job, queueDepth, maxQueuedWork);
|
||||||
|
if (!ticket.accepted) return {ticket, false, queueDepth};
|
||||||
|
|
||||||
|
worker.post([this, ticket, work = std::forward<WorkFn>(work)]() mutable -> rpc::RPCWorker::MainCb {
|
||||||
|
rpc::RPCWorker::MainCb mainCallback;
|
||||||
|
try {
|
||||||
|
mainCallback = work();
|
||||||
|
} catch (...) {
|
||||||
|
mainCallback = nullptr;
|
||||||
|
}
|
||||||
|
return [this, ticket, mainCallback = std::move(mainCallback)]() mutable {
|
||||||
|
if (!completeDispatch(ticket)) return;
|
||||||
|
if (mainCallback) mainCallback();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {ticket, true, queueDepth};
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::atomic<bool>& jobFlag(Job job);
|
||||||
|
const std::atomic<bool>& jobFlag(Job job) const;
|
||||||
|
static std::size_t jobIndex(Job job);
|
||||||
|
|
||||||
|
RefreshScheduler scheduler_;
|
||||||
|
std::atomic<bool> coreInProgress_{false};
|
||||||
|
std::atomic<bool> addressesInProgress_{false};
|
||||||
|
std::atomic<bool> transactionsInProgress_{false};
|
||||||
|
std::atomic<bool> miningInProgress_{false};
|
||||||
|
std::atomic<bool> peersInProgress_{false};
|
||||||
|
std::atomic<bool> priceInProgress_{false};
|
||||||
|
std::atomic<bool> encryptionInProgress_{false};
|
||||||
|
std::atomic<bool> connectionInitInProgress_{false};
|
||||||
|
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> generations_{};
|
||||||
|
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> started_{};
|
||||||
|
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> finished_{};
|
||||||
|
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> skippedInFlight_{};
|
||||||
|
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> skippedQueuePressure_{};
|
||||||
|
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(Job::Count)> staleCallbacks_{};
|
||||||
|
std::array<std::atomic<std::size_t>, static_cast<std::size_t>(Job::Count)> lastQueueDepth_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace services
|
||||||
|
} // namespace dragonx
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user