Commit Graph
85 Commits
Author SHA1 Message Date
DanSandClaude Opus 5.5 b29a09eaa0 perf(chat): sends re-read by origin, chat scans under the scan budget
A z_sendmany submit set off a burst of wallet scans before anything had
changed. Ops now re-read the wallet when they finish, by who started them:
the user's sends and dialog ops get a re-read plus one forced balance once
none of their ops is pending; client auto-shield re-reads unforced; chat
messages and splits refresh only the chat note view. A pending send no
longer forces scans, and chat txids no longer enter the tracked-send set.

Chat ingest skips stored messages before decrypting and caches
undecryptable ones per identity. The inbox scan runs when getwalletinfo's
txcount changed (a cheap probe, at most 2% duty) or a cost-scaled safety
net is due; the note-buffer scan runs on need. Both are admitted by the
scan budget, on a steady clock. The chat payer comes from a per-address
view of verified notes, with debits re-applied across in-flight scans;
splits are sized to one payer and only when they can refill the buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:06:34 -05:00
DanSandClaude Opus 5.5 f86cc4d6b7 perf(net): hold wallet scans at connect, in the background and on tab entry
- Connect gate: after the node starts answering, wallet scans wait until the
  lock state is known and the node knows its tip (or the wallet has fewer
  than 500 transactions), at most 20 s. A node already behind latches the
  sync profile from the connect's getinfo. Meanwhile a list-only pass keeps
  the address list and the cached history up, and the session's first
  balance and per-address balances are read once even while catching up,
  so the wallet shows its last-synced values instead of 0.
- Background: minimized, hidden, or covered without focus (3 s debounce,
  any input cancels) polls only the chain state every 15 s. Wallet scans,
  the mining poll, price, nudges and auto-lock interaction tracking pause;
  auto-lock now fires while minimized. Leaving a held profile marks every
  timer due again.
- Tab entry marks the timers due instead of forcing scans; a stale list or
  a never-loaded history still loads on entry.
- The scan entry points own the hard gates, failed enumerations keep the
  previous lists behind a retry hold, and failed balance calls never
  overwrite known balances with 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:06:34 -05:00
DanSandClaude Opus 5.5 323143e111 perf(net): one scan budget for all wallet scans, gated on what changed
The balance, address scan, history and probe were throttled one by one at
10% each, which added up to 30-40% of dragonxd's cs_main with all of them
due. services::WalletScanGovernor runs one wallet scan at a time across
both RPC lanes and keeps one shared quiet clock at 15% of wall-clock (cut
when the node is behind). Waiters are served first in, first out, with a
head start for what the page shows and a starvation floor of max(30 s
visible / 900 s hidden, 15x the stream's cost). Explicit work (a dirty
set, the user's send, a first load) is never refused but is charged.

Routine scans run only when the tip, the mempool size (getmempoolinfo:
no cs_main) or a wallet-mutation epoch changed since their last good scan,
or a safety sweep is due. Pages that don't show the balance poll it at
most every max(10 min, 50x its cost) (2 min with client-side auto-shield);
coming back into view with an old value fetches it at once. The staleness
badge counts from the last poll that confirmed nothing changed, and hides
while the budget holds the balance within its guarantee.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:06:34 -05:00
DanSandClaude Opus 5.5 879de37d18 perf(history): scan shielded history in bounded passes, not every block
Every block re-ran z_listreceivedbyaddress for every z-address (O(wallet)
each, under cs_main) and a whole-wallet z_listsentbyaddress. On a 40-address
wallet that was ~80% of wall-clock. The history now advances in passes:

- an address is rescanned when it is stale by a number of blocks scaled to
  its measured cost (8 to 500), never-scanned and hinted addresses first
  (a hint: z_listunspent shows a note the history doesn't);
- z_listsentbyaddress covers only the blocks since the last good sweep
  (+100 reorg margin); the whole-wallet sweep runs once per connect, with
  a 10 min / 1 h / 6 h backoff after real failures;
- listtransactions pages are sized from last time, and an unchanged tip
  reuses the transparent rows;
- found-but-unenriched txids carry over as a backlog; only -5 is a
  definitive "not ours" for z_viewtransaction;
- between blocks, a cheap recent poll plus a one-address probe.

Duty model for a 40-address wallet at 10 s per scan: ~9% instead of ~80%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:06:34 -05:00
DanSandClaude Opus 5.5 7ceff1eaaa fix(market): back off failed market fetches instead of retrying every frame
The Market tab calls the exchange-list, price-history and candle
fetches every frame, so a failed fetch (offline, rate-limited, a
venue's API down) was retried on the very next frame, ~20 requests a
second.

- util::FetchBackoff: 30 s doubling to a 10 min cap, +/-20% jitter.
  The exchange list, the price history (a partial result counts as a
  failure) and the per-pair candles each back off on failure.
- Candle backoff is per pair. Switching to another pair still loads at
  once, a failed refresh keeps the on-screen pair's last candles, and
  switching back to a pair that is backing off shows the aggregate
  chart rather than the previous pair's candles.
- The Market tab's refresh button retries at once (at most every 5 s).
  A price fetch (60 s timer, no backoff) that succeeds after failing
  means the network is back, so it lifts all three backoffs.
- A partial price history no longer marks the chart loaded: with no
  fetch timestamp, the 30-minute reload gate measured system uptime
  and blocked retries for the first 30 minutes after boot.
- The exchange list goes through httpGetString (TLS verified, size
  capped) with short timeouts (5 s connect, 12 s total, as it runs on
  the shared RPC worker) instead of its own curl code. A failed fetch
  is retried; it used to be tried only once per session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:52:10 -05:00
DanSandClaude Opus 5.5 575abd4bcc perf(ui): render frames on demand instead of continuously
While theme effects were on (the default) the main loop drew every
frame it could. Where vsync is off (offscreen, some drivers, remote
sessions) an idle wallet spun at ~3,000 fps, 88% of a core; with vsync
it still drew every refresh. Frames are now paced by what needs them:

- full rate while there is input, a held key, a window appearing or
  settling, a smooth scroll or the UI sweep;
- ambient (30 fps focused, 10 fps unfocused) for theme effects, the
  acrylic backdrop, avatar animations and notifications;
- progress (10 fps focused, 4 fps unfocused) for UI that changes
  without input. The updater and bootstrap downloads, console
  commands, lite-wallet operations, seed migration, the loading
  overlay and the lock-screen countdowns ask for it with
  material::RequestProgressFrame();
- otherwise idle, as before: wait for events, with a housekeeping wake
  every 2 s (5 s unfocused).

A software frame cap (0.9x the display refresh) bounds the loop where
vsync is off, and hidden, occluded or minimized windows get no ambient
frames. Reduce Motion freezes the theme-effect clock
(ThemeEffects::setMotionFrozen), so it needs no ambient frames either.

The displayed-balance roll goes through one clamped StepDisplayLerp
(balance_lerp.h). An idle-length frame pushed dt*speed past 1, so the
roll overshot, and under Reduce Motion's speed of 999 it diverged to
inf/NaN. The classic layout had its own copy of the old code.

Idle CPU, Lite build, offscreen (no vsync): 88% -> 5% of a core with
effects on, and 91% -> 3% with effects saved off (with the previous
commit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:52:09 -05:00
DanSandClaude Opus 5.5 71ed92b9f1 feat(wallet): consolidate notes per address, keeping each address's funds in place
Users who keep funds apart under different labelled addresses can now
shed notes (smaller wallet, faster scans/witness rebuilds) without the
funds being merged into a single address.

- Merge/Consolidate dialog: "Where should the funds go?" - Into one
  address (unchanged) or Keep in each address. The latter lists shielded
  addresses with 2+ spendable notes (label, note count, amount;
  too-small ones disabled) and merges each address's notes back into
  that address with z_mergetoaddress [addr] addr. Addresses with more
  notes than the batch size get several batches (the daemon locks a
  batch's notes before returning, so calls take disjoint notes); the
  dialog suggests a second run after those confirm. Per-address
  progress; ops keep running if the dialog is closed. The Consolidate
  (wallet-size) preset opens in this mode; Merge keeps "Into one".
  Shielded only: a transparent address can't take a merge's output.
- services/note_consolidation: groupSpendableNotes, worthConsolidating,
  estimateBatches, consolidateInPlace (pure; driven by a merge-call
  function), with unit tests against a stand-in following dragonxd's
  rules.
- fake_dragonxd: optional note wallet (FAKE_WALLET_NOTES,
  FAKE_MERGE_SECS) with z_mergetoaddress and its operations.
- i18n: 17 new keys in all 8 languages; CJK subset rebuilt.
- UI sweep: modal-consolidate target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:15:56 -05:00
DanSandClaude Opus 5.5 9339bb0663 test(windows): build and run the test suite for Windows from WSL
scripts/test-windows.sh cross-compiles ObsidianDragonTests.exe (and the
fake_dragonxd stub) with mingw into build/windows-tests, reusing the
toolchain and dependency sources from build/windows, and runs it from
WSL through Windows interop (or wine). All tests pass on Windows.

CMake:
- Windows forced BUILD_TESTING OFF in the cache to keep curl's tests
  out, which also removed ObsidianDragonTests from every Windows build.
  Hide it from curl with a scoped variable instead, and restore our
  cache value afterwards (curl's cmake_dependent_option FORCE-writes
  it). Windows still defaults to OFF, so release builds are unchanged.
- ObsidianDragonTests now depends on copy_langs: i18n.cpp includes the
  generated embedded/lang_*.h, so a tree that builds only the tests
  failed (latent on Linux too).
- Link the Windows system libraries the tests need.

Tests:
- testTmpPath/fixtureDir/removeTree helpers: no hard-coded /tmp, a
  runtime fixture-dir override (the compiled-in path is a Linux one),
  and best-effort cleanup that can't abort the suite.
- Close the explorer cache before deleting its directory (Windows
  can't delete an open file).
- Compile out the address-book and wallet-index round-trips on Windows:
  getConfigDir() uses SHGetFolderPath there, which no environment
  variable redirects, so they would overwrite the real
  %APPDATA%\ObsidianDragon files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:24:51 -05:00
DanSandClaude Opus 5.5 7b6e8a8a8e test(daemon): add fake_dragonxd, a slow dragonxd stand-in for stop/kill testing
A scriptable fake dragonxd that reproduces, in minutes, the incident where
the GUI hard-killed a node busy with an hour-long witness rebuild:

- dragonxd's HTTP-RPC queue (8 workers + 16 queued, -rpcthreads /
  -rpcworkqueue honoured): a full queue answers an immediate plain-text
  500 "Work queue depth exceeded", and a request whose client gave up
  keeps its slot until a worker reaches it
- a global cs_main held by a "witness rebuild" that logs the real
  markers; every method except stop/help takes it
- warmup -28 with the daemon's allow-list, an optional startup replay
- dragonxd's shutdown order: stop (or SIGTERM on POSIX) unbinds RPC,
  503s open connections, waits for the rebuild, then logs
  "Shutdown: In progress..." / "Shutdown: done" and exits 0

Everything is appended to <datadir>/fake_dragonxd.events; a missing
"clean-exit" line means the process was killed. One portable C++17 file
(POSIX + Winsock), OFF by default (-DDRAGONX_BUILD_FAKE_DAEMON=ON), built
into <build>/fake_dragonxd/ so it can't shadow the real bin/dragonxd.
run-gui-with-fake.sh runs the GUI against it in a throwaway HOME.

Against master's GUI on Linux it reproduces the bug: closing during a
rebuild queues stop behind cs_main-blocked workers, then the GUI sends
SIGTERM and SIGKILL mid-rebuild, with no clean exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:24:42 -05:00
DanSandClaude Opus 4.8 fae8b20fb7 fix: FAQ CJK glyphs, outgoing-tx history, lite encryption, migrate & sync safety
Bug-report fixes plus adversarial-audit follow-ups. Bumps full 2.0.1->2.0.2 and
lite 1.1.0->1.1.1.

i18n / fonts:
- Rebuild the NotoSansCJK subset with the glyphs the FAQ back-fill introduced
  (什/么/门/做); Chinese FAQ titles rendered as "??" on zh (and ja/ko).

Transaction history (enumeration gaps):
- Surface outgoing shielded z->z sends via z_listsentbyaddress (listtransactions
  and z_listreceivedbyaddress never report them).
- Surface z->t deshields by parsing z_listsentbyaddress transparentSends (which
  z_viewtransaction does not expose); dedupe t->t against listtransactions.
- Key send-row dedup on address so equal-value multi-output sends aren't dropped.

Lite wallet (key safety):
- The async create/restore/open path the UI uses now applies the passphrase:
  encrypt a new/restored wallet, unlock an existing one. It previously discarded
  the passphrase, storing the seed/keys in PLAINTEXT (create/restore) or leaving
  an encrypted wallet locked (open).

Migrate-to-seed (fund safety):
- Persist a sweep-submitted marker at broadcast so a resumed migration can't treat
  a ~0 balance (an unconfirmed in-flight sweep) as an empty wallet and adopt (swap
  wallet.dat) before the sweep confirms; best-effort locate routes to the confirm gate.

Sync / threading:
- Gate isSynced() on a peer-derived tip (tip_known) so a peerless node isn't reported
  synced (Send against a stale balance); add a "no peers" status banner.
- Guard daemon_status_ with a mutex (monitor-thread write vs UI-thread read).

Tests: extends tests/test_phase4.cpp across all of the above; suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwycPDQSfEKtrTS2uC4JeT
2026-09-04 04:02:52 -05:00
DanSandClaude Opus 4.8 c7e48c16f8 feat(lite): version 1.1.0 + variant-aware FAQ
Bump DRAGONX_LITE_VERSION 1.0.0 -> 1.1.0: since lite-1.0.0 the Lite variant
gained the whole 2.0.x shared UI/UX + diagnostics + i18n + perf work and this
session's chat delete/block — a minor bump (new features, no breaking change).

Make the FAQ variant-aware (walletFaq(fullNode)) so the Lite build reads
correctly: "What is ObsidianDragonLite?", encryption in the Wallet tab (not
Node & Security), migrate-to-seed hidden, node-specific answers reworded
neutrally ("the wallet syncs"), a new "Lite Wallet" subcategory (server model +
privacy tradeoff) standing in for the hidden Daemon group, and the redundant
single "Wallet" group tab dropped when there's no Daemon group to switch to.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-02 01:12:08 -05:00
DanSandClaude Opus 4.8 d27f387d6d feat(ui): in-app FAQ, DPI-scaling audit fixes, mining/stratum polish + localize strings
Bundles the session's UI work (the touched files carry several of these
changes together, so they are committed as one coherent UI batch):

- FAQ: new RenderFaqDialog + data-driven faq_content, opened from a
  status-bar "?" (and the Windows title bar), styled like the Wallets
  modal with search, Wallet/Daemon tabs, and smooth scroll.
- DPI/font-scale audit: multiply hand-drawn absolute geometry by
  Layout::dpiScale() across ~30 files so nothing renders native-size at
  HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x).
- Mining: chart now fills the horizontal space; thread stepper +/- buttons
  match the input-box height; move the stratum-host toggle into
  Node & Security (v1.3.0+).
- Settings: fix the auto-shield status text overlapping the grid.
- Sidebar: drop the peer-count badge on the Network button.
- i18n: wrap 193 hardcoded literals with TR() (keys/translations added in
  the preceding i18n commit), so the security/PIN/lock flow, seed-backup
  wizard, and witness-rebuild dialog localize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 19:37:16 -05:00
DanSandClaude Opus 4.8 6ee81a5abe fix: security-audit remediation (15 findings), empty-wallet warning, and send/chat/console/shutdown UX
Security audit remediation (15 confirmed findings from the codebase audit):
- H-02: scrub+delete the decrypt-flow plaintext key export on ALL exit paths
  (RAII guard) and purge stale obsidiandecryptexport* files at startup.
- M-01/L-03/L-04/L-05/L-07: sodium_memzero the Set-PIN and encrypt-PIN worker
  passphrase/PIN copies, the RPC Basic-auth string (auth_), the exported/imported
  key buffers (App::wipeSecrets, called from ~App and before main's _Exit), and
  the first-run wizard "Skip" buffers.
- M-03/M-04/M-05/L-06: return locked COPIES from XmrigManager/EmbeddedDaemon
  getters (dedicated error_mutex_; DaemonController::lastError now by value),
  route xmrig last_error_ writes through a locked setter, and wrap
  shutdown_status_/wizard_stop_status_ in a locking GuardedStatus
  (wizard_stopping_external_ -> std::atomic).
- M-02: persist after a console send/shield/import in the lite backend.
- L-01: require the confirm click for z_shieldcoinbase/z_mergetoaddress.
- L-02: quote/escape each Windows daemon argv per the MSDN CommandLineToArgvW rules.
- L-08: pin json/tomlplusplus/libwebp FetchContent to immutable commit SHAs.
- I-01: extract updater archives from the already-verified in-memory buffer
  (no disk re-read TOCTOU).

Feature: warn once (full-node) when the active wallet loads empty while a sibling
wallet file in the datadir holds keys. A funded salvage wallet.<ts>.bak routes to
the recovery/Restore flow; a funded sibling .dat routes to the wallet manager.
Per-wallet-file dismissal; gated on synced + address-list-loaded to avoid false
positives on warm reconnect / spent-down wallets.

UX fixes:
- send: show the TOTAL balance (with a spendable "available" note) in the source
  dropdown and keep pending-change addresses visible.
- chat: insert emoji at the cursor position; restrict new-chat recipients to
  shielded (z) addresses.
- console: optional auto-focus of the command input on tab open (off by default).
- shutdown: when "stop external daemon" is on, keep the shutdown screen up until
  the external node actually exits, showing live status.

Adversarially reviewed; verified across full-node, lite, and Windows builds; tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-18 13:18:49 -05:00
DanSandClaude Opus 4.8 8ffcd9cc8c build(node): bundle dragonx-wallet-rebuild in Linux + Windows releases
Wires the wallet-rebuild recovery helper into the release pipeline so a shipped
build actually carries it (the app locates it next to dragonxd).

- CMakeLists: link the vendored STATIC Berkeley DB for the helper — add
  Threads::Threads + dl (Linux) / ws2_32 (Windows) that the static libdb-6.2 needs
  (the system shared lib pulled those in transitively; the static one doesn't).
- build.sh (Linux + Windows): pass BDB_INCLUDE_DIR/BDB_LIBRARY explicitly at
  configure, pointing at external/dragonx/depends/<triple>/{include,lib/libdb-6.2.a}
  — the same libdb the daemon links, so the helper's output is a v6.2 btree the
  bundled dragonxd reads. Explicit paths bypass find_library (and the mingw
  toolchain's sysroot-only find restriction). Guarded: no depends → helper simply
  not built/bundled. Strip + copy the helper next to dragonxd(.exe) in both bundles.

Verified: Linux links the vendored libdb-6.2.a statically (no dynamic libdb) and
rebuilds the real broken wallet correctly; the helper cross-compiles cleanly with
mingw against the vendored Windows libdb-6.2.a to a PE32+ x64 exe. Full app +
helper build, suite green (1/1), build.sh syntax OK.

Remaining: macOS Berkeley DB (no in-tree depends artifact) + resource-embedding as
an alternative to side-by-side bundling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-10 11:38:20 -05:00
DanSandClaude Opus 4.8 bc183257b7 feat(node): in-app "Rebuild wallet database" recovery for a BDB-inconsistent wallet
Automates the manual recovery that fixed a wallet.dat with stale Berkeley DB
extent metadata (the "main" subdb metapage records a low last_pgno while its live
data spans thousands of pages beyond it). A tolerant page-walk reads every record,
but the daemon's BDB verify rejects the file and auto-salvages it — finding nothing
and shrinking the wallet to empty on each restart (the salvage cascade that looks
like fund loss). Plain "Restore original" can't fix it (hands the same broken file
back → re-salvage); a rebuild must produce a fresh, consistent DB.

Pieces (Approach A from the design workflow — out-of-process helper keeps AGPL
Berkeley DB out of the GPLv3 GUI):
- util/wallet_file_probe.h: extractWalletBtreeRecords() — sibling to parseWalletBtree
  that collects raw (key,value) bytes (same bounds-checked, subdb-aware walk).
  Records copied verbatim → encrypted key material passes through as opaque
  ciphertext (no passphrase). Overflow-page values (only large tx history) are
  skipped + counted; a rescan rebuilds history — funds unaffected.
- tools/wallet_rebuild/main.cpp: dragonx-wallet-rebuild CLI — reads via the tolerant
  reader, writes the records into a fresh BDB "main" btree via libdb (DB_EXCL, never
  overwrites), prints a JSON summary. New BDB-guarded CMake target.
- App::rebuildWalletDatabase(): picks the largest readable wallet/.bak as source,
  stops the daemon, runs the helper, VERIFIES the output (readable BDB with keys)
  before swapping, moves the current wallet aside (kept, timestamped), installs the
  rebuilt one, clears the stale BDB env, sets -rescan, restarts. Copy/rename only —
  never deletes. Result surfaced via the existing pumpWalletRestore channel.
- Wired as the preferred action on the existing wallet-auto-recovery dialog
  (shown only when the helper is present). Full-node only; lite-safe.

Verified end-to-end against the real broken wallet: helper reads 3,808 t-keys + 1
z-key + HD seed and the daemon LOADS the rebuilt output with no salvage. Adds
extractWalletBtreeRecords coverage. Build clean, suite green (1/1).

Remaining (follow-up): release packaging — build.sh bundling the helper built
against the vendored per-platform static libdb (DRAGONX_BDB_ROOT), and a macOS
Berkeley DB port (no in-tree artifact).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-10 11:26:55 -05:00
DanSandClaude Opus 4.8 a1d3964e34 fix(lite): require exactly 24 words on first-run restore (crash on valid seed)
The lite first-run restore wizard enabled Restore for {12,15,18,21,24}-word
phrases, but the SDXL backend only accepts 24-word / 32-byte-entropy seeds:
LightWallet::new does copy_from_slice(&phrase.entropy()) into a [u8;32]
(lightwallet.rs:231), which panics on 16/20/24/28-byte entropy. Mnemonic::
from_phrase accepts the shorter valid phrase, and the restore FFI
litelib_initialize_new_from_phrase (lib.rs:127) has no catch_unwind (unlike
litelib_execute), so the panic unwinds across extern "C" -> process abort
(UB on the pinned rustc 1.63). A user restoring a legitimate 12-word seed
from another wallet crashed the app.

The Settings restore gate was already tightened to == 24 (6ff1fda) but the
first-run wizard gate (df14533) was never updated — same restore path, two
verdicts, crash only via the more-common first-run path.

Add shared util/seed_phrase.{h,cpp} as the single source of truth:
- normalizeSeedPhrase: fold NBSP/en/em/ideographic/narrow spaces to ASCII,
  strip zero-width marks, collapse+trim (word bytes untouched)
- seedPhraseWordCount
- isCompleteRecoveryPhrase(int) == 24  (the sole SDXL contract)

Both restore gates now count via the normalizer and gate via
isCompleteRecoveryPhrase, and both submit the normalized phrase. This closes
the crash, reconciles the two gates so they can't drift again, and — because
tiny-bip39 splits on literal ASCII space with no NFKD — makes an NBSP-pasted
24-word seed (common from PDFs/note apps) restore correctly instead of being
undercounted and rejected.

Adds testSeedPhraseHelpers. Suite green (1/1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-09 01:32:40 -05:00
DanSandClaude Opus 4.8 16244d84a0 chore(release): bump version to 2.0.1
Full-node ObsidianDragon 2.0.0 -> 2.0.1 (single source of truth: the project() VERSION in
CMakeLists.txt). Verified the generated header renders "2.0.1 (ObsidianDragon)". The Lite
variant is versioned independently (DRAGONX_LITE_VERSION, unchanged at 1.0.0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 22:53:23 -05:00
DanSandClaude Opus 4.8 d188a08db7 test(daemon): add F1/F2 process-lifecycle integration tests; fix clobbered start error
Links the real EmbeddedDaemon into the ObsidianDragonTests target (its deps were
already present) and adds two POSIX integration tests that exercise the actual
fork/exec/waitpid fixes headlessly:

- testExecFailureReported (F2): start() against a non-executable file must fail with a
  precise "not executable or wrong architecture" reason.
- testDaemonCrashDetected (F1): a short-lived child that exits abnormally is still
  detected (crash_count_ increments) while isRunning() is hammered from the test
  thread — a regression test for the reap race.

Writing the F2 test surfaced a real bug: start()'s failure branch called
setState(State::Error, "Failed to start dragonxd process"), and setState stores the
Error message into last_error_ — clobbering the precise message startProcess() had
just set, so getLastError()/the UI only ever saw the generic string. Fixed to pass the
preserved detail to setState, so the precise reason survives and now also reaches the
state callback (crash panel / status).

ctest 1/1, green including the two new integration tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 13:11:15 -05:00
DanSandClaude Opus 4.8 b561406c23 fix(build): make macOS release builds work natively
The --mac-release path (full-node and --lite-backend) had never run on a real
Mac and broke on several Linux-only assumptions:

- build.sh version parser used GNU sed \+ (BSD sed reads it literally), so it
  aborted before compiling — switched to POSIX [[:space:]][[:space:]]*.
- build.sh libsodium universal check used GNU grep \| — switched to grep -E.
- libwebp's cpu.cmake applies -mno-sse2 to its reference DSP files; under a
  universal build (-arch arm64;x86_64) that lands on the x86_64 slice, where it
  disables _Float16 and breaks the SDK 26 <math.h>. Added an idempotent
  FetchContent PATCH_COMMAND (cmake/patch-libwebp-simd.cmake) to neutralize it.
- The SDXL lite backend static lib needs Security + CoreFoundation frameworks on
  macOS; added them to the imported dragonx_lite_backend target for APPLE.
- Added a DRAGONX_MAC_ARCHS override plus auto-detect: with --lite-backend the
  app is built for the arch(es) the backend .a actually provides (its pinned
  ring 0.16.11 is x86_64-only), instead of failing to link a universal app.
- build-lite-backend-artifact.sh uses bash 4+ (mapfile); added a re-exec guard
  for macOS's stock bash 3.2 and added bash to setup.sh's macOS core deps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:03:33 -05:00
DanSandClaude Fable 5 bcee4bfe72 fix(security): apply audit remediations to dev (ported + dev-only)
Dev-branch audit (docs/_archive/security-audit-dev-2026-07-22.md) re-found the
master issues (absent on dev) plus new ones in dev-only code. Applied here;
full-node build + test suite green; the subtle fixes were adversarially re-verified.

Ported from the master remediation (adapted to dev's code):
- bootstrap: reject zip-slip / path-traversal archive members (isSafeArchivePath)
  before writing (S2-1). (dev already fail-closes on a missing checksum.)
- xmrig updater: fail closed when a signature is required but no key is pinned (F1-1).
- http_download.httpGetString: 16 MiB hard cap + MAXFILESIZE on the shared
  metadata/price fetch (F1-2 / caps the updater + exchange paths at one site).
- rpc_client: explicit SSL_VERIFYPEER/VERIFYHOST (F4-1) and a 256 MiB response
  cap in WriteCallback (F4-3).
- lite_connection_service: reject remote http:// lite servers, loopback only (L1-1).
  Loopback is matched by a strict dotted-decimal 127.0.0.0/8 check (not a
  startsWith("127.") prefix, which would wrongly accept 127.0.0.1.evil.com), with
  userinfo/fragment stripping.
- lite controller: propagate encrypt/decrypt save() failure instead of reporting
  success (F7-1).
- xmrig_manager: chmod(0600) the pool config before writing secrets (F5-2).
- app: clear the copied secret from the OS clipboard on shutdown (F3b-1).
- export_transactions: neutralize CSV/spreadsheet formula injection (F13-1).
- build pipeline: build-from-source lite backend + remove the self-attested
  CMake signature gate (F15-1); pinned+verified appimagetool (F15-3/4);
  verified Sapling params in setup.sh (F15-6); build.sh exits 0 on success.
  (F14-1 empty-quoted-arg and F8-2 NUL-termination were already fixed on dev.)

Dev-only findings:
- rpc_client.callRaw: scrub the raw buffer + parsed tree (templated scrubJsonSecrets
  for ordered_json) so console dumpprivkey/z_exportkey keys don't linger in freed
  heap (N1-1).
- seed_wallet_creator: wipe the exported mnemonic on the failure path so a discarded
  failed result never carries a live seed (W1-2).
- export_all_keys: write the plaintext key dump 0600 + atomically via
  writeFileAtomically (U1-2).
- chat_database: restrict chat_messages.sqlite and its WAL/SHM sidecars to owner-only (C3-1).

Not done (need a decision, documented in the report):
- Chat header metadata (cid/z/p) rides outside the AEAD (C1/C2) — binding it is a
  wire-protocol change requiring SilentDragonXLite interop review.
- Bootstrap lacks an offline-rooted signature (S2-2 residual) — needs signing infra.
- Console scrollback retains console-typed key-export output in plaintext (N1-1
  residual) — inherent to an echoing console; would need output redaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:08:24 -05:00
DanSandClaude Opus 4.8 17525003c5 feat(ui): themed DragonX logo — runtime-rasterized SVG recolored per skin
Replace the shared app logo (Overview header, first-run wizard, lock screen) with
the DragonX mark rendered from res/img/logos/logo_dragonx.svg, recolored to each
theme: the dragon body takes the theme accent (Primary()), the inner detail stays
light. Vendor nanosvg (memononen, zlib/public-domain) in libs/nanosvg/ and add
util::LoadTextureFromSvg (parse a copy, recolor shapes by luminance, rasterize to
RGBA, upload via the existing CreateRawTexture — GL + DX11). The SVG is embedded as
a string (src/embedded/logo_dragonx_svg.h) so it's available in every build.

The load moves into App::ensureLogoTexture(), called at the top of render() BEFORE
the wizard/lock early-returns (so those screens get the logo too, which they never
did before), and re-rasterizes when the accent or the dark/light variant changes.
The per-skin PNG remains a fallback if rasterization ever fails; logo-texture
lifetime is now freed on replace + on theme switch (was leaked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:28:51 -05:00
DanSandClaude Opus 4.8 a78b44246e build(freetype): color-emoji rendering via a cross-platform FreeType backend
The chat "Emoji style: Color" option renders the merged emoji in color (COLR/CPAL
Twemoji) instead of the monochrome NotoEmoji subset. This needs FreeType, which the
default stb_truetype rasterizer can't do for color glyphs.

- Vendor imgui_freetype (matches the bundled 1.92 ImFontLoader API) and embed a 1.4 MB
  COLRv0 Twemoji font (no libpng/harfbuzz needed).
- CMake gains an optional FreeType path: native Linux/macOS use the system FreeType via
  find_package; the mingw-w64 cross-compile has none, so build.sh --win-release now
  cross-builds a minimal static FreeType (scripts/build-freetype-mingw.sh) and passes it
  in. Absent FreeType => graceful monochrome fallback, so no build breaks.
- Typography selects the FreeType loader + the color font (LoadColor) when color emoji is
  on, else the stb loader + mono subset; toggling reloads the atlas. Both the DX11 and
  OpenGL backends already support the 1.92 RGBA dynamic atlas, so color glyphs render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 18:44:19 -05:00
DanSandClaude Opus 4.8 923d086092 feat(images): WebP decode via libwebp + more stb formats
Broaden avatar/image support:

- Add libwebp (FetchContent, static, decode-only) so WebP loads. Built from
  source for Linux / mingw-Windows / macOS-osxcross identically — the cross
  sysroots have no webp, so vendoring from source is the one portable path;
  encode/tool builds are disabled to avoid pulling in libpng/zlib. Linked as
  webp + webpdemux (the latter for animated WebP, wired next).
- texture_loader routes all decode through DecodeImageRGBA: sniffs the RIFF/
  WEBP header and uses libwebp (WebPDecodeRGBAInto into a free()-able buffer),
  else stb — so every existing caller (avatars, QR, thumbnails) gains WebP for
  free with no allocator mismatch.
- Enable stb's TGA / PSD / PNM / PIC decoders and add .webp/.tga/.psd/.pnm/
  .ppm/.pgm/.pic to the image-picker + avatar-library extension lists.

Verified: libwebp builds static and a real .webp decodes to correct RGBA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:57:01 -05:00
DanSandClaude Opus 4.8 72bf59149d feat(wallet): per-wallet data isolation foundation (P1)
First phase of multi-wallet support: keep each wallet's data separate so loading
a different wallet no longer shows the previous one's data, and lay the metadata
groundwork for the wallet-files list.

- Address book scoped per wallet: AddressBookEntry gains a "scope" ("global" or a
  wallet-identity hash); the Contacts tab shows global + current-wallet contacts
  (App::activeWalletIdentityHash) with a "show in every wallet" toggle + globe
  badge. Legacy entries migrate to "global". Scope-aware de-dup allows the same
  address across different wallets.
- Wallet metadata index (data/wallet_index -> wallets.json): file-keyed cache of
  balance / address count / identity / size / last-opened / synced-here, since
  those can't be read off a wallet.dat without loading it. Populated on connect +
  address refresh (change-detecting upsert). Plus an active_wallet_file setting
  for the -wallet=<name> switch coming in P2.

Unit-tested (testAddressBookScope, testWalletIndex): migration, visibility filter,
scope-aware de-dup, upsert change-detection, save/reload round-trip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:57:23 -05:00
DanSandClaude Opus 4.8 a86cb8f0f3 feat(debug): full UI screenshot sweep — captures modals/dialogs/flows/states
The existing sweep only captures tabs. This adds a "Full UI sweep" (DEBUG
OPTIONS) that also drives every normally-hidden surface into view and captures
it under every skin, so all the UI a normal sweep misses can be reviewed at
once.

- The capture unit becomes a "surface" (SweepTarget: a base tab, optionally
  with a setup()/teardown() that forces a modal / multi-step flow / state
  overlay on top). Tabs are surfaces with a null setup. The existing tab sweep
  is folded into the same state machine (one code path). All sweep logic +
  the catalog + demo data move into a new src/app_sweep.cpp.
- Runs OFFLINE against injected demo data (installDemoWalletData snapshots then
  restores the real state_ — WalletState isn't copy-assignable, so a targeted
  field snapshot is used) and fires NO live op: capture_mode_ guards the
  seed-backup RPC, auto-lock, and the migration pumps; the async-firing dialogs
  are neutralized by pre-setting their state (fetch_started_ + demo phrase;
  seed_migration_step_ set directly). Verified: 0 RPC/secret/send calls.
- Catalog (v1): tabs + the bool-flag modals (import/export key, backup,
  seed-backup, about, settings) + the 6 migrate-to-seed steps + 4 wizard steps
  + lock/warmup/not-ready overlays + the send-confirm popup (via a new
  ui::SweepShowSendConfirm debug hook, since its state is send_tab-static).
- Overlay/blur surfaces settle 8 frames (blur backdrop freeze); setup re-runs
  each frame (OpenPopup popups must re-fire). updateScreenshotSweep now runs
  before the first-run-wizard early-return in render() so the sweep advances
  while a wizard step is shown. main.cpp drops vsync during a sweep so it isn't
  throttled to the compositor's unfocused rate. Output:
  <config>/screenshots-full/<surface>/<skin>.png + index.md.
- DRAGONX_FULL_SWEEP=1 runs the sweep headlessly then quits (CI / verification).

Verified end-to-end under WSLg: 288 PNGs (9 skins x 32 surfaces), all 1200x720,
[Sweep] done, clean quit, no daemon. Byte-identity across runs holds only for
static skins on data-free surfaces — the rest vary by animated theme effects /
live price / relative timestamps (expected for a review tool, not a bug).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 16:57:54 -05:00
DanSandClaude Opus 4.8 718cb82c13 feat(wallet): migrate-to-seed, Phase 1 — isolated seed-wallet creation
First, fund-safe step of "migrate a legacy wallet to a seed wallet": mint a
brand-new BIP39 mnemonic wallet in an ISOLATED throwaway datadir so the user
can later sweep funds into it. No funds move and the real wallet.dat / main
daemon are never touched — the isolated node runs concurrently on its own port.

- EmbeddedDaemon: one-shot isolated-datadir override (setNextStartOverride)
  consumed on the next start(); a skip-default-port-check for an isolated
  instance beside the main daemon; and a tcpPortInUse() probe. The datadir's
  basename must be the assetchain name (DRAGONX) and the daemon reads
  <datadir>/DRAGONX.conf itself, so no -conf is passed (both learned from
  live testing against the hd-transparent-keys daemon).
- SeedWalletCreator (src/daemon): picks a free port, writes a throwaway conf,
  starts an isolated dragonxd with -usemnemonic=1 -connect=0, waits for RPC,
  calls z_exportmnemonic + z_getnewaddress (the sweep target), stops it, and
  keeps/scrubs the temp datadir. Off-thread; the seed is wiped after use.
- App: beginCreateSeedWallet (background) -> pumpSeedMigration (main-thread
  handoff) -> renderSeedMigrationDialog (Intro -> Working -> Show seed ->
  Error), reusing the seed_display grid + clipboard auto-clear.
- Settings: "Migrate to seed…" button (full-node gated) + persisted pending
  migration state (dest address + temp datadir) for Phase 2 to adopt.

Validated end-to-end against the hd-transparent-keys daemon: the isolated
node comes up in ~4s and returns a 24-word mnemonic + a shielded z-address.
Requires that daemon (the bundled Jun-28 build lacks z_exportmnemonic). The
migration modal uses English literals for now (as renderBackupDialog does);
the whole flow gets translated once Phase 2 finalizes it. Phase 2 (sweep +
adopt) is next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 12:53:36 -05:00
DanSandClaude Opus 4.8 6c08a08127 feat(chat): enable HushChat by default + legacy (non-mnemonic) wallet support
Flip DRAGONX_ENABLE_CHAT to default ON (a fresh configure now builds chat in),
and make full-node chat work for every wallet + any daemon.

Legacy compat: chat identity is derived from the wallet's mnemonic
(z_exportmnemonic) for a portable, SDXLite-compatible identity — but a legacy
random-seed wallet has no mnemonic, and released daemons don't have that RPC at
all. Provisioning now falls back to a stable z-address's spending key
(z_exportkey) in those cases: a functional, wallet-local identity that works on
any daemon. Since a chat identity is local (peers learn your public key from
your message headers, not your derivation), interop is unaffected; only
cross-client portability needs the mnemonic. The spending key is an in-memory
KDF input over a key the wallet already holds, wiped after use — no new exposure.

Stability: the chosen chat z-address (the reply-to in headers AND the legacy
identity source) is now persisted in settings (chat_reply_zaddr), so the
identity + reply address don't shift when new addresses are generated.
chatReplyZaddr() picks the smallest spendable z-addr once and reuses it.

CLAUDE.md updated to reflect the default flip. Verified: Linux + Windows build
with chat ON, fresh-configure default confirmed ON, ctest 100%, hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:16:35 -05:00
DanSandClaude Opus 4.8 4db609fb52 feat(chat): composer + contact requests — outgoing construction (Phase 4)
Add the outgoing side of HushChat: compose messages and start conversations.
The wire construction is the byte-exact inverse of the receive parser and is
proven by a self-consistent round-trip (build → parse → decrypt).

- src/chat/chat_outgoing.{h,cpp} (pure): buildOutgoingMessage encrypts via
  encryptOutgoing and buildOutgoingContactRequest carries plaintext; both emit
  the header memo JSON ({h,v,z,cid,t,e,p}, which nlohmann serializes
  alphabetically to match SilentDragonXLite) + the payload memo, validating the
  512-byte memo limit, the 64-hex peer key, and the "no leading '{'" rule for
  request text. My public key goes in header "p"; the peer's key is the
  encryption recipient.
- ChatService: composeMessage/composeContactRequest (encrypt with the held
  identity) + recordOutgoing (echo an Outgoing ChatMessage into the store + DB —
  we never harvest our own sends, so this is the only local record).
- App: sendChatMessage(cid,text) sources the peer z-addr + public key from the
  conversation, composes, and records a random-id echo; startChatConversation
  (zaddr,text) mints a random cid + composes a plaintext contact request;
  chatReplyZaddr() picks a spendable z-addr. broadcastChatMemos() is the Phase-5
  transport seam (network delivery + real-SDXL interop verification land there).
- Chat tab: a message composer (shown once the peer's key is known, else a
  "waiting for reply" hint) + a "New conversation" modal that sends a contact
  request to a z-address.
- Tests: outgoing round-trip through the receive parser + decrypt, contact-request
  passthrough, validation guards, and ChatService compose + recordOutgoing echo.

Adversarially reviewed (crypto/interop, secret hygiene, logic, UI) — 0 findings.
Gated by DRAGONX_ENABLE_CHAT (default OFF). Verified: Linux + Windows(mingw)
build with chat ON, ctest 100%, hygiene clean; caches restored to the OFF default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 15:30:41 -05:00
DanSandClaude Opus 4.8 980a100edd feat(chat): read-only Chat sidebar tab (Phase 3)
Add the first user-visible HushChat surface: a Chat tab in the sidebar with a
two-pane, read-only conversation view — a conversation list on the left and the
selected thread on the right — reading from the App-owned ChatService store.

- New src/ui/windows/chat_tab.{h,cpp} (RenderChatTab(App*), mirroring the
  Contacts tab). Conversations are sorted by most-recent activity; peer
  z-addresses resolve to contact names via the address book when known; each
  message shows sender + timestamp + wrapped body, with a contact-request tag
  and a read-only footer (composing arrives in a later phase). Empty states
  cover "wallet locked / identity not ready" and "no conversations yet".
- Sidebar wiring: NavPage::Chat + registry entry (static_assert stays balanced),
  NavPageSurface + GetNavIconMD (ICON_MD_CHAT), dispatch in app.cpp, trace/sweep
  page names, and App::chatService() accessor. The tab is gated on the new
  WalletUiSurface::Chat, which returns DRAGONX_ENABLE_CHAT != 0 — so it only
  appears in chat-enabled builds and is hidden (and unreachable) by default.
- i18n: English defaults for the chat nav label + hint strings.

Verified: Linux + Windows(mingw) build with chat ON, ctest 100%, hygiene clean;
caches restored to the OFF default. The screenshot sweep now includes the Chat
tab (sweepPageName "chat") for per-theme visual review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 15:05:40 -05:00
DanSandClaude Opus 4.8 46eec37013 feat(chat): persistent, seed-encrypted message store (Phase 2)
Swap the in-memory-only chat store for durable sqlite persistence with real
per-transaction timestamps, encrypted at rest under a key derived from the
wallet's own seed (no passphrase, works on encrypted and unencrypted wallets).

- ChatDatabase (src/chat/chat_database.{h,cpp}): sqlite store mirroring
  data::TransactionHistoryCache. unlockWithSecret(seed) derives a 32-byte AEAD
  storage key and a wallet-partition tag via domain-separated keyed BLAKE2b
  (generichash) contexts. Every record — bodies, peer z-addrs, threading,
  timestamps — is crypto_aead_xchacha20poly1305_ietf-encrypted with a random
  nonce and the wallet tag as associated data; even the dedup key is a keyed
  hash of txid+position, so nothing about your conversations is in cleartext on
  disk. Rows are partitioned per-wallet; a different seed sees nothing. Messages
  are decrypted once at ingest then re-encrypted under the storage key, so
  load() needs only the storage key, not the chat identity.
- ChatService: ingest() now stamps each message with its own transaction time
  (txid->time map + fallback) and writes new (store-deduped) messages through to
  the database; loadFromDatabase() rehydrates the in-memory read model on unlock.
- App: unlock the chat DB with the same seed in provisionChatIdentityFromSecret
  and load prior history; lock the DB + clear the decrypted in-memory store on
  relock and on lite-controller rebuild.

Adversarial review (4 confirmed findings, all fixed): don't provision if the
wallet locks mid-fetch (re-check isLocked at completion); wipe the serialized
plaintext temporary in append(); trim the seed into a separate fully-wiped
buffer (no residue past a shrunk size()); scrub the mnemonic copy in the RPC
json result.

Tests: ChatDatabase round-trip (persist/reload, field + order fidelity), dedup,
per-wallet isolation, lock inertness, and ChatService write-through + reload
without an identity. Gated by DRAGONX_ENABLE_CHAT (default OFF). Verified:
Linux + Windows(mingw) build with chat ON, ctest 100%, hygiene clean; caches
restored to the OFF default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:49:33 -05:00
DanSandClaude Opus 4.8 ba03de938e feat(chat): message model, in-memory store, and receive service
Phase 1 Steps 4-6 (the receive pipeline, minus the App/sync wiring which
lands next).

- chat_message: the in-memory ChatMessage model (direction, kind, txid, cid,
  peer zaddr/pubkey, body, timestamp, payload_position). No libsodium.
- chat_store: threads messages by conversation_id and dedups by
  (txid, payload_position) so re-scanning the chain never double-inserts.
- chat_service: owns the long-lived chat identity keypair (move-disabled,
  wiped on destruction/clear) and the store. ingest() decrypts each Message
  (drops undecryptable ones silently — no plaintext/memo logging), passes a
  ContactRequest's plaintext through, and threads the result. No-op without an
  identity.

Tests: a metadata batch decrypts into a threaded conversation; re-ingest
dedups; a contact request carries through; a wrong identity decrypts nothing;
no-identity ingest is a no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:11:05 -05:00
DanSandClaude Opus 4.8 2502487e44 feat(chat): seed-derived identity + secretstream crypto core
Phase 1 crypto foundation (gated by DRAGONX_ENABLE_CHAT; the pure primitives
are always compiled + unit-tested, the feature gate lives at the future
service layer).

- chat_identity: derive the DragonX-native X25519 (crypto_kx) identity from a
  stable per-wallet secret via a domain-separated keyed BLAKE2b KDF
  (crypto_generichash, context "DragonX-HushChat-Identity-v1") into a clean
  32-byte crypto_kx seed. Deterministic; skips SDXL's UTF-8-hex-seed quirk
  (that's the Phase-4 import path). Per §5.6.
- chat_crypto: encryptOutgoing (server_tx) / decryptIncoming (client_rx) via
  crypto_secretstream_xchacha20poly1305, byte-exact per Appendix A.3/A.4 so
  DragonX interoperates with SilentDragonXLite. Single chunk, TAG_FINAL;
  decrypt enforces both the Poly1305 tag and TAG_FINAL (stricter than SDXL).
  Every session key / seed / stream state / plaintext scratch is sodium_memzero'd
  on all paths; no secret is ever logged.
- Tests: encrypt->decrypt round-trip (incl. empty + UTF-8), identity
  determinism, feature-gate + empty-secret handling, and malformed/tampered/
  wrong-key inputs all fail safely.

Adversarially security-reviewed (3 lenses: secret hygiene, crypto correctness,
SDXL wire-interop) — confirmed byte-compatible with the SDXL reference in both
directions. Fixes from review: check the secretstream_push return before
reporting Ok; allow the empty-plaintext ciphertext (== ABYTES) so encrypt/decrypt
round-trip symmetrically; document the caller-owns-and-wipes secret contract.

Interop caveat: round-trip proves self-consistency; a real captured SDXL
message is still needed to prove wire-compat end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 19:54:47 -05:00
DanSandClaude Opus 4.8 ddd53dc006 feat(send): add a shared contact picker to the recipient field
Phase 0c. The Send recipient field only offered Paste and a tx-history
suggestion list — no way to pick a saved contact. Add a compact contacts
icon button next to Paste that opens a picker popup over the App-owned
address book; selecting a contact fills the recipient (re-validated next
frame).

- New header-only src/ui/windows/contact_picker.h: ContactPickerPopup(id,
  book, outBuf, outSz) — a reusable popup listing contacts ([Z]/[T] tag +
  label + short address) that copies the chosen address into the caller's
  buffer. Kept out of the material layer so the design-system headers stay
  free of data/ deps; reserved for Chat "new conversation" later.
- send_tab: shrink the recipient input to make room for the icon button
  (ICON_MD_CONTACTS), wire the popup to app->addressBook(). Coexists with the
  existing Paste + suggestion affordances.
- i18n: send_contacts_button tooltip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 17:04:58 -05:00
DanSandClaude Opus 4.8 ee59a9e2cd feat(ui): promote the address book to a Contacts sidebar tab
Phase 0b. The address book was buried in Settings behind a modal dialog.
Make it a first-class "Contacts" tab (which will also become the chat
roster), rendered inline in the main content area.

- New NavPage::Contacts (after History; ICON_MD_CONTACTS) +
  WalletUiSurface::Contacts. isUiSurfaceAvailable's `default: return true`
  shows it in BOTH variants; uiSurfaceNeedsWalletData's default keeps it
  usable before wallet data loads. All the touchpoints wired: NavPageSurface,
  GetNavIconMD, the app.cpp dispatch case, the app_network.cpp tracePageName
  case, and the `contacts` i18n label.
- New src/ui/windows/contacts_tab.{h,cpp}: RenderContactsTab lifts the
  toolbar + table + add/edit modal out of address_book_dialog, rendered in a
  BeginChild scroll region (peers_tab pattern) instead of an overlay; the
  add/edit form stays a modal layered over the tab. Reuses the existing
  address_book_* i18n keys and the dialogs.address-book schema.
- Delete address_book_dialog.{h,cpp}; remove its app.cpp render pump and the
  dead App::show_address_book_. The Settings "Address Book…" button now
  navigates to the tab (setCurrentPage) instead of opening the modal, so the
  Tools & Actions grid layout is untouched.
- CMake: swap the dialog sources for contacts_tab.

Behavior-preserving move; search/sort/keyboard/contrast land in 0d. Visual
check pending the per-theme screenshot sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 16:59:34 -05:00
DanSandClaude Opus 4.8 ccbb0783b5 chore(ui): remove dead dialog duplicates (Phase 2 cleanup)
Grep-verified dead, zero live callers (the live paths all go through
BeginOverlayDialog and already got the blur backdrop):
- App::renderAboutDialog (app.cpp/app.h) — dispatch uses ui::RenderAboutDialog.
- src/ui/windows/import_key_dialog.{h,cpp} (ImportKeyDialog class) — live path is
  App::renderImportKeyDialog.
- src/ui/windows/backup_wallet_dialog.{h,cpp} (BackupWalletDialog class) — live
  path is App::renderBackupDialog.
Drop the two dead files from CMakeLists. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 12:29:38 -05:00
DanSandClaude Opus 4.8 9c533fa485 refactor(audit): batch 5 — share the updater HTTP/download layer
XmrigUpdater and DaemonUpdater carried byte-identical libcurl code —
httpGet, downloadToFile, and the write/progress callbacks — including the
security-critical TLS-verify (SSL_VERIFYPEER=1 / SSL_VERIFYHOST=2) and
max-body-size options, duplicated across two signature-verified
download-and-execute files that had to be hand-kept in sync.

Hoist that machinery into src/util/http_download.{h,cpp}:
- util::httpGetString(url, logTag)
- util::httpDownloadToFile(url, dest, logTag, maxBytes, onProgress)
threading progress + cancellation through a std::function (returning false
aborts the transfer) instead of the class `this` pointer. Each updater's
httpGet/downloadToFile become one-line forwarders that bind their own log
tag and archive-size cap (64 MiB miner / 256 MiB node, both preserved).

Every curl option is preserved byte-identically (verified by diffing the
setopt lines against the pre-change code — the only deltas are the
now-parameterized MAXFILESIZE cap and the XFERINFODATA payload). No behavior
change; the TLS/size-guard options now live in exactly one place.

Deliberately scoped DOWN per the audit's own guidance: the divergent
per-updater logic (release DTOs/parsers, checksum-table parsing, version
compare, and installResolved's per-member-SHA vs transitive-trust verify,
install strategy, and chmod scope) stays in each class — NOT merged behind a
base-class/virtual-hook layer, which would raise review cost on
signature-verified execute-code and risk a subtle security regression. The
shared-DTO/parser consolidation (~77 test refs of churn on stable code) is
left as a separate, lower-value follow-up.

Full-node + Lite build clean; ctest 1/1; source-hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:17:44 -05:00
DanSandClaude Opus 4.8 7891c689fb refactor(audit): batch 2 — remove dead scaffolding from the shipping binary
Two dead-code removals surfaced by the audit; no runtime behavior change.

1. Delete the lite-lifecycle "readiness" scaffold (~1,586 lines). The pure
   dry-run evaluateLiteWalletServerLifecycleReadiness (whose own status text
   says "wallet lifecycle execution is still disabled in this scaffold") had
   zero callers; executeLiteWalletServerSelectionUi had zero callers; and
   executeLiteWalletLifecycleUiRequest's only caller was a fallback in
   settings_page reached only when app->liteWallet() is null. Deleted
   lite_wallet_server_lifecycle_readiness.{h,cpp} and the readiness /
   UI-execution machinery + enums + translation tables from both adapters,
   keeping the genuinely-live helpers (liteConnectionSettingsFromAppSettings,
   applyLiteConnectionSettingsToAppSettings, redactLiteServerSelectionValue,
   and the LiteWalletLifecycleUiExecutionInput DTO the live create/open/
   restore path still uses). The null-backend fallback now sets a plain
   "Lite wallet backend unavailable" status. This is exactly the
   readiness/scaffold pattern CLAUDE.md forbids regrowing.

2. Split the HushChat fixture/capture-manifest/seed-projection tooling
   (~2,050 lines, incl. the libsodium seed-projection) out of
   chat_protocol.cpp (1854 -> 284) and chat_protocol.h (586 -> 105) into a
   new chat_fixture_tooling.{h,cpp} compiled ONLY into the HushChatFixtureCheck
   dev tool — never into the app, lite, or test binaries (verified via nm).
   The app keeps only the runtime slice (memo parsing + tx metadata
   extraction) that network_refresh_service actually calls. Also moved the
   decrypt-preflight/hex helpers, which likewise had no runtime caller.

Note: the HushChat split is on gated-off experimental code and should be
coordinated with the pending dormant-HushChat-content handling (commit
af06b8b) before the lite PR — done here as a pure mechanical split, no redesign.

Full-node + Lite + HushChatFixtureCheck build clean; ctest 1/1; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:20:53 -05:00
DanSandClaude Opus 4.8 ff20a8a44e feat(console): terminal polish — lite/color-coded toggles, dark bg, monospace font
Work through the console items in todo.md:

- Lite filter toggles (#5): replace the single hasLogFilters() bool with a
  ConsoleLogFilterCaps struct so each backend advertises which toggles apply.
  Full node = daemon/errors/rpc-trace/app; lite = errors-only + app (its
  diagnostics ring maps to the App/Error channels; the text filter is always
  shown). Lite previously showed no toggles at all.
- Color-coded toggles (#6): each filter checkbox is tinted with its channel's
  accent color (daemon=blue, errors=red, rpc=secondary, app=teal) via a new
  channelAccentColor() that also drives the output's left accent bar — one source
  of truth for channel color.
- Darker terminal look (#7): drop the light glass rectangle on the input; both
  output and input now get a terminal-dark overlay (tabs.console.bg-darken-alpha,
  default 110) and the input field blends into it (transparent frame bg).
- Monospace font (#8): bundle Ubuntu Mono (res/fonts/UbuntuMono-R.ttf, Ubuntu
  Font License — same family as the existing Ubuntu fonts) via INCBIN, load it at
  caption size as Type().mono(), and render console output + input in it so
  pretty-printed JSON columns and terminal text align. Falls back to the
  proportional caption font if unavailable.

Full-node + lite build clean; ctest green; source hygiene clean; sandboxed
startup smoke confirms the mono font loads + atlas builds without crashing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:53:08 -05:00
DanSandClaude Opus 4.8 18c1de87db refactor(console): phase 4b — extract ConsoleScrollController
Pull the auto-scroll concern out of ConsoleTab into a focused, ImGui-free
ConsoleScrollController (console_scroll_controller.{h,cpp}). It owns the three
coupled fields that were loose on the god-class — auto_scroll_, the wheel-up
cooldown, and the new-lines-while-scrolled-up backlog count — and the state
machine tying them together: wheel-up detaches + starts a cooldown, the cooldown
gates the at-bottom re-enable check, backlog counting happens only while
detached, and re-pinning (checkbox / jump pill / reaching the bottom) clears it.

The view keeps only what needs ImGui: it measures scroll position, hands the
controller a plain `atBottom` bool + frame delta, and issues the actual
SetScrollHereY. ConsoleTab loses three more members and all the scattered
auto-scroll bookkeeping collapses to scroll_.* calls.

Adds testConsoleScrollController: pinned start ignores backlog, wheel-up detach +
cooldown gating, re-enable only when at bottom, checkbox toggle semantics, and
the jump-to-bottom pill. Full-node + lite build clean; ctest green; source
hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:52:15 -05:00
DanSandClaude Opus 4.8 0cdbbd024e refactor(console): phase 4 — extract ConsoleSelectionController from the god-class
Pull the text-selection concern out of ConsoleTab into a focused, ImGui-free
ConsoleSelectionController (console_selection_controller.{h,cpp}). It owns the
anchor/caret state and all the fiddly logic around it: the mouse-drag lifecycle
(beginDrag/updateDrag/endDrag), select-all, range ordering, text extraction over
the visible model, and the index shift applied when the buffer cap evicts lines
from the top.

ConsoleTab now delegates: the four scattered selection fields (is_selecting_,
has_selection_, sel_anchor_, sel_end_) and five helper methods (selectionStart/
End, isPosBeforeOrEqual, getSelectedText, clearSelection) collapse to a single
selection_ member plus a thin selectedText() wrapper. screenToTextPos (still
view-side — it needs the frame's layout) now returns the shared ConsoleTextPos,
so ConsoleTab::TextPos is retired.

The byte-offset / eviction math was previously only reachable through the live
ImGui renderer; it now has direct unit coverage (testConsoleSelectionController:
ordering, drag lifecycle, upward drag, select-all, partial/full/no-op eviction
shift, and filtered extraction). Full-node + lite build clean; ctest green;
source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:46:06 -05:00
DanSandClaude Opus 4.8 779c9682d4 refactor(console): phase 3 — thread-safe ConsoleModel ingest queue; fix data race
Introduce ConsoleModel (console_model.{h,cpp}): an ImGui-free, thread-safe line
store. Producers on any thread call ingest(), which only briefly locks a small
pending queue; the main thread calls drain() once per frame to move pending
lines into the visible deque and enforce the line cap, reporting how many were
added and evicted from the front.

This fixes the real data race. Previously a single lines_mutex_ guarded both the
line container AND the UI-only interaction state (selection, scroll, auto-scroll
counters), and it was held across the whole render pass — yet render() touched
auto_scroll_ / new_lines_since_scroll_ outside that lock, racing the background
RPC-trace and app-logger producers that call addLine() on worker threads.

Now:
- addLine() only ingests (lock-free w.r.t. the model); safe from any thread.
- render() drains once per frame, then does the cap/selection/scroll bookkeeping
  on the main thread (new drainModel()).
- The visible deque + all selection/scroll fields are main-thread-only, so every
  lines_mutex_ lock is gone — including the one wrapping the entire renderOutput.

Adds testConsoleModel() covering ingest/drain ordering, the line-cap eviction
counts, view-only clear (queued lines survive), and an 8-thread concurrent
ingest/drain stress with no lost lines. Full-node + lite build clean; ctest
green; source hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:35:25 -05:00
DanSandClaude Opus 4.8 19e83eb12a refactor(console): phase 1 — extract pure ConsoleTextLayout (wrap/hit-test/selection)
Lift the console's word-wrap layout, screen->text hit-testing, and selection extraction
out of ConsoleTab into a pure, ImGui-free module (console_text_layout.{h,cpp}). Text
measurement is injected via a ConsoleTextMeasure interface — production uses an
ImFont-backed impl (identical CalcWordWrapPositionA/CalcTextSizeA calls as before), while
tests drive a fixed-width stub.

- BuildConsoleLayout: per-line wrap segments + heights + cumulative Y (was inline in
  renderOutput).
- HitTestConsoleLayout: point -> (visibleRow, byte col) (was screenToTextPos).
- ExtractConsoleSelection: selected text across visible lines (was getSelectedText).
- ConsoleTab now holds a single `ConsoleLayout layout_` (replacing 4 mutable members;
  drops the dead cached_wrap_width_) and calls the pure functions.
- New unit test testConsoleTextLayout covers wrapping, multi-row hit-testing, bottom
  clamp, and filtered/unfiltered selection — the biggest previously-untested surface.

No behavior change (algorithm preserved verbatim). Full-node + lite build clean, ctest
green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:58:29 -05:00
DanSandClaude Opus 4.8 b5001ad4db refactor(console): unify lite + full-node console behind a pluggable executor
The two consoles were separate implementations (the rich full-node ConsoleTab and a
minimal RenderLiteConsoleTab). Introduce ConsoleCommandExecutor to abstract the only
real differences — command execution + log source — so both variants share the one rich
terminal (the lite console now gains selection, zoom, copy, and JSON-colored output).

- console_command_executor.{h,cpp}: interface + FullNodeConsoleExecutor (dragonxd log
  ingestion + RPC command execution, result queue) and LiteConsoleExecutor (lite
  diagnostics ring + backend runConsoleCommand/takeConsoleResult, connection/sync status).
- ConsoleTab::render() now takes a ConsoleCommandExecutor&; log ingestion, command
  submission, and command results flow through it. The RPC command-reference popup and
  the daemon/errors/rpc-trace/app filter toggles are gated on the executor's capabilities;
  the toolbar status dot + a status header come from the executor.
- Safety preserved: `clear`/`cls` is intercepted as a view-only clear in the shared UI so
  it is NEVER forwarded (the lite backend's `clear` wipes tx history); command output is
  not persisted to diagnostics.
- App owns the executor (created per variant) and routes both NavPages to console_tab_;
  lite_console_tab.{h,cpp} deleted.

Needs runtime verification of BOTH consoles (full-node RPC + lite: clear, seed/export
secrecy, sync status, command results).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:59:17 -05:00
DanSandClaude Opus 4.8 d0586e5e75 feat(mining): pool registry, schema-aware stats fetcher, pool-select setting
Data + service layer for supporting multiple mining pools:
- pool_registry: KnownPool table (pool.dragonx.is + pool.dragonx.cc) carrying
  each pool's stratum host, xmrig algo, and stats-API schema; pure helpers
  findKnownPoolByUrl / resolvePoolAlgo / parsePoolHashrate / chooseWeightedPool
  (weighted-random by hashrate with incumbent stickiness).
- pool_stats_service: background, schema-aware hashrate fetcher (libcurl, TLS,
  browser UA for Cloudflare) modelled on XmrigUpdater.
- settings: pool_select_mode (manual | auto_balance), string-serialized.
- i18n strings + unit tests (both stats schemas, algo resolution, weighted pick).

Note: pool.dragonx.cc's stratum host is us.dragonx.cc:3333 — the .cc domain is
only the Cloudflare-proxied web/API host and does not accept stratum on :3333.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:35:13 -05:00
DanSandClaude Opus 4.8 24e8fb4942 chore: bump full-node to 2.0.0; stop tracking the generated manifest
Set project(ObsidianDragon VERSION 2.0.0) (was 1.3.0); the lite variant keeps
its own independent DRAGONX_LITE_VERSION (1.0.0). res/ObsidianDragon.manifest is
generated per-variant by configure_file from res/ObsidianDragon.manifest.in, so
it was wrongly tracked (it kept showing dirty, stamped with whichever variant
built last) - untrack it and gitignore it; the .in template remains the source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:20:56 -05:00
DanSandClaude Opus 4.8 4473e7e00a feat(updater): in-app dragonxd updater + browse-all-releases
Add a full-node daemon updater (util/DaemonUpdater + daemon_download_dialog)
reachable from Settings -> NODE & SECURITY: downloads/verifies (SHA-256 +
enforced ed25519 signature) and atomically installs the latest dragonxd from
the project Gitea, with a "Restart daemon now" step. Add a shared "Browse all
releases..." picker (release_list_view) to both the miner and daemon updaters
so users can pin older/pre-release builds. Pure no-I/O cores
(daemon_updater_core / xmrig_updater_core) are unit-tested; sign-daemon-release.sh
signs release archives offline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 21:27:13 -05:00
DanSandClaude Opus 4.8 b6567ee196 refactor(balance): extract shared rendering components into balance_components.{h,cpp} (audit #10)
First slice of decomposing balance_tab.cpp (3449 lines). The five rendering helpers used by every
balance layout — UpdateBalanceLerp, RenderCompactHero, RenderSharedAddressList (599 lines, the
drag-reorderable address list), RenderSharedRecentTx, RenderSyncBar — are moved verbatim into
balance_components.cpp. balance_tab.cpp is now 2680 lines.

Clean extraction: the helpers' interactive statics (drag/copy/hide/show) are function-local and
move WITH them; the only file-scope state they share is the balance-lerp animation values
(s_dispTotal/Shielded/Transparent/Unconfirmed) and s_generating_z_address, now non-static and
declared `extern` in balance_components.h (defined once in balance_tab.cpp, so both TUs share the
same objects). RenderCompactHero's default arg moved to the header declaration. The layouts (still
in balance_tab.cpp) call the helpers via the new header.

Verified: full-node + Windows + lite build (links cleanly -> extern state resolves), tests,
hygiene. This touches every layout's address list / recent-tx / hero / sync bar, so needs a
hands-on pass across the balance layouts before the next slice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 19:50:44 -05:00
DanSandClaude Opus 4.8 1a8d6fd30f refactor(mining): extract the Mode toggle into mining_mode_toggle.{h,cpp} (audit #10, slice 4)
Final slice of decomposing mining_tab.cpp. The ~529-line "Mode toggle" section (SOLO | POOL
segmented control + pool URL/worker inputs) is moved verbatim into RenderMiningModeToggle().
mining_tab.cpp is now 311 lines (was 2628) — just the tab dispatch, thread-sync glue, benchmark
advance, section-budget setup, and four card calls.

State the toggle mutates is passed BY REFERENCE so behaviour is identical: the pool-mode flag,
the settings-dirty flag, and the pool URL / worker char[256] buffers (the text inputs write into
them) — passed as char(&)[256] references and named with their original identifiers so the body
stays byte-identical.

Verified: full-node + Windows + lite build, tests, hygiene. Audit #10 complete: the 2628-line
monolith is now five focused files (earnings, stats, controls, mode-toggle + the 311-line shell).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 18:42:16 -05:00
DanSandClaude Opus 4.8 fa240e7b99 refactor(mining): extract the Controls/CPU-grid card into mining_controls.{h,cpp} (audit #10, slice 3)
Third and largest slice of decomposing mining_tab.cpp. The ~843-line "Controls" card (CPU-core
grid + drag-to-select, mining start/stop button, benchmark + miner-update controls) is moved
verbatim into RenderMiningControls(). mining_tab.cpp is now 839 lines (was 2628 originally).

The most coupled section, so mutated state is passed BY REFERENCE — the benchmark
(ThreadBenchmark&), selected thread count (int&), and drag state (bool&/int&) — with local
reference aliases so the body stays byte-identical and interactions (drag, benchmark, start/stop)
behave exactly as before. Read-only context is passed by value/const; the compiler verified
const-correctness. Local statics inside the block moved with it.

Verified: full-node + Windows + lite build, tests, hygiene, no startup crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:57:09 -05:00
DanSandClaude Opus 4.8 e21f7bf8aa refactor(mining): extract the Hashrate+Stats card into mining_stats.{h,cpp} (audit #10, slice 2)
Second slice of decomposing mining_tab.cpp. The ~313-line "Hashrate + Stats" card (stat values +
hashrate chart / live-log view) is moved verbatim into RenderMiningStats(); mining_tab.cpp is now
1680 lines (was 1992 after slice 1, 2628 originally). Body byte-identical apart from a s_pool_mode
alias; the chart/log toggle statics (s_show_pool_log/s_show_solo_log) moved with the card, and the
log buffer was already a function-local static. No App dependency in this section.

Verified: full-node + Windows + lite build, tests, hygiene, clean smoke start. Pending hands-on
visual check before the next slice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:03:51 -05:00