Commit Graph
53 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 1ea584311e fix(lite): honor the backend sync result with retry and a failed state
A failed backend sync was silently swallowed: the reply was never
inspected, so a sync that errored out left the wallet showing a
"syncing" state that never completed and never reported a problem.

- The sync and rescan replies are now parsed. On failure the partial
  progress is saved and the sync is retried with exponential back-off
  (5s doubling up to 60s, six attempts in all), then an explicit failed
  state is published: done, sync not complete, and the backend's error
  text carried through.
- A failed rescan is retried as a plain sync, because a rescan has
  already cleared the wallet and re-running it would throw away the
  partial progress just saved.
- A terminal error, such as a reorg deeper than the wallet can unwind,
  stops the retries immediately instead of waiting out the attempts.
- Recovery is observed, not guessed: once a later refresh's server-bound
  reads all succeed, the failed state clears itself.

Behaviour change: a sync failure now surfaces to the user as a failed
state with a message, instead of a frozen "syncing", and the client
retries on its own with back-off.

Tests: a parser case for the success, failure, and terminal replies, and
fake-backend controller cases for retrying after a failure, giving up
after the attempt cap, a terminal reorg stopping at once, a failed
rescan retrying as sync, cancel stopping the back-off, and the failed
state self-healing on a good refresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 21:42:23 -05:00
DanSandClaude Opus 4.8 5ae5e5213b fix(lite): sort lite transactions newest-first to match the UI panels
The lite backend lists transactions oldest-first, but every consumer of
the transaction list assumes newest-first and takes a prefix of it, so
the Recent Transactions / Recent Sends / Recent Received panels and the
CSV export showed the oldest records instead of the latest.

- The UI copy is now sorted pending (0-conf) first, then newest-first by
  timestamp, the same order as the History tab's default.
- The sort is stable, so several records that share a txid (a send entry
  plus its per-note entries, or same-block transactions) keep a
  deterministic order across refreshes. That keeps the order-sensitive
  change check reporting "unchanged" and avoids bumping the transaction
  revision every couple of seconds.

Tests: a new case covers the pending-first, newest-first ordering, a
stable order within one txid, and the revision staying unchanged when
the identical model is re-applied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 21:42:22 -05:00
DanSandClaude Opus 5.5 eac0993512 fix(lite): require a confirmed 8+ character passphrase to encrypt
The Lite "Encrypt wallet" field and the Create/Restore passphrase had no
confirmation and no minimum length; only an empty passphrase was
refused. Encrypting locks the wallet at once, and a locked wallet cannot
reveal its seed, so a typo there could cost a user without a seed backup
their funds. The full-node encrypt dialog already requires 8 characters
and a confirmation.

This applies to new passphrases only. Opening and unlocking are
unchanged: an existing wallet with a shorter passphrase still opens and
unlocks.

- A pure liteEncryptPassphraseValid(pass, confirm): at least 8 bytes and
  equal to the confirmation. It takes string_views, so the page's secret
  buffers are never copied.
- liteLifecyclePassphraseAcceptable(op, pass, confirm) gates the Settings
  create/open/restore request. Open is never checked, because its
  passphrase field also unlocks an existing wallet. Create and Restore
  are checked only when a passphrase was entered; an empty one still
  creates an unencrypted wallet.
- Encrypt gets a Confirm field. The button stays disabled until the
  passphrase is valid, and a hint says why (too short, with the count,
  or not matching).
- Create/Restore show a Confirm row once a passphrase is typed, with the
  same hint. Validate stays disabled while the passphrase is invalid, so
  a click never wipes a typed seed over a passphrase typo. The request
  builder re-checks the rule.
- The confirmation buffers are wiped with their passphrase: after a run
  or an encrypt, and when the Confirm row is hidden.
- Once the wallet is closed, the Settings page wipes the Encrypt,
  Confirm and Decrypt passphrase buffers whole every time, also when no
  secret was revealed; before, closing the wallet wiped them only while
  clearing a revealed backup or secret. They are zeroed in full because
  an emptied field can still hold the old tail past its first NUL.
- New copy under Encrypt: "Anyone with the wallet file can try
  passphrases offline. Back up your seed phrase first and use a long
  passphrase."
- i18n: three new keys (lite_confirm_label, tt_lite_confirm_pass,
  lite_encrypt_offline_warning) in i18n.cpp and the 8 language files,
  added additively. The hints reuse the wizard's wiz_pass_too_short and
  wiz_pass_mismatch. No new CJK glyphs, so the subset font is unchanged.

Tests: the validator rejects empty, short (7) and mismatched or
unconfirmed passphrases, and accepts 8 or more with a match. The request
gate always accepts Open, even "abc" with no confirmation, accepts
Create/Restore without a passphrase, and applies the rule to them
otherwise. An encrypted, locked wallet still opens and unlocks through
the controller with the 3-character passphrase "abc".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00
DanSandClaude Opus 5.5 e52c9e1b92 fix(lite): classify imported keys before calling the backend
importKeyCore guessed the key type from its first character, then
retried a failed import with the other command. That retry is what sent
shielded keys to the backend's transparent-key decoder, which panicked
on them and poisoned the wallet lock. A WIF typo that was still
valid base58 also reached the backend unchecked.

- The key is classified first, and exactly one backend command runs, or
  none:
  - secret-extended-key-... and zxview... go to "import";
  - a Base58Check WIF with version 0xBC or 0x80 and the compressed flag
    goes to "timport";
  - everything else is refused before any backend call.
  There is never a retry with the other command. A testnet (0xEF) WIF
  is not accepted, because the Lite app is mainnet-only.
- New fixed, untranslated reasons: "uncompressed transparent keys are
  not supported by the Lite wallet", a full-node incoming viewing key
  (zivks) is explained as such, and anything else is "unrecognized key
  format". The key is never echoed.
- The backend's refusal while the wallet is encrypted now maps to "wallet
  is encrypted - remove encryption to import a private key". It is
  checked before "locked", because unlocking would not help.
- base58Decode reserves its buffers up front, and it and
  decodeBase58Check wipe their copies of the decoded bytes with
  sodium_memzero on every return path, since the input can be a private
  key.
- exportSeed rejects the all-zero-entropy phrase (23 x "abandon" + "art").
  A backend built before the seed-export fix returns it for a wallet
  whose lock failed halfway (seed wiped, wallet still unlocked); it is
  never a real backup and must not seed a chat identity.
- An open that the backend recovered from the .bak ("OK: recovered from
  backup; damaged file kept as <name>") is written to the lite log.

Tests: the retry control now expects one "import" and no "timport", and
"typo-key" makes no backend call; the controller's transparent-import
test uses a real compressed WIF. A new classifier test covers both WIF
versions, the shielded and viewing keys, a single call on failure, the
uncompressed, zivks, testnet, bad-flag and bad-checksum refusals, the
encrypted reason and its lite-log line, the zero-entropy seed and the
recovery log line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00
DanSandClaude Opus 5.5 860916d203 perf(history): add a transactions revision counter to WalletState
WalletState had no change signal for its transaction list, so a UI
cache had to fingerprint all N transactions every frame to notice an
update. It now carries tx_revision, a counter that only increases;
clear() on disconnect bumps it instead of resetting it, so a cache
built before a reconnect or wallet switch can never match again.

- Every writer of the list bumps it: the full-node refresh, loading the
  cached history, adding, completing and removing a pending send, a
  mining-address relabel, the lite refresh and the screenshot demo
  rows.
- The full-node refresh and the lite refresh (every ~2 s) swap in the
  new list only when it differs field by field, the amount compared
  bit-exactly because -0.0 and +0.0 print differently. An unchanged
  refresh keeps the list and the revision; the compare costs O(N) per
  refresh, not per frame.

Nothing reads the counter yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:50:03 -05:00
DanSandClaude Opus 5.5 bb84243114 fix(lite): keep a rejected import key out of the log and Settings
The backend's error for a key it doesn't recognize repeats the key
("'<key>' was not recognized as either a spending key or a viewing
key"). Import passed that text on as its error, so a mistyped private
key was written to the lite log (shown in the Console tab) and to the
Settings status line. A failed import now carries a fixed reason
(unrecognized format, wallet locked, key already exists, could not be
decoded, or rejected), which is all the log records. Settings shows
"Couldn't import the key — check it, unlock the wallet (if encrypted)
and try again." instead, translated in all eight languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:09:48 -05:00
DanSandClaude Opus 5.5 55975901a2 perf(lite): move wallet ops and server switches off the UI thread
New address, Import key, Encrypt and Remove encryption called the
backend on the UI thread. Each ends in the backend's save, which waits on
the lock a scan holds for its whole duration and every refresh read holds
briefly, so during a scan the window froze until the scan ended. A create
or restore with a passphrase encrypted and saved just after its first
scan had started: for a restore, a freeze for the whole initial scan. A
server switch destroyed the old controller on the UI thread and joined
its refresh worker, which can sit for minutes in a read against a dead
server (each balance, notes or list read retries for ~62 s).

- The four ops run one at a time on a background lane and their results
  are applied on the main thread; Settings shows "Working…" meanwhile.
  Key and passphrase copies are wiped after the call, or when a queued
  op is dropped.
- A create or restore encrypts and saves (an open unlocks) before the
  first scan starts, so the save takes milliseconds and the scan runs on
  the encrypted file.
- Import, Encrypt, Remove encryption and Receive's New address are
  disabled while a scan runs ("Wait for blockchain to sync"); Redownload
  blocks is disabled while an op is queued.
- A switch retires the old controller without blocking (queued ops
  dropped, pending ones reported failed, its client shut down) and
  destroys it on a background thread. It no longer saves on the way out:
  the new client re-scans the blocks since the last save, as after a
  restart.
- A switch waits for an op in flight, at most 15 s, so the op can't
  finish and save on the old client while the new one owns the wallet
  file. The Network tab already shows the new selection meanwhile.
- The refresh worker stops between backend commands, skips the remaining
  server reads once one fails, and then retries after 4, 8, then 15 s
  instead of every 2 s.
- Quit waits up to 2.5 s for an op in flight, e.g. Encrypt's save.

Encrypt and Remove encryption now report a failed save: the backend
returns it as a JSON error that the old check never saw, so the failure
was reported as success. The trimmed copy of a key typed into Import is
now wiped too, and stopping the refresh worker can no longer miss its
wakeup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:09:48 -05:00
DanSandClaude Opus 5.5 7fa7c3d6da fix(lite): drop the 15 s startup overlay and a TLS connection per refresh
- The refresh-model mapper marks the encryption state known when the
  backend reports it. The wallet tabs wait for that, so every Lite
  launch sat behind the loading overlay until the 15 s "assume
  unencrypted" fallback.
- Each 2 s refresh skips the backend's `info` (a fresh TLS connection
  and GetLightdInfo, only a fallback for the chat tip, which the
  explicit height call already provides) and the gateway's `height`
  (replaced by that explicit call, which runs after the sync commands
  so it sees the newest block).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:52:10 -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 f30fdc5ed1 feat(lite): add lite6 + lite7 to the default lite-wallet server list
Ship https://lite6.dragonx.is and https://lite7.dragonx.is as default
lightwalletd servers in both the Settings defaults and the connection-service
fallback list. Existing installs (whose saved server list predates these)
pick them up via a load-time merge that appends any missing default server —
safe because servers are hidden, never deleted, so a removed server isn't
resurrected. Consolidates the Settings default list into a single
Settings::defaultLiteServers() source of truth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-02 04:15:29 -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 ea26c0cbbb fix(balance): stop the displayed balance cratering during a pending shielded send
Sending a small amount from an address holding a large balance made the displayed
balance collapse to ~0 until the tx confirmed. A shielded spend consumes the whole
source note; the change returns as a 0-confirmation note, and every balance query
used the default minconf=1 — so the spent note dropped out and the change wasn't
counted yet.

Split every balance into two views:

- DISPLAY (balance / privateBalance / transparentBalance / totalBalance) — now
  queried at minconf=0, so it INCLUDES the user's own pending change and no longer
  craters. This is what the Overview, balance tab, market portfolio and receive
  tab show. unconfirmedBalance is now populated (= total - spendable).
- SPENDABLE (new spendableBalance / spendable*Balance) — confirmed (minconf>=1),
  what z_sendmany (run at minconf=1) can actually spend. The Send form's available/
  Max/validation, the from-address selection, the drag-to-transfer dialog cap, the
  chat pay-from and the auto-shield gate all size off these, so they never offer
  0-conf change the daemon would reject.

Implementation: a single z_listunspent(0)/listunspent(0), partitioned per-note by
"confirmations">=1; z_gettotalbalance called at minconf 0 (display) and 1
(spendable); the z_getbalance fallback queries both. applyPendingSendDelta (the
optimistic post-send debit) now touches ONLY the spendable fields — debiting the
display too would re-crater it on top of the honest minconf=0 RPC. Lite mirrors
spendableBalance = balance (its per-address balance is already confirmed) so lite
sends aren't zeroed. The confirmed-only gates (seed-migration/sweep z_gettotalbalance,
sweep z_getbalance(addr,1), z_sendmany's minconf arg) are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-13 16:40:14 -05:00
DanSandClaude Opus 4.8 5b6ba5094b docs(lite): clarify SDXL viewing-key HRP differs from the full node
Audit of the lite import path found no false-rejection defect (no client-side
gate; the two-command fallback in importKey makes the U/5/K/L prefix guess
non-binding; lite send reuses the now-P2SH-fixed send_tab helpers). But the
"zxview" viewing-key comment — which was WRONG in the full node (fixed earlier)
— is genuinely CORRECT here: SDXL's import takes an extended full viewing key
(zxviews…, hrp_sapling_viewing_key), whereas the full node's z_importviewingkey
takes an incoming viewing key (zivks…). The two are not interchangeable.

Add a note so nobody "harmonizes" the two gates and reintroduces the full-node
bug. Comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-09 00:59:29 -05:00
DanSandClaude Opus 4.8 05b00b158b fix(wallet): surface silent save failures + stale-state cleanups (W5-1, W5-2, W6-1, W6-3)
P2 robustness batch (localized):

- W5-1 (Med, lite): persistAfterBroadcast returned false on a persistent post-send/shield
  save failure, but both callers discarded it and it never logged — completely silent. It
  now liteLogs the failure (the spent note re-derives on the next sync, so it's a
  robustness gap, not fund loss).

- W5-2 (Med, lite): the post-sync and post-rescan save results (in the detached scan
  threads) were ignored; both now liteLog on failure. LiteDiagnostics::log is mutex-guarded,
  so it's safe from those threads.

- W6-1 (Med): WalletState::clear() didn't reset mining/pool_mining, so a wallet switch could
  briefly show the previous wallet's hashrate/blocks. Now reset in clear() (the daemon
  restarts on switch, so mining genuinely stops).

- W6-3 (Low): AddressBook::load() cleared entries_ then threw on the first non-object array
  element — discarding EVERY contact. It now guards is_object() + per-entry try/catch,
  skipping and counting malformed entries.

Build-clean; ctest 1/1. Remaining P2: W6-2 (refresh-staleness badge — needs UI, overlaps
the diagnostics Foundation bundle). See docs/wallet-hardening.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 15:22:13 -05:00
DanSandClaude Opus 4.8 c7d163f44a feat(lite): wire the create-time passphrase into the lite encrypt/unlock flow (W5-3)
The lite create/open/restore requests carried a passphrase field that the UI collected
(a labeled, masked "passphrase" input) but the backend initialize* calls never used —
so a user could believe their lite wallet was passphrase-protected at creation when it
did nothing. It now has a real meaning, wired in LiteWalletController:

- create / restore -> encryptWallet(passphrase): the backend encrypts + locks + saves
  the brand-new wallet.
- open -> unlockWallet(passphrase), but only when encryptionStatus() reports the existing
  wallet is actually encrypted + locked (no spurious unlock on an unencrypted wallet).

encryptWallet/unlockWallet take their own copy of the passphrase and wipe it; the
request copy is still wiped as before. A post-create encrypt failure is liteLog'd (the
wallet still exists, so the create is not failed).

Six existing lite-controller tests carried an incidental "hunter2" create passphrase from
when the field was dead; removed (they exercise non-encryption flows and want an
unencrypted wallet), and added testLiteWalletControllerCreateEncryptsWithPassphrase to
prove the new behavior. Completes the wallet-hardening P0-A cluster (7/7). ctest 1/1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 14:39:02 -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 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 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 dcd09dc542 refactor(audit): batch 7 — RPC/settings/lite/effects mechanical dedups
Six behavior-preserving consolidations from the audit:

- RPCClient::call() overloads share a private performCall() (payload dump +
  curl_easy_perform + http code) and a static parseRpcResult() (error->RpcError
  extraction). The timeout overload restores the prior timeout in both the
  success and catch paths, exactly as before.
- The six UnifiedCallback methods delegate to one splitUnified() that builds
  the (Callback, ErrorCallback) pair once (null-check preserved).
- One liteTrimCopy() in lite_connection_service replaces 5 file-local
  trim-copy helpers across the lite slice.
- lite_wallet_controller's inline height JSON parse now routes through the
  tested parseLiteHeightResponse().
- theme_effects.cpp: unpackRGB()/scaledAlpha() replace 14 RGB-unpack + 5
  alpha-scale copy-paste blocks.
- settings.cpp load() uses loadScalar()/loadClamped() helpers for ~44 scalar
  fields. Besides removing boilerplate this HARDENS loading: ~44 fields that
  previously only checked contains() now also verify the JSON type, so a
  malformed value in settings.json falls back to the default instead of
  throwing/misreading. Custom cases (enum parses, legacy migrations, arrays)
  stay inline; save() is unchanged.

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 19:56:54 -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 f58d009703 fix(lite): show real block height when synced (was 0)
The lite status bar showed "blocks: 0" once fully synced. The backend's
`syncstatus` only includes synced_blocks/total_blocks WHILE actively scanning;
at rest it returns just {"syncing":"false"}, so the parsed syncedBlocks was 0
and became state.sync.blocks. On the synced refresh path, additionally query the
backend `height` command (wallet last-scanned height, a fast local read) and use
it as the synced height/tip, so the block count is correct at rest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 11:36:58 -05:00
DanSandClaude Opus 4.8 b3251e9244 feat(lite): mirror errors into the lite Console (copyable), not just toasts
Lite send/shield, unlock, and key-import failures were shown only as transient
toasts — impossible to copy. Route them through liteLog() so they also appear in
the lite Console (which has a Copy button), alongside the lifecycle/open/sync
errors the controller already logs:
- send/shield broadcast failures (App broadcast-result delivery)
- wallet unlock failure
- key import failure (controller; logs the error text only, never the key)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:59:34 -05:00
DanSandClaude Opus 4.8 5547ab1cac feat(lite): add "Redownload blocks" (rescan from lite server) to Settings
Add a maintenance option for the lite wallet to re-download and re-scan every
block from the lite server — useful when balances or history look wrong.

- LiteWalletController::startRescan() runs the backend `rescan` command (which
  clears the wallet's synced block cache and re-syncs from its birthday) on a
  detached thread, reusing the existing sync progress/refresh machinery: it
  resets syncDone_ so refreshModel() shows progress again and refreshes data on
  completion. No-op if no wallet is open or a scan is already running.
- scanInProgress() exposes the initial-sync-or-rescan state.
- Settings (lite, open wallet) gains a "Redownload blocks" button behind a
  confirmation modal, disabled while a scan is running. i18n strings added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:05:27 -05:00
DanSandClaude Opus 4.8 4a65dce947 feat(lite): make the Console tab interactive (run backend commands)
The lite backend's litelib_execute() is the same command interface as
silentdragonxlite-cli (balance, info, height, list, notes, addresses, sync,
syncstatus, new, send, shield, encrypt, …), so the lite Console can be a real
interactive console — like the full-node RPC console — instead of a read-only
diagnostics log.

Controller: add an async arbitrary-command runner mirroring the broadcast
pattern — runConsoleCommand() splits "<command> [args]" (the first token is the
command, the remainder is passed as the single arg string litelib_execute
expects, since it does NOT whitespace-split), runs the bridge call on a detached
thread that captures the shared bridge (never `this`), and delivers the result
to a main-thread slot drained by takeConsoleResult(). Results are NEVER routed
through LiteDiagnostics (seed/export can return secrets).

Console tab: a command input (Enter to run, Up/Down history via the shared
console_input_model helpers) over a unified scroll buffer that interleaves the
automatic diagnostics events with user command I/O, colour-coded, with the live
status header preserved. The input is disabled while a command runs.

Two backend footguns are intercepted at the UI layer before forwarding:
`clear` (the backend command WIPES wallet tx history — re-bound to clearing the
view, what the user expects) and `quit`/`exit` (would only save; the embedded
backend must stay running with the app).

Test: runConsoleCommand drives the fake backend (info -> raw response; "new zs"
-> exercises the command/arg split; blank line rejected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 20:32:03 -05:00
DanSandClaude Opus 4.8 6f9123f651 feat(lite): async + failover for Settings-page create/open/restore
The Settings page drove the controller's synchronous createWallet/openWallet/
restoreWallet, which blocks the UI thread on the (often flaky) lightwalletd and
gives up after the first server. Add a generic async lifecycle path that mirrors
the async-open failover but carries the full request (passphrase, restore seed/
birthday/account/overwrite):

  - beginCreateWalletAsync / beginOpenWalletAsync / beginRestoreWalletAsync run
    on a detached thread that builds its OWN local LiteWalletLifecycleService
    from captured value copies + the shared bridge (never `this`, so it can
    safely outlive the controller). Each request type's serverUrl override field
    feeds the failover: try the preferred server, then every other usable
    default; stop on the first ready wallet or a structural block; keep the
    preferred server's error on total failure. The request's secrets are wiped
    once the attempt finishes.
  - pumpLifecycleResult() finalizes on the main thread (flip walletOpen, persist,
    start sync) and caches the result for the UI; wired into App::update next to
    pumpAsyncOpen(). beginAsyncLifecycle() now also yields to an in-flight
    lifecycle request so the auto-open loop can't race it on the same bridge.
  - settings_page kicks off the async op, disables the button while in flight,
    and polls the cached result each frame for the status/summary.

Tests: testLiteWalletControllerAsyncLifecycleFailover covers async create (with
passphrase) and restore failing over preferred->fallback, plus all-servers-down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:42:47 -05:00
DanSandClaude Opus 4.8 320c659689 feat(lite): async wallet creation with server failover
Mirror the async-open path for wallet creation. beginOpenExisting() and
beginCreateWallet() now both delegate to beginAsyncLifecycle(bool create),
which runs the backend init on a detached thread and walks the failover
server list (preferred server first, then all usable defaults), reporting
the preferred server's error on total failure. The first-run wizard's
Create button drives this through a non-blocking "creating" poll state so
the UI no longer freezes while the backend contacts a (possibly flaky)
lightwalletd. The created seed response is securely wiped immediately and
read back via exportSeed for the reveal/verify steps.

Safe because litelib_initialize_new contacts the server before writing any
wallet file and LightClient::new errors if a wallet already exists, so a
failed candidate leaves no partial state.

Tests: fake backend's initialize_new now honors the dead/warmup server
substrings; testLiteWalletControllerOpenFailover gains a create-failover
case (preferred dead, fallback good -> walletOpen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:29:59 -05:00
DanSandClaude Opus 4.8 788d71a549 fix(lite): report the preferred server's error on a failed open
The failover overwrote outcome.error on every attempt, so a total open failure
reported whichever (often broken) fallback was tried last — e.g. lite5's
"CertNotValidForName" — instead of what the user's preferred server actually did.
Keep the first (preferred) server's error as the summary so "Open failed: …" names
the actionable reason; the per-server attempts are still in the Console log, and
the warmup flag is still set if any server was warming up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 22:26:06 -05:00
DanSandClaude Opus 4.8 3d4b013b0c fix(lite): fast retry when a server is only warming up (-28)
When the preferred lightwalletd server is reachable but warming up (JSON-RPC -28
/ "Activating best chain"), the failover treated it like a dead server and fell
through to the others, so the wallet didn't open until the next 20s retry — even
though the healthy server was ready within seconds.

Detect the warmup error during failover, flag it on the open outcome
(lastOpenWasWarmup()), and have the App retry on a short ~4s interval in that case
instead of 20s, so the wallet opens promptly once warmup clears. A unit test
covers a warming-preferred + dead-fallback open setting the flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 21:26:14 -05:00
DanSandClaude Opus 4.8 7e568e4bf1 fix(lite): always-populated Console (live status) + single-instance log
The Console could look empty if the wallet produced few events. Make it useful
in every state and remove a cross-platform footgun:

- Add a live status header read straight from the controller (connected /
  connecting / disconnected, sync %, and the last open error) — independent of the
  diagnostics event log, so the Console always shows the current connection +
  wallet-open state even when the log is sparse.
- Move LiteDiagnostics::instance() into a single .cpp so there is exactly one
  instance across the binary, rather than relying on the linker folding an
  inline-function static across translation units (a known fragility, especially
  on mingw/Windows — the most likely cause of a stuck-empty event log there).

Verified the writer and reader share one instance on Linux; builds clean for
full-node, lite, and Windows cross-compile; tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 19:28:28 -05:00
DanSandClaude Opus 4.8 85a1080b52 feat(lite): Console tab with connection + open/create diagnostics
The lite variant had no visibility into why a wallet failed to open — just a
"disconnected" spinner. Add a lite-only Console tab (full-node keeps its RPC
console) that shows a live diagnostic log.

- LiteDiagnostics: a small thread-safe, bounded ring buffer (header-only). The
  controller writes to it from its background threads: each failover server
  attempt and result, wallet open/create/restore outcomes, sync start, and
  blocked-open reasons. The App logs controller (re)builds with the preferred
  server.
- lite_console_tab: a terminal-styled, read-only view of the log (newest at the
  bottom, error/success lines coloured) with Clear / Copy / Auto-scroll. Reachable
  even when the wallet is locked (it's diagnostics, no secrets). Registered as
  NavPage::LiteConsole, gated lite-only via WalletUiSurface::LiteConsole.

A unit test drives an open-with-failover and asserts the log records the
connection attempt and the successful open. Built clean for full-node, lite, and
Windows cross-compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:46:25 -05:00
DanSandClaude Opus 4.8 dbeae3ac98 feat(lite): async wallet open with server failover
Opening an existing lite wallet ran synchronously on the UI thread and used a
single server, so a dead/unreachable lightwalletd server froze startup for the
connect timeout and then stranded the wallet ("disconnected" spinner) — and the
DragonX lite servers are flaky (often several down at once).

Add LiteWalletController::beginOpenExisting() / pumpAsyncOpen(): the open runs on
a background thread (mirroring the sync/broadcast shared-lifetime pattern — it
captures only shared_ptrs + value copies, never `this`), trying the preferred
server first and then every other usable default until one succeeds. The main
thread finalizes the result (flips walletOpen, starts sync) or records the reason.
The rollout gate is still checked up-front on the main thread.

App: auto-open now calls beginOpenExisting() and pumps it each tick, retrying on
a 20s interval so a transient outage self-heals once a server returns; a failed
open surfaces its reason (notification + Network tab) instead of a silent spinner.

Tested: a fake bridge that fails specific servers exercises both
preferred-dead -> fallback-opens and all-dead -> fails-with-reason. Built clean
for full-node, lite, and Windows cross-compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:53:24 -05:00
DanSandClaude Opus 4.8 8c2c1c2aaf fix(lite): import-key fallback on mis-routed key + clamp shield fee
- importKey routed transparent vs. shielded purely by the first character, which
  can mis-route (e.g. testnet/regtest WIFs). On failure, try the other import
  command before reporting an error (each validates the encoding, so a wrong
  command rejects rather than mis-imports). The key copy is wiped after both tries.
- Clamp the shield dialog's fee input to [0, 1] DRGX, mirroring the UTXO-limit
  clamp, so a negative or fat-fingered huge fee can't be submitted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:25:44 -05:00
DanSandClaude Opus 4.8 5c883d4b91 fix(lite): faithful tx list, balances, and persistence on partial results
- A Send record carries its recipient in outgoing_metadata, not the top-level
  address/memo, so sent txs showed a blank destination + memo. Surface the first
  recipient (single-recipient case) into the transaction list.
- A tolerated partial refresh where the notes/utxo command failed (addresses
  present, spendable outputs absent) zeroed every per-address balance, which looks
  like fund loss. Preserve the last-known per-address balances in that case.
- Retry the post-send/shield save once on transient failure instead of ignoring
  the result (the backend does not auto-save after send/shield).
- An unparseable broadcast response now uses cautious wording ("status could not
  be confirmed — check Transactions before retrying") rather than implying a hard
  failure, avoiding a blind double-spend retry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:18:05 -05:00
DanSandClaude Opus 4.8 732d892d4d feat(lite): ObsidianDragonLite Network tab — server browser
A lite-wallet-only "Network" tab (full-node keeps the Peers tab; exactly one shows per variant)
to manage lightwalletd servers, replacing the basic selector that was in Settings.

- Card list of servers with per-server latency + status dot, DNS host + resolved IP, and an
  Official/Custom pill. Official DragonX servers get a glowing outline.
- Pick a server (Sticky) by clicking its card, or toggle "use a random server" (Random mode);
  selection applies immediately (App::rebuildLiteWallet(force=true) tears down + rebuilds the
  controller against the new server and resyncs — its dtor detaches the uninterruptible sync
  thread, so this doesn't block).
- Add custom servers; hide/unhide servers (persisted set, revealed by a "Show hidden" toggle).
- Latency/IP come from a new background probe (util/LiteServerProbe): libcurl CONNECT_ONLY does
  the TCP+TLS handshake (works for gRPC lightwalletd, no HTTP response needed), recording
  APPCONNECT_TIME as latency and CURLINFO_PRIMARY_IP. Auto-runs on tab open + a Refresh button.

Wiring: WalletUiSurface::LiteNetwork (gated !fullNodePagesAvailable) + NavPage::LiteNetwork in
the sidebar + app.cpp dispatch; settings gains a hidden-servers set; isOfficialLiteServer() added
to lite_connection_service. The Settings page lite-server selector + its plumbing are removed
(single source of truth = the tab).

Reuses the existing server model (LiteServerPreference, Sticky/Random, selectLiteServer) and UI
primitives (DrawGlassPanel, ThemeEffects glow, peers-tab ping-dot idiom). Unit-tested
(liteServerHost, isOfficialLiteServer) + an env-gated live probe (verified vs lite.dragonx.is:
online, latency, IP). Both variants + lite-backend build; suite passes; hygiene clean; GUI
smoke-launched without crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:09:27 -05:00
DanSandClaude Opus 4.8 b3c2282b53 feat(lite): runtime kill-switch + staged-rollout gate (M5b)
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>
2026-06-06 12:01:08 -05:00
DanSandClaude Opus 4.8 a5da5562cf refactor(lite): remove dead backend artifact-contract/resolver scaffold
lite_backend_artifact_{contract,resolver}.{cpp,h} (~1,960 lines) were
app-linked but never invoked: all 14 public entry points
(evaluateLiteBackendArtifactContract/Resolver, evaluateLiteBackendActivation-
Readiness, the resolve*/...Name helpers) had zero callers in the app, the
lite_smoke tool, build scripts, or surviving tests. The real backend load
path (LiteClientBridge::linkedSdxl) uses direct litelib_* externs, and the
DRAGONX_ENABLE_LITE_BACKEND symbol check is done in CMake against the symbols
inventory (FATAL_ERROR on a missing symbol) — not via these C++ files. The
files were saturated with churn markers (disabled / dry-dispatch / scaffold).

- Delete the four artifact files and their 8 CMakeLists references.
- Drop the orphaned test cruft in test_phase4.cpp: the contract include,
  5 type aliases, and 3 never-called helpers (heapConstructPlanResult,
  makeReadyLiteBackendArtifactProvenance, liteBackendArtifactContractHasIssue)
  left over from the already-removed bridge-runtime tests.
- Correct the CLAUDE.md lite-wallet description (it credited these files with
  backend validation that CMake actually performs) and drop the stale
  lite_bridge_runtime mention.

Both variants build; full test suite passes; source-hygiene check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 11:05:38 -05:00
DanSandClaude Opus 4.8 c676ec8287 refactor(lite): extract owned-string core, drop dead bridge-runtime scaffold
lite_bridge_runtime.{cpp,h} was ~25k lines of dry-dispatch / dynamic-loader
scaffolding that the shipping wallet never used: 0 of its 122 public types
reached the app binary. The only live code on the bridge path was the
owned-string memory-safety helper — LiteClientBridge::linkedSdxl() already
loads the backend via direct litelib_* externs in lite_client_bridge.cpp.

- Extract LiteBridgeOwnedString + liteBridgeRuntimeTakeOwnedString into
  src/wallet/lite_owned_string.{h,cpp} (the copy-before-free / free-once /
  wipe / "Error:"-classify boundary), with the runtime-friend coupling removed.
- Point lite_client_bridge.cpp at the new header.
- Delete lite_bridge_runtime.{cpp,h} and the 16 runtime-only tests +
  their fixtures/aliases in test_phase4.cpp; keep the 5 owned-string tests
  (retargeted) and restore testGeneratedResourceBehavior, which had been
  caught in the runtime-test line range.
- Swap the CMake source/header references.

Both variants build; full test suite passes; source-hygiene check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 08:56:55 -05:00
DanSandClaude Opus 4.8 50b0419dfe feat(lite): wallet encryption controller layer (encrypt/unlock/lock/decrypt)
Wire the backend passphrase-encryption commands into LiteWalletController:

- encryptWallet / decryptWallet (take passphrase by value, securely wipe it,
  save after), unlockWallet / lockWallet (bring spending keys into/out of
  memory), and encryptionStatus() -> {encrypted, locked}. All return
  failure-safe results; errors arrive as {"error":..} or "Error:" (handled).
- Fold encryptionstatus into refreshModel() (polled every cycle, available even
  mid-sync since it reads local wallet state) and apply it in
  applyLiteRefreshModelToWalletState, so WalletState.isEncrypted()/isLocked()
  track the backend — which gates the existing locked/auto-lock UI.

Backend contracts verified against the SDXL source: encrypt/unlock/decrypt take
the passphrase as the single arg; lock takes none; encryptionstatus returns
{"encrypted","locked"}; ops return {"result":"success"} / {"error":..}.

Tests: testLiteWalletControllerEncryption drives encrypt -> lock -> unlock ->
decrypt via encryptionStatus(), checks empty-passphrase + closed-wallet rejection,
and that the status folds into WalletState. Fake models the state machine.

GUI wiring (encrypt in Settings, unlock prompt / lock action) is the follow-up;
the backend create flow remains unencrypted by default until encrypt is run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 17:50:53 -05:00
DanSandClaude Opus 4.8 76c2ac5db8 feat(lite): auto-open existing wallet on startup + gate full-node RPC refreshes
Auto-open: on the first update() tick (kept off init() so a slow
initialize_existing network call can't freeze startup before the window), if a
wallet file exists, open it. initialize_existing needs no passphrase — it loads
the file; a previously-synced + saved wallet resumes from its height (fast)
instead of rescanning from the checkpoint. Adds LiteWalletController::walletExists()
(bridge.walletExists on the connection's chain) + a chainName_ member.

RPC-refresh gating: the earlier connected=walletOpen() fix (so the wallet UI is
enabled in lite) had a side effect — the full-node periodic + per-page RPC
refreshes (mining/balance/peers/txs, and setCurrentPage's immediate refresh)
gate on state_.connected, so they began firing in lite and failing
("X error: Not connected"). Re-gate those on ACTUAL RPC connectivity
(rpc_ && rpc_->isConnected()) instead of the lite proxy. Full-node is unchanged
(state_.connected ⟺ rpc connected there); lite no longer issues any RPC.

Runtime-verified in WSLg with a pre-seeded wallet: app auto-opens (Starting
Mempool + sync begins), and "Not connected" / getMiningInfo / RPC-connect noise
all drop to 0 — a fully clean lite run. tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 16:29:27 -05:00
DanSandClaude Opus 4.8 5d317f6be3 feat(lite): M5a — wallet persistence after sync/send/shield
Verified against the SDXL Rust source that the backend auto-saves only on
new-address / import / rescan; it does NOT save after sync, send, or shield, and
litelib_shutdown merely sets a flag. So without intervention a first sync
(~30 min) and any sent transaction are lost on restart.

The controller now triggers the backend `save` at exactly the right points:
- after the detached `sync` completes — and BEFORE syncDone_ is set, so a
  syncComplete() observer always sees a fully persisted wallet;
- after a successful send / shield (the doSend/doShield cores; skipped on
  failure so a failed broadcast doesn't write);
- a guarded best-effort flush in the destructor, only when syncDone_ and no
  broadcast is in flight, so shutdown never blocks on the wallet lock held by an
  uninterruptible scan or in-progress proving;
- plus a public saveWallet() for explicit/periodic saves.

Wallet-file crash recovery (.dat / .dat.bak rotation) is already handled inside
the backend.

Tests: testLiteWalletControllerM5Persistence proves saves fire after
sync/send/shield and explicit saveWallet(), and do NOT fire on a failed send or
with no wallet open (fake gains a save counter). Plan doc updated; M5b
(packaging/CI/signing/rollout) remains.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:48:44 -05:00
DanSandClaude Opus 4.8 a677c09984 refactor(lite): drop 4 unused OOP wrapper classes over free functions
Each of these classes wrapped an existing free function with a one-line
delegating method and was never instantiated anywhere (verified: no references
outside their own translation unit, not even within their own .cpp beyond the
definition) — the redundant "wrapper layer" pattern CLAUDE.md warns against:

- LiteWalletLifecycleUiExecutionAdapter      -> executeLiteWalletLifecycleUiRequest
- LiteWalletServerSelectionUiExecutionAdapter -> executeLiteWalletServerSelectionUi
- LiteWalletServerLifecycleReadinessPlanner   -> evaluateLiteWalletServerLifecycleReadiness
- LiteBackendActivationReadinessAdapter       -> evaluateLiteBackendActivationReadiness

The live free functions (the actual entry points used by the UI/runtime) are
unchanged. Both targets build, test suite passes, source-hygiene clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:40:26 -05:00
DanSandClaude Opus 4.8 6611d57147 refactor(lite): remove dead parallel refresh/readiness scaffolding (~3.1k lines)
The lite-wallet tree carried a second, unused refresh+readiness architecture
that never reached the shipping binary — exactly the churn CLAUDE.md warns
against. The live refresh path is controller -> gateway.refresh ->
mapLiteWalletRefreshResult -> applyLiteRefreshModelToWalletState; this parallel
stack was dead weight.

Verified unused (their public types/functions are referenced only within the
cluster), then deleted (8 files / 16 incl. headers):
- lite_wallet_refresh_service            (LiteWalletRefreshService + gateway adapters)
- lite_wallet_app_refresh_coordinator
- lite_wallet_app_refresh_orchestrator
- lite_wallet_refresh_readiness_policy
- lite_wallet_state_apply_plan
- lite_wallet_state_apply_executor
- lite_wallet_sync_app_refresh_integration
- lite_wallet_sync_execution_readiness

Severed three thin couplings into the cluster from live files:
- state_mapper: dropped the dead mapLiteWalletRefreshServiceResult and switched
  its include from refresh_service.h to gateway.h (where the live
  LiteWalletRefreshResult/Bundle DTOs actually live).
- server_lifecycle_readiness: dropped the unused syncLifecycleInput member +
  converter and the sync_app_refresh_integration include.
- artifact_resolver: relocated the three LIVE artifact-input structs
  (LiteWalletSdxlArtifact{Symbols,}Input, LiteWalletLinkedBackendReadinessInput)
  out of sync_execution_readiness.h — their only real consumers — into
  artifact_resolver.h, then dropped the include.

Also removed the dead DRAGONX_LONG_LITE_BATCH CMake machinery (its source var
was empty; on Windows it generated a broken lite_batch90_receipt_plan.cpp that
#included an empty path) and the stale .cpp/.h entries in CMakeLists.

Lite source files: 44 -> 30. Lite + full-node configure, both targets build,
test suite passes, source-hygiene clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:25:02 -05:00
DanSandClaude Opus 4.8 6a4e98b7ed feat(lite): M4 — send/shield/import/export/seed via controller + bridge
Add the spend & backup surface to LiteWalletController, with the real SDXL
backend contracts verified against the Rust source:

- send / shield: ASYNC (detached broadcast thread + takeBroadcastResult() slot,
  mirroring the sync thread's shared-lifetime pattern, since sapling proving can
  take seconds), plus synchronous *Blocking cores for tests. send uses the
  JSON-array form ([{address,amount,memo}]) because litelib_execute passes the
  whole args string as ONE argument (no whitespace split) — the space-separated
  CLI form would never parse. send/shield report failure via {"error":..} in the
  body (NOT an "Error:" prefix), so the result is derived from the parsed JSON.
- importKey: auto-detects transparent WIF (U/5/K/L -> timport) vs shielded key
  (-> import); takes the key by value and securely wipes it before returning.
- exportPrivateKeys / exportSeed: synchronous local reads returning SECRET
  material (flagged: no logging; caller wipes after the user saves the backup).
- broadcast thread is detached in the dtor (captures shared bridge + flag + slot,
  never `this`), so it is safe to outlive the controller.

Tests: testLiteWalletControllerM4 drives send (success / no-recipients /
{"error":..} / async-slot delivery / pre-open rejection), shield, export, seed,
and import (shielded + WIF + pre-open). Fake backend returns the real command
shapes + a g_liteFakeSendFails error toggle.

GUI wiring (send_tab button, backup/import UI) is deferred like the M3 UI hop
(GUI-unverifiable here). Plan doc updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:06:19 -05:00
DanSandClaude Opus 4.8 043cdc7128 fix(lite): address adversarial audit findings in session's lite work
Re-audited this session's lite-wallet changes (originally written at medium
effort) and fixed the genuine issues found:

- walletReady (open path): litelib_initialize_existing returns the bare string
  "OK", which is NOT valid JSON, so the previous `json::accept(value)` check
  marked a *successful* open as not-ready. Key off a non-empty success response
  instead (the bridge already maps "Error:"/null to failure). Drops the now
  unused nlohmann include.
- sync progress: while the detached sync thread is still running, syncDone_ is
  authoritative — don't surface the backend's transient idle syncstatus
  ({"syncing":"false"} -> parser progress=1.0/complete=true) as a misleading
  100%/done. Force complete=false and zero the bogus 1.0 in the progress model.
- per-address balance: also exclude `pending` outputs (notes/utxos from an
  unconfirmed received tx) so per-address figures match confirmed/available.
- secret wiping: the settings page left the page-local request copies
  (input.request.*Request.{passphrase,seedPhrase}) unwiped, and the
  validation-only fallback path wiped nothing. Replace the single-path memzero
  with an RAII scrubber that wipes both the UI char buffers and the request
  string copies on every return path.
- concurrency: document that concurrent bridge->execute() is intentionally
  unguarded — litelib serializes wallet access internally via
  Arc<RwLock<LightWallet>>, so a C++ mutex is unnecessary and would defeat the
  sync/syncstatus concurrency the design relies on. syncLaunched_ -> atomic.

Tests: fake backend now returns the real init shapes (seed object for
create/restore, bare "OK" for open) and a new open-path case guards the
walletReady regression. Removed an unreliable alloc==freed leak assert from the
thread-bearing controller test (kept in the thread-free bridge test). Also fixed
a stray CMake indent and removed ~220MB of untracked build/debug scratch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:28:37 -05:00
DanSandClaude Opus 4.8 2daea67a1e feat(lite): M3 — new-address generation + sync-indicator confirmation
- LiteWalletController::newAddress(shielded) runs the backend "new" command ("zs"/"R" ->
  do_new_address), parses the ["addr"] response, and returns the new address; the next
  refresh lists it. Fast (local derivation), safe on the UI thread.
- fake_lite_backend returns ["zs1fakenew"]/["R1fakenew"] for "new" by args.
- testLiteWalletControllerNewAddress covers shielded/transparent + no-wallet error.

Also confirmed (no code needed): the sync-progress indicator already works for lite —
balance_tab reads state.sync.* which M2b-3 populates. Per-address balances landed in M2.

Remaining M3 is pure UI wiring (receive_tab button -> newAddress, loading/empty states),
which isn't verifiable without a GUI session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:36:00 -05:00
DanSandClaude Opus 4.8 e6b91ca661 feat(lite): per-address balances from unspent notes/utxos
applyLiteRefreshModelToWalletState now derives each address's balance by summing its unspent
notes/utxos (excluding spent and unconfirmed-spent outputs) instead of the aggregate-only
zeros, so the Receive/Balance UI shows per-address amounts. The notes parser shape is
confirmed against do_list_notes in the backend source.

testLitePerAddressBalances covers the summing + spent-exclusion. Completes M2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 10:24:36 -05:00
DanSandClaude Opus 4.8 268eba6321 fix(lite): gateway refresh degrades gracefully on a failed command
LiteWalletGateway::refresh() aborted the entire refresh on the first command whose bridge
call or parse failed — which turned a single real-backend shape mismatch (e.g. syncstatus)
into a total, empty-everything refresh. Since the balance/addresses/list real shapes are
still unverified and we've already hit shape drift twice, make refresh resilient:

- Run every planned command; assembleLiteWalletRefreshBundle already skips failed results.
- result.ok = any usable data came back (bundle.complete still reflects all-succeeded).
- One command's failure now degrades gracefully — the other sections still populate.

testLiteWalletGatewayRefreshSkipsFailedCommand (fake balance returns invalid JSON) asserts
the refresh still succeeds with addresses/transactions/info populated and balance skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 06:57:10 -05:00
DanSandClaude Opus 4.8 3119440cd9 fix(lite): non-blocking, non-hanging sync (Finding B)
The backend `sync` command is a blocking, uninterruptible full chain scan (do_sync(true);
does not honor the shutdown flag), and balance/list block until synced. Previously
startSync() ran on the main thread (would freeze wallet creation) and the worker could
block, making the destructor join() hang at shutdown.

Redesign:
- bridge is now std::shared_ptr<LiteClientBridge>, shared with a detached sync thread so
  detaching is safe and litelib_shutdown isn't called while a running sync still holds the
  bridge; the controller's own ref prevents premature shutdown during normal operation.
- startSync() launches the blocking `sync` on a detached thread (non-blocking; never joined).
- refreshModel() gates on syncDone_: while syncing it publishes syncstatus progress only;
  once synced it does the full balance/addresses/list refresh (now fast).
- destructor joins only the fast poll worker and detaches the sync thread -> no hang.
- syncComplete() accessor added.

Tests (deterministic, via a blocking-sync fake; counters made atomic for the detached
thread): testLiteWalletControllerShutdownDoesNotHangDuringSync (destructor returns <1.5s
with sync blocked); refresh/worker tests wait for syncComplete()/a balance-bearing model.
Stable across repeated runs; lite+backend and full-node apps build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 06:35:26 -05:00
DanSandClaude Opus 4.8 59c55e33f8 fix(lite): parse real syncstatus shapes (idle vs in-progress)
The real backend returns syncstatus as idle {"syncing":"false"} (string) or in-progress
{"syncing":"true","synced_blocks":N,"total_blocks":M} (commands.rs:83-87), but
parseLiteSyncStatusResponse hard-required the block fields and failed whenever the wallet
wasn't actively syncing — so sync/progress never updated in the real app.

- Read "syncing" as a string; require synced_blocks/total_blocks only when syncing=true;
  idle => complete, synced/total 0.
- fake_lite_backend syncstatus now uses the real "syncing":"true" shape.
- testLiteSyncStatusParserRealShapes covers idle, in-progress, and missing-counts-while-syncing.
- Verified against the live backend via lite_smoke --refresh (syncstatus parse_ok=1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 22:52:42 -05:00
DanSandClaude Opus 4.8 a8b5d2f6a3 feat(lite): M2b-3 — background refresh worker + App::update hook
- LiteWalletController owns a background std::thread worker that, once a wallet is ready,
  refreshes every ~4s and publishes a copyable LiteWalletAppRefreshModel under a mutex.
  Worker auto-starts on lifecycle-ready and is stopped+joined in the destructor. status_
  is written only on the main thread; walletOpen_/syncStarted_ are atomic.
- App::update() calls takeRefreshedModel() and applies it into state_ on the main thread
  (WalletState is non-copyable, so the model crosses the thread boundary, not the state),
  so the existing Balance/Receive/Transactions tabs populate from lite data.
- refreshWalletState() refactored onto refreshModel() (pure, worker-safe).
- testLiteWalletControllerWorkerProducesModel verifies the worker publishes a populated
  model (stable across repeated runs). Builds clean in all configs.

Real-backend smoke (lite_smoke --refresh now runs real output through the parsers) found
two integration bugs, documented in the plan for follow-up:
- syncstatus parser requires synced_blocks/total_blocks but the real idle response is
  {"syncing":"false"} (string), so it fails to parse when not actively syncing.
- the first data query (balance/list) blocks on a full chain sync, which would hang the
  worker's shutdown join — needs a cancel/timeout path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 22:38:34 -05:00