Commit Graph

30 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
012341b1a4 feat(lite): M2b-1/2 — shared-bridge refactor + sync/refresh into WalletState
Shared-bridge refactor (litelib is a global singleton; every LiteClientBridge calls
litelib_shutdown() on destruction, so services must not each own one):
- LiteWalletLifecycleService, LiteWalletGateway, LiteSyncService now take a non-owning
  LiteClientBridge*; LiteWalletController owns the single bridge and passes &bridge_.

Sync + controller refresh:
- LiteSyncService::startSync executes the real "sync" command (was a stub).
- LiteWalletController: startSync() (auto-fires when a wallet becomes ready) and
  refreshWalletState(WalletState&) — polls syncstatus, runs gateway.refresh(), maps the
  bundle, applies balances/addresses/transactions/sync into WalletState.

Tests:
- fake_lite_backend.h returns command-shaped JSON (per tests/fixtures/lite/result_parsers.json).
- testLiteWalletControllerRefreshPopulatesState drives the full path against the fake.
- Surfaced + worked around a real integration issue: parseLiteInfoResponse requires
  latest_block_height and the gateway aborts the whole refresh on the first command's
  parse failure (fragile vs partial backend responses; hardening tracked for M2b-3).

Verified: ctest green; lite+backend, full-node, lite-no-backend apps + lite_smoke build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 22:24:18 -05:00
5586f334a4 feat(lite): real backend integration — controller, M0-M2a wiring, smoke tool, tests
- LiteWalletController (src/wallet/lite_wallet_controller.*): App-owned; runs real
  create/open/restore via the linked SDXL bridge with allowBridgeCalls=true; wipes
  seed/passphrase with sodium_memzero; persists on a ready wallet. M2a:
  applyLiteRefreshModelToWalletState maps a parsed refresh bundle into WalletState
  (zatoshi->DRGX, z/t split, tx typing + confirmations, sync progress).
- App wiring: liteWallet() accessor + init() construction when supportsLiteBackend();
  persist -> settings save.
- settings_page: "Validate" reroutes to the controller for real execution (validation-
  only fallback otherwise); wipes UI secret buffers after submit.
- chain name default -> "main" with load-time migration of legacy "DRAGONX"
  (settings.cpp), preventing the backend "Unknown chain" panic.
- M0: build.sh --lite-backend flag; lite_smoke real-backend tool + CMake targets;
  tests/fake_lite_backend.h deterministic harness.
- Tests (test_phase4): injectable-fake bridge, controller lifecycle, chain-name
  migration, refresh->WalletState mapping; plus the lite test-suite churn-cleanup rewrite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 21:15:44 -05:00
863d015628 feat(lite): lite wallet foundation (inherited working-tree state)
Preserve the previously-uncommitted lite wallet implementation and related dev WIP
under version control:
- src/wallet/ lite services: client bridge, bridge runtime, connection, lifecycle,
  sync, gateway, result parsers, state mapper, artifact contract/resolver, refresh
  services, UI adapters, wallet_backend/capabilities. (Includes two small M1 fixes:
  lifecycle walletReady now parses the response; default chain name -> "main".)
- src/chat/ chat protocol; tests/fixtures/ (lite + hushchat); tools/hushchat_fixture_check.cpp;
  scripts/build-lite-backend-artifact.sh.
- Pre-existing modified app_network/security/wizard, network_refresh_service, sidebar,
  mining_tab, bootstrap dialog, and version headers captured as-is.

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