Adds a fail-open, local-only gate that decides whether the lite wallet may run,
so a post-release issue can disable it and rollout can be staged — without any
phone-home (privacy posture: no runtime network fetch; the per-install rollout
bucket is a hashed, never-transmitted local id).
- wallet/lite_rollout_policy.{h,cpp}: a pure decision core. Order — emergency env
kill-switch (absolute) -> local override -> manifest gates (global enable /
version floor-ceiling / blocklist / staged-rollout permille) -> fail-open allow.
Plus a JSON manifest loader (missing/invalid -> fail-open) and FNV-1a bucketing.
- Threads the decision through LiteWalletController -> LiteWalletLifecycleService:
new availability() reason RolloutDisabled blocks create/open/restore and surfaces
the gate's user-facing message via the lifecycle status.
- App::rebuildLiteWallet() resolves it from: DRAGONX_LITE_KILL_SWITCH (env), the
lite_rollout setting (auto/force_on/force_off), and a locally-cached manifest at
<config-dir>/lite_rollout.json. install id generated once via libsodium.
- Settings: persist lite_rollout override + the install id.
A signed remote fetcher can populate the manifest cache later without touching the
policy. Unit-tested (version compare, bucketing, override/env precedence, manifest
gates, staged rollout, loader fail-open, controller integration) and runtime-verified
on Linux (env kill-switch, manifest disable, control sync). Both variants build;
full suite passes; hygiene clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.8 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-framerender(), tab dispatchsrc/app_network.cpp— RPC orchestration, sync, peers, daemon lifecyclesrc/app_security.cpp— encryption, PIN/lock screen, key import/export, backupsrc/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/_batchchurn. This directory previously held ~160 deadlite_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-commithook) 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_BUILDdefine; renames the app toObsidianDragonLiteand excludes embedded-daemon / full-node assets (Sapling params, asmap, dragonxd).DRAGONX_ENABLE_LITE_BACKEND→ links a real external lite backend. Requires--lite, link modeimported, ABIsdxl-c-v1, and a symbols inventory file (built byscripts/build-lite-backend-artifact.sh); CMake hard-fails if any requiredlitelib_*symbol is missing.DRAGONX_ENABLE_CHAT→DRAGONX_ENABLE_CHATdefine 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
saveat those points), and passphrase encryption (encrypt/unlock/lock/decrypt + Settings UI + send-time & startup unlock; the backend locks immediately onencrypt). All controller-tested against the fake backend (tests/fake_lite_backend.h) and smoke-verified against the real SDXL backend viatools/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 fromlite_wallet_->walletOpen(), not RPC). - Packaging (verified):
./build.sh --lite-backend --linux-release(zip + AppImage) and--win-release(cross-compiled.exe; first build the Windows backend artifact withscripts/build-lite-backend-artifact.sh --platform windows). Both 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) feedingLiteWalletLifecycleService::availability()(newRolloutDisabledreason). Inputs: the emergency env varDRAGONX_LITE_KILL_SWITCH(absolute — not evenforce_onbypasses it); alite_rolloutsetting (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 inApp::rebuildLiteWallet(); the disable message surfaces via the lifecycle status. Unit-tested + runtime-verified (env / manifest / control). - Remaining (M5b): macOS packaging, CI backend-artifact build + signing.
- To publish: rename branch →
feat/lite-wallet, base the PR ondev(the full-node UX is already there), and handle the dormant gated-OFF HushChat content bundled in commitaf06b8b.
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/.
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 viaschema::UI()— do not hardcode pixel sizes/offsets in code. - i18n: user-facing strings are translated via
src/util/i18n; translation JSON lives inres/lang/(de,es,fr,ja,ko,pt,ru,zh, English fallback in code). Translation/font helper scripts are inscripts/(gen_*.py, CJK subset tooling). - Commits: the history uses Conventional Commits (
feat(scope): …,fix(scope): …). PRs targetmaster.