3 Commits

Author SHA1 Message Date
dan_s
3e6136983a update links 2026-03-03 01:20:03 -06:00
dan_s
2c1862aed3 change release output names 2026-02-28 15:28:40 -06:00
dan_s
4b815fc9d1 feat: blockchain rescan via daemon restart + status bar progress
- Fix z_importwallet to use full path instead of filename only
- Add rescanBlockchain() method that restarts daemon with -rescan flag
- Track rescan progress via daemon output parsing and getrescaninfo RPC
- Display rescan progress in status bar with animated indicator when starting
- Improve dark theme card contrast: lighter surface-variant, tinted borders, stronger rim-light
2026-02-28 15:06:35 -06:00
390 changed files with 29523 additions and 84042 deletions

173
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,173 @@
# Copilot Instructions — DragonX ImGui Wallet
## UI Layout: All values in `ui.toml`
**Every UI layout constant must be defined in `res/themes/ui.toml` and read at runtime via the schema API.** Never hardcode pixel sizes, ratios, rounding values, thicknesses, or spacing constants directly in C++ source files. This is critical for maintainability, theming support, and hot-reload.
### Schema API reference
The singleton is accessed via `schema::UI()` (header: `#include "../schema/ui_schema.h"`).
| Method | Returns | Use for |
|---|---|---|
| `drawElement(section, name)` | `DrawElementStyle` | Custom DrawList layout values (`.size`, `.height`, `.thickness`, `.radius`, `.opacity`) |
| `button(section, name)` | `ButtonStyle` | Button width/height/font |
| `input(section, name)` | `InputStyle` | Input field dimensions |
| `label(section, name)` | `LabelStyle` | Label styling |
| `table(section, name)` | `TableStyle` | Table layout |
| `window(section, name)` | `WindowStyle` | Window/dialog dimensions |
| `combo(section, name)` | `ComboStyle` | Combo box styling |
| `slider(section, name)` | `SliderStyle` | Slider styling |
| `checkbox(section, name)` | `CheckboxStyle` | Checkbox styling |
| `separator(section, name)` | `SeparatorStyle` | Separator/divider styling |
### Section naming convention
Sections use dot-separated paths matching the file/feature:
- `tabs.send`, `tabs.receive`, `tabs.transactions`, `tabs.mining`, `tabs.peers`, `tabs.market` — tab-specific values
- `tabs.balance` — balance/home tab
- `components.main-layout`, `components.settings-page` — shared components
- `dialogs.about`, `dialogs.backup`, etc. — dialog-specific values
- `sidebar` — navigation sidebar
### How to add a new layout value
1. **Add the entry to `res/themes/ui.toml`** under the appropriate section:
```toml
[tabs.example]
my-element = {size = 42.0}
```
2. **Read it in C++** instead of using a literal:
```cpp
// WRONG — hardcoded
float myValue = 42.0f;
// CORRECT — schema-driven
float myValue = schema::UI().drawElement("tabs.example", "my-element").size;
```
3. For values used as min/max pairs with scaling:
```cpp
// WRONG
float h = std::max(18.0f, 24.0f * vScale);
// CORRECT
float h = std::max(
schema::UI().drawElement("tabs.example", "row-min-height").size,
schema::UI().drawElement("tabs.example", "row-height").size * vScale
);
```
### What belongs in `ui.toml`
- Pixel sizes (card heights, icon sizes, bar widths/heights)
- Ratios (column width ratios, max-width ratios)
- Rounding values (`FrameRounding`, corner radius)
- Thickness values (accent bars, chart lines, borders)
- Dot/circle radii
- Fade zones, padding constants
- Min/max dimension bounds
- Font selection (via schema font name strings, resolved with `S.resolveFont()`)
- Colors (via `schema::UI().resolveColor()` or color variable references like `"var(--primary)"`)
- Animation durations (transition times, fade durations, pulse speeds)
- Business logic values (fee amounts, ticker strings, buffer sizes, reward amounts)
### What does NOT belong in `ui.toml`
- Spacing that already goes through `Layout::spacing*()` or `spacing::dp()`
### Legacy system: `UILayout`
`UILayout::instance()` is the older layout system still used for fonts, typography, panels, and global spacing. New layout values should use `schema::UI().drawElement()` instead. Do not add new keys to `UILayout`.
### Validation
After editing `ui.toml`, always validate:
```bash
python3 -c "import toml; toml.load('res/themes/ui.toml'); print('Valid TOML')"
```
Or with the C++ toml++ parser (which is what the app uses):
```bash
cd build && make -j$(nproc)
```
### Build
```bash
# Linux
cd build && make -j$(nproc)
# Windows cross-compile
./build.sh --win-release
```
## Plans
When asked to "create a plan", always create a new markdown document in the `docs/` directory with the plan contents.
## Icons: Use Material Design icon font, never Unicode symbols
**Never use raw Unicode symbols or emoji characters** (e.g. ``, ``, ``, `🔍`, `📬`, `⚠️`, ``) for icons in C++ code. Always use the **Material Design Icons font** via the `ICON_MD_*` defines from `#include "../../embedded/IconsMaterialDesign.h"`.
### Icon font API
| Method | Size | Fallback |
|---|---|---|
| `Type().iconSmall()` | 14px | Body2 |
| `Type().iconMed()` | 18px | Body1 |
| `Type().iconLarge()` | 24px | H5 |
| `Type().iconXL()` | 40px | H3 |
### Correct usage
```cpp
#include "../../embedded/IconsMaterialDesign.h"
// WRONG — raw Unicode symbol
itemSpec.leadingIcon = "↙";
// CORRECT — Material Design icon codepoint
itemSpec.leadingIcon = ICON_MD_CALL_RECEIVED;
// WRONG — emoji for search
searchSpec.leadingIcon = "🔍";
// CORRECT — Material Design icon
searchSpec.leadingIcon = ICON_MD_SEARCH;
// For rendering with icon font directly:
ImGui::PushFont(Type().iconSmall());
ImGui::TextUnformatted(ICON_MD_ARROW_DOWNWARD);
ImGui::PopFont();
```
### Why
Raw Unicode symbols and emoji render inconsistently across platforms and may not be present in the loaded text fonts. The Material Design icon font is always loaded and provides consistent, scalable glyphs on both Linux and Windows.
### Audit for Unicode symbols
Before completing any task that touches UI code, search for and replace any raw Unicode symbols that may have been introduced. Common symbols to look for:
| Unicode | Replacement |
|---------|-------------|
| `` `` | `ICON_MD_PLAY_ARROW` |
| `` `` `` `` | `ICON_MD_STOP` or `ICON_MD_SQUARE` |
| `` `` `` `` | `ICON_MD_FIBER_MANUAL_RECORD` or `ICON_MD_CIRCLE` |
| `` `` `` `` | `ICON_MD_ARROW_UPWARD`, `_DOWNWARD`, `_BACK`, `_FORWARD` |
| `` `` `` `` | `ICON_MD_CALL_RECEIVED`, `_MADE`, etc. |
| `` `` | `ICON_MD_CHECK` |
| `` `` `` | `ICON_MD_CLOSE` |
| `` `⚠️` | `ICON_MD_WARNING` |
| `` `` | `ICON_MD_INFO` |
| `🔍` | `ICON_MD_SEARCH` |
| `📋` | `ICON_MD_CONTENT_COPY` or `ICON_MD_DESCRIPTION` |
| `🛡` `🛡️` | `ICON_MD_SHIELD` |
| `` | `ICON_MD_HOURGLASS_EMPTY` |
| `🔄` `` `` | `ICON_MD_SYNC` or `ICON_MD_REFRESH` |
| `` `⚙️` | `ICON_MD_SETTINGS` |
| `🔒` | `ICON_MD_LOCK` |
| `` `` | `ICON_MD_STAR` or `ICON_MD_STAR_BORDER` |

22
.gitignore vendored
View File

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

102
CLAUDE.md
View File

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

View File

@@ -3,32 +3,12 @@
# Released under the GPLv3
cmake_minimum_required(VERSION 3.20)
# macOS: set deployment target and universal architectures BEFORE project()
# so they propagate to all targets, including FetchContent dependencies (SDL3, etc.)
if(APPLE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum macOS version" FORCE)
# Build universal binary (Apple Silicon + Intel) unless the user explicitly set architectures
if(NOT DEFINED CMAKE_OSX_ARCHITECTURES OR CMAKE_OSX_ARCHITECTURES STREQUAL "")
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures" FORCE)
endif()
endif()
project(ObsidianDragon
VERSION 2.0.0
project(ObsidianDragon
VERSION 1.0.0
LANGUAGES C CXX
DESCRIPTION "DragonX Cryptocurrency Wallet"
)
# Pre-release suffix (e.g. "-rc1", "-beta2"). Leave empty for stable releases.
set(DRAGONX_VERSION_SUFFIX "")
# ObsidianDragonLite is versioned INDEPENDENTLY of the full-node app above. The active variant's
# version flows to the generated header, the Windows .rc/manifest, and build.sh's release names via
# DRAGONX_APP_VERSION* (resolved in the lite/full block below).
set(DRAGONX_LITE_VERSION "1.0.0")
set(DRAGONX_LITE_VERSION_SUFFIX "")
# C++17 standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -42,132 +22,6 @@ endif()
# Options
option(DRAGONX_USE_SYSTEM_SDL3 "Use system SDL3 instead of fetching" ON)
option(DRAGONX_ENABLE_EMBEDDED_DAEMON "Enable embedded dragonxd support" ON)
option(DRAGONX_BUILD_LITE "Build ObsidianDragonLite variant without full-node features" OFF)
option(DRAGONX_ENABLE_LITE_BACKEND "Enable real lite wallet backend integration" OFF)
option(DRAGONX_ENABLE_CHAT "Enable the HushChat protocol/UI integration" ON)
set(DRAGONX_LITE_BACKEND_LIBRARY "" CACHE FILEPATH "Path to a prebuilt SDXL-compatible lite backend library")
set(DRAGONX_LITE_BACKEND_INCLUDE_DIR "" CACHE PATH "Optional include directory for SDXL-compatible lite backend headers")
set(DRAGONX_LITE_BACKEND_EXTRA_LIBS "" CACHE STRING "Additional libraries needed by the SDXL-compatible lite backend")
set(DRAGONX_LITE_BACKEND_LINK_MODE "imported" CACHE STRING "Lite backend link mode; Phase 1 supports imported only")
set_property(CACHE DRAGONX_LITE_BACKEND_LINK_MODE PROPERTY STRINGS imported)
set(DRAGONX_LITE_BACKEND_ABI "sdxl-c-v1" CACHE STRING "Expected lite backend C ABI version")
set(DRAGONX_LITE_BACKEND_SYMBOLS_FILE "" CACHE FILEPATH "Path to generated lite backend exported-symbol inventory")
set(DRAGONX_LITE_BACKEND_MANIFEST "" CACHE FILEPATH "Optional path to generated lite backend artifact manifest")
option(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE "Require verified signature metadata in the lite backend artifact manifest" OFF)
set(DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS
litelib_wallet_exists
litelib_initialize_new
litelib_initialize_new_from_phrase
litelib_initialize_existing
litelib_execute
litelib_rust_free_string
litelib_check_server_online
litelib_shutdown
)
if(DRAGONX_BUILD_LITE)
set(DRAGONX_APP_NAME "ObsidianDragonLite")
set(DRAGONX_BINARY_NAME "ObsidianDragonLite")
# NOTE: do NOT FORCE-write DRAGONX_ENABLE_EMBEDDED_DAEMON=OFF into the cache here. A forced
# cache write persists into a later full-node reconfigure of the same build dir and silently
# disables the embedded daemon — the binary still embeds/extracts, but isUsingEmbeddedDaemon()
# returns false, so it "unpacks dragonxd but never starts" (the 1.3.0 regression). It is also
# redundant: makeWalletCapabilities() already forces the embedded-daemon capability off for any
# lite build via `fullNodeBuild && embeddedDaemonCompiled`, so lite never launches a daemon
# regardless of this flag. build.sh sets the flag explicitly per variant to defeat stale caches.
set(DRAGONX_APP_VERSION "${DRAGONX_LITE_VERSION}")
set(DRAGONX_APP_VERSION_SUFFIX "${DRAGONX_LITE_VERSION_SUFFIX}")
else()
set(DRAGONX_APP_NAME "ObsidianDragon")
set(DRAGONX_BINARY_NAME "ObsidianDragon")
set(DRAGONX_APP_VERSION "${PROJECT_VERSION}")
set(DRAGONX_APP_VERSION_SUFFIX "${DRAGONX_VERSION_SUFFIX}")
endif()
# Split the active version into numeric components for the generated header + Windows VERSIONINFO.
string(REPLACE "." ";" _dragonx_ver_parts "${DRAGONX_APP_VERSION}")
list(GET _dragonx_ver_parts 0 DRAGONX_APP_VERSION_MAJOR)
list(GET _dragonx_ver_parts 1 DRAGONX_APP_VERSION_MINOR)
list(GET _dragonx_ver_parts 2 DRAGONX_APP_VERSION_PATCH)
set(DRAGONX_LITE_BACKEND_READY OFF)
if(DRAGONX_ENABLE_LITE_BACKEND)
if(NOT DRAGONX_BUILD_LITE)
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND is only supported with DRAGONX_BUILD_LITE=ON")
endif()
if(NOT DRAGONX_LITE_BACKEND_LINK_MODE STREQUAL "imported")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_LINK_MODE currently supports only 'imported'; runtime dynamic loading is a later bridge-runtime phase")
endif()
if(NOT DRAGONX_LITE_BACKEND_ABI STREQUAL "sdxl-c-v1")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_ABI must be sdxl-c-v1")
endif()
if(NOT DRAGONX_LITE_BACKEND_LIBRARY)
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND requires DRAGONX_LITE_BACKEND_LIBRARY to point at an SDXL-compatible artifact")
endif()
if(NOT EXISTS "${DRAGONX_LITE_BACKEND_LIBRARY}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_LIBRARY does not exist: ${DRAGONX_LITE_BACKEND_LIBRARY}")
endif()
if(NOT DRAGONX_LITE_BACKEND_SYMBOLS_FILE)
message(FATAL_ERROR "DRAGONX_ENABLE_LITE_BACKEND requires DRAGONX_LITE_BACKEND_SYMBOLS_FILE generated by scripts/build-lite-backend-artifact.sh")
endif()
if(NOT EXISTS "${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE does not exist: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
endif()
file(STRINGS "${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}" DRAGONX_LITE_BACKEND_SYMBOL_LINES)
if(NOT DRAGONX_LITE_BACKEND_SYMBOL_LINES)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE is empty: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
endif()
foreach(DRAGONX_LITE_REQUIRED_SYMBOL IN LISTS DRAGONX_LITE_BACKEND_REQUIRED_SYMBOLS)
list(FIND DRAGONX_LITE_BACKEND_SYMBOL_LINES "${DRAGONX_LITE_REQUIRED_SYMBOL}" DRAGONX_LITE_SYMBOL_INDEX)
if(DRAGONX_LITE_SYMBOL_INDEX EQUAL -1)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_SYMBOLS_FILE is missing required symbol: ${DRAGONX_LITE_REQUIRED_SYMBOL}")
endif()
endforeach()
if(DRAGONX_LITE_BACKEND_MANIFEST AND NOT EXISTS "${DRAGONX_LITE_BACKEND_MANIFEST}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST does not exist: ${DRAGONX_LITE_BACKEND_MANIFEST}")
endif()
if(DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE)
if(NOT DRAGONX_LITE_BACKEND_MANIFEST)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires DRAGONX_LITE_BACKEND_MANIFEST")
endif()
file(READ "${DRAGONX_LITE_BACKEND_MANIFEST}" DRAGONX_LITE_BACKEND_MANIFEST_JSON)
string(JSON DRAGONX_LITE_SIGNATURE_STATUS ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_STATUS_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_status)
if(DRAGONX_LITE_SIGNATURE_STATUS_ERROR)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing signature verification status")
endif()
if(NOT DRAGONX_LITE_SIGNATURE_STATUS STREQUAL "verified")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verified signature metadata")
endif()
string(JSON DRAGONX_LITE_SIGNATURE_VERIFIED_SHA ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verified_artifact_sha256)
string(JSON DRAGONX_LITE_ARTIFACT_SHA ERROR_VARIABLE DRAGONX_LITE_ARTIFACT_SHA_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" artifact sha256)
if(DRAGONX_LITE_SIGNATURE_VERIFIED_SHA_ERROR OR DRAGONX_LITE_ARTIFACT_SHA_ERROR)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST is missing artifact/signature SHA-256 metadata")
endif()
if(NOT DRAGONX_LITE_SIGNATURE_VERIFIED_SHA STREQUAL DRAGONX_LITE_ARTIFACT_SHA)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_MANIFEST signature metadata does not verify the artifact SHA-256")
endif()
string(JSON DRAGONX_LITE_SIGNATURE_PERFORMED ERROR_VARIABLE DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR GET "${DRAGONX_LITE_BACKEND_MANIFEST_JSON}" signature_verification verification_performed)
if(DRAGONX_LITE_SIGNATURE_PERFORMED_ERROR OR NOT DRAGONX_LITE_SIGNATURE_PERFORMED)
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE requires verification_performed=true")
endif()
endif()
add_library(dragonx_lite_backend UNKNOWN IMPORTED)
set_target_properties(dragonx_lite_backend PROPERTIES
IMPORTED_LOCATION "${DRAGONX_LITE_BACKEND_LIBRARY}"
)
if(DRAGONX_LITE_BACKEND_INCLUDE_DIR)
if(NOT IS_DIRECTORY "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
message(FATAL_ERROR "DRAGONX_LITE_BACKEND_INCLUDE_DIR does not exist: ${DRAGONX_LITE_BACKEND_INCLUDE_DIR}")
endif()
set_target_properties(dragonx_lite_backend PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${DRAGONX_LITE_BACKEND_INCLUDE_DIR}"
)
endif()
set(DRAGONX_LITE_BACKEND_READY ON)
endif()
include(CTest)
# Output directories
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
@@ -240,32 +94,6 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(tomlplusplus)
# SQLite amalgamation - local Explorer block-summary cache
FetchContent_Declare(
sqlite3
URL https://www.sqlite.org/2024/sqlite-amalgamation-3450300.zip
URL_HASH SHA256=ea170e73e447703e8359308ca2e4366a3ae0c4304a8665896f068c736781c651
)
FetchContent_GetProperties(sqlite3)
if(NOT sqlite3_POPULATED)
FetchContent_Populate(sqlite3)
endif()
file(GLOB SQLITE3_AMALGAMATION_C CONFIGURE_DEPENDS
${sqlite3_SOURCE_DIR}/sqlite3.c
${sqlite3_SOURCE_DIR}/*/sqlite3.c
)
if(NOT SQLITE3_AMALGAMATION_C)
message(FATAL_ERROR "SQLite amalgamation source not found")
endif()
list(GET SQLITE3_AMALGAMATION_C 0 SQLITE3_SOURCE_FILE)
get_filename_component(SQLITE3_INCLUDE_DIR ${SQLITE3_SOURCE_FILE} DIRECTORY)
add_library(sqlite3_amalgamation STATIC ${SQLITE3_SOURCE_FILE})
target_include_directories(sqlite3_amalgamation PUBLIC ${SQLITE3_INCLUDE_DIR})
target_compile_definitions(sqlite3_amalgamation PRIVATE
SQLITE_THREADSAFE=1
SQLITE_OMIT_LOAD_EXTENSION
)
# libcurl for HTTPS RPC connections (more reliable than cpp-httplib with OpenSSL 3.x)
if(WIN32)
# For Windows cross-compilation, fetch and build libcurl statically
@@ -319,9 +147,6 @@ elseif(APPLE)
if(EXISTS ${CMAKE_SOURCE_DIR}/libs/libsodium-mac/lib/libsodium.a)
set(SODIUM_LIBRARY ${CMAKE_SOURCE_DIR}/libs/libsodium-mac/lib/libsodium.a)
set(SODIUM_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/libs/libsodium-mac/include)
elseif(EXISTS ${CMAKE_SOURCE_DIR}/libs/libsodium/lib/libsodium.a)
set(SODIUM_LIBRARY ${CMAKE_SOURCE_DIR}/libs/libsodium/lib/libsodium.a)
set(SODIUM_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/libs/libsodium/include)
endif()
else()
# Linux: prefer system libsodium, fall back to local build
@@ -405,70 +230,21 @@ set(APP_SOURCES
src/app_network.cpp
src/app_security.cpp
src/app_wizard.cpp
src/services/network_refresh_service.cpp
src/services/refresh_scheduler.cpp
src/services/wallet_security_controller.cpp
src/services/wallet_security_workflow.cpp
src/services/wallet_security_workflow_executor.cpp
src/chat/chat_protocol.cpp
src/chat/chat_crypto.cpp
src/chat/chat_identity.cpp
src/chat/chat_store.cpp
src/chat/chat_service.cpp
src/chat/chat_database.cpp
src/chat/chat_outgoing.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_diagnostics.cpp
src/wallet/lite_wallet_controller.cpp
src/wallet/lite_result_parsers.cpp
src/wallet/lite_sync_service.cpp
src/wallet/lite_wallet_gateway.cpp
src/wallet/lite_wallet_state_mapper.cpp
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
src/wallet/lite_wallet_server_selection_adapter.cpp
src/wallet/lite_wallet_lifecycle_service.cpp
src/data/wallet_state.cpp
src/data/transaction_history_cache.cpp
src/ui/theme.cpp
src/ui/theme_loader.cpp
src/ui/explorer/explorer_block_cache.cpp
src/ui/material/color_theme.cpp
src/ui/material/typography.cpp
src/ui/notifications.cpp
src/ui/windows/main_window.cpp
src/ui/windows/balance_tab.cpp
src/ui/windows/balance_components.cpp
src/ui/windows/balance_address_list.cpp
src/ui/windows/balance_recent_tx.cpp
src/ui/windows/balance_tab_helpers.cpp
src/ui/windows/send_tab.cpp
src/ui/windows/receive_tab.cpp
src/ui/windows/transactions_tab.cpp
src/ui/windows/mining_tab.cpp
src/ui/windows/mining_earnings.cpp
src/ui/windows/mining_stats.cpp
src/ui/windows/mining_controls.cpp
src/ui/windows/mining_mode_toggle.cpp
src/ui/windows/mining_benchmark.cpp
src/ui/windows/mining_pool_panel.cpp
src/ui/windows/mining_tab_helpers.cpp
src/ui/windows/peers_tab.cpp
src/ui/windows/network_tab.cpp
src/ui/windows/explorer_tab.cpp
src/ui/windows/market_tab.cpp
src/ui/windows/console_tab.cpp
src/ui/windows/console_command_executor.cpp
src/ui/windows/console_command_reference.cpp
src/ui/windows/console_input_model.cpp
src/ui/windows/console_model.cpp
src/ui/windows/console_output_model.cpp
src/ui/windows/console_scroll_controller.cpp
src/ui/windows/console_selection_controller.cpp
src/ui/windows/console_tab_helpers.cpp
src/ui/windows/console_text_layout.cpp
src/ui/windows/settings_window.cpp
src/ui/pages/settings_page.cpp
src/ui/windows/about_dialog.cpp
@@ -476,13 +252,14 @@ set(APP_SOURCES
src/ui/windows/transaction_details_dialog.cpp
src/ui/windows/qr_popup_dialog.cpp
src/ui/windows/validate_address_dialog.cpp
src/ui/windows/contacts_tab.cpp
src/ui/windows/chat_tab.cpp
src/ui/windows/address_book_dialog.cpp
src/ui/windows/shield_dialog.cpp
src/ui/windows/request_payment_dialog.cpp
src/ui/windows/block_info_dialog.cpp
src/ui/windows/import_key_dialog.cpp
src/ui/windows/export_all_keys_dialog.cpp
src/ui/windows/export_transactions_dialog.cpp
src/ui/windows/backup_wallet_dialog.cpp
src/ui/widgets/qr_code.cpp
src/rpc/rpc_client.cpp
src/rpc/rpc_worker.cpp
@@ -491,31 +268,16 @@ set(APP_SOURCES
src/data/address_book.cpp
src/data/exchange_info.cpp
src/util/logger.cpp
src/util/async_task_manager.cpp
src/util/amount_format.cpp
src/util/address_validation.cpp
src/util/base64.cpp
src/util/single_instance.cpp
src/util/i18n.cpp
src/util/text_format.cpp
src/util/platform.cpp
src/util/payment_uri.cpp
src/util/texture_loader.cpp
src/util/noise_texture.cpp
src/daemon/embedded_daemon.cpp
src/daemon/seed_wallet_creator.cpp
src/daemon/daemon_controller.cpp
src/daemon/lifecycle_adapters.cpp
src/daemon/xmrig_manager.cpp
src/util/bootstrap.cpp
src/util/lite_server_probe.cpp
src/util/pool_registry_core.cpp
src/util/pool_stats_service.cpp
src/util/http_download.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
src/util/daemon_updater.cpp
src/util/daemon_updater_core.cpp
src/util/secure_vault.cpp
src/ui/effects/framebuffer.cpp
src/ui/effects/blur_shader.cpp
@@ -546,77 +308,33 @@ endif()
set(APP_HEADERS
src/app.h
src/services/network_refresh_service.h
src/services/refresh_scheduler.h
src/services/wallet_security_controller.h
src/services/wallet_security_workflow.h
src/services/wallet_security_workflow_executor.h
src/wallet/wallet_capabilities.h
src/wallet/wallet_backend.h
src/wallet/lite_owned_string.h
src/wallet/lite_rollout_policy.h
src/wallet/lite_client_bridge.h
src/wallet/lite_connection_service.h
src/wallet/lite_result_parsers.h
src/wallet/lite_sync_service.h
src/wallet/lite_wallet_gateway.h
src/wallet/lite_wallet_state_mapper.h
src/wallet/lite_wallet_lifecycle_ui_adapter.h
src/wallet/lite_wallet_server_selection_adapter.h
src/wallet/lite_wallet_lifecycle_service.h
src/chat/chat_protocol.h
src/chat/chat_crypto.h
src/chat/chat_identity.h
src/chat/chat_message.h
src/chat/chat_store.h
src/chat/chat_service.h
src/chat/chat_database.h
src/chat/chat_outgoing.h
src/config/version.h
src/data/wallet_state.h
src/data/transaction_history_cache.h
src/ui/theme.h
src/ui/theme_loader.h
src/ui/explorer/explorer_block_cache.h
src/ui/notifications.h
src/ui/windows/main_window.h
src/ui/windows/balance_tab.h
src/ui/windows/balance_address_list.h
src/ui/windows/balance_recent_tx.h
src/ui/windows/balance_tab_helpers.h
src/ui/windows/send_tab.h
src/ui/windows/receive_tab.h
src/ui/windows/transactions_tab.h
src/ui/windows/mining_tab.h
src/ui/windows/mining_benchmark.h
src/ui/windows/mining_pool_panel.h
src/ui/windows/mining_tab_helpers.h
src/ui/windows/peers_tab.h
src/ui/windows/explorer_tab.h
src/ui/windows/market_tab.h
src/ui/windows/console_channel.h
src/ui/windows/console_command_reference.h
src/ui/windows/console_input_model.h
src/ui/windows/console_model.h
src/ui/windows/console_output_model.h
src/ui/windows/console_scroll_controller.h
src/ui/windows/console_selection_controller.h
src/ui/windows/console_tab.h
src/ui/windows/console_tab_helpers.h
src/ui/windows/settings_window.h
src/ui/windows/about_dialog.h
src/ui/windows/key_export_dialog.h
src/ui/windows/transaction_details_dialog.h
src/ui/windows/qr_popup_dialog.h
src/ui/windows/validate_address_dialog.h
src/ui/windows/contacts_tab.h
src/ui/windows/chat_tab.h
src/ui/windows/contact_picker.h
src/ui/windows/address_book_dialog.h
src/ui/windows/shield_dialog.h
src/ui/windows/request_payment_dialog.h
src/ui/windows/block_info_dialog.h
src/ui/windows/import_key_dialog.h
src/ui/windows/export_all_keys_dialog.h
src/ui/windows/export_transactions_dialog.h
src/ui/windows/backup_wallet_dialog.h
src/ui/widgets/qr_code.h
src/rpc/rpc_client.h
src/rpc/rpc_worker.h
@@ -626,8 +344,6 @@ set(APP_HEADERS
src/data/address_book.h
src/data/exchange_info.h
src/util/logger.h
src/util/async_task_manager.h
src/util/amount_format.h
src/util/base64.h
src/util/single_instance.h
src/util/i18n.h
@@ -635,9 +351,6 @@ set(APP_HEADERS
src/util/payment_uri.h
src/util/secure_vault.h
src/daemon/embedded_daemon.h
src/daemon/seed_wallet_creator.h
src/daemon/daemon_controller.h
src/daemon/lifecycle_adapters.h
src/daemon/xmrig_manager.h
src/ui/effects/framebuffer.h
src/ui/effects/blur_shader.h
@@ -660,14 +373,11 @@ endif()
# Windows application icon + VERSIONINFO (.rc -> .res -> linked into .exe)
if(WIN32)
set(OBSIDIAN_ICO_PATH "${CMAKE_SOURCE_DIR}/res/img/ObsidianDragon.ico")
# Generate manifest with version from project()
configure_file(
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest.in
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest
@ONLY
)
set(OBSIDIAN_MANIFEST_PATH "${CMAKE_SOURCE_DIR}/res/ObsidianDragon.manifest")
# Generate .rc with version from project()
# Version numbers for the VERSIONINFO resource block
set(DRAGONX_VER_MAJOR 1)
set(DRAGONX_VER_MINOR 0)
set(DRAGONX_VER_PATCH 0)
set(DRAGONX_VERSION "1.0.0")
configure_file(
${CMAKE_SOURCE_DIR}/res/ObsidianDragon.rc
${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc
@@ -676,15 +386,6 @@ if(WIN32)
set(WIN_RC_FILE ${CMAKE_BINARY_DIR}/generated/ObsidianDragon.rc)
endif()
# Generate version values from the single project(VERSION ...) declaration.
# Keep the build-specific app name in the build tree so full/lite configures do
# not rewrite a tracked source header.
configure_file(
${CMAKE_SOURCE_DIR}/src/config/version.h.in
${CMAKE_BINARY_DIR}/generated/dragonx_generated_version.h
@ONLY
)
# Generate INCBIN font embedding source with absolute paths to .ttf files
configure_file(
${CMAKE_SOURCE_DIR}/src/embedded/embedded_fonts.cpp.in
@@ -692,22 +393,6 @@ configure_file(
@ONLY
)
# INCBIN uses .incbin assembler directives that reference font files at
# assembly time — CMake doesn't track these implicit dependencies.
# Tell CMake that the generated source depends on the actual font binaries
# so a font file change triggers recompilation.
set_source_files_properties(
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
PROPERTIES OBJECT_DEPENDS
"${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-R.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Light.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/Ubuntu-Medium.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/UbuntuMono-R.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialIcons-Regular.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/MaterialDesignIcons-Pickaxe-Subset.ttf;\
${CMAKE_SOURCE_DIR}/res/fonts/NotoSansCJK-Subset.ttf"
)
add_executable(ObsidianDragon
${APP_SOURCES}
${CMAKE_BINARY_DIR}/generated/embedded_fonts.cpp
@@ -718,8 +403,6 @@ add_executable(ObsidianDragon
${WIN_RC_FILE}
)
set_target_properties(ObsidianDragon PROPERTIES OUTPUT_NAME "${DRAGONX_BINARY_NAME}")
target_include_directories(ObsidianDragon PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/embedded
@@ -739,66 +422,13 @@ target_link_libraries(ObsidianDragon PRIVATE
SDL3::SDL3
nlohmann_json::nlohmann_json
tomlplusplus::tomlplusplus
sqlite3_amalgamation
${CURL_LIBRARIES}
${SODIUM_LIBRARY}
)
if(DRAGONX_LITE_BACKEND_READY)
target_link_libraries(ObsidianDragon PRIVATE dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS})
# Real-backend smoke tool (only built when a real lite backend is linked).
add_executable(lite_smoke
tools/lite_smoke.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_result_parsers.cpp
)
target_include_directories(lite_smoke PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/generated
${SODIUM_INCLUDE_DIR}
)
target_compile_definitions(lite_smoke PRIVATE DRAGONX_ENABLE_LITE_BACKEND=1)
target_link_libraries(lite_smoke PRIVATE
dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS}
nlohmann_json::nlohmann_json
${SODIUM_LIBRARY}
)
if(UNIX)
target_link_libraries(lite_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
endif()
# Real-backend SEND smoke tool — drives the exact GUI send path (bridge.execute("send", ...)).
add_executable(lite_send_smoke
tools/lite_send_smoke.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_result_parsers.cpp
)
target_include_directories(lite_send_smoke PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_BINARY_DIR}/generated
${SODIUM_INCLUDE_DIR}
)
target_compile_definitions(lite_send_smoke PRIVATE DRAGONX_ENABLE_LITE_BACKEND=1)
target_link_libraries(lite_send_smoke PRIVATE
dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS}
nlohmann_json::nlohmann_json
${SODIUM_LIBRARY}
)
if(UNIX)
target_link_libraries(lite_send_smoke PRIVATE ${CMAKE_DL_LIBS} pthread)
endif()
endif()
# Platform-specific settings
if(WIN32)
target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi iphlpapi d3d11 dxgi d3dcompiler dcomp)
target_link_libraries(ObsidianDragon PRIVATE ws2_32 winmm imm32 version setupapi dwmapi crypt32 wldap32 psapi d3d11 dxgi d3dcompiler dcomp)
# Hide console window in release builds
if(CMAKE_BUILD_TYPE STREQUAL "Release")
set_target_properties(ObsidianDragon PROPERTIES WIN32_EXECUTABLE TRUE)
@@ -823,11 +453,7 @@ endif()
# Compile definitions
target_compile_definitions(ObsidianDragon PRIVATE
DRAGONX_DEBUG
DRAGONX_LITE_BUILD=$<BOOL:${DRAGONX_BUILD_LITE}>
DRAGONX_ENABLE_EMBEDDED_DAEMON=$<BOOL:${DRAGONX_ENABLE_EMBEDDED_DAEMON}>
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
DRAGONX_ENABLE_CHAT=$<BOOL:${DRAGONX_ENABLE_CHAT}>
$<$<CONFIG:Debug>:DRAGONX_DEBUG>
)
if(WIN32)
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_USE_DX11)
@@ -835,26 +461,6 @@ else()
target_compile_definitions(ObsidianDragon PRIVATE DRAGONX_HAS_GLAD)
endif()
add_executable(HushChatFixtureCheck
tools/hushchat_fixture_check.cpp
src/chat/chat_protocol.cpp
src/chat/chat_fixture_tooling.cpp
)
target_include_directories(HushChatFixtureCheck PRIVATE
${CMAKE_SOURCE_DIR}/src
${SODIUM_INCLUDE_DIR}
)
target_link_libraries(HushChatFixtureCheck PRIVATE
nlohmann_json::nlohmann_json
${SODIUM_LIBRARY}
)
target_compile_definitions(HushChatFixtureCheck PRIVATE
DRAGONX_ENABLE_CHAT=0
)
# -----------------------------------------------------------------------------
# Copy resources
# -----------------------------------------------------------------------------
@@ -874,44 +480,17 @@ elseif(EXISTS ${CMAKE_SOURCE_DIR}/../SilentDragonX/res/Ubuntu-R.ttf)
)
endif()
# Copy language files at BUILD time (not just cmake configure time)
# so edits to res/lang/*.json are picked up by 'make' without re-running cmake.
# Copy language files
file(GLOB LANG_FILES ${CMAKE_SOURCE_DIR}/res/lang/*.json)
if(LANG_FILES)
find_program(XXD_EXECUTABLE NAMES xxd)
if(NOT XXD_EXECUTABLE)
message(WARNING "xxd not found; runtime language JSON files will be copied, but embedded build/generated/embedded/lang_*.h files will not be regenerated")
endif()
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/generated/embedded)
foreach(LANG_FILE ${LANG_FILES})
get_filename_component(LANG_FILENAME ${LANG_FILE} NAME)
add_custom_command(
OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME}
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${LANG_FILE}
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME}
DEPENDS ${LANG_FILE}
COMMENT "Copying ${LANG_FILENAME}"
configure_file(
${LANG_FILE}
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME}
COPYONLY
)
list(APPEND LANG_OUTPUTS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/lang/${LANG_FILENAME})
# Also regenerate the embedded header so the binary always has fresh translations
if(XXD_EXECUTABLE)
get_filename_component(LANG_CODE ${LANG_FILENAME} NAME_WE)
set(LANG_HEADER ${CMAKE_BINARY_DIR}/generated/embedded/lang_${LANG_CODE}.h)
add_custom_command(
OUTPUT ${LANG_HEADER}
COMMAND ${XXD_EXECUTABLE} -i "res/lang/${LANG_FILENAME}" > "${LANG_HEADER}"
DEPENDS ${LANG_FILE}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Embedding lang_${LANG_CODE}.h"
)
list(APPEND LANG_OUTPUTS ${LANG_HEADER})
endif()
endforeach()
add_custom_target(copy_langs ALL DEPENDS ${LANG_OUTPUTS})
add_dependencies(ObsidianDragon copy_langs)
message(STATUS " Language files: ${LANG_FILES}")
endif()
@@ -923,39 +502,14 @@ embed_resource(
${CMAKE_BINARY_DIR}/generated/ui_toml_embedded.h
ui_toml
)
embed_resource(
${CMAKE_SOURCE_DIR}/res/default_banlist.txt
${CMAKE_BINARY_DIR}/generated/default_banlist_embedded.h
default_banlist
)
# Note: xmrig is embedded via build.sh (embedded_data.h) for Windows builds,
# following the same pattern as daemon embedding.
# Expand and copy theme files at BUILD time — skin files get layout sections
# from ui.toml appended automatically so users can see/edit all properties.
# Source skin files stay minimal; the merged output goes to build/bin/res/themes/.
find_package(Python3 QUIET COMPONENTS Interpreter)
if(NOT Python3_FOUND)
find_program(Python3_EXECUTABLE NAMES python3 python)
endif()
# Copy theme files at BUILD time (not just cmake configure time)
# so edits to res/themes/*.toml are picked up by 'make' without re-running cmake.
file(GLOB THEME_FILES ${CMAKE_SOURCE_DIR}/res/themes/*.toml)
if(THEME_FILES AND Python3_EXECUTABLE)
add_custom_command(
OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes/.expanded
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/scripts/expand_themes.py
${CMAKE_SOURCE_DIR}/res/themes
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes
COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes/.expanded
DEPENDS ${THEME_FILES} ${CMAKE_SOURCE_DIR}/scripts/expand_themes.py
COMMENT "Expanding theme files (merging layout from ui.toml)"
)
add_custom_target(copy_themes ALL DEPENDS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res/themes/.expanded)
add_dependencies(ObsidianDragon copy_themes)
message(STATUS " Theme files: ${THEME_FILES} (build-time expansion via Python)")
elseif(THEME_FILES)
# Fallback: plain copy if Python is not available
message(WARNING "Python3 not found; copying theme files without expand_themes.py layout merge")
if(THEME_FILES)
foreach(THEME_FILE ${THEME_FILES})
get_filename_component(THEME_FILENAME ${THEME_FILE} NAME)
add_custom_command(
@@ -970,7 +524,7 @@ elseif(THEME_FILES)
endforeach()
add_custom_target(copy_themes ALL DEPENDS ${THEME_OUTPUTS})
add_dependencies(ObsidianDragon copy_themes)
message(STATUS " Theme files: ${THEME_FILES} (plain copy, Python not found)")
message(STATUS " Theme files: ${THEME_FILES}")
endif()
# Copy image files (including backgrounds/ subdirectories and logos/)
@@ -1005,133 +559,20 @@ install(TARGETS ObsidianDragon
)
install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res
DESTINATION share/${DRAGONX_BINARY_NAME}
DESTINATION share/ObsidianDragon
OPTIONAL
)
# -----------------------------------------------------------------------------
# Tests
# -----------------------------------------------------------------------------
if(BUILD_TESTING)
add_executable(ObsidianDragonTests
tests/test_phase4.cpp
src/services/network_refresh_service.cpp
src/services/refresh_scheduler.cpp
src/services/wallet_security_controller.cpp
src/services/wallet_security_workflow.cpp
src/services/wallet_security_workflow_executor.cpp
src/chat/chat_protocol.cpp
src/chat/chat_crypto.cpp
src/chat/chat_identity.cpp
src/chat/chat_store.cpp
src/chat/chat_service.cpp
src/chat/chat_database.cpp
src/chat/chat_outgoing.cpp
src/wallet/lite_owned_string.cpp
src/wallet/lite_rollout_policy.cpp
src/wallet/lite_client_bridge.cpp
src/wallet/lite_connection_service.cpp
src/wallet/lite_diagnostics.cpp
src/wallet/lite_wallet_controller.cpp
src/wallet/lite_result_parsers.cpp
src/wallet/lite_sync_service.cpp
src/wallet/lite_wallet_gateway.cpp
src/wallet/lite_wallet_state_mapper.cpp
src/wallet/lite_wallet_lifecycle_ui_adapter.cpp
src/wallet/lite_wallet_server_selection_adapter.cpp
src/wallet/lite_wallet_lifecycle_service.cpp
src/ui/explorer/explorer_block_cache.cpp
src/ui/windows/balance_address_list.cpp
src/ui/windows/balance_recent_tx.cpp
src/ui/windows/console_input_model.cpp
src/ui/windows/console_model.cpp
src/ui/windows/console_output_model.cpp
src/ui/windows/console_scroll_controller.cpp
src/ui/windows/console_selection_controller.cpp
src/ui/windows/console_tab_helpers.cpp
src/ui/windows/console_text_layout.cpp
src/ui/windows/mining_benchmark.cpp
src/ui/windows/mining_pool_panel.cpp
src/ui/windows/mining_tab_helpers.cpp
src/util/payment_uri.cpp
src/util/amount_format.cpp
src/util/address_validation.cpp
src/util/i18n.cpp
src/util/text_format.cpp
src/data/wallet_state.cpp
src/data/transaction_history_cache.cpp
src/daemon/lifecycle_adapters.cpp
src/rpc/connection.cpp
src/config/settings.cpp
src/resources/embedded_resources.cpp
src/util/secure_vault.cpp
src/util/platform.cpp
src/util/logger.cpp
src/util/lite_server_probe.cpp
src/util/pool_registry_core.cpp
src/util/http_download.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
src/util/daemon_updater.cpp
src/util/daemon_updater_core.cpp
${MINIZ_SOURCES}
)
target_include_directories(ObsidianDragonTests PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/resources
${CMAKE_SOURCE_DIR}/libs
${CMAKE_BINARY_DIR}/generated
${IMGUI_DIR}
${SODIUM_INCLUDE_DIR}
${CURL_INCLUDE_DIRS}
${MINIZ_DIR}
)
target_link_libraries(ObsidianDragonTests PRIVATE
nlohmann_json::nlohmann_json
sqlite3_amalgamation
${SODIUM_LIBRARY}
${CURL_LIBRARIES}
)
target_compile_definitions(ObsidianDragonTests PRIVATE
DRAGONX_ENABLE_CHAT=$<BOOL:${DRAGONX_ENABLE_CHAT}>
DRAGONX_LITE_BUILD=$<BOOL:${DRAGONX_BUILD_LITE}>
DRAGONX_ENABLE_EMBEDDED_DAEMON=$<BOOL:${DRAGONX_ENABLE_EMBEDDED_DAEMON}>
DRAGONX_ENABLE_LITE_BACKEND=$<BOOL:${DRAGONX_LITE_BACKEND_READY}>
DRAGONX_TEST_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures"
)
if(DRAGONX_LITE_BACKEND_READY)
target_link_libraries(ObsidianDragonTests PRIVATE dragonx_lite_backend ${DRAGONX_LITE_BACKEND_EXTRA_LIBS})
endif()
if(UNIX)
target_link_libraries(ObsidianDragonTests PRIVATE ${CMAKE_DL_LIBS})
endif()
add_test(NAME ObsidianDragonPhase4Tests COMMAND ObsidianDragonTests)
endif()
# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------
message(STATUS "")
message(STATUS "DragonX ImGui Wallet Configuration:")
message(STATUS " Version: ${DRAGONX_APP_VERSION}${DRAGONX_APP_VERSION_SUFFIX} (${DRAGONX_APP_NAME})")
message(STATUS " Version: ${PROJECT_VERSION}")
message(STATUS " Build type: ${CMAKE_BUILD_TYPE}")
message(STATUS " C++ Standard: ${CMAKE_CXX_STANDARD}")
message(STATUS " ImGui dir: ${IMGUI_DIR}")
message(STATUS " SDL3 found: ${SDL3_FOUND}")
message(STATUS " Sodium lib: ${SODIUM_LIBRARY}")
message(STATUS " Lite build: ${DRAGONX_BUILD_LITE}")
message(STATUS " Lite requested: ${DRAGONX_ENABLE_LITE_BACKEND}")
message(STATUS " Lite backend: ${DRAGONX_LITE_BACKEND_READY}")
message(STATUS " Lite lib: ${DRAGONX_LITE_BACKEND_LIBRARY}")
message(STATUS " Lite symbols: ${DRAGONX_LITE_BACKEND_SYMBOLS_FILE}")
message(STATUS " Lite manifest: ${DRAGONX_LITE_BACKEND_MANIFEST}")
message(STATUS " Lite signature: ${DRAGONX_LITE_BACKEND_REQUIRE_SIGNATURE}")
message(STATUS "")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

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

606
build.sh
View File

@@ -5,7 +5,7 @@
#
# Usage:
# ./build.sh # Dev build (Linux, debug-friendly)
# ./build.sh --linux-release # Linux release (zip + AppImage)
# ./build.sh --linux-release # Linux release + AppImage
# ./build.sh --win-release # Windows cross-compile (mingw-w64)
# ./build.sh --mac-release # macOS .app bundle + DMG
# ./build.sh --linux-release --win-release # Multiple targets
@@ -20,9 +20,7 @@
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# VERSION is resolved per-variant from CMakeLists.txt (the single source of truth) after arg
# parsing — see the APP_BASENAME block below. Placeholder until then.
VERSION=""
VERSION="1.0.0"
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
@@ -43,8 +41,6 @@ DO_DEV=false
DO_LINUX=false
DO_WIN=false
DO_MAC=false
DO_LITE=false
DO_LITE_BACKEND=false
CLEAN=false
BUILD_TYPE="Release"
@@ -55,13 +51,9 @@ DragonX Wallet — Unified Build Script
Usage: $0 [options]
Targets (at least one required, or none for dev build):
--linux-release Linux release (zip + AppImage) -> release/linux/
--linux-release Linux release build + AppImage -> release/linux/
--win-release Windows cross-compile (mingw-w64) -> release/windows/
--mac-release macOS .app bundle + DMG -> release/mac/
--lite Build ObsidianDragonLite variant (no embedded daemon/full-node features)
--lite-backend Like --lite, and link the real SDXL litelib backend artifact
(auto-discovers build/lite-backend/<platform>/; build it with
scripts/build-lite-backend-artifact.sh, or set DRAGONX_LITE_BACKEND_DIR)
Build trees are stored under build/{linux,windows,mac}/
@@ -79,10 +71,9 @@ Cross-compiling from Linux:
Examples:
$0 # Quick dev build (Linux)
$0 --linux-release # Linux release (zip + AppImage)
$0 --linux-release # Linux release + AppImage
$0 --win-release # Windows cross-compile
$0 --mac-release # macOS bundle + DMG (native or osxcross)
$0 --lite-backend --mac-release # macOS ObsidianDragonLite.app + DMG (lite backend)
$0 --clean --linux-release --win-release # Clean + both
EOF
exit 0
@@ -94,8 +85,6 @@ while [[ $# -gt 0 ]]; do
--linux-release) DO_LINUX=true; shift ;;
--win-release) DO_WIN=true; shift ;;
--mac-release) DO_MAC=true; shift ;;
--lite) DO_LITE=true; shift ;;
--lite-backend) DO_LITE=true; DO_LITE_BACKEND=true; shift ;;
-c|--clean) CLEAN=true; shift ;;
-d|--debug) BUILD_TYPE="Debug"; shift ;;
-j) JOBS="$2"; shift 2 ;;
@@ -109,92 +98,6 @@ if ! $DO_LINUX && ! $DO_WIN && ! $DO_MAC; then
DO_DEV=true
fi
APP_BASENAME="ObsidianDragon"
CMAKE_LITE_ARGS=()
# Always set the variant flag EXPLICITLY (ON and OFF) so switching variants in a shared build dir
# can't reuse a stale cached value (e.g. a prior --lite build leaving DRAGONX_BUILD_LITE=ON).
if $DO_LITE; then
APP_BASENAME="ObsidianDragonLite"
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=ON")
# Lite never embeds/launches a daemon; set it explicitly too for cache hygiene.
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=OFF")
info "Lite mode enabled: building ${APP_BASENAME}"
else
CMAKE_LITE_ARGS+=("-DDRAGONX_BUILD_LITE=OFF")
# Re-assert the embedded daemon ON for full-node builds, EXPLICITLY, so a build dir whose cache
# was poisoned OFF by a prior --lite configure (or any stale value) is healed — otherwise the
# full-node app extracts dragonxd but never launches it (isUsingEmbeddedDaemon() == false).
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_EMBEDDED_DAEMON=ON")
fi
# Resolve the release version string for the active variant from CMakeLists.txt (single source of
# truth): the full-node app uses project() VERSION + DRAGONX_VERSION_SUFFIX; ObsidianDragonLite uses
# DRAGONX_LITE_VERSION + DRAGONX_LITE_VERSION_SUFFIX.
_cml="$SCRIPT_DIR/CMakeLists.txt"
_full_ver=$(sed -n 's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' "$_cml" | head -1)
_full_suffix=$(sed -n 's/^set(DRAGONX_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
_lite_ver=$(sed -n 's/^set(DRAGONX_LITE_VERSION[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
_lite_suffix=$(sed -n 's/^set(DRAGONX_LITE_VERSION_SUFFIX[[:space:]]*"\([^"]*\)").*/\1/p' "$_cml" | head -1)
if $DO_LITE; then
VERSION="${_lite_ver}${_lite_suffix}"
else
VERSION="${_full_ver}${_full_suffix}"
fi
[ -n "$_full_ver" ] && [ -n "$VERSION" ] || { err "Could not parse version from CMakeLists.txt"; exit 1; }
info "Release version: ${VERSION} (${APP_BASENAME})"
# ── Lite backend (real SDXL litelib) linking ─────────────────────────────────
# Enables DRAGONX_ENABLE_LITE_BACKEND with an imported artifact produced by
# scripts/build-lite-backend-artifact.sh. Auto-discovers build/lite-backend/<platform>/;
# override the directory with DRAGONX_LITE_BACKEND_DIR.
if $DO_LITE_BACKEND; then
# Artifact platform follows the cross target when exactly one non-host release is requested,
# so `--lite-backend --win-release` links the Windows backend (not the host's) automatically.
case "$(uname -s)" in
Linux) lb_platform="linux" ;;
Darwin) lb_platform="macos" ;;
*) lb_platform="linux" ;;
esac
if $DO_WIN && ! $DO_LINUX && ! $DO_MAC; then lb_platform="windows"; fi
if $DO_MAC && ! $DO_LINUX && ! $DO_WIN; then lb_platform="macos"; fi
lb_dir="${DRAGONX_LITE_BACKEND_DIR:-$SCRIPT_DIR/build/lite-backend/$lb_platform}"
lb_lib=""
for cand in "$lb_dir"/libsilentdragonxlite.a "$lb_dir"/libsilentdragonxlite.so "$lb_dir"/silentdragonxlite.lib; do
[[ -f "$cand" ]] && { lb_lib="$cand"; break; }
done
lb_symbols="$lb_dir/lite-backend-symbols.txt"
lb_manifest="$lb_dir/lite-backend-artifact-manifest.json"
if [[ -z "$lb_lib" || ! -f "$lb_symbols" ]]; then
err "Lite backend artifact not found under: $lb_dir"
err "Build it first: ./scripts/build-lite-backend-artifact.sh --platform $lb_platform"
err "Or set DRAGONX_LITE_BACKEND_DIR to an existing artifact directory."
exit 1
fi
CMAKE_LITE_ARGS+=(
"-DDRAGONX_ENABLE_LITE_BACKEND=ON"
"-DDRAGONX_LITE_BACKEND_LIBRARY=$lb_lib"
"-DDRAGONX_LITE_BACKEND_SYMBOLS_FILE=$lb_symbols"
"-DDRAGONX_LITE_BACKEND_LINK_MODE=imported"
"-DDRAGONX_LITE_BACKEND_ABI=sdxl-c-v1"
)
[[ -f "$lb_manifest" ]] && CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_MANIFEST=$lb_manifest")
# A Rust x86_64-pc-windows-gnu staticlib pulls in Win32 system libs (rustls/schannel, ring,
# dirs, std) that the app doesn't already link. The set is rustc's `--print native-static-libs`
# for the backend (winapi_* shims mapped to the real mingw import libs); all exist in mingw-w64.
if [[ "$lb_platform" == "windows" ]]; then
CMAKE_LITE_ARGS+=("-DDRAGONX_LITE_BACKEND_EXTRA_LIBS=advapi32;ws2_32;kernel32;bcrypt;cfgmgr32;credui;crypt32;cryptnet;fwpuclnt;gdi32;msimg32;ncrypt;ntdll;ole32;opengl32;secur32;shell32;synchronization;user32;winspool;userenv")
fi
info "Lite backend enabled ($lb_platform): $lb_lib"
else
# Explicit OFF so a prior --lite-backend configure in a shared build dir can't leave it ON
# (which would then fail the BUILD_LITE=OFF guard in CMake).
CMAKE_LITE_ARGS+=("-DDRAGONX_ENABLE_LITE_BACKEND=OFF")
fi
should_bundle_full_node_assets() {
! $DO_LITE
}
# ── Helper: find resource files ──────────────────────────────────────────────
find_sapling_params() {
local dirs=(
@@ -219,10 +122,10 @@ find_sapling_params() {
find_asmap() {
local paths=(
"$SCRIPT_DIR/external/dragonx/asmap.dat"
"$SCRIPT_DIR/external/dragonx/contrib/asmap/asmap.dat"
"$HOME/dragonx/asmap.dat"
"$HOME/dragonx/contrib/asmap/asmap.dat"
"$SCRIPT_DIR/external/hush3/asmap.dat"
"$SCRIPT_DIR/external/hush3/contrib/asmap/asmap.dat"
"$HOME/hush3/asmap.dat"
"$HOME/hush3/contrib/asmap/asmap.dat"
"$SCRIPT_DIR/../asmap.dat"
"$SCRIPT_DIR/asmap.dat"
"$SCRIPT_DIR/../SilentDragonX/asmap.dat"
@@ -244,37 +147,37 @@ bundle_linux_daemon() {
local dest="$1"
local found=0
local daemon_paths=(
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/dragonxd"
"$SCRIPT_DIR/../dragonxd"
"$SCRIPT_DIR/external/dragonx/src/dragonxd"
"$HOME/dragonx/src/dragonxd"
local launcher_paths=(
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/hush-arrakis-chain"
"$SCRIPT_DIR/../hush-arrakis-chain"
"$SCRIPT_DIR/external/hush3/src/hush-arrakis-chain"
"$HOME/hush3/src/hush-arrakis-chain"
)
for p in "${daemon_paths[@]}"; do
for p in "${launcher_paths[@]}"; do
if [[ -f "$p" ]]; then
cp "$p" "$dest/dragonxd"; chmod +x "$dest/dragonxd"
info " Bundled dragonxd"; found=1; break
cp "$p" "$dest/hush-arrakis-chain"; chmod +x "$dest/hush-arrakis-chain"
info " Bundled hush-arrakis-chain"; found=1; break
fi
done
local cli_paths=(
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/dragonx-cli"
"$SCRIPT_DIR/../dragonx-cli"
"$SCRIPT_DIR/external/dragonx/src/dragonx-cli"
"$HOME/dragonx/src/dragonx-cli"
local hushd_paths=(
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/hushd"
"$SCRIPT_DIR/../hushd"
"$SCRIPT_DIR/external/hush3/src/hushd"
"$HOME/hush3/src/hushd"
)
for p in "${cli_paths[@]}"; do
for p in "${hushd_paths[@]}"; do
if [[ -f "$p" ]]; then
cp "$p" "$dest/dragonx-cli"; chmod +x "$dest/dragonx-cli"
info " Bundled dragonx-cli"; break
cp "$p" "$dest/hushd"; chmod +x "$dest/hushd"
info " Bundled hushd"; break
fi
done
local dragonxd_paths=(
"$SCRIPT_DIR/prebuilt-binaries/dragonxd-linux/dragonxd"
"$SCRIPT_DIR/../dragonxd"
"$SCRIPT_DIR/external/dragonx/src/dragonxd"
"$HOME/dragonx/src/dragonxd"
"$SCRIPT_DIR/external/hush3/src/dragonxd"
"$HOME/hush3/src/dragonxd"
)
for p in "${dragonxd_paths[@]}"; do
if [[ -f "$p" ]]; then
@@ -294,14 +197,7 @@ bundle_linux_daemon() {
# ═══════════════════════════════════════════════════════════════════════════════
build_dev() {
header "Dev Build ($(uname -s) / $BUILD_TYPE)"
# Use platform-appropriate build directory
if [[ "$(uname -s)" == "Darwin" ]]; then
local bd="$SCRIPT_DIR/build/mac"
export MACOSX_DEPLOYMENT_TARGET="11.0"
else
local bd="$SCRIPT_DIR/build/linux"
fi
local bd="$SCRIPT_DIR/build/linux"
if $CLEAN; then
info "Cleaning $bd ..."; rm -rf "$bd"
@@ -312,18 +208,17 @@ build_dev() {
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=ON \
"${CMAKE_LITE_ARGS[@]}"
-DDRAGONX_USE_SYSTEM_SDL3=ON
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/${APP_BASENAME}" ]] || { err "Build failed"; exit 1; }
info "Dev binary: $bd/bin/${APP_BASENAME} ($(du -h "bin/${APP_BASENAME}" | cut -f1))"
[[ -f "bin/ObsidianDragon" ]] || { err "Build failed"; exit 1; }
info "Dev binary: $bd/bin/ObsidianDragon ($(du -h bin/ObsidianDragon | cut -f1))"
}
# ═══════════════════════════════════════════════════════════════════════════════
# RELEASE: LINUX — build + strip + bundle daemon + zip + AppImage
# RELEASE: LINUX — build + strip + bundle daemon + AppImage
# ═══════════════════════════════════════════════════════════════════════════════
build_release_linux() {
header "Release: Linux x86_64"
@@ -340,64 +235,32 @@ build_release_linux() {
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=ON \
"${CMAKE_LITE_ARGS[@]}"
-DDRAGONX_USE_SYSTEM_SDL3=ON
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; }
[[ -f "bin/ObsidianDragon" ]] || { err "Linux build failed"; exit 1; }
info "Stripping ..."
strip "bin/${APP_BASENAME}"
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
strip bin/ObsidianDragon
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
if should_bundle_full_node_assets; then
# ── Bundle daemon ────────────────────────────────────────────────────
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
# ── Bundle Sapling params ────────────────────────────────────────────
SAPLING_SPEND="" SAPLING_OUTPUT=""
find_sapling_params && {
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
info "Bundled Sapling params"
} || warn "Sapling params not found — not bundled"
else
info "Lite mode: skipping daemon and Sapling/asmap bundling"
fi
# ── Bundle daemon ────────────────────────────────────────────────────────
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
# ── Package: release/linux/ ──────────────────────────────────────────────
# Remove only THIS variant's prior artifacts so full-node and lite releases can coexist in the
# same output dir (both ObsidianDragon* and ObsidianDragonLite* end up under release/linux/).
rm -rf "$out"
mkdir -p "$out"
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.AppImage"
local DIST="${APP_BASENAME}-${VERSION}-Linux-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp bin/ObsidianDragon "$out/"
[[ -f bin/hush-arrakis-chain ]] && cp bin/hush-arrakis-chain "$out/"
[[ -f bin/hushd ]] && cp bin/hushd "$out/"
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$out/"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$out/"
cp -r bin/res "$out/" 2>/dev/null || true
cp "bin/${APP_BASENAME}" "$dist_dir/"
if should_bundle_full_node_assets; then
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
fi
# Bundle xmrig for mining support
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
cp -r bin/res "$dist_dir/" 2>/dev/null || true
# ── Zip ──────────────────────────────────────────────────────────────────
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
fi
rm -rf "$dist_dir"
# ── AppImage (single-file) ───────────────────────────────────────────────
# ── AppImage ─────────────────────────────────────────────────────────────
info "Creating AppImage ..."
local APPDIR="$bd/AppDir"
rm -rf "$APPDIR"
@@ -406,29 +269,22 @@ build_release_linux() {
"$APPDIR/usr/share/icons/hicolor/256x256/apps" \
"$APPDIR/usr/share/ObsidianDragon/res"
cp "bin/${APP_BASENAME}" "$APPDIR/usr/bin/"
cp bin/ObsidianDragon "$APPDIR/usr/bin/"
cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true
if should_bundle_full_node_assets; then
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$APPDIR/usr/bin/"
# Daemon data files must be alongside the daemon binary (usr/bin/)
# because dragonxd searches relative to its own directory.
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
fi
# Bundle xmrig for mining support
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
# Daemon inside AppImage
[[ -f bin/hush-arrakis-chain ]] && cp bin/hush-arrakis-chain "$APPDIR/usr/bin/"
[[ -f bin/hushd ]] && cp bin/hushd "$APPDIR/usr/bin/"
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/share/ObsidianDragon/"
# Desktop entry
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<DESK
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<'DESK'
[Desktop Entry]
Type=Application
Name=DragonX Wallet
Comment=DragonX Cryptocurrency Wallet
Exec=${APP_BASENAME}
Exec=ObsidianDragon
Icon=ObsidianDragon
Categories=Finance;Network;
Terminal=false
@@ -459,14 +315,14 @@ SVG
cp "$APPDIR/ObsidianDragon.svg" "$APPDIR/ObsidianDragon.png" 2>/dev/null || true
# AppRun
cat > "$APPDIR/AppRun" <<APPRUN
cat > "$APPDIR/AppRun" <<'APPRUN'
#!/bin/bash
SELF=\$(readlink -f "\$0")
HERE=\${SELF%/*}
export DRAGONX_RES_PATH="\${HERE}/usr/share/ObsidianDragon/res"
export LD_LIBRARY_PATH="\${HERE}/usr/lib:\${LD_LIBRARY_PATH}"
cd "\${HERE}/usr/share/ObsidianDragon"
exec "\${HERE}/usr/bin/${APP_BASENAME}" "\$@"
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
export DRAGONX_RES_PATH="${HERE}/usr/share/ObsidianDragon/res"
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}"
cd "${HERE}/usr/share/ObsidianDragon"
exec "${HERE}/usr/bin/ObsidianDragon" "$@"
APPRUN
chmod +x "$APPDIR/AppRun"
@@ -494,11 +350,32 @@ APPRUN
local ARCH
ARCH=$(uname -m)
local IMG_NAME="ObsidianDragon-${ARCH}.AppImage"
cd "$bd"
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" 2>/dev/null && {
cp "${APP_BASENAME}-${VERSION}-${ARCH}.AppImage" "$out/${APP_BASENAME}-${VERSION}.AppImage"
info "AppImage: $out/${APP_BASENAME}-${VERSION}.AppImage ($(du -h "$out/${APP_BASENAME}-${VERSION}.AppImage" | cut -f1))"
} || warn "AppImage creation failed — binaries zip still in release/linux/"
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "$IMG_NAME" 2>/dev/null && {
cp "$IMG_NAME" "$out/"
# Rename to match Windows convention: ObsidianDragon.AppImage
mv "$out/$IMG_NAME" "$out/ObsidianDragon.AppImage"
info "AppImage: $out/ObsidianDragon.AppImage ($(du -h "$out/ObsidianDragon.AppImage" | cut -f1))"
} || warn "AppImage creation failed (appimagetool issue) — raw binary still in release/linux/"
# Clean up: keep only AppImage + zip in release/linux/
if [[ -f "$out/ObsidianDragon.AppImage" ]]; then
# AppImage succeeded — remove everything except AppImage
find "$out" -maxdepth 1 -type f ! -name 'ObsidianDragon.AppImage' -delete
rm -rf "$out/res" 2>/dev/null
# Create zip matching Windows naming convention
local DIST="ObsidianDragon-Linux-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp "$out/ObsidianDragon.AppImage" "$dist_dir/"
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
fi
rm -rf "$dist_dir"
fi
info "Linux release artifacts: $out/"
ls -lh "$out/"
@@ -537,22 +414,6 @@ build_release_win() {
exit 1
fi
# ── Patch libwinpthread + libpthread to remove VERSIONINFO resources ────
# mingw-w64's libwinpthread.a and libpthread.a each ship a version.o
# with their own VERSIONINFO ("POSIX WinThreads for Windows") that
# collides with ours during .rsrc merge, causing Task Manager to show
# the wrong process description.
local PATCHED_LIB_DIR="$bd/patched-lib"
mkdir -p "$PATCHED_LIB_DIR"
for plib in libwinpthread.a libpthread.a; do
local SYS_LIB="/usr/x86_64-w64-mingw32/lib/$plib"
if [[ -f "$SYS_LIB" ]]; then
cp -f "$SYS_LIB" "$PATCHED_LIB_DIR/$plib"
x86_64-w64-mingw32-ar d "$PATCHED_LIB_DIR/$plib" version.o 2>/dev/null || true
info "Patched $plib (removed version.o VERSIONINFO resource)"
fi
done
# ── Toolchain file ───────────────────────────────────────────────────────
cat > "$bd/mingw-toolchain.cmake" <<TOOLCHAIN
set(CMAKE_SYSTEM_NAME Windows)
@@ -564,7 +425,7 @@ set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_EXE_LINKER_FLAGS "-static -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -L$PATCHED_LIB_DIR -lwinpthread -Wl,--no-whole-archive")
set(CMAKE_EXE_LINKER_FLAGS "-static -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive")
set(CMAKE_CXX_FLAGS "\${CMAKE_CXX_FLAGS} -static")
set(CMAKE_C_FLAGS "\${CMAKE_C_FLAGS} -static")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
@@ -606,48 +467,26 @@ HDR
# ── Daemon binaries ──────────────────────────────────────────────
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
if should_bundle_full_node_assets; then
if [[ -d "$DD" && -f "$DD/dragonxd.exe" ]]; then
info "Embedding daemon binaries ..."
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
local sym=$(echo "$f" | sed 's/[^a-zA-Z0-9]/_/g')
if [[ -f "$DD/$f" ]]; then
cp -f "$DD/$f" "$RES/$f"
info " Staged $f ($(du -h "$DD/$f" | cut -f1))"
echo "INCBIN(${sym}, \"$RES/$f\");" >> "$GEN/embedded_data.h"
else
echo "extern \"C\" { static const uint8_t* g_${sym}_data = nullptr; }" >> "$GEN/embedded_data.h"
echo "static const unsigned int g_${sym}_size = 0;" >> "$GEN/embedded_data.h"
fi
done
else
warn "prebuilt-binaries/dragonxd-win/ not found — wallet-only build"
fi
if [[ -d "$DD" && -f "$DD/hushd.exe" ]]; then
info "Embedding daemon binaries ..."
echo -e "\n#define HAS_EMBEDDED_DAEMON 1\n" >> "$GEN/embedded_data.h"
for f in hushd.exe hush-cli.exe hush-tx.exe dragonxd.bat dragonx-cli.bat; do
local sym=$(echo "$f" | sed 's/[^a-zA-Z0-9]/_/g')
if [[ -f "$DD/$f" ]]; then
cp -f "$DD/$f" "$RES/$f"
info " Staged $f ($(du -h "$DD/$f" | cut -f1))"
echo "INCBIN(${sym}, \"$RES/$f\");" >> "$GEN/embedded_data.h"
else
echo "extern \"C\" { static const uint8_t* g_${sym}_data = nullptr; }" >> "$GEN/embedded_data.h"
echo "static const unsigned int g_${sym}_size = 0;" >> "$GEN/embedded_data.h"
fi
done
else
info "Lite mode: skipping embedded daemon binaries"
warn "prebuilt-binaries/dragonxd-win/ not found — wallet-only build"
fi
# ── xmrig binary (from prebuilt-binaries/xmrig-hac/) ────────────────
local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac"
# The published DRG-XMRig archives ship the binary inside a versioned subdir, not as a flat
# xmrig.exe. Extract it from the matching win-x64 zip if it isn't already staged — otherwise
# the embed below never fires (HAS_EMBEDDED_XMRIG stays undefined) and the wallet ships with
# no miner ("xmrig binary not found" at runtime).
if [[ ! -f "$XMRIG_DIR/xmrig.exe" ]]; then
local _xz; _xz=$(ls "$XMRIG_DIR"/drg-xmrig-*-win-x64.zip 2>/dev/null | head -1)
if [[ -n "$_xz" ]] && command -v unzip >/dev/null 2>&1; then
local _xtmp; _xtmp=$(mktemp -d)
# -j flattens the versioned subdir; check the file (not unzip's exit code, which is
# non-zero if a pattern matches nothing).
unzip -j -o "$_xz" '*xmrig.exe' -d "$_xtmp" >/dev/null 2>&1 || true
if [[ -f "$_xtmp/xmrig.exe" ]]; then
cp -f "$_xtmp/xmrig.exe" "$XMRIG_DIR/xmrig.exe"
info " Extracted xmrig.exe from $(basename "$_xz")"
fi
rm -rf "$_xtmp"
fi
fi
if [[ -f "$XMRIG_DIR/xmrig.exe" ]]; then
cp -f "$XMRIG_DIR/xmrig.exe" "$RES/xmrig.exe"
info " Staged xmrig.exe ($(du -h "$XMRIG_DIR/xmrig.exe" | cut -f1))"
@@ -691,13 +530,9 @@ HDR
echo "};" >> "$GEN/embedded_data.h"
# ── Overlay themes ───────────────────────────────────────────────
# Expand skin files with layout sections from ui.toml before embedding
echo -e "\n// ---- Bundled overlay themes ----" >> "$GEN/embedded_data.h"
local THEME_STAGE_DIR="$bd/_expanded_themes"
mkdir -p "$THEME_STAGE_DIR"
python3 "$SCRIPT_DIR/scripts/expand_themes.py" "$SCRIPT_DIR/res/themes" "$THEME_STAGE_DIR"
local THEME_TABLE="" THEME_COUNT=0
for tf in "$THEME_STAGE_DIR"/*.toml; do
for tf in "$SCRIPT_DIR/res/themes"/*.toml; do
local tbn=$(basename "$tf")
[[ "$tbn" == "ui.toml" ]] && continue
local tsym=$(echo "$tbn" | sed 's/[^a-zA-Z0-9]/_/g')
@@ -730,57 +565,53 @@ HDR
cmake "$SCRIPT_DIR" \
-DCMAKE_TOOLCHAIN_FILE="$bd/mingw-toolchain.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
"${CMAKE_LITE_ARGS[@]}"
-DDRAGONX_USE_SYSTEM_SDL3=OFF
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; }
info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)"
[[ -f "bin/ObsidianDragon.exe" ]] || { err "Windows build failed"; exit 1; }
info "Binary: $(du -h bin/ObsidianDragon.exe | cut -f1)"
# ── Package: release/windows/ ────────────────────────────────────────────
# Remove only THIS variant's prior artifacts so full-node and lite releases coexist here.
rm -rf "$out"
mkdir -p "$out"
rm -rf "$out/${APP_BASENAME}-"* "$out/${APP_BASENAME}.exe"
local DIST="${APP_BASENAME}-${VERSION}-Windows-x64"
local DIST="ObsidianDragon-Windows-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp "bin/${APP_BASENAME}.exe" "$dist_dir/"
cp bin/ObsidianDragon.exe "$dist_dir/"
local DD="$SCRIPT_DIR/prebuilt-binaries/dragonxd-win"
if should_bundle_full_node_assets; then
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
for f in dragonxd.bat dragonx-cli.bat hushd.exe hush-cli.exe hush-tx.exe; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
# Bundle Sapling params + asmap for the zip distribution
# (The single-file exe has these embedded via INCBIN, but the zip
# needs them on disk so the daemon can find them in its work dir.)
for f in sapling-spend.params sapling-output.params asmap.dat; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
else
info "Lite mode: skipping daemon and Sapling/asmap assets in Windows zip"
fi
cat > "$dist_dir/README.txt" <<'README'
DragonX Wallet - Windows Edition
================================
# Bundle xmrig for mining support
local XMRIG_WIN="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig.exe"
[[ -f "$XMRIG_WIN" ]] && { cp "$XMRIG_WIN" "$dist_dir/"; info "Bundled xmrig.exe"; } || warn "xmrig.exe not found — mining unavailable in zip"
SINGLE-FILE DISTRIBUTION
========================
This wallet is a true single-file executable with all resources embedded.
Just run ObsidianDragon.exe — no additional files needed!
cp -r bin/res "$dist_dir/" 2>/dev/null || true
On first run, the wallet will automatically extract:
- Sapling parameters to %APPDATA%\ZcashParams\
- asmap.dat to %APPDATA%\Hush\DRAGONX\
# ── Single-file exe (all resources embedded) ────────────────────────────
cp "bin/${APP_BASENAME}.exe" "$out/${APP_BASENAME}-${VERSION}.exe"
info "Single-file exe: $out/${APP_BASENAME}-${VERSION}.exe ($(du -h "$out/${APP_BASENAME}-${VERSION}.exe" | cut -f1))"
For support: https://git.dragonx.is/dragonx/ObsidianDragon
README
# Copy single-file exe to release dir
cp bin/ObsidianDragon.exe "$out/"
# ── Zip ──────────────────────────────────────────────────────────────────
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
# Clean up: keep .zip + single-file exe, remove loose directory
rm -rf "$dist_dir"
fi
rm -rf "$dist_dir"
info "Windows release artifacts: $out/"
ls -lh "$out/"
@@ -876,9 +707,7 @@ build_release_mac() {
fi
info "macOS cross-compiler: $OSXCROSS_CXX (arch: $MAC_ARCH)"
else
# Native macOS: build universal binary (arm64 + x86_64)
MAC_ARCH="universal"
export MACOSX_DEPLOYMENT_TARGET="11.0"
MAC_ARCH=$(uname -m)
fi
header "Release: macOS ($MAC_ARCH$(${IS_CROSS} && echo ' — cross-compile'))"
@@ -955,67 +784,41 @@ TOOLCHAIN
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"} \
"${CMAKE_LITE_ARGS[@]}"
${COMPILER_RT:+-DOSXCROSS_COMPILER_RT="$COMPILER_RT"}
else
# Build libsodium as universal if needed
local need_sodium=false
if [[ ! -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]] && \
[[ ! -f "$SCRIPT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
need_sodium=true
elif [[ -f "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
# Rebuild if existing lib is not universal (single-arch won't link)
if ! lipo -info "$SCRIPT_DIR/libs/libsodium/lib/libsodium.a" 2>/dev/null | grep -q "arm64.*x86_64\|x86_64.*arm64"; then
info "Existing libsodium is not universal — rebuilding ..."
rm -rf "$SCRIPT_DIR/libs/libsodium"
need_sodium=true
fi
fi
if $need_sodium; then
info "Building libsodium (universal) ..."
"$SCRIPT_DIR/scripts/fetch-libsodium.sh"
fi
info "Configuring (native universal arm64+x86_64) ..."
info "Configuring (native) ..."
cmake "$SCRIPT_DIR" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_FLAGS_RELEASE="-O3 -DNDEBUG" \
-DDRAGONX_USE_SYSTEM_SDL3=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
"${CMAKE_LITE_ARGS[@]}"
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0
fi
info "Building with $JOBS jobs ..."
cmake --build . -j "$JOBS"
[[ -f "bin/${APP_BASENAME}" ]] || { err "macOS build failed"; exit 1; }
[[ -f "bin/ObsidianDragon" ]] || { err "macOS build failed"; exit 1; }
# Strip — use osxcross strip for cross-builds
if $IS_CROSS; then
local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip"
if [[ -x "$STRIP_CMD" ]]; then
info "Stripping (osxcross) ..."
"$STRIP_CMD" "bin/${APP_BASENAME}"
"$STRIP_CMD" bin/ObsidianDragon
else
warn "osxcross strip not found at $STRIP_CMD — skipping"
fi
else
info "Stripping ..."
strip "bin/${APP_BASENAME}"
# Verify universal binary
if command -v lipo &>/dev/null; then
info "Architecture info:"
lipo -info "bin/${APP_BASENAME}"
fi
strip bin/ObsidianDragon
fi
info "Binary: $(du -h "bin/${APP_BASENAME}" | cut -f1)"
info "Binary: $(du -h bin/ObsidianDragon | cut -f1)"
# ── Create .app bundle ───────────────────────────────────────────────────
rm -rf "$out"
mkdir -p "$out"
local APP="$out/${APP_BASENAME}.app"
local APP="$out/ObsidianDragon.app"
local CONTENTS="$APP/Contents"
local MACOS="$CONTENTS/MacOS"
local RESOURCES="$CONTENTS/Resources"
@@ -1024,62 +827,40 @@ TOOLCHAIN
mkdir -p "$MACOS" "$RESOURCES/res" "$FRAMEWORKS"
# Main binary
cp "bin/${APP_BASENAME}" "$MACOS/"
chmod +x "$MACOS/${APP_BASENAME}"
cp bin/ObsidianDragon "$MACOS/"
chmod +x "$MACOS/ObsidianDragon"
# Resources
cp -r bin/res/* "$RESOURCES/res/" 2>/dev/null || true
if should_bundle_full_node_assets; then
# Daemon binaries (macOS native, from dragonxd-mac/)
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
if [[ -d "$daemon_dir" ]]; then
for f in dragonxd dragonx-cli dragonx-tx; do
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; chmod +x "$MACOS/$f"; info " Bundled $f"; }
done
for f in sapling-spend.params sapling-output.params; do
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; info " Bundled $f"; }
done
elif ! $IS_CROSS; then
# Native macOS: try standard paths
local daemon_paths=(
"$SCRIPT_DIR/../dragonxd"
"$HOME/dragonx/src/dragonxd"
)
for p in "${daemon_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonxd"; chmod +x "$MACOS/dragonxd"; info " Bundled dragonxd"; break; }
done
local cli_paths=(
"$SCRIPT_DIR/../dragonx-cli"
"$HOME/dragonx/src/dragonx-cli"
)
for p in "${cli_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/dragonx-cli"; chmod +x "$MACOS/dragonx-cli"; info " Bundled dragonx-cli"; break; }
done
else
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
fi
# Daemon binaries (macOS native, from dragonxd-mac/)
local daemon_dir="$SCRIPT_DIR/prebuilt-binaries/dragonxd-mac"
if [[ -d "$daemon_dir" ]]; then
for f in hush-arrakis-chain hushd hush-cli hush-tx dragonxd; do
[[ -f "$daemon_dir/$f" ]] && { cp "$daemon_dir/$f" "$MACOS/"; chmod +x "$MACOS/$f"; info " Bundled $f"; }
done
elif ! $IS_CROSS; then
# Native macOS: try standard paths
local launcher_paths=(
"$SCRIPT_DIR/../hush-arrakis-chain"
"$HOME/hush3/src/hush-arrakis-chain"
)
for p in "${launcher_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/hush-arrakis-chain"; chmod +x "$MACOS/hush-arrakis-chain"; info " Bundled hush-arrakis-chain"; break; }
done
local hushd_paths=(
"$SCRIPT_DIR/../hushd"
"$HOME/hush3/src/hushd"
)
for p in "${hushd_paths[@]}"; do
[[ -f "$p" ]] && { cp "$p" "$MACOS/hushd"; chmod +x "$MACOS/hushd"; info " Bundled hushd"; break; }
done
else
info "Lite mode: skipping macOS daemon and Sapling/asmap bundling"
warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling"
fi
# xmrig binary (from prebuilt-binaries/xmrig-hac/)
local XMRIG_MAC="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
if [[ -f "$XMRIG_MAC" ]]; then
cp "$XMRIG_MAC" "$MACOS/xmrig"
chmod +x "$MACOS/xmrig"
info " Bundled xmrig"
else
warn "xmrig not found — mining unavailable in .app"
fi
if should_bundle_full_node_assets; then
# asmap.dat — placed in MacOS/ so the daemon finds it next to its binary
find_asmap 2>/dev/null && {
cp "$ASMAP_DAT" "$MACOS/asmap.dat"
info " Bundled asmap.dat"
}
fi
# asmap.dat
find_asmap 2>/dev/null && cp "$ASMAP_DAT" "$RESOURCES/asmap.dat" && info " Bundled asmap.dat"
# Bundle SDL3 dylib
local sdl_dylib=""
@@ -1099,34 +880,26 @@ TOOLCHAIN
# Fix the rpath so the binary finds SDL3 in Frameworks/
if $IS_CROSS; then
local INSTALL_NAME_TOOL="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-install_name_tool"
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
[[ -x "$INSTALL_NAME_TOOL" ]] && "$INSTALL_NAME_TOOL" -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/ObsidianDragon" 2>/dev/null || true
else
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/${APP_BASENAME}" 2>/dev/null || true
install_name_tool -change "@rpath/$sdl_name" "@executable_path/../Frameworks/$sdl_name" "$MACOS/ObsidianDragon" 2>/dev/null || true
fi
info " Bundled $sdl_name"
fi
# Launcher script (ensures working dir + dylib path). Uses ${APP_BASENAME} so the lite
# variant (ObsidianDragonLite) gets a correctly-named launcher + .bin pair.
mv "$MACOS/${APP_BASENAME}" "$MACOS/${APP_BASENAME}.bin"
cat > "$MACOS/${APP_BASENAME}" <<LAUNCH
# Launcher script (ensures working dir + dylib path)
mv "$MACOS/ObsidianDragon" "$MACOS/ObsidianDragon.bin"
cat > "$MACOS/ObsidianDragon" <<'LAUNCH'
#!/bin/bash
DIR="\$(cd "\$(dirname "\$0")" && pwd)"
export DYLD_LIBRARY_PATH="\$DIR/../Frameworks:\$DYLD_LIBRARY_PATH"
export DRAGONX_RES_PATH="\$DIR/../Resources/res"
cd "\$DIR/../Resources"
exec "\$DIR/${APP_BASENAME}.bin" "\$@"
DIR="$(cd "$(dirname "$0")" && pwd)"
export DYLD_LIBRARY_PATH="$DIR/../Frameworks:$DYLD_LIBRARY_PATH"
export DRAGONX_RES_PATH="$DIR/../Resources/res"
cd "$DIR/../Resources"
exec "$DIR/ObsidianDragon.bin" "$@"
LAUNCH
chmod +x "$MACOS/${APP_BASENAME}"
chmod +x "$MACOS/ObsidianDragon"
# Info.plist — display name + bundle id differ per variant so lite and full-node .apps
# can coexist; the executable matches the launcher (${APP_BASENAME}); the icon is shared.
local APP_DISPLAY_NAME="DragonX Wallet"
local APP_BUNDLE_ID="is.hush.dragonx"
if $DO_LITE; then
APP_DISPLAY_NAME="DragonX Wallet Lite"
APP_BUNDLE_ID="is.hush.dragonx.lite"
fi
# Info.plist
cat > "$CONTENTS/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
@@ -1134,17 +907,17 @@ LAUNCH
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>${APP_DISPLAY_NAME}</string>
<string>DragonX Wallet</string>
<key>CFBundleDisplayName</key>
<string>${APP_DISPLAY_NAME}</string>
<string>DragonX Wallet</string>
<key>CFBundleIdentifier</key>
<string>${APP_BUNDLE_ID}</string>
<string>is.hush.dragonx</string>
<key>CFBundleVersion</key>
<string>${VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION}</string>
<key>CFBundleExecutable</key>
<string>${APP_BASENAME}</string>
<string>ObsidianDragon</string>
<key>CFBundleIconFile</key>
<string>ObsidianDragon</string>
<key>CFBundlePackageType</key>
@@ -1205,28 +978,19 @@ PLIST
info ".app bundle created: $APP"
# ── Zip the .app bundle ──────────────────────────────────────────────────
local APP_ZIP="${APP_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.app.zip"
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$APP_ZIP" "${APP_BASENAME}.app")
info "App zip: $out/$APP_ZIP ($(du -h "$out/$APP_ZIP" | cut -f1))"
fi
# ── Create DMG ───────────────────────────────────────────────────────────
local DMG_BASENAME="DragonX_Wallet"
$DO_LITE && DMG_BASENAME="DragonX_Wallet_Lite"
local DMG_NAME="${DMG_BASENAME}-${VERSION}-macOS-${MAC_ARCH}.dmg"
local DMG_NAME="DragonX_Wallet-${VERSION}-macOS-${MAC_ARCH}.dmg"
if command -v create-dmg &>/dev/null; then
# create-dmg (works on macOS; also available on Linux via npm)
info "Creating DMG with create-dmg ..."
create-dmg \
--volname "${APP_DISPLAY_NAME}" \
--volname "DragonX Wallet" \
--volicon "$RESOURCES/ObsidianDragon.icns" \
--window-pos 200 120 \
--window-size 600 400 \
--icon-size 100 \
--icon "${APP_BASENAME}.app" 150 190 \
--icon "ObsidianDragon.app" 150 190 \
--app-drop-link 450 190 \
--no-internet-enable \
"$out/$DMG_NAME" \
@@ -1241,7 +1005,7 @@ PLIST
mkdir -p "$staging"
cp -a "$APP" "$staging/"
ln -s /Applications "$staging/Applications"
hdiutil create -volname "${APP_DISPLAY_NAME}" \
hdiutil create -volname "DragonX Wallet" \
-srcfolder "$staging" \
-ov -format UDZO \
"$out/$DMG_NAME" 2>/dev/null && {
@@ -1257,7 +1021,7 @@ PLIST
cp -a "$APP" "$staging/"
# Can't create a real symlink to /Applications in an ISO, but the .app
# is the important part — users drag it to Applications manually.
genisoimage -V "${APP_DISPLAY_NAME}" \
genisoimage -V "DragonX Wallet" \
-D -R -apple -no-pad \
-o "$out/$DMG_NAME" \
"$staging" 2>/dev/null && {

View File

@@ -40,7 +40,7 @@ extern "C" {
* read-only segment just like a normal const array would.
*/
#if defined(__APPLE__)
# define INCBIN_SECTION "__DATA,__const"
# define INCBIN_SECTION ".const_data"
# define INCBIN_MANGLE "_"
#else
# define INCBIN_SECTION ".rodata"

View File

View File

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

View File

@@ -4,13 +4,6 @@
// Use numeric ordinal 1 so LoadIcon(hInst, MAKEINTRESOURCE(1)) finds it.
1 ICON "@OBSIDIAN_ICO_PATH@"
// ---------------------------------------------------------------------------
// Application Manifest — declares DPI awareness, common controls v6,
// UTF-8 code page, and application identity. Without this, Windows may
// fall back to legacy process grouping in Task Manager.
// ---------------------------------------------------------------------------
1 24 "@OBSIDIAN_MANIFEST_PATH@"
// ---------------------------------------------------------------------------
// VERSIONINFO — sets the description shown in Task Manager, Explorer
// "Details" tab, and other Windows tools. Without this, MinGW-w64
@@ -19,8 +12,8 @@
#include <winver.h>
VS_VERSION_INFO VERSIONINFO
FILEVERSION @DRAGONX_APP_VERSION_MAJOR@,@DRAGONX_APP_VERSION_MINOR@,@DRAGONX_APP_VERSION_PATCH@,0
PRODUCTVERSION @DRAGONX_APP_VERSION_MAJOR@,@DRAGONX_APP_VERSION_MINOR@,@DRAGONX_APP_VERSION_PATCH@,0
FILEVERSION @DRAGONX_VER_MAJOR@,@DRAGONX_VER_MINOR@,@DRAGONX_VER_PATCH@,0
PRODUCTVERSION @DRAGONX_VER_MAJOR@,@DRAGONX_VER_MINOR@,@DRAGONX_VER_PATCH@,0
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS 0x0L
FILEOS VOS_NT_WINDOWS32
@@ -31,14 +24,14 @@ BEGIN
BEGIN
BLOCK "040904B0" // US-English, Unicode
BEGIN
VALUE "CompanyName", "DragonX Developers\0"
VALUE "FileDescription", "@DRAGONX_APP_NAME@ Wallet\0"
VALUE "FileVersion", "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@\0"
VALUE "InternalName", "@DRAGONX_APP_NAME@\0"
VALUE "LegalCopyright", "Copyright 2024-2026 DragonX Developers. GPLv3.\0"
VALUE "OriginalFilename", "@DRAGONX_APP_NAME@.exe\0"
VALUE "ProductName", "@DRAGONX_APP_NAME@\0"
VALUE "ProductVersion", "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@\0"
VALUE "CompanyName", "The Hush Developers\0"
VALUE "FileDescription", "ObsidianDragon Wallet\0"
VALUE "FileVersion", "@DRAGONX_VERSION@\0"
VALUE "InternalName", "ObsidianDragon\0"
VALUE "LegalCopyright", "Copyright 2024-2026 The Hush Developers. GPLv3.\0"
VALUE "OriginalFilename", "ObsidianDragon.exe\0"
VALUE "ProductName", "ObsidianDragon\0"
VALUE "ProductVersion", "@DRAGONX_VERSION@\0"
END
END
BLOCK "VarFileInfo"

View File

@@ -1,10 +0,0 @@
# Default Ban List — DragonX Wallet
# IPs listed here are banned automatically when the wallet connects.
# One IP or subnet per line. Comments start with #. Blank lines are ignored.
#
# Examples:
# 192.168.1.100
# 10.0.0.0/8
# 203.0.113.42
#
# Rebuild the wallet after editing this file.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,8 +37,8 @@ images = { background_image = "backgrounds/texture/drgx_bg.png", logo = "logos/l
--on-secondary = "#000000"
--on-background = "#F0E0D8"
--on-surface = "#F0E0D8"
--on-surface-medium = "rgba(240,224,216,0.85)"
--on-surface-disabled = "rgba(240,224,216,0.58)"
--on-surface-medium = "rgba(240,224,216,0.7)"
--on-surface-disabled = "rgba(240,224,216,0.44)"
--error = "#FF5252"
--on-error = "#000000"
--success = "#81C784"
@@ -485,12 +485,12 @@ suggestion-trunc-len = { size = 50 }
fee-rounding = { size = 10.0 }
amount-bar-max-btn-width = { size = 80.0 }
amount-bar-height = { size = 22.0 }
confirm-popup-max-width = { size = 560.0 }
confirm-popup-max-width = { size = 420.0 }
confirm-addr-card-height = { size = 28.0 }
confirm-amount-card-height = { size = 96.0 }
confirm-row-step = { size = 24.0 }
confirm-val-col-x = { size = 150.0 }
confirm-usd-col-x = { size = 116.0 }
confirm-amount-card-height = { size = 70.0 }
confirm-row-step = { size = 16.0 }
confirm-val-col-x = { size = 90.0 }
confirm-usd-col-x = { size = 80.0 }
progress-card-height = { size = 36.0 }
progress-card-height-txid = { size = 52.0 }
progress-card-pad-x = { size = 12.0 }
@@ -516,10 +516,10 @@ error-icon-inset = { size = 20.0 }
error-btn-rounding = { size = 4.0 }
progress-glass-rounding-ratio = { size = 0.75 }
confirm-addr-card-min-height = { size = 24.0 }
confirm-val-col-min-x = { size = 112.0 }
confirm-usd-col-min-x = { size = 92.0 }
confirm-amount-card-min-height = { size = 82.0 }
confirm-row-step-min = { size = 20.0 }
confirm-val-col-min-x = { size = 70.0 }
confirm-usd-col-min-x = { size = 60.0 }
confirm-amount-card-min-height = { size = 54.0 }
confirm-row-step-min = { size = 12.0 }
action-btn-min-height = { size = 26.0 }
recent-icon-min-size = { size = 3.5 }
recent-icon-size = { size = 5.0 }
@@ -566,7 +566,7 @@ txid-trunc-len = { size = 14 }
txid-label-x-offset = { size = 20.0 }
txid-copy-btn-right-offset = { size = 50.0 }
txid-copy-btn-y-offset = { size = 2.0 }
confirm-popup-width-ratio = { size = 0.92 }
confirm-popup-width-ratio = { size = 0.85 }
confirm-glass-rounding-ratio = { size = 0.75 }
confirm-addr-trunc-len = { size = 48 }
confirm-divider-thickness = { size = 1.0 }
@@ -766,7 +766,7 @@ control-card-min-height = { size = 60.0 }
active-cell-border-thickness = { size = 1.5 }
cell-border-thickness = { size = 1.0 }
button-icon-y-ratio = { size = 0.42 }
button-label-y-ratio = { size = 0.72 }
button-label-y-ratio = { size = 0.78 }
chart-line-thickness = { size = 1.5 }
details-card-min-height = { size = 50.0 }
ram-bar = { height = 6.0, rounding = 3.0, opacity = 0.65 }
@@ -779,11 +779,8 @@ pool-url-input = { width = 300.0, height = 28.0 }
pool-worker-input = { width = 200.0, height = 28.0 }
log-panel-height = { size = 120.0 }
log-panel-min = { size = 60.0 }
idle-row-height = { size = 28.0 }
idle-combo-width = { size = 64.0 }
[tabs.peers]
refresh-button = { size = 110.0 }
table-min-height = 150.0
table-height-ratio = 0.45
version-column-width = 150.0
@@ -810,7 +807,6 @@ dir-pill-padding = { size = 4.0 }
dir-pill-y-offset = { size = 1.0 }
dir-pill-y-bottom = { size = 3.0 }
dir-pill-rounding = { size = 3.0 }
seed-badge-padding = { size = 3.0 }
tls-badge-min-width = { size = 20.0 }
tls-badge-width = { size = 28.0 }
tls-badge-rounding = { size = 3.0 }
@@ -874,7 +870,7 @@ accent-stripe-inset-ratio = { size = 0.0 }
accent-stripe-left-offset = { size = 0.0 }
accent-stripe-width = { size = 4.0 }
accent-stripe-rounding = { size = 1.5 }
chart-y-axis-min-padding = { size = 54.0 }
chart-y-axis-min-padding = { size = 40.0 }
chart-y-axis-padding = { size = 70.0 }
chart-dot-min-radius = { size = 1.5 }
chart-dot-radius = { size = 2.0 }
@@ -894,21 +890,8 @@ pair-bar-arrow-size = { size = 28.0 }
exchange-combo-width = { size = 180.0 }
exchange-top-gap = { size = 0.0 }
[tabs.explorer]
search-input-width = { size = 400.0 }
search-button-width = { size = 100.0 }
search-bar-height = { size = 32.0 }
row-height = { size = 32.0 }
row-rounding = { size = 4.0 }
tx-row-height = { size = 28.0 }
label-column = { size = 160.0 }
detail-modal-width = { size = 700.0 }
detail-max-height = { size = 600.0 }
scroll-fade-zone = { size = 24.0 }
[tabs.console]
input-area-padding = 8.0
bg-darken-alpha = { size = 110.0 } # black overlay alpha (0-255) for the terminal-dark output + input
output-line-spacing = 2.0
output = { line-spacing = 2 }
scroll-multiplier = { size = 3.0 }
@@ -931,7 +914,7 @@ scanline-speed = { size = 40.0 }
scanline-height = { size = 36.0 }
scanline-alpha = { size = 8.0 }
scanline-gap = { size = 2.0 }
scanline-line-alpha = { size = 2.0 }
scanline-line-alpha = { size = 4.0 }
scanline-glow-spread = { size = 4.0 }
scanline-glow-intensity = { size = 0.6 }
scanline-glow-color = { size = 255.0 }
@@ -1169,8 +1152,6 @@ key-input = { height = 150 }
rescan-height-input = { width = 100 }
import-button = { width = 120, font = "button" }
close-button = { width = 100, font = "button" }
paste-preview-alpha = { size = 0.3 }
paste-preview-max-chars = { size = 200 }
[components]
@@ -1219,9 +1200,6 @@ notification-progress = { color = "var(--primary)", height = 4, position = 18 }
fill-alpha = { size = 12.0 }
noise-alpha = { size = 14.0 }
[components.overlay-dialog]
confirm-btn-height = { size = 40.0 }
[components.qr-code]
module-scale = { size = 4 }
border-modules = { size = 2 }
@@ -1234,10 +1212,9 @@ width = { size = 140.0 }
collapsed-width = { size = 64.0 }
collapse-anim-speed = { size = 10.0 }
auto-collapse-threshold = { size = 800.0 }
section-gap = { size = 8.0 }
section-gap = { size = 4.0 }
section-label-pad-left = { size = 16.0 }
section-label-pad-bottom = { size = 4.0 }
item-height = { size = 36.0 }
item-height = { size = 42.0 }
item-pad-x = { size = 8.0 }
min-height = { size = 360.0 }
margin-top = { size = -12 }
@@ -1253,8 +1230,8 @@ icon-half-size = { size = 7.0 }
icon-label-gap = { size = 8.0 }
badge-radius-dot = { size = 4.0 }
badge-radius-number = { size = 8.0 }
button-spacing = { size = 6.0 }
bottom-padding = { size = 4.0 }
button-spacing = { size = 4.0 }
bottom-padding = { size = 0.0 }
exit-icon-gap = { size = 4.0 }
cutout-shadow-alpha = { size = 55 }
cutout-highlight-alpha = { size = 8 }
@@ -1270,8 +1247,8 @@ inset-shadow-fade-ratio = { size = 5.0 }
[components.content-area]
padding-x = 0.0
padding-y = 0.0
margin-top = { size = 6.0 }
margin-bottom = { size = -39.0 }
margin-top = { size = 4.0 }
margin-bottom = { size = -40.0 }
edge-fade-zone = { size = 0.0 }
[components.content-area.window]
@@ -1283,10 +1260,10 @@ page-fade-speed = { size = 8.0 }
collapse-hysteresis = { size = 60.0 }
header-icon = { icon-dark = "logos/logo_ObsidianDragon_dark.png", icon-light = "logos/logo_ObsidianDragon_light.png" }
coin-icon = { icon = "logos/logo_dragonx_128.png" }
header-title = { font = "subtitle1", size = 12.0, pad-x = 8.0, pad-y = 10.0, logo-gap = 4.0, opacity = 0.7, offset-y = -2.0 }
header-title = { font = "subtitle1", size = 14.0, pad-x = 16.0, pad-y = 12.0, logo-gap = 8.0, opacity = 0.7 }
[components.main-window.window]
padding = [12, 36]
padding = [12, 38]
[components.shutdown]
content-height = { height = 120.0 }
@@ -1328,13 +1305,8 @@ wallet-btn-padding = { size = 24.0 }
rpc-label-min-width = { size = 70.0 }
rpc-label-width = { size = 85.0 }
security-combo-width = { size = 120.0 }
node-grid-breakpoint = { size = 900.0 }
port-input-min-width = { size = 60.0 }
port-input-width-ratio = { size = 0.4 }
idle-combo-width = { size = 64.0 }
# Reserved height basis for the About-card logo; the logo is drawn scaled to the
# card's actual height (aspect-preserved) and capped to this * aspect in width.
about-logo-size = { size = 150.0 }
[components.main-layout]
app-bar-height = { size = 64.0 }
@@ -1385,17 +1357,6 @@ summary = { min-width = 280.0, max-width = 400.0, width-ratio = 0.32, min-height
side-panel = { min-width = 280.0, max-width = 450.0, width-ratio = 0.4, min-height = 200.0, max-height = 350.0, height-ratio = 0.8 }
table = { min-height = 150.0, height-ratio = 0.45, min-remaining = 100.0, default-reserve = 30.0 }
[dialog]
width-default = 480.0
width-lg = 600.0
width-xl = 660.0
min-width = 280.0
form-width = 400.0
action-width = 100.0
action-gap = 8.0
max-height-ratio = 0.94
compact-bottom-ratio = 0.64
[button]
min-width = 180.0
width = 140.0
@@ -1528,6 +1489,7 @@ title = { font = "h5" }
input = { width = 320.0, height = 40.0 }
unlock-button = { width = 320.0, height = 44.0, font = "subtitle1" }
error-text = { font = "caption" }
backdrop-alpha = { opacity = 0.0 }
mode-toggle = { font = "caption" }
# ---------------------------------------------------------------------------

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,122 +0,0 @@
#!/usr/bin/env python3
"""
Build-time theme expander — merges layout sections from ui.toml into skin files.
Called by CMake during build. Reads source skin files + ui.toml, writes merged
output files to the build directory. Source files are never modified.
Usage:
python3 expand_themes.py <source_themes_dir> <output_themes_dir>
For each .toml file in source_themes_dir (except ui.toml), the script:
1. Copies the skin file contents (theme/palette/backdrop/effects)
2. Appends all layout sections from ui.toml (fonts, tabs, components, etc.)
3. Writes the merged result to output_themes_dir/<filename>
ui.toml itself is copied unchanged.
"""
import re
import sys
import os
# Sections to SKIP when extracting from ui.toml (theme-specific, already in skins)
SKIP_SECTIONS = {"theme", "theme.palette", "backdrop", "effects"}
def extract_layout_sections(ui_toml_path):
"""Extract non-theme sections from ui.toml as a string."""
sections = []
current_section = None
current_lines = []
section_re = re.compile(r'^\[{1,2}([^\]]+)\]{1,2}\s*$')
with open(ui_toml_path, 'r') as f:
for line in f:
m = section_re.match(line.strip())
if m:
if current_section is not None or current_lines:
sections.append((current_section, current_lines))
current_section = m.group(1).strip()
current_lines = [line]
else:
current_lines.append(line)
if current_section is not None or current_lines:
sections.append((current_section, current_lines))
layout_parts = []
for section_name, lines in sections:
if section_name is None:
# Preamble: only include top-level key=value lines
kv_lines = [l for l in lines
if l.strip() and not l.strip().startswith('#') and '=' in l]
if kv_lines:
layout_parts.append(''.join(kv_lines))
continue
if section_name in SKIP_SECTIONS:
continue
layout_parts.append(''.join(lines))
return '\n'.join(layout_parts)
def main():
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <source_themes_dir> <output_themes_dir>")
sys.exit(1)
src_dir = sys.argv[1]
out_dir = sys.argv[2]
ui_toml = os.path.join(src_dir, "ui.toml")
if not os.path.exists(ui_toml):
print(f"ERROR: ui.toml not found at {ui_toml}")
sys.exit(1)
os.makedirs(out_dir, exist_ok=True)
layout_content = extract_layout_sections(ui_toml)
separator = (
"\n# ===========================================================================\n"
"# Layout & Component Properties\n"
"# All values below can be customized per-theme. Edit and save to see\n"
"# changes reflected in the app in real time via hot-reload.\n"
"# ===========================================================================\n\n"
)
for fname in sorted(os.listdir(src_dir)):
if not fname.endswith('.toml'):
continue
src_path = os.path.join(src_dir, fname)
dst_path = os.path.join(out_dir, fname)
if fname == "ui.toml":
# Copy ui.toml unchanged
with open(src_path, 'r') as f:
content = f.read()
with open(dst_path, 'w') as f:
f.write(content)
print(f" COPY {fname}")
continue
# Skin file — append layout sections
with open(src_path, 'r') as f:
skin = f.read()
if not skin.endswith('\n'):
skin += '\n'
merged = skin + separator + layout_content
if not merged.endswith('\n'):
merged += '\n'
with open(dst_path, 'w') as f:
f.write(merged)
lines = merged.count('\n')
print(f" MERGE {fname}{lines} lines")
print("Done.")
if __name__ == "__main__":
main()

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="${SCRIPT_DIR}/build/linux"
VERSION="1.2.0"
VERSION="1.0.0"
# Colors for output
RED='\033[0;31m'
@@ -137,7 +137,7 @@ if [ -f "bin/ObsidianDragon" ]; then
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/hush-arrakis-chain"
"${SCRIPT_DIR}/../hush-arrakis-chain"
"${SCRIPT_DIR}/hush-arrakis-chain"
"$HOME/dragonx/src/hush-arrakis-chain"
"$HOME/hush3/src/hush-arrakis-chain"
)
for lpath in "${LAUNCHER_PATHS[@]}"; do
@@ -155,7 +155,7 @@ if [ -f "bin/ObsidianDragon" ]; then
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/hushd"
"${SCRIPT_DIR}/../hushd"
"${SCRIPT_DIR}/hushd"
"$HOME/dragonx/src/hushd"
"$HOME/hush3/src/hushd"
)
for hpath in "${HUSHD_PATHS[@]}"; do
@@ -172,7 +172,7 @@ if [ -f "bin/ObsidianDragon" ]; then
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/dragonxd"
"${SCRIPT_DIR}/../dragonxd"
"${SCRIPT_DIR}/dragonxd"
"$HOME/dragonx/src/dragonxd"
"$HOME/hush3/src/dragonxd"
)
for dpath in "${DRAGONXD_PATHS[@]}"; do
@@ -189,8 +189,8 @@ if [ -f "bin/ObsidianDragon" ]; then
"${SCRIPT_DIR}/prebuilt-binaries/dragonxd-linux/asmap.dat"
"${SCRIPT_DIR}/../asmap.dat"
"${SCRIPT_DIR}/asmap.dat"
"$HOME/dragonx/asmap.dat"
"$HOME/dragonx/contrib/asmap/asmap.dat"
"$HOME/hush3/asmap.dat"
"$HOME/hush3/contrib/asmap/asmap.dat"
)
for apath in "${ASMAP_PATHS[@]}"; do

View File

@@ -130,8 +130,8 @@ done
# Look for asmap.dat
ASMAP_PATHS=(
"$HOME/dragonx/asmap.dat"
"$HOME/dragonx/contrib/asmap/asmap.dat"
"$HOME/hush3/asmap.dat"
"$HOME/hush3/contrib/asmap/asmap.dat"
"$SCRIPT_DIR/../asmap.dat"
"$SCRIPT_DIR/asmap.dat"
"$SCRIPT_DIR/../SilentDragonX/asmap.dat"

414
scripts/setup.sh Executable file
View File

@@ -0,0 +1,414 @@
#!/usr/bin/env bash
# ── scripts/setup.sh ────────────────────────────────────────────────────────
# DragonX Wallet — Development Environment Setup
# Copyright 2024-2026 The Hush Developers
# Released under the GPLv3
#
# Detects your OS/distro, installs build prerequisites, fetches libraries,
# and validates the environment so you can build immediately with:
#
# ./build.sh # dev build
# ./build.sh --win-release # Windows cross-compile
# ./build.sh --linux-release # Linux release + AppImage
#
# Usage:
# ./scripts/setup.sh # Interactive — install everything needed
# ./scripts/setup.sh --check # Just report what's missing, don't install
# ./scripts/setup.sh --all # Install dev + all cross-compile targets
# ./scripts/setup.sh --win # Also install Windows cross-compile deps
# ./scripts/setup.sh --mac # Also install macOS cross-compile deps
# ./scripts/setup.sh --sapling # Also download Sapling params (~51 MB)
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
ok() { echo -e " ${GREEN}${NC} $1"; }
miss() { echo -e " ${RED}${NC} $1"; }
skip() { echo -e " ${YELLOW}${NC} $1"; }
info() { echo -e "${GREEN}[*]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
err() { echo -e "${RED}[ERROR]${NC} $1"; }
header(){ echo -e "\n${CYAN}── $1 ──${NC}"; }
# ── Parse args ───────────────────────────────────────────────────────────────
CHECK_ONLY=false
SETUP_WIN=false
SETUP_MAC=false
SETUP_SAPLING=false
while [[ $# -gt 0 ]]; do
case $1 in
--check) CHECK_ONLY=true; shift ;;
--win) SETUP_WIN=true; shift ;;
--mac) SETUP_MAC=true; shift ;;
--sapling) SETUP_SAPLING=true; shift ;;
--all) SETUP_WIN=true; SETUP_MAC=true; SETUP_SAPLING=true; shift ;;
-h|--help)
sed -n '2,/^# ─\{10\}/{ /^# ─\{10\}/d; s/^# \?//p; }' "$0"
exit 0
;;
*) err "Unknown option: $1"; exit 1 ;;
esac
done
# ── Detect OS / distro ──────────────────────────────────────────────────────
detect_os() {
OS="$(uname -s)"
DISTRO="unknown"
PKG=""
case "$OS" in
Linux)
if [[ -f /etc/os-release ]]; then
. /etc/os-release
case "${ID:-}" in
ubuntu|debian|linuxmint|pop|elementary|zorin|neon)
DISTRO="debian"; PKG="apt" ;;
fedora|rhel|centos|rocky|alma)
DISTRO="fedora"; PKG="dnf" ;;
arch|manjaro|endeavouros|garuda)
DISTRO="arch"; PKG="pacman" ;;
opensuse*|suse*)
DISTRO="suse"; PKG="zypper" ;;
void)
DISTRO="void"; PKG="xbps" ;;
gentoo)
DISTRO="gentoo"; PKG="emerge" ;;
*)
# Fallback: check for package managers
command -v apt &>/dev/null && { DISTRO="debian"; PKG="apt"; } ||
command -v dnf &>/dev/null && { DISTRO="fedora"; PKG="dnf"; } ||
command -v pacman &>/dev/null && { DISTRO="arch"; PKG="pacman"; }
;;
esac
fi
;;
Darwin)
DISTRO="macos"
command -v brew &>/dev/null && PKG="brew"
;;
*)
err "Unsupported OS: $OS"
exit 1
;;
esac
}
# ── Package lists per distro ────────────────────────────────────────────────
# Core: minimum to do a dev build (Linux native)
pkgs_core_debian="build-essential cmake git pkg-config
libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev
libxinerama-dev libxi-dev libxkbcommon-dev libwayland-dev
libsodium-dev libcurl4-openssl-dev"
pkgs_core_fedora="gcc gcc-c++ cmake git pkg-config
mesa-libGL-devel libX11-devel libXcursor-devel libXrandr-devel
libXinerama-devel libXi-devel libxkbcommon-devel wayland-devel
libsodium-devel libcurl-devel"
pkgs_core_arch="base-devel cmake git pkg-config
mesa libx11 libxcursor libxrandr libxinerama libxi
libxkbcommon wayland libsodium curl"
pkgs_core_macos="cmake"
# Windows cross-compile (from Linux)
pkgs_win_debian="mingw-w64 zip"
pkgs_win_fedora="mingw64-gcc mingw64-gcc-c++ zip"
pkgs_win_arch="mingw-w64-gcc zip"
# macOS cross-compile helpers (osxcross is separate)
pkgs_mac_debian="genisoimage icnsutils"
pkgs_mac_fedora="genisoimage"
pkgs_mac_arch="cdrtools"
# ── Helpers ──────────────────────────────────────────────────────────────────
has_cmd() { command -v "$1" &>/dev/null; }
# Install packages for the detected distro
install_pkgs() {
local pkgs="$1"
local desc="$2"
if $CHECK_ONLY; then
warn "Would install ($desc): $pkgs"
return
fi
info "Installing $desc packages..."
case "$PKG" in
apt) sudo apt-get update -qq && sudo apt-get install -y $pkgs ;;
dnf) sudo dnf install -y $pkgs ;;
pacman) sudo pacman -S --needed --noconfirm $pkgs ;;
zypper) sudo zypper install -y $pkgs ;;
brew) brew install $pkgs ;;
*) err "No supported package manager found for $DISTRO"
echo "Please install manually: $pkgs"
return 1 ;;
esac
}
# Select the right package list variable
get_pkgs() {
local category="$1" # core, win, mac
local var="pkgs_${category}_${DISTRO}"
echo "${!var:-}"
}
# ── Check individual tools ──────────────────────────────────────────────────
MISSING=0
check_tool() {
local cmd="$1"
local label="${2:-$1}"
if has_cmd "$cmd"; then
ok "$label"
else
miss "$label (not found: $cmd)"
MISSING=$((MISSING + 1))
fi
}
check_file() {
local path="$1"
local label="$2"
if [[ -f "$path" ]]; then
ok "$label"
else
miss "$label ($path)"
MISSING=$((MISSING + 1))
fi
}
check_dir() {
local path="$1"
local label="$2"
if [[ -d "$path" ]]; then
ok "$label"
else
miss "$label ($path)"
MISSING=$((MISSING + 1))
fi
}
# ═════════════════════════════════════════════════════════════════════════════
# MAIN
# ═════════════════════════════════════════════════════════════════════════════
echo -e "${BOLD}DragonX Wallet — Development Setup${NC}"
echo "═══════════════════════════════════"
detect_os
info "Detected: $OS / $DISTRO (package manager: ${PKG:-none})"
# ── 1. Core build dependencies ──────────────────────────────────────────────
header "Core Build Dependencies"
core_pkgs="$(get_pkgs core)"
if [[ -z "$core_pkgs" ]]; then
warn "No package list for $DISTRO — check README for manual instructions"
else
# Check if key tools are already present
NEED_CORE=false
has_cmd cmake && has_cmd g++ && has_cmd pkg-config || NEED_CORE=true
if $NEED_CORE; then
install_pkgs "$core_pkgs" "core build"
else
ok "Core tools already installed (cmake, g++, pkg-config)"
fi
fi
check_tool cmake "cmake"
check_tool g++ "g++ (C++ compiler)"
check_tool git "git"
check_tool make "make"
# ── 2. libsodium ────────────────────────────────────────────────────────────
header "libsodium"
SODIUM_OK=false
# Check system libsodium
if pkg-config --exists libsodium 2>/dev/null; then
ok "libsodium (system, $(pkg-config --modversion libsodium))"
SODIUM_OK=true
elif [[ -f "$PROJECT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
ok "libsodium (local build)"
SODIUM_OK=true
else
miss "libsodium not found"
if ! $CHECK_ONLY; then
info "Building libsodium from source..."
"$SCRIPT_DIR/fetch-libsodium.sh" && SODIUM_OK=true
fi
fi
# ── 3. Windows cross-compile (optional) ─────────────────────────────────────
header "Windows Cross-Compile"
if $SETUP_WIN; then
win_pkgs="$(get_pkgs win)"
if [[ -n "$win_pkgs" ]]; then
install_pkgs "$win_pkgs" "Windows cross-compile"
fi
# Set posix thread model if available
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
if ! $CHECK_ONLY; then
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
fi
fi
# Fetch libsodium for Windows
if [[ ! -f "$PROJECT_DIR/libs/libsodium-win/lib/libsodium.a" ]]; then
if ! $CHECK_ONLY; then
info "Building libsodium for Windows target..."
"$SCRIPT_DIR/fetch-libsodium.sh" --win
else
miss "libsodium-win (not built yet)"
fi
else
ok "libsodium-win"
fi
fi
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
ok "mingw-w64 ($(x86_64-w64-mingw32-g++-posix --version 2>/dev/null | head -1 || x86_64-w64-mingw32-g++ --version 2>/dev/null | head -1))"
else
if $SETUP_WIN; then
miss "mingw-w64"
else
skip "mingw-w64 (use --win to install)"
fi
fi
# ── 4. macOS cross-compile (optional) ───────────────────────────────────────
header "macOS Cross-Compile"
if $SETUP_MAC; then
mac_pkgs="$(get_pkgs mac)"
if [[ -n "$mac_pkgs" ]]; then
install_pkgs "$mac_pkgs" "macOS cross-compile helpers"
fi
# Fetch libsodium for macOS
if [[ ! -f "$PROJECT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
if ! $CHECK_ONLY; then
info "Building libsodium for macOS target..."
"$SCRIPT_DIR/fetch-libsodium.sh" --mac
else
miss "libsodium-mac (not built yet)"
fi
else
ok "libsodium-mac"
fi
fi
if [[ -d "$PROJECT_DIR/external/osxcross/target" ]] || [[ -d "${OSXCROSS:-}/target" ]]; then
ok "osxcross"
else
if $SETUP_MAC; then
miss "osxcross (must be set up manually — see README)"
else
skip "osxcross (use --mac to set up macOS deps)"
fi
fi
# ── 5. Sapling parameters ───────────────────────────────────────────────────
header "Sapling Parameters"
SAPLING_DIR=""
for d in "$HOME/.zcash-params" "$HOME/.hush-params"; do
if [[ -f "$d/sapling-spend.params" && -f "$d/sapling-output.params" ]]; then
SAPLING_DIR="$d"
break
fi
done
# Also check project-local locations
if [[ -z "$SAPLING_DIR" ]]; then
for d in "$PROJECT_DIR/prebuilt-binaries/dragonxd-linux" "$PROJECT_DIR/prebuilt-binaries/dragonxd-win"; do
if [[ -f "$d/sapling-spend.params" && -f "$d/sapling-output.params" ]]; then
SAPLING_DIR="$d"
break
fi
done
fi
if [[ -n "$SAPLING_DIR" ]]; then
ok "sapling-spend.params ($(du -h "$SAPLING_DIR/sapling-spend.params" | cut -f1))"
ok "sapling-output.params ($(du -h "$SAPLING_DIR/sapling-output.params" | cut -f1))"
elif $SETUP_SAPLING; then
if ! $CHECK_ONLY; then
info "Downloading Sapling parameters (~51 MB)..."
PARAMS_DIR="$HOME/.zcash-params"
mkdir -p "$PARAMS_DIR"
SPEND_URL="https://z.cash/downloads/sapling-spend.params"
OUTPUT_URL="https://z.cash/downloads/sapling-output.params"
curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" && \
ok "Downloaded sapling-spend.params"
curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \
ok "Downloaded sapling-output.params"
fi
else
skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)"
fi
# ── 6. Binary directories ───────────────────────────────────────────────────
header "Binary Directories"
for platform in dragonxd-linux dragonxd-win dragonxd-mac xmrig; do
dir="$PROJECT_DIR/prebuilt-binaries/$platform"
if [[ -d "$dir" ]]; then
# Count actual files (not .gitkeep)
count=$(find "$dir" -maxdepth 1 -type f ! -name '.gitkeep' | wc -l)
if [[ $count -gt 0 ]]; then
ok "prebuilt-binaries/$platform/ ($count files)"
else
skip "prebuilt-binaries/$platform/ (empty — place binaries here)"
fi
else
if ! $CHECK_ONLY; then
mkdir -p "$dir"
touch "$dir/.gitkeep"
ok "Created prebuilt-binaries/$platform/"
else
miss "prebuilt-binaries/$platform/"
fi
fi
done
# ── Summary ──────────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════"
if [[ $MISSING -eq 0 ]]; then
echo -e "${GREEN}${BOLD} Setup complete — ready to build!${NC}"
echo ""
echo " Quick start:"
echo " ./build.sh # Dev build"
echo " ./build.sh --linux-release # Linux release + AppImage"
echo " ./build.sh --win-release # Windows cross-compile"
else
echo -e "${YELLOW}${BOLD} $MISSING item(s) still need attention${NC}"
if $CHECK_ONLY; then
echo ""
echo " Run without --check to install automatically:"
echo " ./scripts/setup.sh"
echo " ./scripts/setup.sh --all # Include cross-compile + Sapling"
fi
fi
echo "═══════════════════════════════════════════"

View File

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

View File

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

840
setup.sh
View File

@@ -1,840 +0,0 @@
#!/usr/bin/env bash
# ── setup.sh ────────────────────────────────────────────────────────────────
# DragonX Wallet — Development Environment Setup
# Copyright 2024-2026 The Hush Developers
# Released under the GPLv3
#
# Detects your OS/distro, installs build prerequisites, fetches libraries,
# and validates the environment so you can build immediately with:
#
# ./build.sh # dev build
# ./build.sh --win-release # Windows cross-compile
# ./build.sh --linux-release # Linux release + AppImage
#
# Usage:
# ./setup.sh # Interactive — install everything needed
# ./setup.sh --check # Just report what's missing, don't install
# ./setup.sh --all # Install dev + all cross-compile targets
# ./setup.sh --win # Also install Windows cross-compile deps
# ./setup.sh --mac # Also install macOS cross-compile deps
# ./setup.sh --sapling # Also download Sapling params (~51 MB)
# ./setup.sh -j6 # Use 6 threads for building
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ── Parallel builds ─────────────────────────────────────────────────────────
# Default to 1 core; use -jN to increase (e.g. -j6)
NPROC=1
# ── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
ok() { echo -e " ${GREEN}${NC} $1"; }
miss() { echo -e " ${RED}${NC} $1"; }
skip() { echo -e " ${YELLOW}${NC} $1"; }
info() { echo -e "${GREEN}[*]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
err() { echo -e "${RED}[ERROR]${NC} $1"; }
header(){ echo -e "\n${CYAN}── $1 ──${NC}"; }
# ── Parse args ───────────────────────────────────────────────────────────────
CHECK_ONLY=false
SETUP_WIN=false
SETUP_MAC=false
SETUP_SAPLING=false
while [[ $# -gt 0 ]]; do
case $1 in
--check) CHECK_ONLY=true; shift ;;
--win) SETUP_WIN=true; shift ;;
--mac) SETUP_MAC=true; shift ;;
--sapling) SETUP_SAPLING=true; shift ;;
--all) SETUP_WIN=true; SETUP_MAC=true; SETUP_SAPLING=true; shift ;;
-j*) NPROC="${1#-j}"; shift ;;
-h|--help)
sed -n '2,/^# ─\{10\}/{ /^# ─\{10\}/d; s/^# \?//p; }' "$0"
exit 0
;;
*) err "Unknown option: $1"; exit 1 ;;
esac
done
# Apply parallel build flag (after arg parsing so -jN override takes effect)
# NOTE: We do NOT export MAKEFLAGS globally — that interferes with autotools
# builds (dragonx daemon). Instead, -j is passed explicitly where needed.
# ── Detect OS / distro ──────────────────────────────────────────────────────
detect_os() {
OS="$(uname -s)"
DISTRO="unknown"
PKG=""
case "$OS" in
Linux)
if [[ -f /etc/os-release ]]; then
. /etc/os-release
case "${ID:-}" in
ubuntu|debian|linuxmint|pop|elementary|zorin|neon)
DISTRO="debian"; PKG="apt" ;;
fedora|rhel|centos|rocky|alma)
DISTRO="fedora"; PKG="dnf" ;;
arch|manjaro|endeavouros|garuda)
DISTRO="arch"; PKG="pacman" ;;
opensuse*|suse*)
DISTRO="suse"; PKG="zypper" ;;
void)
DISTRO="void"; PKG="xbps" ;;
gentoo)
DISTRO="gentoo"; PKG="emerge" ;;
*)
# Fallback: check for package managers
command -v apt &>/dev/null && { DISTRO="debian"; PKG="apt"; } ||
command -v dnf &>/dev/null && { DISTRO="fedora"; PKG="dnf"; } ||
command -v pacman &>/dev/null && { DISTRO="arch"; PKG="pacman"; } ||
true
;;
esac
fi
;;
Darwin)
DISTRO="macos"
if command -v brew &>/dev/null; then PKG="brew"; fi
;;
*)
err "Unsupported OS: $OS"
exit 1
;;
esac
}
# ── Package lists per distro ────────────────────────────────────────────────
# Core: minimum to do a dev build (Linux native)
pkgs_core_debian="build-essential cmake git pkg-config
libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev
libxinerama-dev libxi-dev libxkbcommon-dev libwayland-dev
libsodium-dev libcurl4-openssl-dev
autoconf automake libtool wget python3 xxd"
pkgs_core_fedora="gcc gcc-c++ cmake git pkg-config
mesa-libGL-devel libX11-devel libXcursor-devel libXrandr-devel
libXinerama-devel libXi-devel libxkbcommon-devel wayland-devel
libsodium-devel libcurl-devel
autoconf automake libtool wget python3 vim-common"
pkgs_core_arch="base-devel cmake git pkg-config
mesa libx11 libxcursor libxrandr libxinerama libxi
libxkbcommon wayland libsodium curl
autoconf automake libtool wget python xxd"
pkgs_core_macos="cmake python xxd"
# Windows cross-compile (from Linux)
pkgs_win_debian="mingw-w64 zip"
pkgs_win_fedora="mingw64-gcc mingw64-gcc-c++ zip"
pkgs_win_arch="mingw-w64-gcc zip"
# macOS cross-compile helpers (osxcross is separate)
pkgs_mac_debian="genisoimage icnsutils"
pkgs_mac_fedora="genisoimage"
pkgs_mac_arch="cdrtools"
# ── Helpers ──────────────────────────────────────────────────────────────────
has_cmd() { command -v "$1" &>/dev/null; }
# Install packages for the detected distro
install_pkgs() {
local pkgs="$1"
local desc="$2"
if $CHECK_ONLY; then
warn "Would install ($desc): $pkgs"
return
fi
info "Installing $desc packages..."
case "$PKG" in
apt) sudo apt-get update -qq && sudo apt-get install -y $pkgs ;;
dnf) sudo dnf install -y $pkgs ;;
pacman)
# Try pacman first; fall back to AUR helper for packages not in official repos
if ! sudo pacman -S --needed --noconfirm $pkgs 2>/dev/null; then
if command -v yay &>/dev/null; then
yay -S --needed --noconfirm $pkgs
elif command -v paru &>/dev/null; then
paru -S --needed --noconfirm $pkgs
else
err "pacman failed and no AUR helper (yay/paru) found"
echo "Some packages may be in the AUR. Install yay or paru, then retry."
return 1
fi
fi
;;
zypper) sudo zypper install -y $pkgs ;;
brew) brew install $pkgs ;;
*) err "No supported package manager found for $DISTRO"
echo "Please install manually: $pkgs"
return 1 ;;
esac
}
# Select the right package list variable
get_pkgs() {
local category="$1" # core, win, mac
local var="pkgs_${category}_${DISTRO}"
echo "${!var:-}"
}
# ── Check individual tools ──────────────────────────────────────────────────
MISSING=0
check_tool() {
local cmd="$1"
local label="${2:-$1}"
if has_cmd "$cmd"; then
ok "$label"
else
miss "$label (not found: $cmd)"
MISSING=$((MISSING + 1))
fi
}
check_file() {
local path="$1"
local label="$2"
if [[ -f "$path" ]]; then
ok "$label"
else
miss "$label ($path)"
MISSING=$((MISSING + 1))
fi
}
check_dir() {
local path="$1"
local label="$2"
if [[ -d "$path" ]]; then
ok "$label"
else
miss "$label ($path)"
MISSING=$((MISSING + 1))
fi
}
# ═════════════════════════════════════════════════════════════════════════════
# MAIN
# ═════════════════════════════════════════════════════════════════════════════
echo -e "${BOLD}DragonX Wallet — Development Setup${NC}"
echo "═══════════════════════════════════"
detect_os
info "Detected: $OS / $DISTRO (package manager: ${PKG:-none})"
# ── 1. Core build dependencies ──────────────────────────────────────────────
header "Core Build Dependencies"
core_pkgs="$(get_pkgs core)"
if [[ -z "$core_pkgs" ]]; then
warn "No package list for $DISTRO — check README for manual instructions"
else
# Check if key tools are already present
NEED_CORE=false
has_cmd cmake && has_cmd g++ && has_cmd pkg-config && has_cmd python3 && has_cmd xxd || NEED_CORE=true
if $NEED_CORE; then
install_pkgs "$core_pkgs" "core build"
else
ok "Core tools already installed (cmake, g++, pkg-config)"
fi
fi
check_tool cmake "cmake"
check_tool g++ "g++ (C++ compiler)"
check_tool git "git"
check_tool make "make"
check_tool python3 "python3 (theme expansion)"
check_tool xxd "xxd (embedded language headers)"
# ── 2. libsodium ────────────────────────────────────────────────────────────
header "libsodium"
SODIUM_OK=false
# Check system libsodium
if pkg-config --exists libsodium 2>/dev/null; then
ok "libsodium (system, $(pkg-config --modversion libsodium))"
SODIUM_OK=true
elif [[ -f "$PROJECT_DIR/libs/libsodium/lib/libsodium.a" ]]; then
ok "libsodium (local build)"
SODIUM_OK=true
else
miss "libsodium not found"
if ! $CHECK_ONLY; then
info "Building libsodium from source..."
"$PROJECT_DIR/scripts/fetch-libsodium.sh" && SODIUM_OK=true
fi
fi
# ── 3. Windows cross-compile (optional) ─────────────────────────────────────
header "Windows Cross-Compile"
if $SETUP_WIN; then
win_pkgs="$(get_pkgs win)"
if [[ -n "$win_pkgs" ]]; then
install_pkgs "$win_pkgs" "Windows cross-compile"
fi
# Set posix thread model if available
if has_cmd update-alternatives && [[ "$PKG" == "apt" ]]; then
if ! $CHECK_ONLY; then
sudo update-alternatives --set x86_64-w64-mingw32-gcc \
/usr/bin/x86_64-w64-mingw32-gcc-posix 2>/dev/null || true
sudo update-alternatives --set x86_64-w64-mingw32-g++ \
/usr/bin/x86_64-w64-mingw32-g++-posix 2>/dev/null || true
fi
fi
# Fetch libsodium for Windows
if [[ ! -f "$PROJECT_DIR/libs/libsodium-win/lib/libsodium.a" ]]; then
if ! $CHECK_ONLY; then
info "Building libsodium for Windows target..."
"$PROJECT_DIR/scripts/fetch-libsodium.sh" --win
else
miss "libsodium-win (not built yet)"
fi
else
ok "libsodium-win"
fi
fi
if has_cmd x86_64-w64-mingw32-g++-posix || has_cmd x86_64-w64-mingw32-g++; then
ok "mingw-w64 ($(x86_64-w64-mingw32-g++-posix --version 2>/dev/null | head -1 || x86_64-w64-mingw32-g++ --version 2>/dev/null | head -1))"
else
if $SETUP_WIN; then
miss "mingw-w64"
else
skip "mingw-w64 (use --win to install)"
fi
fi
# ── 4. macOS cross-compile (optional) ───────────────────────────────────────
header "macOS Cross-Compile"
if $SETUP_MAC; then
mac_pkgs="$(get_pkgs mac)"
if [[ -n "$mac_pkgs" ]]; then
install_pkgs "$mac_pkgs" "macOS cross-compile helpers"
fi
# Fetch libsodium for macOS
if [[ ! -f "$PROJECT_DIR/libs/libsodium-mac/lib/libsodium.a" ]]; then
if ! $CHECK_ONLY; then
# Requires osxcross — skip gracefully if not available
if [[ -d "$PROJECT_DIR/external/osxcross/target" ]] || [[ -d "${OSXCROSS:-}/target" ]]; then
info "Building libsodium for macOS target..."
"$PROJECT_DIR/scripts/fetch-libsodium.sh" --mac || warn "libsodium-mac build failed"
else
skip "libsodium-mac (requires osxcross — see README)"
fi
else
miss "libsodium-mac (not built yet)"
fi
else
ok "libsodium-mac"
fi
fi
if [[ -d "$PROJECT_DIR/external/osxcross/target" ]] || [[ -d "${OSXCROSS:-}/target" ]]; then
ok "osxcross"
else
if $SETUP_MAC; then
miss "osxcross (must be set up manually — see README)"
else
skip "osxcross (use --mac to set up macOS deps)"
fi
fi
# ── 5. Sapling parameters ───────────────────────────────────────────────────
header "Sapling Parameters"
SAPLING_DIR=""
for d in "$HOME/.zcash-params" "$HOME/.hush-params"; do
if [[ -f "$d/sapling-spend.params" && -f "$d/sapling-output.params" ]]; then
SAPLING_DIR="$d"
break
fi
done
# Also check project-local locations
if [[ -z "$SAPLING_DIR" ]]; then
for d in "$PROJECT_DIR/prebuilt-binaries/dragonxd-linux" "$PROJECT_DIR/prebuilt-binaries/dragonxd-win"; do
if [[ -f "$d/sapling-spend.params" && -f "$d/sapling-output.params" ]]; then
SAPLING_DIR="$d"
break
fi
done
fi
if [[ -n "$SAPLING_DIR" ]]; then
ok "sapling-spend.params ($(du -h "$SAPLING_DIR/sapling-spend.params" | cut -f1))"
ok "sapling-output.params ($(du -h "$SAPLING_DIR/sapling-output.params" | cut -f1))"
elif $SETUP_SAPLING; then
if ! $CHECK_ONLY; then
info "Downloading Sapling parameters (~51 MB)..."
PARAMS_DIR="$HOME/.zcash-params"
mkdir -p "$PARAMS_DIR"
SPEND_URL="https://z.cash/downloads/sapling-spend.params"
OUTPUT_URL="https://z.cash/downloads/sapling-output.params"
curl -fSL -o "$PARAMS_DIR/sapling-spend.params" "$SPEND_URL" && \
ok "Downloaded sapling-spend.params"
curl -fSL -o "$PARAMS_DIR/sapling-output.params" "$OUTPUT_URL" && \
ok "Downloaded sapling-output.params"
fi
else
skip "Sapling params not found (use --sapling to download, or they'll be extracted at runtime from embedded builds)"
fi
# ── 6. DragonX daemon ────────────────────────────────────────────────────────
header "DragonX Daemon"
DRAGONX_SRC="$PROJECT_DIR/external/dragonx"
DRAGONXD_LINUX="$PROJECT_DIR/prebuilt-binaries/dragonxd-linux"
DRAGONXD_WIN="$PROJECT_DIR/prebuilt-binaries/dragonxd-win"
DRAGONXD_MAC="$PROJECT_DIR/prebuilt-binaries/dragonxd-mac"
DAEMON_BINS="dragonxd dragonx-cli dragonx-tx hushd hush-arrakis-chain hush-cli hush-tx"
DAEMON_DATA="asmap.dat sapling-spend.params sapling-output.params"
# Helper: clone / update dragonx source
clone_dragonx_if_needed() {
if [[ ! -d "$DRAGONX_SRC" ]]; then
info "Cloning dragonx..."
git clone https://git.dragonx.is/DragonX/dragonx.git "$DRAGONX_SRC"
else
ok "dragonx source already present"
info "Switching to dragonx branch and pulling latest..."
(cd "$DRAGONX_SRC" && git checkout dragonx 2>/dev/null && git pull --ff-only 2>/dev/null || true)
fi
}
# Helper: copy data files (asmap, sapling params) into a destination dir
copy_daemon_data() {
local dest="$1"
for p in "$DRAGONX_SRC/asmap.dat" "$DRAGONX_SRC/contrib/asmap/asmap.dat"; do
if [[ -f "$p" ]]; then
cp "$p" "$dest/asmap.dat"
ok " Installed asmap.dat"
break
fi
done
for p in sapling-spend.params sapling-output.params; do
if [[ -f "$DRAGONX_SRC/$p" ]]; then
cp "$DRAGONX_SRC/$p" "$dest/"
ok " Installed $p"
fi
done
}
# ── Stale-daemon guard ───────────────────────────────────────────────────────
# A prebuilt daemon binary is only rebuilt on its platform's flag (--win/--mac),
# and build.sh merely BUNDLES whatever binary already exists — so a daemon left
# over from an old source revision silently ships in the wallet (e.g. the Network
# tab once reported v1.0.1 while the source was v1.0.2). These helpers compare the
# version baked into a prebuilt binary against the dragonx source and flag drift.
STALE_DAEMON=0
# MAJOR.MINOR.REVISION from the checked-out dragonx source (empty if unavailable).
dragonx_source_version() {
local hdr="$DRAGONX_SRC/src/clientversion.h"
[[ -f "$hdr" ]] || return 1
local maj min rev
maj=$(awk '/#define[ \t]+CLIENT_VERSION_MAJOR/{print $3; exit}' "$hdr")
min=$(awk '/#define[ \t]+CLIENT_VERSION_MINOR/{print $3; exit}' "$hdr")
rev=$(awk '/#define[ \t]+CLIENT_VERSION_REVISION/{print $3; exit}' "$hdr")
[[ -n "$maj" && -n "$min" && -n "$rev" ]] || return 1
printf '%s.%s.%s' "$maj" "$min" "$rev"
}
# vX.Y.Z baked into a built daemon binary (the daemon embeds "vX.Y.Z-<githash>").
# Uses grep -a so no `strings`/binutils dependency is required.
dragonx_binary_version() {
local bin="$1"
[[ -f "$bin" ]] || return 1
LC_ALL=C grep -aoE 'v[0-9]+\.[0-9]+\.[0-9]+-[0-9a-f]{6,}' "$bin" 2>/dev/null \
| head -1 | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/'
}
# Compare a prebuilt daemon against the source; warn (and set STALE_DAEMON) on drift.
# $1 = label, $2 = binary path, $3 = rebuild flag(s) (e.g. "--win", "" for Linux)
daemon_version_guard() {
local label="$1" bin="$2" rebuild_hint="$3"
[[ -f "$bin" ]] || return 0
local src bv
src=$(dragonx_source_version) || return 0 # no source checked out → can't compare
bv=$(dragonx_binary_version "$bin")
[[ -n "$bv" ]] || return 0 # couldn't read the binary's version
if [[ "$bv" == "$src" ]]; then
ok " $label daemon is v$bv (matches dragonx source)"
else
warn " $label daemon is v$bv but dragonx source is v$src — STALE"
warn " rebuild so the wallet ships the current daemon: ./setup.sh${rebuild_hint:+ $rebuild_hint}"
STALE_DAEMON=1
fi
}
# ── Linux daemon ─────────────────────────────────────────────────────────────
# Skip Linux daemon build if only cross-compile targets were requested
# and we already have Linux binaries (avoids contaminating the build tree)
SKIP_LINUX_DAEMON=false
if ($SETUP_WIN || $SETUP_MAC) && [[ -f "$DRAGONXD_LINUX/dragonxd" || -f "$DRAGONXD_LINUX/hushd" ]]; then
SKIP_LINUX_DAEMON=true
fi
# Clean previous prebuilt daemon binaries so we always rebuild
if ! $CHECK_ONLY && ! $SKIP_LINUX_DAEMON; then
for f in $DAEMON_BINS $DAEMON_DATA; do
rm -f "$DRAGONXD_LINUX/$f" 2>/dev/null || true
done
fi
if $CHECK_ONLY; then
if [[ -f "$DRAGONXD_LINUX/dragonxd" ]] || [[ -f "$DRAGONXD_LINUX/hushd" ]]; then
ok "dragonxd daemon (Linux) present"
daemon_version_guard "Linux" "$DRAGONXD_LINUX/dragonxd" ""
else
miss "dragonxd daemon (Linux) not built"
fi
elif $SKIP_LINUX_DAEMON; then
skip "dragonxd (Linux) — skipped, binaries already present (cross-compile only)"
daemon_version_guard "Linux" "$DRAGONXD_LINUX/dragonxd" ""
else
clone_dragonx_if_needed
info "Building dragonx daemon for Linux (this may take a while)..."
(
cd "$DRAGONX_SRC"
bash build.sh -j"$NPROC"
)
# Copy binaries to prebuilt-binaries
mkdir -p "$DRAGONXD_LINUX"
local_found=0
for f in dragonxd dragonx-cli dragonx-tx hushd hush-arrakis-chain hush-cli hush-tx; do
if [[ -f "$DRAGONX_SRC/src/$f" ]]; then
cp "$DRAGONX_SRC/src/$f" "$DRAGONXD_LINUX/"
chmod +x "$DRAGONXD_LINUX/$f"
ok " Installed $f"
local_found=1
fi
done
copy_daemon_data "$DRAGONXD_LINUX"
if [[ $local_found -eq 0 ]]; then
err "DragonX daemon (Linux) build failed — no binaries found"
MISSING=$((MISSING + 1))
else
ok "DragonX daemon built and installed to prebuilt-binaries/dragonxd-linux/"
fi
fi
# ── Windows daemon (cross-compile, only with --win or --all) ─────────────────
if ! $SETUP_WIN; then
skip "dragonxd (Windows) — use --win to cross-compile"
daemon_version_guard "Windows" "$DRAGONXD_WIN/dragonxd.exe" "--win"
elif $CHECK_ONLY; then
if [[ -f "$DRAGONXD_WIN/dragonxd.exe" ]] || [[ -f "$DRAGONXD_WIN/hushd.exe" ]]; then
ok "dragonxd daemon (Windows) present"
daemon_version_guard "Windows" "$DRAGONXD_WIN/dragonxd.exe" "--win"
else
miss "dragonxd daemon (Windows) not built"
fi
else
clone_dragonx_if_needed
# Clean previous Windows prebuilt binaries
if ! $CHECK_ONLY; then
rm -f "$DRAGONXD_WIN"/*.exe "$DRAGONXD_WIN"/*.bat 2>/dev/null || true
for f in $DAEMON_DATA; do rm -f "$DRAGONXD_WIN/$f" 2>/dev/null || true; done
fi
info "Building dragonx daemon for Windows (cross-compile, this may take a long time)..."
(
cd "$DRAGONX_SRC"
bash build.sh --win-release -j"$NPROC"
)
# Copy binaries from release directory
mkdir -p "$DRAGONXD_WIN"
local_found=0
# Find the release subdirectory (e.g. release/dragonx-1.0.0-win64/)
WIN_RELEASE_DIR=$(find "$DRAGONX_SRC/release" -maxdepth 1 -type d -name '*win64*' 2>/dev/null | head -1)
for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do
if [[ -n "$WIN_RELEASE_DIR" ]] && [[ -f "$WIN_RELEASE_DIR/$f" ]]; then
cp "$WIN_RELEASE_DIR/$f" "$DRAGONXD_WIN/"
ok " Installed $f"
local_found=1
elif [[ -f "$DRAGONX_SRC/src/$f" ]]; then
cp "$DRAGONX_SRC/src/$f" "$DRAGONXD_WIN/"
ok " Installed $f (from src/)"
local_found=1
fi
done
# .bat launchers
for f in dragonxd.bat dragonx-cli.bat bootstrap-dragonx.bat; do
if [[ -n "$WIN_RELEASE_DIR" ]] && [[ -f "$WIN_RELEASE_DIR/$f" ]]; then
cp "$WIN_RELEASE_DIR/$f" "$DRAGONXD_WIN/"
ok " Installed $f"
fi
done
copy_daemon_data "$DRAGONXD_WIN"
if [[ $local_found -eq 0 ]]; then
err "DragonX daemon (Windows) build failed — no binaries found"
MISSING=$((MISSING + 1))
else
ok "DragonX daemon (Windows) built and installed to prebuilt-binaries/dragonxd-win/"
fi
fi
# ── macOS daemon (only with --mac or --all) ──────────────────────────────────
if ! $SETUP_MAC; then
skip "dragonxd (macOS) — use --mac to cross-compile"
daemon_version_guard "macOS" "$DRAGONXD_MAC/dragonxd" "--mac"
elif $CHECK_ONLY; then
if [[ -f "$DRAGONXD_MAC/dragonxd" ]] || [[ -f "$DRAGONXD_MAC/hushd" ]]; then
ok "dragonxd daemon (macOS) present"
daemon_version_guard "macOS" "$DRAGONXD_MAC/dragonxd" "--mac"
else
miss "dragonxd daemon (macOS) not built"
fi
else
clone_dragonx_if_needed
# Clean previous macOS prebuilt binaries
if ! $CHECK_ONLY; then
for f in $DAEMON_BINS $DAEMON_DATA; do
rm -f "$DRAGONXD_MAC/$f" 2>/dev/null || true
done
fi
# macOS build requires either native macOS or osxcross
if [[ "$OSTYPE" == "darwin"* ]]; then
info "Building dragonx daemon for macOS (native)..."
(
cd "$DRAGONX_SRC"
bash build.sh -j"$NPROC"
)
else
info "Building dragonx daemon for macOS (requires osxcross)..."
OSXCROSS=""
for d in "$PROJECT_DIR/external/osxcross" "$HOME/osxcross" "/opt/osxcross"; do
if [[ -d "$d/target/bin" ]]; then
OSXCROSS="$d"
break
fi
done
if [[ -z "$OSXCROSS" ]]; then
warn "osxcross not found — skipping macOS daemon build"
warn "Install osxcross to external/osxcross to enable macOS cross-compilation"
else
info "Using osxcross at $OSXCROSS"
(
cd "$DRAGONX_SRC"
export PATH="$OSXCROSS/target/bin:$PATH"
bash util/build-mac-cross.sh 2>/dev/null || bash util/build-mac.sh
)
fi
fi
# Copy binaries
mkdir -p "$DRAGONXD_MAC"
local_found=0
for f in dragonxd dragonx-cli dragonx-tx hushd hush-arrakis-chain hush-cli hush-tx; do
if [[ -f "$DRAGONX_SRC/src/$f" ]]; then
cp "$DRAGONX_SRC/src/$f" "$DRAGONXD_MAC/"
chmod +x "$DRAGONXD_MAC/$f"
ok " Installed $f"
local_found=1
fi
done
copy_daemon_data "$DRAGONXD_MAC"
if [[ $local_found -eq 0 ]]; then
warn "DragonX daemon (macOS) — no binaries found (osxcross may be missing)"
else
ok "DragonX daemon (macOS) built and installed to prebuilt-binaries/dragonxd-mac/"
fi
fi
# Prominent reminder if any prebuilt daemon drifted from the source — these are bundled verbatim
# by build.sh, so a stale binary ships in the wallet (and shows an old version in the Network tab).
if [[ "$STALE_DAEMON" -eq 1 ]]; then
warn "One or more prebuilt daemons are OLDER than the dragonx source (see above)."
warn "build.sh bundles them as-is, so rebuild the stale platform(s) before releasing:"
warn " Linux: ./setup.sh · Windows: ./setup.sh --win · macOS: ./setup.sh --mac"
fi
# ── 7. xmrig-hac (mining binary) ────────────────────────────────────────────
header "xmrig-hac Mining Binary"
XMRIG_SRC="$PROJECT_DIR/external/xmrig-hac"
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac"
# Clean previous prebuilt xmrig binaries so we always rebuild
# Only clean the binary for the platform(s) we are actually building,
# otherwise a plain ./setup.sh deletes xmrig.exe without rebuilding it.
if ! $CHECK_ONLY; then
rm -f "$XMRIG_PREBUILT/xmrig" 2>/dev/null || true
if $SETUP_WIN; then
rm -f "$XMRIG_PREBUILT/xmrig.exe" 2>/dev/null || true
fi
fi
# Helper: clone xmrig-hac if not present
clone_xmrig_if_needed() {
if [[ ! -d "$XMRIG_SRC" ]]; then
info "Cloning xmrig-hac..."
git clone https://git.dragonx.is/dragonx/xmrig-hac.git "$XMRIG_SRC"
else
ok "xmrig-hac source already present"
info "Pulling latest xmrig-hac..."
(cd "$XMRIG_SRC" && git pull --ff-only 2>/dev/null || true)
fi
}
# ── Linux xmrig ─────────────────────────────────────────────────────────────
XMRIG_LINUX="$XMRIG_PREBUILT/xmrig"
if $CHECK_ONLY; then
if [[ -f "$XMRIG_LINUX" ]]; then
ok "xmrig (Linux) ($(du -h "$XMRIG_LINUX" | cut -f1))"
else
miss "xmrig (Linux) not built (run setup without --check to build)"
fi
else
clone_xmrig_if_needed
# Clean previous build
rm -rf "$XMRIG_SRC/build"
# Build dependencies (libuv, hwloc, openssl)
info "Building xmrig-hac dependencies (libuv, hwloc, openssl)..."
(
cd "$XMRIG_SRC/scripts"
sh build_deps.sh
)
ok "xmrig-hac dependencies built"
# Build xmrig
info "Building xmrig-hac (Linux)..."
mkdir -p "$XMRIG_SRC/build"
(
cd "$XMRIG_SRC/build"
cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DWITH_OPENCL=OFF \
-DWITH_CUDA=OFF \
-DWITH_HWLOC=ON \
-DCMAKE_PREFIX_PATH="$XMRIG_SRC/scripts/deps"
make -j"$NPROC"
)
# Copy binary to prebuilt-binaries
mkdir -p "$XMRIG_PREBUILT"
if [[ -f "$XMRIG_SRC/build/xmrig" ]]; then
cp "$XMRIG_SRC/build/xmrig" "$XMRIG_LINUX"
ok "xmrig (Linux) built and installed to prebuilt-binaries/xmrig-hac/"
else
err "xmrig (Linux) build failed — binary not found"
MISSING=$((MISSING + 1))
fi
fi
# ── Windows xmrig (cross-compile, only with --win or --all) ─────────────────
XMRIG_WIN="$XMRIG_PREBUILT/xmrig.exe"
if ! $SETUP_WIN; then
skip "xmrig.exe (Windows) — use --win to cross-compile"
elif $CHECK_ONLY; then
if [[ -f "$XMRIG_WIN" ]]; then
ok "xmrig.exe (Windows) ($(du -h "$XMRIG_WIN" | cut -f1))"
else
miss "xmrig.exe (Windows) not built (run setup --win without --check to build)"
fi
else
clone_xmrig_if_needed
# Clean previous Windows build
rm -rf "$XMRIG_SRC/build-windows"
info "Building xmrig-hac (Windows cross-compile)..."
(
cd "$XMRIG_SRC/scripts"
bash build_windows.sh
)
# Copy binary to prebuilt-binaries
mkdir -p "$XMRIG_PREBUILT"
if [[ -f "$XMRIG_SRC/build-windows/xmrig.exe" ]]; then
cp "$XMRIG_SRC/build-windows/xmrig.exe" "$XMRIG_WIN"
ok "xmrig.exe (Windows) built and installed to prebuilt-binaries/xmrig-hac/"
else
err "xmrig.exe (Windows) build failed — binary not found"
MISSING=$((MISSING + 1))
fi
fi
# ── 8. Binary directories ───────────────────────────────────────────────────
header "Binary Directories"
for platform in dragonxd-linux dragonxd-win dragonxd-mac xmrig; do
dir="$PROJECT_DIR/prebuilt-binaries/$platform"
if [[ -d "$dir" ]]; then
# Count actual files (not .gitkeep)
count=$(find "$dir" -maxdepth 1 -type f ! -name '.gitkeep' | wc -l)
if [[ $count -gt 0 ]]; then
ok "prebuilt-binaries/$platform/ ($count files)"
else
skip "prebuilt-binaries/$platform/ (empty — place binaries here)"
fi
else
if ! $CHECK_ONLY; then
mkdir -p "$dir"
touch "$dir/.gitkeep"
ok "Created prebuilt-binaries/$platform/"
else
miss "prebuilt-binaries/$platform/"
fi
fi
done
# ── Summary ──────────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════"
if [[ $MISSING -eq 0 ]]; then
echo -e "${GREEN}${BOLD} Setup complete — ready to build!${NC}"
echo ""
echo " Quick start:"
echo " ./build.sh # Dev build"
echo " ./build.sh --linux-release # Linux release + AppImage"
echo " ./build.sh --win-release # Windows cross-compile"
else
echo -e "${YELLOW}${BOLD} $MISSING item(s) still need attention${NC}"
if $CHECK_ONLY; then
echo ""
echo " Run without --check to install automatically:"
echo " ./scripts/setup.sh"
echo " ./scripts/setup.sh --all # Include cross-compile + Sapling"
fi
fi
echo "═══════════════════════════════════════════"

File diff suppressed because it is too large Load Diff

565
src/app.h
View File

@@ -9,23 +9,9 @@
#include <functional>
#include <thread>
#include <atomic>
#include <mutex>
#include <chrono>
#include <unordered_map>
#include <unordered_set>
#include <nlohmann/json_fwd.hpp>
#include "data/transaction_history_cache.h"
#include "data/address_book.h"
#include "data/wallet_state.h"
#include "rpc/connection.h"
#include "services/network_refresh_service.h"
#include "services/wallet_security_controller.h"
#include "services/wallet_security_workflow.h"
#include "util/async_task_manager.h"
#include "util/pool_stats_service.h"
#include "wallet/wallet_capabilities.h"
#include "chat/chat_service.h"
#include "chat/chat_database.h"
#include "ui/sidebar.h"
#include "ui/windows/console_tab.h"
#include "imgui.h"
@@ -37,9 +23,8 @@ namespace dragonx {
class RPCWorker;
}
namespace config { class Settings; }
namespace daemon { class DaemonController; class EmbeddedDaemon; class XmrigManager; }
namespace daemon { class EmbeddedDaemon; class XmrigManager; }
namespace util { class Bootstrap; class SecureVault; }
namespace wallet { class LiteWalletController; struct LiteWalletAppRefreshModel; }
}
namespace dragonx {
@@ -138,13 +123,6 @@ public:
* @brief Whether we are in the shutdown phase
*/
bool isShuttingDown() const { return shutting_down_; }
wallet::WalletCapabilities walletCapabilities() const { return wallet::currentWalletCapabilities(); }
bool isLiteBuild() const { return wallet::isLiteBuild(walletCapabilities()); }
bool supportsEmbeddedDaemon() const { return wallet::supportsEmbeddedDaemon(walletCapabilities()); }
bool supportsFullNodeLifecycleActions() const { return wallet::supportsFullNodeLifecycleActions(walletCapabilities()); }
bool supportsSoloMining() const { return wallet::supportsSoloMining(walletCapabilities()); }
bool supportsPoolMining() const { return wallet::supportsPoolMining(walletCapabilities()); }
bool supportsLiteBackend() const { return wallet::supportsLiteBackend(walletCapabilities()); }
/**
* @brief Render the shutdown overlay (called instead of normal UI during shutdown)
@@ -160,42 +138,10 @@ public:
// Accessors for subsystems
rpc::RPCClient* rpc() { return rpc_.get(); }
rpc::RPCWorker* worker() { return worker_.get(); }
// Console backend accessors (fast-lane-preferring, defined in app.cpp where the
// subsystem types are complete) used by the shared console executor.
rpc::RPCClient* consoleRpc();
rpc::RPCWorker* consoleWorker();
daemon::EmbeddedDaemon* consoleDaemon();
daemon::XmrigManager* consoleXmrig();
config::Settings* settings() { return settings_.get(); }
// Lite wallet controller (non-null only in lite builds with a linked backend).
wallet::LiteWalletController* liteWallet() { return lite_wallet_.get(); }
// HushChat service (identity + in-memory message store); the Chat tab reads its store.
chat::ChatService& chatService() { return chat_service_; }
// HushChat composing: construct, broadcast (broadcastChatMemos), and locally echo an outgoing
// message (to a conversation whose peer key we know) / a new-conversation contact request.
void sendChatMessage(const std::string& conversationId, const std::string& text);
void startChatConversation(const std::string& peerZaddr, const std::string& text);
// Debug/sweep convenience: give the Chat tab a demo identity + a few sample conversations
// (in-memory only, not persisted) so the screenshot sweep captures the populated UI. No-op when
// the chat feature is off.
void seedChatDemoData();
// Reason the lite wallet failed to auto-open this session (empty if none / opened OK).
const std::string& liteOpenError() const { return lite_open_error_; }
// Show the lite send-time unlock modal (called when a spend is attempted on a locked wallet).
void requestLiteUnlock() { lite_unlock_prompt_ = true; }
// Lock the lite wallet AND immediately tear down the chat session (the lite backend `lock`
// doesn't update state_.locked until the next poll, so chat secrets would otherwise linger).
bool lockLiteWallet();
// (Re)build the lite controller from current settings so a changed lite-server selection
// takes effect. No-op on non-lite/unlinked builds; preserves a live wallet (see app.cpp).
void rebuildLiteWallet(bool force = false);
WalletState& state() { return state_; }
const WalletState& state() const { return state_; }
const WalletState& getWalletState() const { return state_; }
// Shared contact store (Contacts tab / Send picker / future Chat roster). App-owned so
// every surface reads one source of truth instead of a per-dialog singleton.
data::AddressBook& addressBook() { return address_book_; }
const data::AddressBook& addressBook() const { return address_book_; }
// Connection state (convenience wrappers)
bool isConnected() const { return state_.connected; }
@@ -225,34 +171,10 @@ public:
// Pool mining (xmrig)
void startPoolMining(int threads);
void stopPoolMining();
int getXmrigRequestedThreads() const {
return xmrig_manager_ ? xmrig_manager_->getRequestedThreads() : 0;
}
// True while the pool miner process is live — used to refuse replacing the binary under it.
bool isPoolMinerRunning() const {
return xmrig_manager_ && xmrig_manager_->isRunning();
}
// Auto-balance: latest per-pool hashrate snapshot (for the mining tab pool list),
// and a request to refresh it now (Refresh button / switching into Auto mode).
util::PoolStatsService::Snapshot poolStatsSnapshot() const {
return pool_stats_service_.snapshot();
}
void requestPoolBalanceRefresh() { balance_refresh_pending_ = true; }
// Installed miner version (detected from `xmrig --version`, cached; kicks the one-shot
// detection on first call) so the mining tab can show it before mining starts.
std::string poolMiningInstalledVersion();
// Mine-when-idle state query
bool isIdleMiningActive() const { return idle_mining_active_; }
// Peers
const std::vector<PeerInfo>& getPeers() const { return state_.peers; }
const std::vector<BannedPeer>& getBannedPeers() const { return state_.bannedPeers; }
bool isPeerRefreshInProgress() const {
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Peers);
}
void banPeer(const std::string& ip, int duration_seconds = 86400);
void unbanPeer(const std::string& ip);
void clearBans();
@@ -271,89 +193,33 @@ public:
void unfavoriteAddress(const std::string& addr);
bool isAddressFavorite(const std::string& addr) const;
// Address metadata (labels, icons, custom ordering)
void setAddressLabel(const std::string& addr, const std::string& label);
void setAddressIcon(const std::string& addr, const std::string& icon);
std::string getAddressLabel(const std::string& addr) const;
std::string getAddressIcon(const std::string& addr) const;
int getAddressSortOrder(const std::string& addr) const;
void setAddressSortOrder(const std::string& addr, int order);
int getNextSortOrder() const;
void swapAddressOrder(const std::string& a, const std::string& b);
// Assign dense sort orders (0..N-1) to the given addresses in the given order and
// persist once. Used by drag-reorder so a drop always takes effect (even from the
// default un-ordered state, where a pairwise swap would be a no-op).
void reorderAddresses(const std::vector<std::string>& orderedAddrs);
bool isMiningAddress(const std::string& addr) const;
void setMiningAddress(const std::string& addr, bool mining);
void invalidateAddressValidationCache();
// Key export/import
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
// callback receives (keys, exportedCount, totalAddresses) so callers can detect a keyless/partial export.
void exportAllKeys(std::function<void(const std::string&, int, int)> callback);
void exportAllKeys(std::function<void(const std::string&)> callback);
void importPrivateKey(const std::string& key, std::function<void(bool, const std::string&)> callback);
// Wallet backup
void backupWallet(const std::string& destination, std::function<void(bool, const std::string&)> callback);
// Export the wallet's BIP39 seed phrase (z_exportmnemonic). The callback receives
// (ok, noMnemonic, phrase, error): ok+phrase on success; noMnemonic=true when the
// wallet's seed is not mnemonic-derived (legacy wallet). Full-node only; the phrase
// is a secret and is wiped after the callback returns.
void exportSeedPhrase(std::function<void(bool ok, bool noMnemonic,
const std::string& phrase,
const std::string& error)> callback);
// Transaction operations
void sendTransaction(const std::string& from, const std::string& to,
// Transaction operations
void sendTransaction(const std::string& from, const std::string& to,
double amount, double fee, const std::string& memo,
std::function<void(bool success, const std::string& result)> callback);
// Register a daemon async operation id (z_shieldcoinbase / z_mergetoaddress /
// auto-shield) with the shared opid poller so its eventual success/failure is
// surfaced and balances/transactions refresh on completion. z_sendmany uses the
// richer pending-send path internally; this is for operations with no optimistic
// transaction row of their own.
void trackOperation(const std::string& opid);
// Force refresh
void refreshNow();
void refreshMiningInfo();
void refreshPeerInfo();
void refreshMarketData();
// Fetch the live exchange/pair list from CoinGecko once per session (venues are
// near-static); populates state.market.exchanges. Safe to call every frame.
void refreshExchanges();
// Fetch historical USD price series (CoinGecko market_chart) that back the portfolio
// sparkline intervals; self-throttled to ~30 min. Safe to call every frame.
void refreshMarketChart();
/// @brief Per-category refresh intervals, adjusted by active tab
using RefreshIntervals = services::NetworkRefreshService::Intervals;
/// @brief Get recommended refresh intervals for a given page
static RefreshIntervals getIntervalsForPage(ui::NavPage page);
// UI navigation
void setCurrentPage(ui::NavPage page);
void setCurrentPage(ui::NavPage page) { current_page_ = 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)
void showImportKeyDialog() { show_import_key_ = true; }
void showExportKeyDialog() { show_export_key_ = true; }
void showBackupDialog() { show_backup_ = true; }
void showSeedBackupDialog() { show_seed_backup_ = true; }
void showSeedMigrationDialog(); // opens the migration modal (resumes a pending one at Sweep)
void showAboutDialog() { show_about_ = true; }
// Legacy tab compat — maps int to NavPage
@@ -373,27 +239,9 @@ public:
bool startEmbeddedDaemon();
void stopEmbeddedDaemon();
bool isEmbeddedDaemonRunning() const;
bool isUsingEmbeddedDaemon() const { return supportsEmbeddedDaemon() && use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use && supportsEmbeddedDaemon(); }
void rescanBlockchain(); // restart daemon with -rescan flag (full-history nodes)
// Runtime rescanblockchain RPC starting at a snapshot-available height. Unlike the
// -rescan restart, this works on bootstrapped/pruned nodes (which lack pre-snapshot
// block data), reconciling the wallet's stale spent-state without a daemon restart.
void runtimeRescan(int startHeight);
// Async binary-search probe for the lowest block height the node still has on disk.
// cb(ok, lowestHeight, fullHistory): fullHistory==true when genesis is present (a normal,
// non-bootstrapped node). Runs on the UI thread via the RPC worker callbacks.
void detectLowestAvailableBlockHeight(std::function<void(bool ok, int lowestHeight, bool fullHistory)> cb);
// Flag that a bootstrap just finished so the wallet auto-reconciles spent-state once the
// daemon is back up (consumed in update()).
void markPostBootstrapRescanPending() { post_bootstrap_rescan_pending_ = true; }
bool runtimeRescanActive() const { return runtime_rescan_active_; }
void repairWallet(); // restart daemon with -zapwallettxes=2 (wipe & rebuild wallet tx records)
void reinstallBundledDaemon(); // stop daemon, overwrite installed binary with the bundled one, restart
void deleteBlockchainData(); // stop daemon, delete chain data, restart fresh
bool stopDaemonForBootstrap(); // stop daemon + disconnect for bootstrap, returns true if was running
bool isBootstrapDownloading() const { return bootstrap_downloading_; }
void setBootstrapDownloading(bool v) { bootstrap_downloading_ = v; }
bool isUsingEmbeddedDaemon() const { return use_embedded_daemon_; }
void setUseEmbeddedDaemon(bool use) { use_embedded_daemon_ = use; }
void rescanBlockchain(); // restart daemon with -rescan flag
// Get daemon memory usage in MB (uses embedded daemon handle if available,
// falls back to platform-level process scan for external daemons)
@@ -408,8 +256,6 @@ public:
// Logo texture accessor (wallet branding icon)
ImTextureID getLogoTexture() const { return logo_tex_; }
int getLogoWidth() const { return logo_w_; }
int getLogoHeight() const { return logo_h_; }
// Coin logo texture accessor (DragonX currency icon for balance tab)
ImTextureID getCoinLogoTexture() const { return coin_logo_tex_; }
@@ -439,12 +285,6 @@ public:
// Wallet encryption helpers
void encryptWalletWithPassphrase(const std::string& passphrase);
// Post-encrypt daemon restart: the daemon shuts itself down after
// encryptwallet, so restart it off the main thread. Shared by the
// immediate and deferred encryption continuations. When
// announceRestartStatus is true, connection_status_ is updated first so
// the loading overlay explains the restart.
void restartDaemonAfterEncryption(const char* taskName, bool announceRestartStatus);
void unlockWallet(const std::string& passphrase, int timeout);
void lockWallet();
void changePassphrase(const std::string& oldPass, const std::string& newPass);
@@ -464,7 +304,10 @@ public:
void showChangePassphraseDialog() { show_change_passphrase_ = true; }
void showDecryptDialog() {
show_decrypt_dialog_ = true;
wallet_security_workflow_.reset();
decrypt_phase_ = 0; // passphrase entry
decrypt_step_ = 0;
decrypt_status_.clear();
decrypt_in_progress_ = false;
memset(decrypt_pass_buf_, 0, sizeof(decrypt_pass_buf_));
}
@@ -476,100 +319,8 @@ public:
/// @brief Check if RPC worker has queued results waiting to be processed
bool hasPendingRPCResults() const;
bool hasTransactionSendProgress() const { return send_progress_active_ || send_submissions_in_flight_ > 0 || !pending_opids_.empty(); }
std::string transactionSendProgressText() const;
std::string transactionRefreshProgressText() const;
// Copy a SECRET (seed phrase, private key) to the clipboard and arm an auto-clear: after a
// short delay the clipboard is wiped IF it still holds this secret (so we don't clobber
// something the user copied afterwards). Only a hash of the secret is retained, never the
// plaintext. Call pumpSecretClipboardClear() each frame to action the clear.
void copySecretToClipboard(const std::string& secret);
void pumpSecretClipboardClear();
bool isTransactionRefreshInProgress() const {
return network_refresh_.jobInProgress(services::NetworkRefreshService::Job::Transactions);
}
private:
friend class AppDaemonLifecycleRuntime;
friend class AppDaemonLifecycleTaskContext;
// Global keyboard-shortcut handling, dispatched once per frame from update().
void handleGlobalShortcuts();
bool sendStopCommandSafely(rpc::RPCClient& client, const char* context);
void maybeFinishTransactionSendProgress();
// Shared body of createNewZAddress/createNewTAddress (which are thin public forwarders).
// `shielded` selects the z_getnewaddress/getnewaddress RPC, the "shielded"/"transparent" type
// string, and the z_addresses/t_addresses target (the new AddressInfo is pushed into both that
// list and state_.addresses). Lite builds derive locally via the controller and early-return.
void createNewAddress(bool shielded, std::function<void(const std::string&)> callback);
void upsertPendingSendTransaction(const std::string& opid,
const std::string& from,
const std::string& to,
double amount,
const std::string& memo,
double fee = 0.0);
// Work around a dragonxd note-selection bug: its z_sendmany picks notes to cover the recipient
// total but not the miner fee, so a shielded send whose largest notes sum exactly to the amount
// fails with "Insufficient shielded funds, have H, need H+fee" despite ample balance. When a
// failed opid matches that (H >= the requested amount), re-issue the send once with a tiny
// self-output that lifts the daemon's selection target past the boundary so it grabs another
// note; the recipient still receives the exact amount. Returns true if a retry was issued.
bool maybeRetrySendForFeeGap(const std::string& opid, const std::string& rawMsg);
void resendWithFeeGapWorkaround(const std::string& from, const std::string& to,
double amount, double fee, const std::string& memo,
std::function<void(bool, const std::string&)> callback);
// Shared z_sendmany submit path for sendTransaction (single recipient, markFeeGapRetry=false)
// and resendWithFeeGapWorkaround (recipient + self-output, markFeeGapRetry=true). Owns the
// in-flight increment, the worker post + call, and the result closure (dirty flags, opid
// tracking, pending-send bookkeeping, callback delivery). Callers build `recipients` (and pass
// the recipient `to`/`amount`/`memo`/`fee` used to record the optimistic pending-send row).
// When markFeeGapRetry is set, the returned opid is recorded in send_feegap_retried_opids_ so a
// retry of a retry is reported as a real error.
void submitZSendMany(const std::string& from, const std::string& to, double amount, double fee,
const std::string& memo, const nlohmann::json& recipients,
const char* traceLabel, bool markFeeGapRetry,
std::function<void(bool, const std::string&)> callback);
void markPendingSendTransactionSucceeded(const std::string& opid,
const std::string& txid);
void removePendingSendTransactions(const std::vector<std::string>& opids,
bool restoreBalances);
// Apply a signed per-address balance delta for a pending send: walk z_addresses then
// t_addresses for `fromAddress` and clamp its balance at >=0. A positive `signedAmount`
// restores a debit; a negative one applies it. When `includeAggregates` is set, adjust the
// private/transparent bucket (chosen by the address's leading 'z') and totalBalance with the
// same clamp. Shared by the three pending-send delta sites so they can't drift.
void applyPendingSendDelta(const std::string& fromAddress, double signedAmount,
bool includeAggregates);
// Deliver a deferred z_sendmany result to its waiting UI callback once the opid
// reaches a terminal status. Returns true if a callback was registered (and fired).
bool invokeSendResultCallback(const std::string& opid, bool ok,
const std::string& result);
void applyPendingSendBalanceDeltas(bool includeAggregateBalances);
std::string transactionHistoryCacheWalletIdentity() const;
bool ensureTransactionHistoryCacheUnlockedFor(const std::string& walletIdentity);
void unlockTransactionHistoryCacheWithPassphrase(const std::string& passphrase);
// Shared main-thread continuations for a wallet unlock attempt, so the passphrase
// and PIN paths cannot drift. applyUnlockFailure applies the escalating lockout
// curve on every failed path (a PIN RPC failure used to skip it).
void applyUnlockSuccess(const std::string& passphrase, int timeout);
void applyUnlockFailure(const std::string& errorMessage);
// Clear all rescan + witness-rebuild progress/accumulator state. Shared by the four
// rescan-completion sites so a new witness field can't be forgotten in one copy.
void resetWitnessRescanProgress();
void loadTransactionHistoryCacheIfAvailable();
void storeTransactionHistoryCacheIfAvailable();
void wipePendingTransactionHistoryCachePassphrase();
void resetTransactionHistoryCacheSession();
void pruneShieldedHistoryScanProgress();
void invalidateShieldedHistoryScanProgress(bool persistCache);
// Auto-balance pool selection: drive the periodic hashrate refresh and apply a
// freshly-completed snapshot (weighted-random pick + optional miner restart).
void updatePoolAutoBalance();
void applyPoolAutoBalance(const util::PoolStatsService::Snapshot& snap);
// Subsystems
std::unique_ptr<rpc::RPCClient> rpc_;
std::unique_ptr<rpc::RPCWorker> worker_;
@@ -584,67 +335,8 @@ private:
rpc::ConnectionConfig saved_config_;
std::unique_ptr<config::Settings> settings_;
std::unique_ptr<wallet::LiteWalletController> lite_wallet_; // lite builds w/ linked backend
// Pending send_tab callback for an in-flight lite send (delivered in update() once the
// controller's async broadcast result arrives). Only one lite send runs at a time.
std::function<void(bool, const std::string&)> lite_send_callback_;
// One-shot guard: auto-open an existing lite wallet on the first update() tick (kept off
// init() so a slow initialize_existing network call doesn't freeze startup before the window).
bool lite_autoopen_done_ = false;
double lite_open_last_attempt_ = 0.0; // ImGui time of the last async open attempt (retry timer)
// Reason an existing lite wallet failed to auto-open (e.g. server unreachable). Surfaced in
// the UI so a stuck "disconnected" state isn't silent; cleared once a wallet opens.
std::string lite_open_error_;
// HushChat (experimental; gated by DRAGONX_ENABLE_CHAT — inert when OFF). App owns the chat
// service so the transaction-refresh harvest can decrypt incoming memos into threaded messages.
// The identity is derived from the wallet's OWN SDXLite-compatible seed phrase (full-node
// z_exportmnemonic / lite exportSeed → the same KDF), so it is portable across both variants.
chat::ChatService chat_service_;
chat::ChatDatabase chat_db_; // persistent backing (seed-derived encryption at rest)
bool chat_identity_provisioned_ = false; // identity set on the service this session
bool chat_identity_fetch_in_flight_ = false; // a z_exportmnemonic worker job is pending
bool chat_identity_unavailable_ = false; // provisioning failed definitively (e.g. non-mnemonic wallet)
// Provision the chat identity once the wallet seed is reachable+unlocked (per-tick, both
// variants); derives via deriveChatIdentityFromSecret and wipes the secret. No-op when the
// feature is off, already provisioned, in flight, or unavailable.
void maybeProvisionChatIdentity();
// One-time nudge: on a full-node wallet that has a mnemonic, remind the user (once per
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
void maybeRemindSeedBackup();
// Seed-wallet migration (Phase 1: create a new mnemonic wallet in isolation, no funds moved).
void beginCreateSeedWallet(); // starts the isolated create on a background thread
void pumpSeedMigration(); // main thread: pick up background progress/result each frame
// Phase 2: sweep all legacy funds into the new wallet, then adopt it as the primary wallet.
void refreshSeedMigrationBalance(); // query the legacy total (shown on the Sweep step)
void beginSweepToSeedWallet(); // z_mergetoaddress ["ANY_TADDR","ANY_ZADDR"] -> dest
void pollSweepStatus(); // Confirming step: poll sweep confirmations + legacy balance
void beginAdoptSeedWallet(); // stop daemon -> swap wallet.dat -> restart with -rescan
void provisionChatIdentityFromSecret(std::string secret);
std::string chatReplyZaddr(); // a stable (persisted) wallet z-addr for chat
std::string generateChatLocalId(const char* prefix, int numBytes) const; // unique echo id / cid
bool broadcastChatMemos(const chat::OutgoingChatMemos& memos); // returns true if submitted
bool broadcastChatMemosLite(const chat::OutgoingChatMemos& memos); // lite two-recipient send
void ingestLiteChatMemos(const wallet::LiteWalletAppRefreshModel& model); // lite chat receive harvest
// Lite first-run welcome prompt: dismissed for the session once the user picks an action.
bool lite_firstrun_dismissed_ = false;
// Lite send-time unlock: set to show the unlock modal when a spend is attempted while locked.
bool lite_unlock_prompt_ = false;
// One-shot: prompt to unlock on startup once we learn the auto-opened wallet is encrypted+locked.
bool lite_startup_lock_checked_ = false;
std::unique_ptr<daemon::DaemonController> daemon_controller_;
std::unique_ptr<daemon::EmbeddedDaemon> embedded_daemon_;
std::unique_ptr<daemon::XmrigManager> xmrig_manager_;
// Auto-balance runtime state (pool mining, full-node only). The service fetches
// pool hashrates off-thread; the RNG drives the weighted-random pick.
util::PoolStatsService pool_stats_service_;
std::mt19937 balance_rng_;
long long last_balance_eval_ms_ = 0; // steady-clock ms of the last refresh kick
bool balance_refresh_pending_ = false; // UI asked for an immediate refresh
bool balance_snapshot_seen_ = false; // the current in-flight snapshot was applied
bool exchanges_fetch_started_ = false; // once-per-session CoinGecko tickers fetch
bool chart_fetch_in_flight_ = false; // a market_chart history fetch is on the worker
util::AsyncTaskManager async_tasks_;
bool pending_antivirus_dialog_ = false; // Show Windows Defender help dialog
// Wallet state
@@ -653,22 +345,16 @@ private:
// Shutdown state
std::atomic<bool> shutting_down_{false};
std::atomic<bool> shutdown_complete_{false};
std::atomic<bool> refresh_in_progress_{false};
bool address_list_dirty_ = false; // P8: dedup rebuildAddressList
std::string shutdown_status_;
std::thread shutdown_thread_;
float shutdown_timer_ = 0.0f;
bool force_quit_confirm_ = false;
std::chrono::steady_clock::time_point shutdown_start_time_;
// Daemon restart (e.g. after changing debug log categories)
std::atomic<bool> daemon_restarting_{false};
// Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast
// and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it.
std::atomic<int> pending_delete_result_{-1};
// Encryption state check timeout
float encryption_check_timer_ = 0.0f;
std::thread daemon_restart_thread_;
// UI State
bool quit_requested_ = false;
@@ -678,46 +364,10 @@ private:
bool show_import_key_ = false;
bool show_export_key_ = false;
bool show_backup_ = false;
bool show_seed_backup_ = false;
// Seed-phrase backup dialog state. seed_backup_phrase_ holds a SECRET (the revealed
// mnemonic) and is wiped with sodium_memzero when the dialog closes.
std::string seed_backup_phrase_;
std::string seed_backup_status_;
bool seed_backup_fetch_started_ = false;
bool seed_backup_loading_ = false;
bool seed_backup_no_mnemonic_ = false;
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
// --- Seed-wallet migration (Phase 1: create; Phase 2: sweep + adopt) ---
enum class SeedMigrationStep { Intro, Working, ShowSeed, Sweep, Sweeping, Confirming, Adopting, Done, Error };
bool show_seed_migration_ = false;
SeedMigrationStep seed_migration_step_ = SeedMigrationStep::Intro;
bool seed_migration_in_flight_ = false; // main-thread guard while the bg create task runs
std::string seed_migration_seed_; // SECRET — the revealed phrase, wiped on close
std::string seed_migration_dest_; // new shielded z-address (Phase 2 sweep target)
std::string seed_migration_temp_dir_; // temp datadir root holding the new wallet (kept)
bool seed_migration_backed_up_ = false; // "I've written it down" confirmation
std::string seed_migration_status_; // main-thread display (progress / error text)
double seed_migration_balance_ = 0.0; // legacy total to sweep (shown on the Sweep step)
std::string seed_migration_sweep_txid_; // the z_mergetoaddress sweep transaction id
int seed_migration_sweep_confs_ = 0; // confirmations of the sweep tx (adopt gate: >= 1)
double seed_migration_legacy_remaining_ = -1.0; // legacy balance after sweep (-1 = unknown)
float seed_migration_poll_timer_ = 0.0f; // throttles the Confirming-step poll
bool seed_migration_confirm_in_flight_ = false; // guards the confirm poll
// Cross-thread handoff from the background create task (guarded by seed_migration_mutex_).
std::mutex seed_migration_mutex_;
std::string seed_migration_progress_; // latest progress line
bool seed_migration_done_ = false; // create result ready to consume
bool seed_migration_ok_ = false;
std::string seed_migration_r_seed_, seed_migration_r_dest_, seed_migration_r_tmp_, seed_migration_r_err_;
// Cross-thread handoff from the background adopt task (guarded by seed_migration_mutex_).
bool seed_migration_adopt_done_ = false;
bool seed_migration_adopt_ok_ = false;
std::string seed_migration_adopt_err_;
bool show_address_book_ = false;
// Embedded daemon state
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
bool use_embedded_daemon_ = true;
std::string daemon_status_;
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)
@@ -734,16 +384,6 @@ private:
// Connection
std::string connection_status_ = "Disconnected";
bool connection_in_progress_ = false;
bool remote_rpc_plaintext_warning_shown_ = false;
// Startup daemon-launch diagnostics: bound the "RPC port busy, no config" wait before warning,
// and show the embedded-daemon start failure (binary/params/spawn) only once. Reset on connect.
int daemon_wait_attempts_ = 0;
bool daemon_start_error_shown_ = false;
int daemon_last_seen_crashes_ = 0; // surface each new embedded-daemon crash reason once
bool refresh_policy_syncing_ = false; // whether the sync-throttle refresh profile is active
// Auto-clear for secrets copied to the clipboard. Only a hash of the copied secret is kept.
std::uint64_t clipboard_secret_hash_ = 0;
double clipboard_clear_deadline_ = 0.0;
float loading_timer_ = 0.0f; // spinner animation for loading overlay
// Current page (sidebar navigation)
@@ -751,21 +391,6 @@ private:
ui::NavPage prev_page_ = ui::NavPage::Overview;
float page_alpha_ = 1.0f; // 0→1 fade on page switch
bool sidebar_collapsed_ = false; // true = icon-only mode
// Debug screenshot sweep state.
bool screenshot_sweep_active_ = false;
bool sweep_capture_this_frame_ = false;
int sweep_skin_idx_ = 0;
int sweep_page_idx_ = 0;
int sweep_settle_frames_ = 0; // frames to let a new skin/page settle before capturing
std::vector<std::string> sweep_skins_; // skin ids to cycle
std::vector<ui::NavPage> sweep_pages_; // enabled pages to cycle
std::string sweep_dir_; // output folder for this sweep
std::string sweep_current_path_; // PNG path for the frame about to be captured
std::string sweep_saved_skin_; // restore on completion
ui::NavPage sweep_saved_page_ = ui::NavPage::Overview;
void updateScreenshotSweep(); // called at the top of render() while active
void applySweepTarget(); // apply current (skin,page), compute path, arm settle
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width
@@ -786,9 +411,8 @@ private:
int coin_logo_h_ = 0;
bool coin_logo_loaded_ = false;
// Console tab + its backend executor (full-node RPC or lite backend), created lazily.
// Console tab
ui::ConsoleTab console_tab_;
std::unique_ptr<ui::ConsoleCommandExecutor> console_exec_;
// Pending payment from URI
bool pending_payment_valid_ = false;
@@ -797,124 +421,51 @@ private:
std::string pending_memo_;
std::string pending_label_;
// Per-category refresh timers, policy, and worker queue guards.
services::NetworkRefreshService network_refresh_;
// Timers (in seconds since last update)
float refresh_timer_ = 0.0f;
float price_timer_ = 0.0f;
float fast_refresh_timer_ = 0.0f; // For mining stats
// Refresh intervals (seconds)
static constexpr float REFRESH_INTERVAL = 5.0f;
static constexpr float PRICE_INTERVAL = 60.0f;
static constexpr float FAST_REFRESH_INTERVAL = 1.0f;
// Mining refresh guard (prevents worker queue pileup)
std::atomic<bool> mining_refresh_in_progress_{false};
int mining_slow_counter_ = 0; // counts fast ticks; fires slow refresh every N
// Mining toggle guard (prevents concurrent setgenerate calls)
std::atomic<bool> mining_toggle_in_progress_{false};
// True from a successful startPoolMining() until the miner is confirmed connected/hashing in the
// poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay).
std::atomic<bool> pool_starting_{false};
// Auto-shield guard (prevents concurrent auto-shield operations)
std::atomic<bool> auto_shield_pending_{false};
// P4: Incremental transaction cache
int last_tx_block_height_ = -1; // block height at last full tx fetch
static constexpr int MAX_VIEWTX_PER_CYCLE = 25; // cap z_viewtransaction calls per refresh
std::size_t shielded_history_scan_cursor_ = 0;
bool shielded_history_scan_pending_ = false;
// False until the first full shielded-history scan finishes. Drives the History tab's
// "Loading older history…" progress so the user knows transactions are still streaming in
// after the first batch appears; goes quiet for the routine per-block re-scans afterward.
bool initial_history_scan_complete_ = false;
std::unordered_map<std::string, int> shielded_history_scan_heights_;
// P4b: z_viewtransaction result cache — avoids re-calling the RPC for
// txids we've already enriched. Keyed by txid.
using ViewTxCacheEntry = services::NetworkRefreshService::TransactionViewCacheEntry;
services::NetworkRefreshService::TransactionViewCache viewtx_cache_;
// P4c: Confirmed transaction cache — deeply-confirmed txns (>= 10 confs)
// are accumulated here and reused across refresh cycles. Only
// recent/unconfirmed txns are re-fetched from the daemon each time.
std::vector<TransactionInfo> confirmed_tx_cache_;
std::unordered_set<std::string> confirmed_tx_ids_; // fast lookup
int confirmed_cache_block_ = -1; // block height when cache was last built
// Dirty flags for demand-driven refresh
bool addresses_dirty_ = true; // true → refreshAddresses() will run
bool address_validation_cache_dirty_ = true;
bool transactions_dirty_ = false; // true → force tx refresh regardless of block height
bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect
bool rescan_status_poll_in_progress_ = false;
// True once we've actually observed the rescan running (daemon restarted into -rescan warmup).
// Gates the "rescan complete" detection so a getrescaninfo poll that hits the still-running
// pre-restart daemon (which reports rescanning=false) can't fire a false "complete" instantly.
bool rescan_confirmed_active_ = false;
// A runtime rescanblockchain RPC is in flight (vs the -rescan daemon restart). While set,
// the per-second mining/rescan-status pollers are suppressed (the daemon holds cs_main for
// the whole scan and would block them); completion is signalled by the rescan RPC callback.
bool runtime_rescan_active_ = false;
// Set when a bootstrap completes; consumed once the daemon is connected to auto-run a rescan
// that reconciles the preserved wallet.dat against the freshly-imported chain.
bool post_bootstrap_rescan_pending_ = false;
// Largest "blocks remaining" seen during the current witness-rebuild phase. The daemon's
// "Building Witnesses for block" fraction resets every call (it's re-invoked per connected
// block, each walking from its own start height to the tip), so we derive a stable, monotonic
// overall percentage from how far "remaining" has fallen below this peak. Reset per phase.
int witness_rebuild_total_blocks_ = 0;
// The daemon's primary witness signal is "Setting Initial Sapling Witness for tx <hash>, <i>
// of <N>", logged once per wallet tx as its initial witness is set. The <i> is the tx's slot in
// an UNORDERED map, so it bounces wildly (was the cause of the resetting progress). The honest
// monotonic metric is how many DISTINCT txs have been witnessed (the set only grows; it also
// dedups the daemon's occasional double-prints) over the reported total N.
std::unordered_set<std::string> witness_seen_txids_;
int witness_total_txs_ = 0;
bool opid_poll_in_progress_ = false;
// Consecutive Core-refresh cycles where BOTH core RPCs failed → likely a dead
// connection. After kCoreFailuresBeforeDisconnect, tear down and reconnect.
int consecutive_core_failures_ = 0;
// Pending z_sendmany operation tracking
bool send_progress_active_ = false;
int send_submissions_in_flight_ = 0;
std::vector<std::string> pending_opids_; // opids to poll for completion
struct PendingSendInfo {
std::string from;
std::string to;
std::string memo;
double amount = 0.0;
double fee = 0.0;
std::int64_t timestamp = 0;
};
std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
// Opids issued as a fee-gap auto-retry (see maybeRetrySendForFeeGap). Tracked so a retry that
// fails again is reported to the user instead of looping.
std::unordered_set<std::string> send_feegap_retried_opids_;
// z_sendmany UI callbacks held until the opid reaches a terminal status, so the
// user isn't told "sent successfully" before the tx is actually built/broadcast.
std::unordered_map<std::string, std::function<void(bool, const std::string&)>>
pending_send_callbacks_;
// Txids from completed z_sendmany operations.
// Ensures shielded sends are discoverable by z_viewtransaction
// even when they don't appear in listtransactions or
// z_listreceivedbyaddress.
std::unordered_set<std::string> send_txids_;
// First-run wizard state
WizardPhase wizard_phase_ = WizardPhase::None;
std::unique_ptr<util::Bootstrap> bootstrap_;
bool bootstrap_downloading_ = false; // true while settings bootstrap dialog is active
std::string wizard_pending_passphrase_; // held until daemon connects
std::string wizard_saved_passphrase_; // held until PinSetup completes/skipped
// Wallet security flow state shared by wizard/settings encryption paths.
services::WalletSecurityController wallet_security_;
services::WalletSecurityWorkflow wallet_security_workflow_;
// Deferred encryption (wizard background task)
std::string deferred_encrypt_passphrase_;
std::string deferred_encrypt_pin_;
bool deferred_encrypt_pending_ = false;
// Wizard: stopping an external daemon before bootstrap
bool wizard_stopping_external_ = false;
std::string wizard_stop_status_;
std::thread wizard_stop_thread_;
// PIN vault
std::unique_ptr<util::SecureVault> vault_;
data::TransactionHistoryCache transaction_history_cache_;
data::AddressBook address_book_; // shared contact store; loaded once in init(), self-saves on mutation
std::string pending_transaction_history_cache_passphrase_;
bool transaction_history_cache_loaded_ = false;
// Lock screen state
bool lock_screen_was_visible_ = false; // tracks lock→unlock transitions for auto-focus
@@ -956,7 +507,11 @@ private:
// Decrypt wallet dialog state
bool show_decrypt_dialog_ = false;
int decrypt_phase_ = 0; // 0=passphrase, 1=working, 2=done, 3=error
int decrypt_step_ = 0; // 0=unlock, 1=export, 2=stop, 3=rename, 4=restart, 5=import
char decrypt_pass_buf_[256] = {};
std::string decrypt_status_;
bool decrypt_in_progress_ = false;
// Wizard PIN setup state
char wizard_pin_buf_[16] = {};
@@ -966,19 +521,12 @@ private:
// Auto-lock on idle
std::chrono::steady_clock::time_point last_interaction_ = std::chrono::steady_clock::now();
// Mine-when-idle: auto-start/stop mining based on system idle state
bool idle_mining_active_ = false; // true when mining was auto-started by idle detection
bool idle_scaled_to_idle_ = false; // true when threads have been scaled up for idle
// Private methods - rendering
void renderStatusBar();
void renderLiteFirstRunPrompt(); // lite-only welcome modal when no wallet exists yet
void renderLiteUnlockPrompt(); // lite-only send-time unlock modal
void renderAboutDialog();
void renderImportKeyDialog();
void renderExportKeyDialog();
void renderBackupDialog();
void renderSeedBackupDialog(); // full-node "Back up seed phrase" modal (z_exportmnemonic)
void renderSeedMigrationDialog(); // "Migrate to a seed wallet" guided modal (Phase 1: create)
void renderFirstRunWizard();
void renderLockScreen();
void renderEncryptWalletDialog();
@@ -991,36 +539,15 @@ private:
void tryConnect();
void onConnected();
void onDisconnected(const std::string& reason);
// Set the "node is initializing" UI state (status line + overlay description) from the
// embedded/external daemon's launch state and its own console output (current phase + block
// height), so a connect probe that times out while the daemon loads shows WHAT it's doing.
// `reachableButBusy` is true when the probe connected but got no RPC reply (a timeout),
// false when the daemon is merely launching (not bound yet). Returns the status title.
std::string applyDaemonInitStatus(bool reachableButBusy);
// Tear down a connection that died mid-session (daemon crash / restart / dropped
// socket) so update()'s reconnect branch re-enters tryConnect(). Unlike onDisconnected
// alone, this also rpc_->disconnect()s so rpc_->isConnected() actually flips to false.
void handleLostConnection(const std::string& reason);
void applyDefaultBanlist();
// Private methods - data refresh
void refreshData(); // Orchestrator: dispatches per-category refreshes
void refreshCoreData(); // Balance + blockchain info (can use fast_worker_)
void refreshAddressData(); // Address lists + balances
void refreshTransactionData(); // Transaction list + z_viewtransaction enrichment
void refreshRecentTransactionData(); // Lightweight recent/unconfirmed tx poll
bool refreshEncryptionState(); // Wallet encryption/lock state
void refreshBalance(); // Legacy: balance-only refresh (used by specific callers)
void refreshAddresses(); // Legacy: standalone address refresh
void refreshData();
void refreshBalance();
void refreshAddresses();
void refreshTransactions();
void refreshPrice();
void refreshWalletEncryptionState();
void applyRefreshPolicy(ui::NavPage page);
bool currentPageNeedsWalletDataRefresh() const;
bool shouldRunWalletTransactionRefresh() const;
bool shouldRefreshTransactions() const;
bool shouldRefreshRecentTransactions() const;
void checkAutoLock();
void checkIdleMining();
};
} // namespace dragonx

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,6 @@
#include "rpc/rpc_worker.h"
#include "rpc/connection.h"
#include "config/settings.h"
#include "daemon/daemon_controller.h"
#include "daemon/embedded_daemon.h"
#include "ui/notifications.h"
#include "ui/material/color_theme.h"
@@ -40,48 +39,13 @@ namespace dragonx {
using json = nlohmann::json;
namespace {
struct WizardLowSpecSnapshot {
bool valid = false;
float blur = 0.0f;
float uiOp = 0.0f;
bool fx = false;
bool scanline = false;
};
struct WizardUiState {
float blur_amount = 1.5f;
bool theme_effects = true;
float ui_opacity = 1.0f;
bool low_spec = false;
bool scanline = true;
std::string balance_layout = "classic";
int language_index = 0;
bool appearance_init = false;
WizardLowSpecSnapshot low_spec_snapshot;
float card0_max_h = 0.0f;
float card1_max_h = 0.0f;
double external_last_check = -10.0;
bool daemon_prestarted = false;
};
WizardUiState s_wizardUi;
} // namespace
void App::restartWizard()
{
if (!supportsFullNodeLifecycleActions()) {
ui::Notifications::instance().warning("Lite wallet lifecycle requests are available from Settings as dry-run readiness checks");
return;
}
DEBUG_LOGF("[App] Restarting setup wizard — stopping daemon...\n");
// Reset crash counter for fresh wizard attempt
if (daemon_controller_) {
daemon_controller_->resetCrashCount();
if (embedded_daemon_) {
embedded_daemon_->resetCrashCount();
}
// Disconnect RPC
@@ -92,11 +56,10 @@ void App::restartWizard()
// Stop the embedded daemon in a background thread to avoid
// blocking the UI for up to 32 seconds (RPC stop + process wait).
if (daemon_controller_ && isEmbeddedDaemonRunning()) {
async_tasks_.submit("wizard-restart-stop-daemon", [this](const util::AsyncTaskManager::Token& token) {
if (token.cancelled()) return;
if (embedded_daemon_ && isEmbeddedDaemonRunning()) {
std::thread([this]() {
stopEmbeddedDaemon();
});
}).detach();
}
// Enter wizard — the wizard completion handler already calls
@@ -110,7 +73,6 @@ void App::restartWizard()
// ===========================================================================
void App::renderFirstRunWizard() {
auto& wizardUi = s_wizardUi;
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
@@ -132,6 +94,21 @@ void App::renderFirstRunWizard() {
ImU32 bgCol = ui::material::Surface();
dl->AddRectFilled(winPos, ImVec2(winPos.x + winSize.x, winPos.y + winSize.y), bgCol);
// Handle Done/None — wizard complete
if (wizard_phase_ == WizardPhase::Done || wizard_phase_ == WizardPhase::None) {
wizard_phase_ = WizardPhase::None;
if (!state_.connected) {
if (isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
startEmbeddedDaemon();
}
tryConnect();
}
settings_->setWizardCompleted(true);
settings_->save();
ImGui::End();
return;
}
// --- Determine which of the 3 masonry sections is focused ---
// 0 = Appearance, 1 = Bootstrap, 2 = Encrypt + PIN
int focusIdx = 0;
@@ -281,14 +258,15 @@ void App::renderFirstRunWizard() {
(textCol & 0x00FFFFFF) | IM_COL32(0,0,0,40), 1.0f * dp);
cy += 14.0f * dp;
float& wiz_blur_amount = wizardUi.blur_amount;
bool& wiz_theme_effects = wizardUi.theme_effects;
float& wiz_ui_opacity = wizardUi.ui_opacity;
bool& wiz_low_spec = wizardUi.low_spec;
bool& wiz_scanline = wizardUi.scanline;
std::string& wiz_balance_layout = wizardUi.balance_layout;
int& wiz_language_index = wizardUi.language_index;
bool& wiz_appearance_init = wizardUi.appearance_init;
// Statics for appearance settings
static float wiz_blur_amount = 1.5f;
static bool wiz_theme_effects = true;
static float wiz_ui_opacity = 1.0f;
static bool wiz_low_spec = false;
static bool wiz_scanline = true;
static std::string wiz_balance_layout = "classic";
static int wiz_language_index = 0;
static bool wiz_appearance_init = false;
if (!wiz_appearance_init) {
wiz_blur_amount = settings_->getBlurMultiplier();
wiz_theme_effects = settings_->getThemeEffectsEnabled();
@@ -435,7 +413,7 @@ void App::renderFirstRunWizard() {
// --- Low-spec mode checkbox ---
// Snapshot for restoring settings when low-spec is turned off
WizardLowSpecSnapshot& wiz_lsSnap = wizardUi.low_spec_snapshot;
static struct { bool valid; float blur; float uiOp; bool fx; bool scanline; } wiz_lsSnap = {};
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
@@ -611,7 +589,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Continue##app", ImVec2(btnW, btnH))) {
if (ImGui::Button("Continue##app", ImVec2(btnW, btnH))) {
// Save appearance choices, advance to Bootstrap
settings_->setAcrylicEnabled(wiz_blur_amount > 0.001f);
settings_->setAcrylicQuality(wiz_blur_amount > 0.001f
@@ -633,7 +611,7 @@ void App::renderFirstRunWizard() {
cy += cardPad;
// Lock card height to the tallest content ever seen
float& card0MaxH = wizardUi.card0_max_h;
static float card0MaxH = 0.0f;
card0MaxH = std::max(card0MaxH, cy - card0Top);
card0Bot = card0Top + card0MaxH;
@@ -695,13 +673,8 @@ void App::renderFirstRunWizard() {
} else {
auto prog = bootstrap_->getProgress();
const char* statusTitle;
if (prog.state == util::Bootstrap::State::Downloading)
statusTitle = "Downloading bootstrap...";
else if (prog.state == util::Bootstrap::State::Verifying)
statusTitle = "Verifying checksums...";
else
statusTitle = "Extracting blockchain data...";
const char* statusTitle = (prog.state == util::Bootstrap::State::Downloading)
? "Downloading bootstrap..." : "Extracting blockchain data...";
dl->AddText(bodyFont, bodyFont->LegacySize, ImVec2(cx, cy), textCol, statusTitle);
cy += bodyFont->LegacySize + 12.0f * dp;
@@ -733,28 +706,6 @@ void App::renderFirstRunWizard() {
dimCol, "(wallet.dat is protected)");
cy += captionFont->LegacySize + 6.0f * dp;
}
// Daemon status indicator
{
bool daemonUp = isEmbeddedDaemonRunning();
const std::string& dStatus = getDaemonStatus();
ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200) // green
: IM_COL32(120, 120, 120, 160); // gray
if (dStatus.find("Stopping") != std::string::npos)
dotCol = IM_COL32(255, 167, 38, 200); // orange
float dotR = 3.5f * dp;
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
dotR, dotCol);
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
? "Daemon stopping..."
: "Daemon running")
: "Daemon stopped";
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
cy += captionFont->LegacySize + 6.0f * dp;
}
cy += 12.0f * dp;
// Cancel button
@@ -763,7 +714,7 @@ void App::renderFirstRunWizard() {
float cancelBX = rightX + (colW - cancelW) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(cancelBX, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Cancel##bs", ImVec2(cancelW, cancelH))) {
if (ImGui::Button("Cancel##bs", ImVec2(cancelW, cancelH))) {
bootstrap_->cancel();
}
ImGui::PopStyleVar();
@@ -774,8 +725,6 @@ void App::renderFirstRunWizard() {
auto finalProg = bootstrap_->getProgress();
if (finalProg.state == util::Bootstrap::State::Completed) {
bootstrap_.reset();
// Reconcile the preserved wallet.dat against the new chain once the daemon is up.
markPostBootstrapRescanPending();
wizard_phase_ = WizardPhase::EncryptOffer;
} else {
wizard_phase_ = WizardPhase::BootstrapFailed;
@@ -811,22 +760,18 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton("Retry##bs", ImVec2(btnW2, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
stopDaemonForBootstrap();
if (ImGui::Button("Retry##bs", ImVec2(btnW2, btnH2))) {
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Skip##bsfail", ImVec2(btnW2, btnH2))) {
if (ImGui::Button("Skip##bsfail", ImVec2(btnW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -842,23 +787,17 @@ void App::renderFirstRunWizard() {
if (isFocused) {
static std::atomic<bool> s_extCached{false};
static std::atomic<bool> s_checkInFlight{false};
double& s_extLastCheck = wizardUi.external_last_check;
static double s_extLastCheck = -10.0;
double now = ImGui::GetTime();
if (now - s_extLastCheck >= 2.0 && !s_checkInFlight.load()) {
s_extLastCheck = now;
bool embeddedRunning = isEmbeddedDaemonRunning();
s_checkInFlight.store(true);
async_tasks_.submit("wizard-external-daemon-check", [embeddedRunning](const util::AsyncTaskManager::Token& token) {
if (token.cancelled()) {
s_checkInFlight.store(false);
return;
}
std::thread([embeddedRunning]() {
bool inUse = daemon::EmbeddedDaemon::isRpcPortInUse();
if (!token.cancelled()) {
s_extCached.store(inUse && !embeddedRunning);
}
s_extCached.store(inUse && !embeddedRunning);
s_checkInFlight.store(false);
});
}).detach();
}
externalRunning = s_extCached.load();
}
@@ -896,22 +835,22 @@ void App::renderFirstRunWizard() {
IM_COL32(220, 60, 60, 255)));
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 255, 255));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
if (ImGui::Button("Stop Daemon##wiz", ImVec2(stopW, btnH2))) {
wizard_stopping_external_ = true;
wizard_stop_status_ = "Sending stop command...";
async_tasks_.submit("wizard-stop-external-daemon", [this](const util::AsyncTaskManager::Token& token) {
if (wizard_stop_thread_.joinable()) wizard_stop_thread_.join();
wizard_stop_thread_ = std::thread([this]() {
auto config = rpc::Connection::autoDetectConfig();
if (!config.rpcuser.empty() && !config.rpcpassword.empty()) {
auto tmp_rpc = std::make_unique<rpc::RPCClient>();
if (tmp_rpc->connect(config.host, config.port,
config.rpcuser, config.rpcpassword,
config.use_tls)) {
sendStopCommandSafely(*tmp_rpc, "Wizard external daemon stop");
config.rpcuser, config.rpcpassword)) {
try { tmp_rpc->call("stop"); } catch (...) {}
tmp_rpc->disconnect();
}
}
wizard_stop_status_ = "Waiting for daemon to shut down...";
for (int i = 0; i < 60 && !token.cancelled(); i++) {
for (int i = 0; i < 60; i++) {
std::this_thread::sleep_for(std::chrono::seconds(1));
if (!daemon::EmbeddedDaemon::isRpcPortInUse()) {
wizard_stop_status_ = "Daemon stopped.";
@@ -919,7 +858,6 @@ void App::renderFirstRunWizard() {
return;
}
}
if (token.cancelled()) return;
wizard_stop_status_ = "Daemon did not stop — try manually.";
wizard_stopping_external_ = false;
});
@@ -929,7 +867,7 @@ void App::renderFirstRunWizard() {
ImGui::SetCursorScreenPos(ImVec2(bx + stopW + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Skip##extd", ImVec2(skipW2, btnH2))) {
if (ImGui::Button("Skip##extd", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -951,88 +889,38 @@ void App::renderFirstRunWizard() {
ImU32 warnCol = (textCol & 0x00FFFFFF) | ((ImU32)(255 * warnOpacity) << 24);
float iw = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_WARNING).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx, cy), warnCol, ICON_MD_WARNING);
const char* twText = "Only use bootstrap.dragonx.is or bootstrap2.dragonx.is. Using files from untrusted sources could compromise your node.";
const char* twText = "Only use bootstrap.dragonx.is. Using files from untrusted sources could compromise your node.";
float twWrap = contentW - iw - 4.0f * dp;
ImVec2 twSize = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, twWrap, twText);
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx + iw + 4.0f * dp, cy), warnCol, twText, nullptr, twWrap);
cy += twSize.y + 12.0f * dp;
}
// Daemon status indicator (subtle, before buttons)
{
bool daemonUp = isEmbeddedDaemonRunning();
const std::string& dStatus = getDaemonStatus();
ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200)
: IM_COL32(120, 120, 120, 160);
if (dStatus.find("Stopping") != std::string::npos)
dotCol = IM_COL32(255, 167, 38, 200);
float dotR = 3.5f * dp;
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
dotR, dotCol);
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
? "Daemon stopping..."
: "Daemon running")
: "Daemon stopped";
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
cy += captionFont->LegacySize + 10.0f * dp;
}
// Buttons (only when focused)
if (isFocused) {
float dlBtnW = 150.0f * dp;
float mirrorW = 150.0f * dp;
float dlBtnW = 180.0f * dp;
float skipW2 = 80.0f * dp;
float btnH2 = 40.0f * dp;
float totalBW = dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp + skipW2;
float totalBW = dlBtnW + 12.0f * dp + skipW2;
float bx = rightX + (colW - totalBW) * 0.5f;
// --- Download button (main / Cloudflare) ---
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Primary()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton("Download##bs", ImVec2(dlBtnW, btnH2))) {
// Stop embedded daemon before bootstrap to avoid chain data corruption
stopDaemonForBootstrap();
if (ImGui::Button("Download##bs", ImVec2(dlBtnW, btnH2))) {
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
// --- Mirror Download button ---
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp, cy));
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(ui::material::Surface()));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton("Mirror##bs_mirror", ImVec2(mirrorW, btnH2))) {
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
bootstrap_->start(dataDir, mirrorUrl);
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered()) {
ui::material::Tooltip("Download from mirror (bootstrap2.dragonx.is).\nUse this if the main download is slow or failing.");
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
// --- Skip button ---
ImGui::SetCursorScreenPos(ImVec2(bx + dlBtnW + 8.0f * dp + mirrorW + 8.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Skip##bs", ImVec2(skipW2, btnH2))) {
if (ImGui::Button("Skip##bs", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
}
ImGui::PopStyleVar();
@@ -1043,7 +931,7 @@ void App::renderFirstRunWizard() {
cy += cardPad;
// Lock card height to the tallest content ever seen (but not when collapsed)
float& card1MaxH = wizardUi.card1_max_h;
static float card1MaxH = 0.0f;
if (isCollapsed) {
card1Bot = card1Top + (cy - card1Top);
} else {
@@ -1068,7 +956,7 @@ void App::renderFirstRunWizard() {
// Pre-start daemon when encrypt card becomes focused so it's ready
// by the time the user finishes typing their passphrase
if (isFocused) {
bool& wiz_daemon_prestarted = wizardUi.daemon_prestarted;
static bool wiz_daemon_prestarted = false;
if (!wiz_daemon_prestarted) {
wiz_daemon_prestarted = true;
if (!state_.connected && isUsingEmbeddedDaemon() && !isEmbeddedDaemonRunning()) {
@@ -1127,7 +1015,7 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(ui::material::PrimaryVariant()));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Continue##encok", ImVec2(btnW2, btnH2))) {
if (ImGui::Button("Continue##encok", ImVec2(btnW2, btnH2))) {
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
@@ -1285,24 +1173,10 @@ void App::renderFirstRunWizard() {
cy += captionFont->LegacySize + 6.0f * dp;
}
// Warn + block if the passphrase has leading/trailing whitespace. Silently trimming it would
// change the passphrase the user believes they set and lock them out on the next unlock.
bool passEdgeSpace = false;
if (size_t pl = strlen(encrypt_pass_buf_)) {
char a = encrypt_pass_buf_[0], b = encrypt_pass_buf_[pl - 1];
passEdgeSpace = (a == ' ' || a == '\t' || b == ' ' || b == '\t');
}
if (passEdgeSpace) {
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
ui::material::Error(), "Passphrase has leading/trailing spaces — remove them");
cy += captionFont->LegacySize + 6.0f * dp;
}
// Buttons
{
bool passValid = strlen(encrypt_pass_buf_) >= 8 &&
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0 &&
!passEdgeSpace;
strcmp(encrypt_pass_buf_, encrypt_confirm_buf_) == 0;
// PIN is optional: if entered, must be valid + confirmed
std::string pinStr(wizard_pin_buf_);
bool pinEntered = !pinStr.empty();
@@ -1324,11 +1198,12 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!canEncrypt);
if (ui::material::TactileButton("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
if (ImGui::Button("Encrypt & Continue##wiz", ImVec2(encBtnW, btnH2))) {
// Save passphrase + optional PIN for background processing
wallet_security_.beginDeferredEncryption(
std::string(encrypt_pass_buf_),
(pinEntered && pinOk) ? pinStr : std::string());
deferred_encrypt_passphrase_ = std::string(encrypt_pass_buf_);
if (pinEntered && pinOk)
deferred_encrypt_pin_ = pinStr;
deferred_encrypt_pending_ = true;
// Clear sensitive buffers
memset(encrypt_pass_buf_, 0, sizeof(encrypt_pass_buf_));
@@ -1338,10 +1213,7 @@ void App::renderFirstRunWizard() {
// Start daemon + finish wizard immediately
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
if (!startEmbeddedDaemon()) {
ui::Notifications::instance().warning(
TR("wizard_daemon_start_failed"));
}
startEmbeddedDaemon();
}
tryConnect();
wizard_phase_ = WizardPhase::Done;
@@ -1355,25 +1227,14 @@ void App::renderFirstRunWizard() {
ImGui::SetCursorScreenPos(ImVec2(bx + encBtnW + 12.0f * dp, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton("Skip##enc", ImVec2(skipW2, btnH2))) {
static bool s_skipEncConfirm = false;
if (!s_skipEncConfirm) {
// Skipping stores private keys UNENCRYPTED — require a confirming second click.
s_skipEncConfirm = true;
encrypt_status_ = "Continue WITHOUT encryption? Keys will be stored unencrypted — click Skip again to confirm.";
} else {
s_skipEncConfirm = false;
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
if (!startEmbeddedDaemon()) {
ui::Notifications::instance().warning(
TR("wizard_daemon_start_failed"));
}
}
tryConnect();
if (ImGui::Button("Skip##enc", ImVec2(skipW2, btnH2))) {
wizard_phase_ = WizardPhase::Done;
settings_->setWizardCompleted(true);
settings_->save();
if (!isEmbeddedDaemonRunning() && isUsingEmbeddedDaemon()) {
startEmbeddedDaemon();
}
tryConnect();
}
ImGui::PopStyleVar();
cy += btnH2;

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,12 +4,8 @@
#pragma once
#include <algorithm>
#include <cstddef>
#include <map>
#include <string>
#include <set>
#include <vector>
namespace dragonx {
namespace config {
@@ -55,51 +51,6 @@ public:
*/
static std::string getDefaultPath();
enum class LiteServerSelectionPreferenceMode {
Sticky,
Random
};
// Pool selection mode for the mining tab: Manual (user picks the pool) or
// AutoBalance (the wallet spreads miners across the official pools by hashrate).
enum class PoolSelectMode {
Manual,
AutoBalance
};
struct LiteServerPreference {
std::string url;
std::string label;
bool enabled = true;
};
// A user-defined portfolio entry: a custom label tied to a group of wallet addresses.
// The Market tab's portfolio card sums these addresses' balances under the label.
struct PortfolioEntry {
std::string label;
std::vector<std::string> addresses;
std::string icon; // project_icons wallet-icon name; empty = no icon
unsigned int color = 0; // packed IM_COL32 accent; 0 = theme default
int outlineOpacity = 25; // accent-outline opacity, percent (0-100)
// Per-group price data. priceBasis: 0 = live market (USD), 1 = live market (BTC),
// 2 = DRGX only (no fiat value), 3 = manual price. Defaults preserve prior behavior
// (show DRGX + USD value).
int priceBasis = 0;
double manualPrice = 0.0; // price per DRGX for the Manual basis
std::string manualCurrency = "USD";
bool showDrgx = true; // show the DRGX amount on the card
bool showValue = true; // show the converted/fiat value on the card
bool show24h = false; // show the 24h % change (live-market bases only)
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history)
// Dashboard grid placement on the Market portfolio. col/row in fine ~32px square cells
// (-1 = auto-place), w/h in cells (span). Set when the user drags/resizes a card.
int gridCol = -1;
int gridRow = -1;
int gridW = 8; // default group width (cells); minimum is 8
int gridH = 3; // default group height (cells); minimum is 2, card design adapts
};
// Theme
std::string getTheme() const { return theme_; }
void setTheme(const std::string& theme) { theme_ = theme; }
@@ -108,12 +59,6 @@ public:
std::string getSkinId() const { return skin_id_; }
void setSkinId(const std::string& id) { skin_id_ = id; }
// Stable z-address chosen for HushChat: the reply-to address in outgoing headers, and (for
// legacy/non-mnemonic wallets) the source of the seed-derived chat identity. Persisted so the
// identity + reply address don't shift when new addresses are generated.
std::string getChatReplyZaddr() const { return chat_reply_zaddr_; }
void setChatReplyZaddr(const std::string& z) { chat_reply_zaddr_ = z; }
// Privacy
bool getSaveZtxs() const { return save_ztxs_; }
void setSaveZtxs(bool save) { save_ztxs_ = save; }
@@ -194,75 +139,10 @@ public:
void unfavoriteAddress(const std::string& addr) { favorite_addresses_.erase(addr); }
int getFavoriteAddressCount() const { return (int)favorite_addresses_.size(); }
// Address metadata (labels, icons, custom ordering)
struct AddressMeta {
std::string label;
std::string icon; // material icon name, e.g. "savings"
int sortOrder = -1; // -1 = auto (use default sort)
bool mining = false;
};
const AddressMeta& getAddressMeta(const std::string& addr) const {
static const AddressMeta empty{};
auto it = address_meta_.find(addr);
return it != address_meta_.end() ? it->second : empty;
}
void setAddressLabel(const std::string& addr, const std::string& label) {
address_meta_[addr].label = label;
}
void setAddressIcon(const std::string& addr, const std::string& icon) {
address_meta_[addr].icon = icon;
}
void setAddressSortOrder(const std::string& addr, int order) {
address_meta_[addr].sortOrder = order;
}
bool isMiningAddress(const std::string& addr) const {
auto it = address_meta_.find(addr);
return it != address_meta_.end() && it->second.mining;
}
void setMiningAddress(const std::string& addr, bool mining) {
address_meta_[addr].mining = mining;
}
std::set<std::string> getMiningAddresses() const {
std::set<std::string> addresses;
for (const auto& [addr, meta] : address_meta_) {
if (meta.mining) addresses.insert(addr);
}
return addresses;
}
int getNextSortOrder() const {
int mx = -1;
for (const auto& [k, v] : address_meta_)
if (v.sortOrder > mx) mx = v.sortOrder;
return mx + 1;
}
void swapAddressOrder(const std::string& a, const std::string& b) {
int oa = address_meta_[a].sortOrder;
int ob = address_meta_[b].sortOrder;
address_meta_[a].sortOrder = ob;
address_meta_[b].sortOrder = oa;
}
// First-run wizard
bool getWizardCompleted() const { return wizard_completed_; }
void setWizardCompleted(bool v) { wizard_completed_ = v; }
// Whether the one-time "back up your seed phrase" reminder has already been shown.
bool getSeedBackupReminded() const { return seed_backup_reminded_; }
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
// Pending "migrate to a seed wallet" state (Phase 1 created the wallet; a later sweep/adopt
// step consumes it). dest = the new wallet's sweep-target z-address; tempDir = its datadir.
bool getSeedMigrationPending() const { return seed_migration_pending_; }
void setSeedMigrationPending(bool v) { seed_migration_pending_ = v; }
std::string getSeedMigrationDest() const { return seed_migration_dest_; }
void setSeedMigrationDest(const std::string& v) { seed_migration_dest_ = v; }
std::string getSeedMigrationTempDir() const { return seed_migration_temp_dir_; }
void setSeedMigrationTempDir(const std::string& v) { seed_migration_temp_dir_ = v; }
// The sweep transaction id, persisted once the sweep is submitted — non-empty means the
// migration is past the sweep, so a resume goes to the confirm/adopt stage (not sweep again).
std::string getSeedMigrationSweepTxid() const { return seed_migration_sweep_txid_; }
void setSeedMigrationSweepTxid(const std::string& v) { seed_migration_sweep_txid_ = v; }
// Security — auto-lock timeout (seconds; 0 = disabled)
int getAutoLockTimeout() const { return auto_lock_timeout_; }
void setAutoLockTimeout(int seconds) { auto_lock_timeout_ = seconds; }
@@ -283,47 +163,6 @@ public:
bool getStopExternalDaemon() const { return stop_external_daemon_; }
void setStopExternalDaemon(bool v) { stop_external_daemon_ = v; }
// Daemon — maximum peer connections (0 = daemon default)
int getMaxConnections() const { return max_connections_; }
void setMaxConnections(int v) { max_connections_ = std::max(0, v); }
// Lite wallet server selection
LiteServerSelectionPreferenceMode getLiteServerSelectionMode() const { return lite_server_selection_mode_; }
void setLiteServerSelectionMode(LiteServerSelectionPreferenceMode mode) { lite_server_selection_mode_ = mode; }
std::string getLiteStickyServerUrl() const { return lite_sticky_server_url_; }
void setLiteStickyServerUrl(const std::string& url) { lite_sticky_server_url_ = url; }
std::string getLiteChainName() const { return lite_chain_name_; }
void setLiteChainName(const std::string& chainName) { lite_chain_name_ = chainName; }
std::size_t getLiteRandomSelectionSeed() const { return lite_random_selection_seed_; }
void setLiteRandomSelectionSeed(std::size_t seed) { lite_random_selection_seed_ = seed; }
bool getLitePersistSelectedServer() const { return lite_persist_selected_server_; }
void setLitePersistSelectedServer(bool persist) { lite_persist_selected_server_ = persist; }
const std::vector<LiteServerPreference>& getLiteServers() const { return lite_servers_; }
void setLiteServers(const std::vector<LiteServerPreference>& servers) { lite_servers_ = servers; }
// User-defined portfolio entries (Market tab). "All funds" is implicit, not stored here.
const std::vector<PortfolioEntry>& getPortfolioEntries() const { return portfolio_entries_; }
void setPortfolioEntries(const std::vector<PortfolioEntry>& entries) { portfolio_entries_ = entries; }
// Lite servers the user has hidden from the Network tab (kept by URL, shown via a toggle).
const std::set<std::string>& getLiteHiddenServers() const { return lite_hidden_servers_; }
bool isLiteServerHidden(const std::string& url) const { return lite_hidden_servers_.count(url) > 0; }
void hideLiteServer(const std::string& url) { lite_hidden_servers_.insert(url); }
void unhideLiteServer(const std::string& url) { lite_hidden_servers_.erase(url); }
// Lite wallet rollout / kill-switch (see wallet/lite_rollout_policy.h).
// Override: "auto" (honor rollout manifest), "force_on", or "force_off".
std::string getLiteRolloutOverride() const { return lite_rollout_override_; }
void setLiteRolloutOverride(const std::string& v) { lite_rollout_override_ = v; }
// Stable, locally-generated install id used only to derive the staged-rollout bucket.
// Never transmitted; carries no PII. Generated on first use if empty.
std::string getLiteInstallId() const { return lite_install_id_; }
void setLiteInstallId(const std::string& v) { lite_install_id_ = v; }
// Verbose diagnostic logging (connection attempts, daemon state, port owner, etc.)
bool getVerboseLogging() const { return verbose_logging_; }
void setVerboseLogging(bool v) { verbose_logging_ = v; }
// Daemon — debug logging categories
const std::set<std::string>& getDebugCategories() const { return debug_categories_; }
void setDebugCategories(const std::set<std::string>& cats) { debug_categories_ = cats; }
@@ -341,10 +180,6 @@ public:
bool getLowSpecMode() const { return low_spec_mode_; }
void setLowSpecMode(bool v) { low_spec_mode_ = v; }
// Reduce motion — disables animated transitions for accessibility
bool getReduceMotion() const { return reduce_motion_; }
void setReduceMotion(bool v) { reduce_motion_ = v; }
// Market — last selected exchange + pair
std::string getSelectedExchange() const { return selected_exchange_; }
void setSelectedExchange(const std::string& v) { selected_exchange_ = v; }
@@ -366,53 +201,6 @@ public:
void setPoolHugepages(bool v) { pool_hugepages_ = v; }
bool getPoolMode() const { return pool_mode_; }
void setPoolMode(bool v) { pool_mode_ = v; }
PoolSelectMode getPoolSelectMode() const { return pool_select_mode_; }
void setPoolSelectMode(PoolSelectMode v) { pool_select_mode_ = v; }
// Installed DRG-XMRig release tag (for in-app miner update detection); empty if unknown/bundled.
std::string getXmrigVersion() const { return xmrig_version_; }
void setXmrigVersion(const std::string& v) { xmrig_version_ = v; }
// Mine when idle (auto-start mining when system is idle)
bool getMineWhenIdle() const { return mine_when_idle_; }
void setMineWhenIdle(bool v) { mine_when_idle_ = v; }
int getMineIdleDelay() const { return mine_idle_delay_; }
void setMineIdleDelay(int seconds) { mine_idle_delay_ = std::max(30, seconds); }
// Idle thread scaling — scale thread count instead of start/stop
bool getIdleThreadScaling() const { return idle_thread_scaling_; }
void setIdleThreadScaling(bool v) { idle_thread_scaling_ = v; }
int getIdleThreadsActive() const { return idle_threads_active_; }
void setIdleThreadsActive(int v) { idle_threads_active_ = std::max(0, v); }
int getIdleThreadsIdle() const { return idle_threads_idle_; }
void setIdleThreadsIdle(int v) { idle_threads_idle_ = std::max(0, v); }
bool getIdleGpuAware() const { return idle_gpu_aware_; }
void setIdleGpuAware(bool v) { idle_gpu_aware_ = v; }
// Saved pool URLs (user-managed favorites dropdown)
const std::vector<std::string>& getSavedPoolUrls() const { return saved_pool_urls_; }
void addSavedPoolUrl(const std::string& url) {
// Don't add duplicates
for (const auto& u : saved_pool_urls_) if (u == url) return;
saved_pool_urls_.push_back(url);
}
void removeSavedPoolUrl(const std::string& url) {
saved_pool_urls_.erase(
std::remove(saved_pool_urls_.begin(), saved_pool_urls_.end(), url),
saved_pool_urls_.end());
}
// Saved pool worker addresses (user-managed favorites dropdown)
const std::vector<std::string>& getSavedPoolWorkers() const { return saved_pool_workers_; }
void addSavedPoolWorker(const std::string& addr) {
for (const auto& a : saved_pool_workers_) if (a == addr) return;
saved_pool_workers_.push_back(addr);
}
void removeSavedPoolWorker(const std::string& addr) {
saved_pool_workers_.erase(
std::remove(saved_pool_workers_.begin(), saved_pool_workers_.end(), addr),
saved_pool_workers_.end());
}
// Font scale (user accessibility setting, 1.01.5)
float getFontScale() const { return font_scale_; }
@@ -423,17 +211,12 @@ public:
int getWindowHeight() const { return window_height_; }
void setWindowSize(int w, int h) { window_width_ = w; window_height_ = h; }
// Returns true once after an upgrade (version mismatch detected on load)
bool needsUpgradeSave() const { return needs_upgrade_save_; }
void clearUpgradeSave() { needs_upgrade_save_ = false; }
private:
std::string settings_path_;
// Settings values
std::string theme_ = "dragonx";
std::string skin_id_ = "dragonx";
std::string chat_reply_zaddr_;
bool save_ztxs_ = true;
bool auto_shield_ = true;
bool use_tor_ = false;
@@ -448,78 +231,32 @@ private:
float blur_multiplier_ = 0.10f;
float noise_opacity_ = 0.5f;
bool gradient_background_ = false;
#ifdef _WIN32
float ui_opacity_ = 0.50f; // Card/sidebar opacity (0.31.0, 1.0 = opaque)
float window_opacity_ = 0.75f; // Background alpha (0.31.0, <1 = desktop visible)
#else
float ui_opacity_ = 1.0f; // Mac/Linux: default fully opaque
float window_opacity_ = 1.0f; // Mac/Linux: default fully opaque
#endif
std::string balance_layout_ = "classic";
bool scanline_enabled_ = true;
std::set<std::string> hidden_addresses_;
std::set<std::string> favorite_addresses_;
std::map<std::string, AddressMeta> address_meta_;
bool wizard_completed_ = false;
bool seed_backup_reminded_ = false;
bool seed_migration_pending_ = false;
std::string seed_migration_dest_;
std::string seed_migration_temp_dir_;
std::string seed_migration_sweep_txid_;
int auto_lock_timeout_ = 900; // 15 minutes
int unlock_duration_ = 600; // 10 minutes
bool pin_enabled_ = false;
bool keep_daemon_running_ = false;
bool stop_external_daemon_ = false;
int max_connections_ = 0; // 0 = daemon default
// Lite wallet server preferences. These are user/server settings only;
// wallet secrets, wallet files, and lifecycle state are never stored here.
LiteServerSelectionPreferenceMode lite_server_selection_mode_ = LiteServerSelectionPreferenceMode::Sticky;
std::string lite_sticky_server_url_ = "https://lite.dragonx.is";
std::string lite_chain_name_ = "main"; // SDXL backend chain id; must be main/test/regtest
std::size_t lite_random_selection_seed_ = 0;
bool lite_persist_selected_server_ = true;
std::string lite_rollout_override_ = "auto"; // auto|force_on|force_off
std::string lite_install_id_; // random local-only id; rollout-bucket source
std::vector<LiteServerPreference> lite_servers_ = {
{"https://lite.dragonx.is", "DragonX Lite", true},
{"https://lite1.dragonx.is", "DragonX Lite 1", true},
{"https://lite2.dragonx.is", "DragonX Lite 2", true},
{"https://lite3.dragonx.is", "DragonX Lite 3", true},
{"https://lite4.dragonx.is", "DragonX Lite 4", true},
{"https://lite5.dragonx.is", "DragonX Lite 5", true}
};
std::set<std::string> lite_hidden_servers_; // server URLs hidden from the Network tab
std::vector<PortfolioEntry> portfolio_entries_; // Market tab custom portfolio groups
bool verbose_logging_ = false;
std::set<std::string> debug_categories_;
bool theme_effects_enabled_ = true;
bool low_spec_mode_ = false;
bool reduce_motion_ = false;
std::string selected_exchange_ = "Nonkyc.io";
std::string selected_pair_ = "DRGX/USDT";
std::string selected_exchange_ = "TradeOgre";
std::string selected_pair_ = "DRGX/BTC";
// Pool mining
std::string pool_url_ = "pool.dragonx.is:3433";
std::string pool_url_ = "pool.dragonx.is";
std::string pool_algo_ = "rx/hush";
std::string pool_worker_ = "";
std::string pool_worker_ = "x";
int pool_threads_ = 0;
bool pool_tls_ = false;
bool pool_hugepages_ = true;
bool pool_mode_ = false; // false=solo, true=pool
PoolSelectMode pool_select_mode_ = PoolSelectMode::Manual; // manual vs auto-balance pool choice
std::string xmrig_version_; // installed DRG-XMRig release tag (update detection)
bool mine_when_idle_ = false; // auto-start mining when system idle
int mine_idle_delay_= 120; // seconds of idle before mining starts
bool idle_thread_scaling_ = false; // scale threads instead of start/stop
int idle_threads_active_ = 0; // threads when user active (0 = auto)
int idle_threads_idle_ = 0; // threads when idle (0 = auto = all)
bool idle_gpu_aware_ = true; // treat GPU activity as non-idle
std::vector<std::string> saved_pool_urls_; // user-saved pool URL favorites
std::vector<std::string> saved_pool_workers_; // user-saved worker address favorites
// Font scale (user accessibility, 1.03.0; 1.0 = default)
float font_scale_ = 1.0f;
@@ -527,10 +264,6 @@ private:
// Window size (logical pixels at 1x scale; 0 = use default 1200×775)
int window_width_ = 0;
int window_height_ = 0;
// Wallet version that last wrote this settings file (for upgrade detection)
std::string settings_version_;
bool needs_upgrade_save_ = false; // true when version changed
};
} // namespace config

View File

@@ -1,3 +1,28 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include "dragonx_generated_version.h"
#define DRAGONX_VERSION "1.0.0"
#define DRAGONX_VERSION_MAJOR 1
#define DRAGONX_VERSION_MINOR 0
#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"

View File

@@ -1,32 +0,0 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
// !! DO NOT EDIT generated version output — it is generated from version.h.in by CMake.
// !! Change the version in CMakeLists.txt: project(... VERSION x.y.z ...) for the full-node app,
// !! or DRAGONX_LITE_VERSION for ObsidianDragonLite. DRAGONX_APP_VERSION is the active variant.
#define DRAGONX_VERSION "@DRAGONX_APP_VERSION@@DRAGONX_APP_VERSION_SUFFIX@"
#define DRAGONX_VERSION_MAJOR @DRAGONX_APP_VERSION_MAJOR@
#define DRAGONX_VERSION_MINOR @DRAGONX_APP_VERSION_MINOR@
#define DRAGONX_VERSION_PATCH @DRAGONX_APP_VERSION_PATCH@
#define DRAGONX_APP_NAME "@DRAGONX_APP_NAME@"
#define DRAGONX_ORG_NAME "Hush"
// Default RPC settings
#define DRAGONX_DEFAULT_RPC_HOST "127.0.0.1"
#define DRAGONX_DEFAULT_RPC_PORT "21769"
// Coin parameters
#define DRAGONX_TICKER "DRGX"
#define DRAGONX_COIN_NAME "DragonX"
#define DRAGONX_URI_SCHEME "drgx"
#define DRAGONX_ZATOSHI_PER_COIN 100000000
#define DRAGONX_DEFAULT_FEE 0.0001
// Config file names
#define DRAGONX_CONF_FILENAME "DRAGONX.conf"
#define DRAGONX_WALLET_FILENAME "wallet.dat"

View File

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

View File

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

View File

@@ -1,14 +1,10 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// embedded_daemon.cpp — Manages the dragonxd child process lifecycle:
// binary discovery, process spawn, stdout/stderr monitoring, crash recovery.
#include "embedded_daemon.h"
#include "../config/version.h"
#include "../resources/embedded_resources.h"
#include "../util/platform.h"
#include <cstdio>
#include <cstdlib>
@@ -21,13 +17,12 @@
#include "../util/logger.h"
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <psapi.h>
#include <tlhelp32.h>
#include <shlobj.h>
#include <iphlpapi.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <unistd.h>
#include <signal.h>
@@ -37,9 +32,6 @@
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#ifdef __APPLE__
#include <sys/sysctl.h>
#endif
#endif
namespace fs = std::filesystem;
@@ -62,8 +54,6 @@ std::string EmbeddedDaemon::findDaemonBinary()
char exe_path[MAX_PATH];
GetModuleFileNameA(NULL, exe_path, MAX_PATH);
exe_dir = fs::path(exe_path).parent_path().string();
#elif defined(__APPLE__)
exe_dir = util::Platform::getExecutableDirectory();
#else
char exe_path[4096];
ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
@@ -81,10 +71,15 @@ std::string EmbeddedDaemon::findDaemonBinary()
#ifdef _WIN32
// Check wallet's own directory for manually placed binaries
std::vector<std::string> localPaths = {
exe_dir + "\\hushd.exe",
exe_dir + "\\hush-arrakis-chain.exe",
exe_dir + "\\dragonxd.exe",
exe_dir + "\\dragonxd.bat",
};
#else
std::vector<std::string> localPaths = {
exe_dir + "/hush-arrakis-chain",
exe_dir + "/hushd",
exe_dir + "/dragonxd",
};
#endif
@@ -108,36 +103,41 @@ std::string EmbeddedDaemon::findDaemonBinary()
// ---------------------------------------------------------------
// 3. Search additional well-known locations
// ---------------------------------------------------------------
// IMPORTANT: Always prefer dragonxd.exe directly over .bat wrappers.
// IMPORTANT: Always prefer hushd.exe directly over dragonxd.bat
// Using .bat files causes issues because cmd.exe exits immediately
// while dragonxd.exe continues running, making process monitoring fail.
// while hushd.exe continues running, making process monitoring fail.
std::vector<std::string> search_paths;
#ifdef _WIN32
// Parent directory
if (!exe_dir.empty()) {
search_paths.push_back(exe_dir + "\\..\\hushd.exe");
search_paths.push_back(exe_dir + "\\..\\dragonxd.bat");
search_paths.push_back(exe_dir + "\\..\\dragonxd.exe");
}
search_paths.push_back("C:\\Program Files\\DragonX\\dragonxd.exe");
search_paths.push_back("C:\\Program Files\\DragonX\\hushd.exe");
#else
if (!exe_dir.empty()) {
search_paths.push_back(exe_dir + "/../hush-arrakis-chain");
search_paths.push_back(exe_dir + "/../bin/hush-arrakis-chain");
search_paths.push_back(exe_dir + "/../dragonxd");
}
// Standard Linux locations
search_paths.push_back("/usr/local/bin/dragonxd");
search_paths.push_back("/usr/bin/dragonxd");
search_paths.push_back("/usr/local/bin/hush-arrakis-chain");
search_paths.push_back("/usr/bin/hush-arrakis-chain");
// Home directory
const char* home = getenv("HOME");
if (home) {
search_paths.push_back(std::string(home) + "/dragonx/src/dragonxd");
search_paths.push_back(std::string(home) + "/bin/dragonxd");
search_paths.push_back(std::string(home) + "/hush3/src/hush-arrakis-chain");
search_paths.push_back(std::string(home) + "/hush3/src/dragonxd");
search_paths.push_back(std::string(home) + "/bin/hush-arrakis-chain");
}
#ifdef __APPLE__
// macOS app bundle
search_paths.push_back("/Applications/DragonX.app/Contents/MacOS/dragonxd");
search_paths.push_back("/Applications/DragonX.app/Contents/MacOS/hush-arrakis-chain");
#endif
#endif
@@ -158,66 +158,21 @@ std::vector<std::string> EmbeddedDaemon::getChainParams()
// DragonX chain parameters.
// On Windows, omit -printtoconsole: we tail debug.log instead of piping stdout.
// On Linux, -printtoconsole is used for pipe-based output capture.
// Auto-detect a reasonable -dbcache based on available physical RAM.
// Default LevelDB cache is small (~450MB); larger caches improve sync
// performance and reduce disk I/O — especially on macOS with APFS.
std::string dbcache_arg = "-dbcache=450";
{
#ifdef __APPLE__
// sysctl hw.memsize returns total physical RAM in bytes
int64_t memsize = 0;
size_t len = sizeof(memsize);
if (sysctlbyname("hw.memsize", &memsize, &len, nullptr, 0) == 0 && memsize > 0) {
int totalMB = static_cast<int>(memsize / (1024 * 1024));
// Use ~12.5% of RAM for dbcache, clamped to [450, 4096]
int cache = std::max(450, std::min(4096, totalMB / 8));
dbcache_arg = "-dbcache=" + std::to_string(cache);
}
#elif defined(__linux__)
long pages = sysconf(_SC_PHYS_PAGES);
long page_size = sysconf(_SC_PAGE_SIZE);
if (pages > 0 && page_size > 0) {
int totalMB = static_cast<int>((static_cast<int64_t>(pages) * page_size) / (1024 * 1024));
int cache = std::max(450, std::min(4096, totalMB / 8));
dbcache_arg = "-dbcache=" + std::to_string(cache);
}
#elif defined(_WIN32)
MEMORYSTATUSEX memInfo;
memInfo.dwLength = sizeof(memInfo);
if (GlobalMemoryStatusEx(&memInfo)) {
int totalMB = static_cast<int>(memInfo.ullTotalPhys / (1024 * 1024));
int cache = std::max(450, std::min(4096, totalMB / 8));
dbcache_arg = "-dbcache=" + std::to_string(cache);
}
#endif
DEBUG_LOGF("[INFO] Using %s\n", dbcache_arg.c_str());
}
return {
"-tls=only",
#ifndef _WIN32
"-printtoconsole",
#endif
"-clientname=ObsidianDragon",
"-clientname=DragonXImGui",
"-ac_name=DRAGONX",
"-ac_algo=randomx",
"-ac_halving=3500000",
"-ac_reward=300000000",
"-ac_blocktime=36",
"-ac_private=1",
"-addnode=node.dragonx.is",
"-addnode=node1.dragonx.is",
"-addnode=node2.dragonx.is",
"-addnode=node3.dragonx.is",
"-addnode=node4.dragonx.is",
"-addnode=176.126.87.241",
"-experimentalfeatures",
"-developerencryptwallet",
// Create fresh wallets from a BIP39 mnemonic so their 24-word phrase can be
// exported (z_exportmnemonic) and is portable to SDXLite/ObsidianDragonLite.
// The daemon reads this ONLY inside GenerateNewSeed() when a wallet has no seed
// yet, so it is inert on existing wallets — safe to pass unconditionally.
"-usemnemonic=1",
dbcache_arg
"-developerencryptwallet"
};
}
@@ -279,113 +234,6 @@ std::vector<std::string> EmbeddedDaemon::getRecentLines(int maxLines) const
return lines;
}
// Identify what process owns a given TCP port.
// Returns a string like "PID 1234 (dragonxd.exe)" or "unknown" on failure.
static std::string getPortOwnerInfo(int port)
{
#ifdef _WIN32
DWORD size = 0;
// First call to get required buffer size
GetExtendedTcpTable(nullptr, &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0);
if (size == 0) return "unknown (GetExtendedTcpTable size query failed)";
std::vector<BYTE> buf(size);
DWORD ret = GetExtendedTcpTable(buf.data(), &size, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0);
if (ret != NO_ERROR) return "unknown (GetExtendedTcpTable failed, error " + std::to_string(ret) + ")";
auto* table = reinterpret_cast<MIB_TCPTABLE_OWNER_PID*>(buf.data());
DWORD ownerPid = 0;
for (DWORD i = 0; i < table->dwNumEntries; i++) {
auto& row = table->table[i];
int rowPort = ntohs(static_cast<u_short>(row.dwLocalPort));
// Match port in LISTEN state (MIB_TCP_STATE_LISTEN = 2)
if (rowPort == port && row.dwState == MIB_TCP_STATE_LISTEN) {
ownerPid = row.dwOwningPid;
break;
}
}
if (ownerPid == 0) {
// Maybe it's in ESTABLISHED or another state from our connect probe — try any state
for (DWORD i = 0; i < table->dwNumEntries; i++) {
auto& row = table->table[i];
int rowPort = ntohs(static_cast<u_short>(row.dwLocalPort));
if (rowPort == port && row.dwOwningPid != 0) {
ownerPid = row.dwOwningPid;
break;
}
}
}
if (ownerPid == 0) return "unknown (no PID found for port " + std::to_string(port) + ")";
// Resolve PID to process name
std::string procName = "<unknown>";
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap != INVALID_HANDLE_VALUE) {
PROCESSENTRY32 pe;
pe.dwSize = sizeof(pe);
if (Process32First(snap, &pe)) {
do {
if (pe.th32ProcessID == ownerPid) {
procName = pe.szExeFile;
break;
}
} while (Process32Next(snap, &pe));
}
CloseHandle(snap);
}
return "PID " + std::to_string(ownerPid) + " (" + procName + ")";
#else
// Linux: parse /proc/net/tcp to find the inode, then scan /proc/*/fd
FILE* fp = fopen("/proc/net/tcp", "r");
if (!fp) return "unknown (cannot read /proc/net/tcp)";
char line[512];
unsigned long inode = 0;
while (fgets(line, sizeof(line), fp)) {
unsigned int localPort, state;
unsigned long lineInode;
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X %*X:%*X %*X:%*X %*X %*u %*u %lu",
&localPort, &state, &lineInode) == 3) {
if (static_cast<int>(localPort) == port && state == 0x0A) { // 0x0A = LISTEN
inode = lineInode;
break;
}
}
}
fclose(fp);
if (inode == 0) return "unknown (no listener found for port " + std::to_string(port) + ")";
// Scan /proc/*/fd/* for the matching inode
for (const auto& entry : fs::directory_iterator("/proc")) {
if (!entry.is_directory()) continue;
std::string pidStr = entry.path().filename().string();
if (pidStr.empty() || !std::isdigit(pidStr[0])) continue;
std::string fdDir = "/proc/" + pidStr + "/fd";
try {
for (const auto& fdEntry : fs::directory_iterator(fdDir)) {
char target[512];
ssize_t len = readlink(fdEntry.path().c_str(), target, sizeof(target) - 1);
if (len > 0) {
target[len] = '\0';
std::string t(target);
if (t.find("socket:[" + std::to_string(inode) + "]") != std::string::npos) {
// Found the PID, now get the process name
std::string commPath = "/proc/" + pidStr + "/comm";
FILE* cf = fopen(commPath.c_str(), "r");
std::string procName = "<unknown>";
if (cf) {
char name[256];
if (fgets(name, sizeof(name), cf)) {
procName = name;
while (!procName.empty() && procName.back() == '\n') procName.pop_back();
}
fclose(cf);
}
return "PID " + pidStr + " (" + procName + ")";
}
}
}
} catch (...) { /* permission denied — skip */ }
}
return "unknown (inode " + std::to_string(inode) + " found but no matching PID)";
#endif
}
// Check if a TCP port is already in use (something is LISTENING)
static bool isPortInUse(int port)
{
@@ -403,35 +251,25 @@ static bool isPortInUse(int port)
WSACleanup();
return (result == 0);
#else
// On macOS /proc doesn't exist; on Linux prefer /proc/net/tcp to avoid
// creating sockets. Fall back to connect() if /proc is unavailable.
// Read /proc/net/tcp to check for listeners — avoids creating sockets
// which would add conntrack entries and consume ephemeral ports.
FILE* fp = fopen("/proc/net/tcp", "r");
if (fp) {
char line[256];
unsigned int localPort, state;
bool found = false;
while (fgets(line, sizeof(line), fp)) {
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X", &localPort, &state) == 2) {
if (localPort == static_cast<unsigned int>(port) && state == 0x0A) {
found = true;
break;
}
if (!fp) return false;
char line[256];
unsigned int localPort, state;
bool found = false;
while (fgets(line, sizeof(line), fp)) {
// Format: sl local_address rem_address st ...
// local_address is HEXIP:HEXPORT, state 0x0A = TCP_LISTEN
if (sscanf(line, " %*d: %*X:%X %*X:%*X %X", &localPort, &state) == 2) {
if (localPort == static_cast<unsigned int>(port) && state == 0x0A) {
found = true;
break;
}
}
fclose(fp);
return found;
}
// Fallback (macOS): try to connect
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) return false;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<uint16_t>(port));
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
close(sock);
return (result == 0);
fclose(fp);
return found;
#endif
}
@@ -448,15 +286,13 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
return true;
}
// Check if something is already listening on the RPC port. An isolated instance (migrate-to-
// seed) runs on its own non-default port alongside the main daemon, so it skips this bail.
// Check if something is already listening on the RPC port
int rpc_port = std::atoi(DRAGONX_DEFAULT_RPC_PORT);
if (!skip_port_check_ && isPortInUse(rpc_port)) {
std::string owner = getPortOwnerInfo(rpc_port);
VERBOSE_LOGF("[INFO] Port %d is already in use by %s — external daemon detected, will connect to it.\\n", rpc_port, owner.c_str());
if (isPortInUse(rpc_port)) {
DEBUG_LOGF("[INFO] Port %d is already in use — external daemon detected, will connect to it.\\n", rpc_port);
external_daemon_detected_ = true;
// Don't set Error — the wallet will connect to the running daemon.
setState(State::Stopped, "External daemon detected on port " + std::string(DRAGONX_DEFAULT_RPC_PORT) + " (owned by " + owner + ")");
setState(State::Stopped, "External daemon detected on port " + std::string(DRAGONX_DEFAULT_RPC_PORT));
return false;
}
external_daemon_detected_ = false;
@@ -483,36 +319,12 @@ bool EmbeddedDaemon::start(const std::string& binary_path)
args.push_back("-debug=" + cat);
}
// Append max connections if configured (0 = daemon default)
if (max_connections_ > 0) {
args.push_back("-maxconnections=" + std::to_string(max_connections_));
}
// Add wallet-repair flag if requested (one-shot). -zapwallettxes=2 wipes all wallet tx/note
// records and rebuilds them from the chain; it implies -rescan, so don't also pass -rescan.
if (zap_on_next_start_.exchange(false)) {
DEBUG_LOGF("[INFO] Adding -zapwallettxes=2 flag for wallet repair (zap & rebuild)\n");
args.push_back("-zapwallettxes=2");
rescan_on_next_start_.store(false); // implied by zap; avoid redundant -rescan
} else if (rescan_on_next_start_.exchange(false)) {
// Add -rescan flag if requested (one-shot)
// Add -rescan flag if requested (one-shot)
if (rescan_on_next_start_.exchange(false)) {
DEBUG_LOGF("[INFO] Adding -rescan flag for blockchain rescan\n");
args.push_back("-rescan");
}
// One-shot isolated-datadir override (migrate-to-seed flow): run this start against a
// throwaway datadir, plus any extra args (e.g. -connect=0). Consumed here so later starts
// revert to the normal datadir. The datadir's basename MUST be the assetchain name (DRAGONX)
// or the daemon mis-resolves its conf/port; it reads <datadir>/DRAGONX.conf automatically, so
// no -conf is passed (an explicit -conf confuses the Komodo/Hush path resolution).
if (!override_datadir_.empty()) {
DEBUG_LOGF("[INFO] Isolated start override: -datadir=%s\n", override_datadir_.c_str());
args.push_back("-datadir=" + override_datadir_);
}
for (const auto& a : override_extra_args_) args.push_back(a);
override_datadir_.clear();
override_extra_args_.clear();
if (!startProcess(daemon_path, args)) {
DEBUG_LOGF("[ERROR] Failed to start dragonxd process: %s\\n", last_error_.c_str());
setState(State::Error, "Failed to start dragonxd process");
@@ -591,7 +403,7 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
// Launch daemon with CREATE_NEW_CONSOLE (hidden via SW_HIDE).
// The daemon binary must NOT be in the data directory (%APPDATA%\Hush\DRAGONX)
// — it must be in <exe_dir>/dragonx/ to avoid conflicts with lock files and data.
// — it must be in <exe_dir>/hush3/ to avoid conflicts with lock files and data.
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
@@ -717,7 +529,7 @@ void EmbeddedDaemon::drainOutput()
std::lock_guard<std::mutex> lk(output_mutex_);
appendOutput(buffer, bytes_read);
}
VERBOSE_LOGF("[dragonxd] %s", buffer);
DEBUG_LOGF("[dragonxd] %s", buffer);
debug_log_offset_ += bytes_read;
}
@@ -752,7 +564,7 @@ void EmbeddedDaemon::stop(int wait_ms)
};
if (tracked_alive) {
// Our tracked process (dragonxd.exe launched directly) is still alive.
// Our tracked process (hushd.exe launched directly) is still alive.
// The RPC "stop" was already sent by the caller — wait for it to exit.
DEBUG_LOGF("Waiting up to %d ms for tracked daemon process to exit...\n", wait_ms);
if (!pollWait(process_handle_, wait_ms)) {
@@ -765,33 +577,33 @@ void EmbeddedDaemon::stop(int wait_ms)
process_handle_ = nullptr;
} else {
// Tracked handle is dead (batch file case: cmd.exe already exited).
// The real dragonxd.exe may still be running as an orphan.
// The real hushd.exe may still be running as an orphan.
if (process_handle_ != nullptr) {
CloseHandle(process_handle_);
process_handle_ = nullptr;
}
// Find the real dragonxd.exe process by name
DWORD dragonxd_pid = findProcessByName("dragonxd.exe");
if (dragonxd_pid != 0) {
DEBUG_LOGF("Found orphaned dragonxd.exe (PID %lu) — waiting for RPC stop to take effect...\n", dragonxd_pid);
HANDLE hProc = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, dragonxd_pid);
// Find the real hushd.exe process by name
DWORD hushd_pid = findProcessByName("hushd.exe");
if (hushd_pid != 0) {
DEBUG_LOGF("Found orphaned hushd.exe (PID %lu) — waiting for RPC stop to take effect...\n", hushd_pid);
HANDLE hProc = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, hushd_pid);
if (hProc) {
// RPC stop was already sent — wait for graceful exit
if (!pollWait(hProc, wait_ms)) {
DEBUG_LOGF("Timeout — forcing dragonxd.exe (PID %lu) termination...\n", dragonxd_pid);
DEBUG_LOGF("Timeout — forcing hushd.exe (PID %lu) termination...\n", hushd_pid);
TerminateProcess(hProc, 1);
WaitForSingleObject(hProc, 2000);
}
drainOutput();
CloseHandle(hProc);
DEBUG_LOGF("dragonxd.exe stopped\n");
DEBUG_LOGF("hushd.exe stopped\n");
} else {
DEBUG_LOGF("Could not open dragonxd.exe process (PID %lu), error %lu\n",
dragonxd_pid, GetLastError());
DEBUG_LOGF("Could not open hushd.exe process (PID %lu), error %lu\n",
hushd_pid, GetLastError());
}
} else {
DEBUG_LOGF("No dragonxd.exe process found — daemon may have already exited\n");
DEBUG_LOGF("No hushd.exe process found — daemon may have already exited\n");
}
}
@@ -926,11 +738,11 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
close(pipefd[0]); // Close read end
// Put child in its own process group so we can kill the entire
// group later (including dragonxd spawned by a wrapper script).
// Without this, SIGTERM only kills the shell, leaving dragonxd orphaned.
// group later (including hushd spawned by a wrapper script).
// Without this, SIGTERM only kills the shell, leaving hushd orphaned.
setpgid(0, 0);
// Change to the daemon binary's directory so dragonxd can find
// Change to the daemon binary's directory so hushd can find
// sapling params via its PWD search path (same as CreateProcessA
// lpCurrentDirectory on Windows).
{
@@ -957,7 +769,8 @@ bool EmbeddedDaemon::startProcess(const std::string& binary_path, const std::vec
bool is_script = false;
if (binary_path.size() >= 3) {
std::string ext = binary_path.substr(binary_path.size() - 3);
if (ext == ".sh" || binary_path.find("dragonxd") != std::string::npos) {
if (ext == ".sh" || binary_path.find("hush-arrakis-chain") != std::string::npos ||
binary_path.find("dragonxd") != std::string::npos) {
// Check if it's a script by looking at first bytes
FILE* f = fopen(binary_path.c_str(), "r");
if (f) {
@@ -1014,24 +827,8 @@ double EmbeddedDaemon::getMemoryUsageMB() const
{
if (process_pid_ <= 0) return 0.0;
#ifdef __APPLE__
// macOS: use ps to read RSS for the daemon process and its children
// in the same process group.
char cmd[128];
snprintf(cmd, sizeof(cmd), "ps -o rss= -g %d 2>/dev/null", process_pid_);
FILE* fp = popen(cmd, "r");
if (!fp) return 0.0;
double total_rss_mb = 0.0;
char line[64];
while (fgets(line, sizeof(line), fp)) {
long rss_kb = atol(line);
if (rss_kb > 0) total_rss_mb += static_cast<double>(rss_kb) / 1024.0;
}
pclose(fp);
return total_rss_mb;
#else
// Linux: The tracked PID is often a bash wrapper script; the real daemon
// (dragonxd) is a child in the same process group. Sum VmRSS for every
// The tracked PID is often a bash wrapper script; the real daemon
// (hushd) is a child in the same process group. Sum VmRSS for every
// process whose PGID matches our tracked PID.
double total_rss_mb = 0.0;
@@ -1080,7 +877,6 @@ double EmbeddedDaemon::getMemoryUsageMB() const
}
return total_rss_mb;
#endif // __APPLE__ / Linux
}
bool EmbeddedDaemon::isRunning() const
@@ -1110,7 +906,7 @@ void EmbeddedDaemon::drainOutput()
std::lock_guard<std::mutex> lk(output_mutex_);
appendOutput(buffer, static_cast<size_t>(n));
}
VERBOSE_LOGF("[dragonxd] %s", buffer);
DEBUG_LOGF("[dragonxd] %s", buffer);
}
}
@@ -1121,12 +917,14 @@ void EmbeddedDaemon::stop(int wait_ms)
if (process_pid_ > 0) {
setState(State::Stopping, "Stopping dragonxd...");
// Phase 1: Wait for the daemon to exit naturally.
// The caller (stopEmbeddedDaemon) already sent an RPC "stop" which
// tells the daemon to flush LevelDB, close sockets, and exit cleanly.
// On macOS/APFS the LevelDB flush can take several seconds — we must
// NOT send SIGTERM until the daemon has had enough time to finish.
DEBUG_LOGF("Waiting up to %d ms for daemon to exit after RPC stop...\n", wait_ms);
// Send SIGTERM to the entire process group (negative PID).
// This ensures that if dragonxd is a shell script wrapper,
// both bash AND the actual hushd child receive the signal.
// Without this, only bash is killed and hushd is orphaned.
DEBUG_LOGF("Sending SIGTERM to process group -%d\n", process_pid_);
kill(-process_pid_, SIGTERM);
// Wait for process to exit, draining stdout each iteration
auto start = std::chrono::steady_clock::now();
while (isRunning()) {
drainOutput();
@@ -1135,34 +933,15 @@ void EmbeddedDaemon::stop(int wait_ms)
std::chrono::steady_clock::now() - start).count();
if (elapsed >= wait_ms) {
// Force kill the entire process group
DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_);
kill(-process_pid_, SIGKILL);
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// Phase 2: If still running, send SIGTERM and wait a further 10s.
if (isRunning()) {
DEBUG_LOGF("Daemon did not exit gracefully — sending SIGTERM to process group -%d\n", process_pid_);
kill(-process_pid_, SIGTERM);
auto sigterm_start = std::chrono::steady_clock::now();
while (isRunning()) {
drainOutput();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - sigterm_start).count();
if (elapsed >= 10000) {
// Phase 3: Force kill
DEBUG_LOGF("Forcing dragonxd termination with SIGKILL (group -%d)...\n", process_pid_);
kill(-process_pid_, SIGKILL);
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
} else {
DEBUG_LOGF("Daemon exited cleanly after RPC stop\n");
}
drainOutput(); // read any remaining output
// Reap the child process
@@ -1219,7 +998,7 @@ void EmbeddedDaemon::monitorProcess()
std::lock_guard<std::mutex> lk(output_mutex_);
appendOutput(buffer, static_cast<size_t>(bytes_read));
}
VERBOSE_LOGF("[dragonxd] %s", buffer);
DEBUG_LOGF("[dragonxd] %s", buffer);
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
@@ -1234,10 +1013,5 @@ bool EmbeddedDaemon::isRpcPortInUse()
return isPortInUse(port);
}
bool EmbeddedDaemon::tcpPortInUse(int port)
{
return isPortInUse(port);
}
} // namespace daemon
} // namespace dragonx

View File

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

View File

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

View File

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

View File

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

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