Files
ObsidianDragon/CLAUDE.md
DanS 1393d9ae1a feat(lite): vendor SDXL backend source; source deps from git.dragonx.is
Vendor the two local Rust crates that build the lite backend artifact into
third_party/silentdragonxlite/ (the qtlib C-ABI wrapper + the silentdragonxlitelib
core, with proto/res and all the lite-send fixes), and point
build-lite-backend-artifact.sh's default --backend-dir there, so the lite wallet
builds without the upstream SilentDragonXLite repo.

External build inputs are now only the Rust toolchain + git.dragonx.is + crates.io:
- the 6 librustzcash git deps point at the git.dragonx.is/DragonX/librustzcash
  mirror (pinned rev acff1444), not git.hush.is;
- the Sapling params are gitignored (not committed, no Git LFS) - the build fetches
  them from the git.dragonx.is/DragonX/zcash-params 'sapling-v1' release and verifies
  their SHA-256 before rust-embed bakes them in (ensure_sapling_params).

For fully offline builds, cargo vendor into lib/vendor/ and add a vendored-sources
redirect (vendor/ is gitignored; the script symlinks it into the prepared dir).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:06:42 -05:00

16 KiB

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.

./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.

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/chat_protocol.cpp): experimental HushChat protocol, compiled in only when DRAGONX_ENABLE_CHAT=ON.

Build variants & feature gating

Variants are selected with CMake options (set by build.sh flags), surfaced to C++ as compile definitions:

  • DRAGONX_BUILD_LITE (--lite) → DRAGONX_LITE_BUILD define; renames the app to ObsidianDragonLite and excludes embedded-daemon / full-node assets (Sapling params, asmap, dragonxd).
  • DRAGONX_ENABLE_LITE_BACKEND → links a real external lite backend. Requires --lite, link mode imported, ABI sdxl-c-v1, and a symbols inventory file (built by scripts/build-lite-backend-artifact.sh); CMake hard-fails if any required litelib_* symbol is missing. The backend source is vendored in-tree at third_party/silentdragonxlite/ — the qtlib C-ABI wrapper (lib/, produces libsilentdragonxlite.a) and the silentdragonxlitelib core (silentdragonxlite-cli/lib/, with proto/ + res/). build-lite-backend-artifact.sh defaults --backend-dir there, so the lite wallet builds without the upstream SilentDragonXLite repo. External build inputs are limited to the Rust toolchain (rustc/cargo 1.63) plus two project-controlled sources on git.dragonx.is: the librustzcash crates come from the mirror git.dragonx.is/DragonX/librustzcash (the 6 git = deps in the core Cargo.toml, pinned to rev acff1444…), and the Sapling params are not committed (gitignored) — the build fetches them from the git.dragonx.is/DragonX/zcash-params release sapling-v1 and verifies their SHA-256 before rust-embed bakes them in (ensure_sapling_params; override the URL with SAPLING_PARAMS_BASE_URL). Other crate deps come from crates.io. For a fully offline build, cargo vendor into third_party/silentdragonxlite/lib/vendor/ and add a vendored-sources redirect to lib/.cargo/config.toml (the build script symlinks vendor/ into its prepared dir if present); vendor/ is gitignored.
  • DRAGONX_ENABLE_CHATDRAGONX_ENABLE_CHAT define gating the chat module.

Guard full-node-only code paths with #if DRAGONX_LITE_BUILD / chat code with DRAGONX_ENABLE_CHAT.

Lite wallet status

The Lite variant is functionally complete and runtime-verified on Linux + Windows (work lives on branch cleanup/lite-plan-churn, local-only — not pushed yet):

  • Implemented: lifecycle (create/open/restore + auto-open on startup), sync, refresh, send / shield / import / export / seed, persistence (the backend does not auto-save after sync/send/shield — the controller triggers save at those points), and passphrase encryption (encrypt/unlock/lock/decrypt + Settings UI + send-time & startup unlock; the backend locks immediately on encrypt). All controller-tested against the fake backend (tests/fake_lite_backend.h) and smoke-verified against the real SDXL backend via tools/lite_smoke (incl. a full sync). GUI is wired end-to-end with lite-appropriate wording; the full-node RPC connect loop / wizard / daemon strings are gated out of lite (lite "online" is derived from lite_wallet_->walletOpen(), not RPC).
  • Packaging: ./build.sh --lite-backend --linux-release (zip + AppImage, verified) and --win-release (cross-compiled .exe, verified; first build the Windows backend artifact with scripts/build-lite-backend-artifact.sh --platform windows). macOS --lite-backend --mac-release is wired but not yet verified on this Linux box (needs macOS/osxcross): the .app/launcher/rpath/CFBundleExecutable follow ObsidianDragonLite, full-node assets are skipped, and the lite variant gets its own CFBundleName ("DragonX Wallet Lite"), bundle id (is.hush.dragonx.lite), and DMG name so it can coexist with the full-node app. All variants correctly exclude full-node assets.
  • Rollout / kill-switch (implemented): wallet/lite_rollout_policy.{h,cpp} is a pure, fail-open gate (local-only, no network) feeding LiteWalletLifecycleService::availability() (new RolloutDisabled reason). Inputs: the emergency env var DRAGONX_LITE_KILL_SWITCH (absolute — not even force_on bypasses it); a lite_rollout setting (auto/force_on/force_off); and an optional locally-cached manifest at <config-dir>/lite_rollout.json (global_enabled, min_version/max_version, blocked_versions, rollout_permille, message) keyed for staged rollout on a hashed, never-transmitted per-install id. A signed remote fetcher can populate that cache later without touching the policy. Resolved in App::rebuildLiteWallet(); the disable message surfaces via the lifecycle status. Unit-tested + runtime-verified (env / manifest / control).
  • Remaining (M5b): verify the wired macOS --lite packaging on a Mac/osxcross, CI backend-artifact build + signing.
  • To publish: rename branch → feat/lite-wallet, base the PR on dev (the full-node UX is already there), and handle the dormant gated-OFF HushChat content bundled in commit af06b8b.

The detailed milestone plan and design history (the v2 plan, backend artifact/ABI/signing design docs, the v1 plan, chat specs, etc.) are kept untracked under docs/_archive/.

Miner updater (xmrig)

The mining tab's pool section has an "Update miner…" button that downloads/verifies/installs the latest DRG-XMRig from the project Gitea (util/XmrigUpdater + ui/windows/xmrig_download_dialog.h). Flow: query git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest → pick the asset for this platform (linux-x64 / win-x64 / macos-x86_64; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive SHA-256 (from the release body) and a detached ed25519 signature → miniz-extract the binary (flattening the versioned subdir) into resources::getDaemonDirectory(). The whole archive is verified, so extracted members are trusted by transitivity (no per-member hash check). The pure, no-I/O core is split into xmrig_updater_core.cpp for unit tests; an env-gated (DRAGONX_TEST_NETWORK=1) test exercises the worker live. A "Browse all releases…" button (the /releases list, newest first, pre-releases included) lets users pin an older or pre-release build — same verify/install path via startInstallRelease(); the picker UI is shared with the daemon updater (ui/windows/release_list_view.h).

Signature verification is enforced (kXmrigRequireSignature = true in src/util/xmrig_updater.h), checked against the public key pinned in kXmrigSignaturePublicKeyBase64. Consequence for releases: every drg-xmrig release MUST ship a detached signature per archive or the in-app updater refuses it. To cut a release: build the archives, then scripts/sign-xmrig-release.sh sign <secret.key> <archive.zip>... (OpenSSL-based, no extra deps) and upload each <archive>.sig as a release asset alongside its .zip. The signing secret key must stay offline (it is gitignored: *.ed25519.key); only its base64 public key is pinned in the source. To rotate the key, regenerate (scripts/sign-xmrig-release.sh keygen) and update kXmrigSignaturePublicKeyBase64. An emergency env override is not provided — disabling verification means setting kXmrigSignaturePublicKeyBase64 empty (and rebuilding).

Daemon updater (dragonxd)

Settings → NODE & SECURITY → DAEMON BINARY has a "Check for updates…" button that downloads/verifies/installs the latest dragonxd full node from the project Gitea — the full-node sibling of the xmrig updater (util/DaemonUpdater + ui/windows/daemon_download_dialog.h, pure no-I/O core in daemon_updater_core.cpp; gated full-node-only via supportsFullNodeLifecycleActions()). Flow: query git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/latest → pick the archive for this platform (linux-amd64 / macos / win64; no match → "Unavailable") → libcurl download (TLS verified) → verify the archive SHA-256 (parsed from the release body's markdown checksum table, not xmrig's <hash> <name> lines) and a detached ed25519 signature → miniz-extract the three executables (dragonxd/dragonx-cli/dragonx-tx, flattening the versioned subdir) into resources::getDaemonDirectory(). The archive also bundles Sapling params/asmap, which the updater deliberately leaves to the wallet's own resource extraction. Install is atomic and safe while the node runs (POSIX rename() replaces the in-use binary; Windows moves the locked .exe aside to .old); the new binary takes effect on the next daemon start, so the Done screen offers "Restart daemon now" (App::restartDaemon()). A "Browse all releases…" button (shared release_list_view.h picker) lets users pin a specific/older/pre-release node build via startInstallRelease() — with a downgrade caution, since an older binary may not match current chain data.

Signature verification is enforced (kDaemonRequireSignature = true in src/util/daemon_updater.h), checked against kDaemonSignaturePublicKeyBase64. Consequence for releases: every dragonx release MUST ship a detached <archive>.sig per platform archive or the in-app updater refuses it (as of v1.0.2 the releases publish SHA-256 but no signatures yet — sign + upload them to enable in-app updates). To cut a release: scripts/sign-daemon-release.sh sign <secret.key> dragonx-<ver>-{linux-amd64,macos,win64}.zip (OpenSSL-based) and upload each .sig next to its .zip. The signing secret key stays offline (gitignored *.ed25519.key; this repo's is dragonx-daemon.ed25519.key); only the base64 public key is pinned. To rotate: scripts/sign-daemon-release.sh keygen and update kDaemonSignaturePublicKeyBase64. The generic SHA-256 / ed25519 primitives are shared with the miner updater (util::sha256Hex / util::verifyXmrigSignature).

Versioning

The version has a single source of truth: project(... VERSION 1.2.0 ...) plus DRAGONX_VERSION_SUFFIX in CMakeLists.txt. CMake generates build/.../generated/dragonx_generated_version.h from src/config/version.h.in. Do not hand-edit generated version output or hardcode version strings — bump the project() version in CMakeLists.txt.

Conventions

  • C++17. Match the surrounding code's style per file.
  • Icons: use the Material Design icon font defines (ICON_MD_*); never raw Unicode glyphs.
  • UI layout values belong in res/themes/ui.toml, read via schema::UI() — do not hardcode pixel sizes/offsets in code.
  • i18n: user-facing strings are translated via src/util/i18n; translation JSON lives in res/lang/ (de, es, fr, ja, ko, pt, ru, zh, English fallback in code). Translation/font helper scripts are in scripts/ (gen_*.py, CJK subset tooling).
  • Commits: the history uses Conventional Commits (feat(scope): …, fix(scope): …). PRs target master.