Commit Graph
100 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 4cd27dcd4c fix(i18n): correct the consolidation wording — it doesn't shrink the file
The note/consolidation strings promised the wallet "shrinks" / "stays small",
but consolidation can't shrink wallet.dat (Berkeley DB never shrinks in place) —
it reduces the note count so the wallet scans faster and stops growing, and the
freed space is reclaimed only by Compact. Reword consolidate_desc,
wallet_size_warn, wallet_size_auto_info and tt_wallet_size_warn to say exactly
that (the tooltip now points at Compact). English only; the 8 locales keep their
prior translations until the next pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 17:25:34 -05:00
DanSandClaude Opus 4.8 4dfdf8a36f feat(wallet): add a Compact action to reclaim space after consolidating
Consolidating reduces the note count and stops the wallet growing, but Berkeley
DB never shrinks wallet.dat in place, so the freed space isn't returned to the OS
and the displayed size doesn't drop. Add an explicit "Compact wallet…" action
(next to "Consolidate notes…" in the large-wallet area of Node & Security) that
rewrites the file into a fresh, compact one.

App::compactWallet() arms the daemon's -salvagewallet flag for the next start and
calls restartDaemon() (reusing its full-node / busy / lifecycle guards); the
daemon rewrites wallet.dat into a smaller file, keeps the old one as a timestamped
.bak, and — because -salvagewallet implies -rescan — re-scans the chain. If the
restart is refused the salvage flag is disarmed so it can't fire on a later
restart. A confirmation overlay (mirroring the repair-wallet one) explains the
restart + rescan + that funds/balance are safe and a backup is kept. New strings
wallet_size_compact / tt_wallet_size_compact / compact_started /
confirm_compact_wallet_{title,msg,safe} / compact_wallet_confirm.

Build + tests + hygiene green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 17:24:21 -05:00
DanSandClaude Opus 4.8 1a01de9791 fix(ui): inset the consolidate address-list text from its border
The per-address list boxes (the run-progress list and the selection list) drew
their text flush against the border. The Consolidate dialog is a floating
overlay, which pops its content padding after the content child, so the ambient
WindowPadding is 0 — a bordered child therefore got no inner padding and its
rows touched the border. Push a small inner padding (10px, DPI-scaled) and
AlwaysUseWindowPadding for both lists, reserving it in their heights, so the
rows sit comfortably inset. Build + tests + hygiene green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 16:33:16 -05:00
DanSandClaude Opus 4.8 3b828e8a49 fix(ui): stop the Consolidate modal from jumping around during a run
The overlay dialog is auto-height and vertically centred, so it re-centers
whenever its content height changes. During a consolidation run two lines toggle
and made it jump: the pause notice (waiting for a batch gap / sync / a send),
which flickers on and off every batch, and the "run again" leftovers hint, which
appears partway through. Reserve a fixed slot for the pause notice and, while the
run is live, reserve the leftovers hint's measured height up front, so the
content height stays constant and the modal holds still. The end-of-run
transition to the result view is unchanged. Build + tests + hygiene green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 15:09:27 -05:00
DanSandClaude Opus 4.8 7138f633a8 feat(wallet): enable auto-consolidation by default
Default the auto_consolidate setting to true, so a full-node wallet keeps its
note count down on its own out of the box (users can still turn it off). A
missing key in an older config loads the new default, enabling it on upgrade,
which is intended. The page-state mirror default matches.

Safety is unchanged and reinforced in comments: auto-consolidation reuses the
"keep in each address" engine — z_mergetoaddress [addr] -> addr — so funds NEVER
move between addresses; it only sheds notes within each address. It never uses
the "into one address" sweep. It also only runs when the wallet is idle AND
unlocked (a locked encrypted wallet never auto-runs) and only touches addresses
with more than kAutoConsolidateMinNotes notes, so a fresh or lean wallet stays
dormant. Build + tests + hygiene green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 14:56:22 -05:00
DanSandClaude Opus 4.8 fc9f8d52c3 fix(ui): clamp tooltips to a comfortable width instead of spanning the screen
Audited every tooltip. The shared helper material::Tooltip wrapped ImGui's
SetTooltip with no width limit, so a long tooltip laid out on one line and
stretched across the whole display — most visible on the newly reworded
note/consolidation tooltips. 258 call sites go through this helper, so the fix is
central:

- material::Tooltip / BeginTooltip now push a text wrap position (420 logical px,
  DPI-scaled) so long text wraps to a comfortable column; short text is
  unaffected (it's already narrower). A caller that pushes its own wrap pos nests
  cleanly. Also fixes a latent style-stack leak when ImGui::BeginTooltip returns
  false (the pushed style is now popped).

Six sites bypassed the helper with raw ImGui tooltips. The two that actually
stretched are migrated to material::Tooltip: the alert/bell toast-hover showing a
full (now ~250-char) message, and the wallets dialog's folder-path hover. The
chat over-cap tooltip is migrated too for consistency. The remaining three
(two wallet-recovery dialog tips, the chat composer's parts tooltip) already
clamp via their own PushTextWrapPos and are left as-is.

Build + tests + hygiene green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 14:45:39 -05:00
DanSandClaude Opus 4.8 d83169c963 feat(wallet): pause routine wallet scans during a user-run consolidation
A manual consolidation's merges compete with the GUI's routine wallet scans for
the daemon's cs_main: the run won't fire the next merge while any scan is in
flight, and because each merge mutates the wallet it re-triggers the balance /
history scans, which then delay the following merge. The WalletScanGovernor
already serializes all of these one-at-a-time under a cs_main budget, so the fix
is to pause the heavy routine streams — not every RPC — while a user run owns the
budget.

WalletScanGovernor gains per-stream routine suppression (tryAdmit returns empty
for a paused stream, which keeps waiting and resumes the instant it is unpaused;
explicit needs via beginExplicit are never paused), plus a convenience that
pauses the balance / addresses / history / recent-tx group together. App::update
pauses that group while consolidation_.active() AND the run is user-started —
an auto (background) run is left gentle and untouched, and the cheap sync / peer
/ mempool / operation-status polls keep running so sync and disconnects stay
visible. The end-of-run forced balance refresh repaints once the run finishes.

Note: a manual run is bounded (≤ maxBatches per click, ~minutes), so the balance
and recent-tx views are only briefly stale — an incoming payment shows once the
run finishes or is stopped. Unit-tested (testWalletScanGovernor): a paused stream
is not admitted but keeps waiting, explicit bypasses it, unpausing re-admits, and
chat/Adhoc are never in the paused group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 14:22:53 -05:00
DanSandClaude Opus 4.8 9335e2bffb fix(wallet): always flag a user-started consolidation as manual
startConsolidation reset the shared ConsolidationRun but never cleared
auto_consolidate_run_. In the rare window where a background auto cycle was torn
down by a disconnect (abort) before pumpAutoConsolidation cleared its flag, a
user-started run could inherit the stale flag and be treated as an auto cycle —
silent (no merge-done/failed toasts) and auto-dismissed. Clear the flag on a
successful manual start so a user run always keeps its own notifications and
lifecycle. No behavior change in the normal case. Build + tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 14:05:47 -05:00
DanSandClaude Opus 4.8 112752e8af i18n(wallet): translate the note-consolidation strings (8 languages)
Translate the three new keys (auto_consolidate, tt_auto_consolidate,
wallet_size_auto_info) and re-translate the three reworded ones (wallet_size_warn,
tt_wallet_size_warn, consolidate_desc) into de, es, fr, ja, ko, pt, ru, zh
(res/lang/*.json, 2509 → 2512 keys each), so the "what are notes / why
consolidate" education and the auto-consolidation toggle read natively in every
language. Each was grounded in its file's existing terminology for consolidate /
transaction / fee / balance / sync / shielded, with a single consistent rendering
of the crypto term "note" per language (e.g. ru «заметка», zh 票据, witness data
见证数据).

Rebuilt the CJK subset font (NotoSansCJK-Subset.ttf, 2115 → 2123 glyphs) for the
eleven new glyphs the translations introduce (千堆巧幣悄枚溜財 + ko 띄슷였).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 13:34:05 -05:00
DanSandClaude Opus 4.8 045289115d feat(wallet): explain notes + why to consolidate in the large-wallet nudge
The large-wallet notification just said "This wallet file is large" — users who
don't know what a shielded "note" is had no idea what to do or why. The nudge now
teaches: its message explains that each payment is stored as a separate note,
that notes pile up and enlarge/slow the wallet, and that consolidating merges
them while leaving the balance unchanged. The text wraps in full in the bell/
alert panel and the Settings wallet-size banner (and shows on hover from the
ellipsized toast), and the hover tooltip adds the detail plus points at the new
Auto-consolidate toggle.

When Auto-consolidate is ON the nudge no longer stays silent — it shows a calm
one-time info (wallet_size_auto_info) telling the user the background
consolidator is already handling it, so the accumulating notes never look like an
unexplained problem. When it's OFF, the educational warning keeps its
"Consolidate notes…" action that opens the dialog.

The Consolidate dialog's own intro (consolidate_desc) is likewise expanded from a
one-liner into a plain-language explanation of what notes are and why consolidate.

English source updated; the reworded existing keys keep their prior (valid, if
terser) translations and the new wallet_size_auto_info falls back to English
pending a translation pass. Build + tests + hygiene green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 13:27:36 -05:00
DanSandClaude Opus 4.8 64294d1518 feat(wallet): optional background auto-consolidation (Settings toggle)
A mining/pool wallet can accumulate tens of thousands of shielded notes, and the
manual "Consolidate" run — capped at 20 batches per address per click — needs
many re-runs to flatten a 10k+ note address. This adds a hands-off alternative:
a Settings toggle ("Auto-consolidate notes", full-node, off by default) that
trickle-merges notes in the background while the wallet is idle, so it sheds
notes over hours/days without the user ever opening the dialog.

Deliberately gentle, per the request: a small batch (15 inputs/tx vs 50 manual)
and only 3 batches per address per cycle, with cycles ~10 min apart — each cycle
is a handful of tiny merges that barely touch the node. It only acts when the
wallet is genuinely idle (walletScanReady: connected, synced, unlocked, node not
busy, not rescanning) and no send / shield / manual run is in progress, and it
goes dormant (re-scanning hourly) once every address is tidy.

Implementation reuses the existing paced services::ConsolidationRun engine in
"keep in each address" mode, so there is NO fund-path change and the run's
pacing / failure breakers / note-locking all apply:
- App::pumpAutoConsolidation (pumped from update() after pumpConsolidation):
  finalizes a finished cycle (schedules the cooldown + dismisses the run so the
  manual dialog never shows a stale auto result), else starts a new cycle when
  idle and the cooldown has elapsed.
- postAutoConsolidationScan: a quiet z_listunspent scan (the dialog's loadScope
  minus the UI state), session-guarded; on result picks candidates and starts.
- startConsolidationQuiet: the same guards as startConsolidation but silent, with
  a gentle RunConfig; finishConsolidation stays silent for auto runs (logs only).
- services::autoConsolidateCandidates (pure, unit-tested): addresses above the
  note floor that are worth merging; plus the gentle kAutoConsolidate* constants.
- The one-time large-wallet nudge is suppressed while auto-consolidation is on.

Settings plumbing (auto_consolidate, default false) + a daemon-gated toggle in
the Wallet tab's options, with a tooltip. Build + tests + hygiene green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 13:18:00 -05:00
DanSandClaude Opus 4.8 54f8359bd4 i18n(chat): translate the tier-3 larger-message strings (8 languages)
Translate the five new chat strings — chat_parts_label, chat_txns_label,
chat_parts_explain, chat_parts_explain_multi, chat_receiving_large — into de, es,
fr, ja, ko, pt, ru, zh (res/lang/*.json, 2504 → 2509 keys each), additively and
matching each file's existing terminology for transaction / fee / message. The
transaction abbreviation uses each locale's natural short form (Tx / tx / 件 /
건 / транз. / 笔).

Rebuilt the CJK subset font (NotoSansCJK-Subset.ttf, 2111 → 2115 glyphs) for the
four new glyphs the translations introduce (产 割 拆 in zh, 뉩 in ko) so they
render instead of tofu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 12:53:56 -05:00
DanSandClaude Opus 4.8 dba97c8b35 feat(chat): show a progress hint while a tier-3 message is still arriving
Until now an incoming large message was invisible until every part landed.
ChatService::pendingLargeMessages() exposes the in-progress tier-3 receives
(conversation, chunks gathered / chunks needed) from the reassembly buffer, and
the chat tab renders a subtle centred line below the thread — "Receiving a large
message…  24 / 36 parts" — that updates each scan and disappears when the message
completes and surfaces as an ordinary bubble. A complete-but-withheld (sensitive
vault locked) group is not reported as incoming.

New string chat_receiving_large (English; 8 locales fall back). Unit test extended:
after the first slice the receive is reported in progress; after the whole message
arrives it is no longer pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 12:47:37 -05:00
DanSandClaude Opus 4.8 acb6446076 feat(chat): raise the composer to the tier-3 cap + show the transaction count
Front end of tier-3 larger messages. The composer body cap goes from 4000 to
16000 bytes (kHushChatMaxMessageChunks = 64 chunks ⇒ up to 4 transactions), so a
message past the single-transaction ceiling now actually composes and sends via
the tier-3 path. The buffer and static_assert grow to match.

The beside-the-gauge indicator (and its hover tooltip) now distinguish the two
tiers: while a long message still fits one transaction it shows the part count as
before; once it spills across several it shows the TRANSACTION count — the real
cost the user asked to see — and the tooltip explains it is split across several
transactions, the recipient sees it only once every part has arrived, so it takes
longer to deliver and costs a fee per transaction. New strings chat_txns_label /
chat_parts_explain_multi (English; 8 locales fall back pending a translation pass).

Tier 3 is now end to end: compose → split → broadcast → reassemble → display.
A receive-side "incoming N/M parts" progress hint remains a future refinement
(an incomplete large message simply appears once whole).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 12:41:04 -05:00
DanSandClaude Opus 4.8 e61772a825 feat(chat): broadcast a tier-3 message across its transactions under one echo
Send side of tier-3 larger messages. sendChatMessage now composes via
composeMessageParts (splitOutgoingMessage), which returns one OutgoingChatMemos
per transaction, and enqueues each on the existing note-buffer coordinator — so
the parts drain sequentially against verified notes exactly like ordinary chat
sends, with no fund-path change (each part is the same pinned 0-value
chatSendOutputs z_sendmany).

A single echo bubble represents the whole message. A new per-echo outstanding-
part counter holds it at "Sending" until the LAST part is broadcast, then flips
it to Sent; if any part hits a hard failure the remaining queued parts are purged
(a message missing a slice can never reassemble, so sending the rest only wastes
fees) and the whole message is failed once — its existing Retry recomposes and
re-splits from the body. A transient-funds deferral still retries the part in
place. The counter map is cleared alongside the send queue on a wallet reset.

Inert until the composer cap is raised (the current 4000-char cap yields a
single tier-2 transaction); that + the multi-transaction composer indicator land
next. Build + tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 12:36:40 -05:00
DanSandClaude Opus 4.8 5e33bf2c41 feat(chat): tier-3 reassembly accumulator in the chat service
Second half of tier-3 larger messages: the receive side. ChatService now buffers
the slices of a multi-transaction message (keyed by the shared message id) as
they arrive — across transactions and across scans — and reassembles the whole
ciphertext once every chunk is present, then decrypts and surfaces it as one
message. The buffer is NOT persisted: a rescan re-sends every slice, so it
re-derives from chain exactly like the control reducer, and survives a restart
mid-receive (the next full scan re-feeds the siblings and completes it).

Ingest's per-message build/decrypt/guard logic is refactored into one shared
lambda so a reassembled tier-3 message passes the identical block / on-chain-hide
/ sensitive-vault / tombstone checks as a single-tx message. A completed group is
retired after it surfaces (deduped across scans by its stable canonical txid —
the smallest sibling txid); a group withheld only because the sensitive vault is
locked is kept so a later unlock + scan completes it. Our own slices (sender ==
us) are skipped — the local echo records the outgoing message. The pending map is
bounded (256 groups) against a flood of never-completing partials.

Unit-tested (testHushChatTier3Reassembly): a ~9000-char message's first slice
alone surfaces nothing; once every slice has arrived it reassembles, decrypts to
the original and surfaces once; a re-scan dedups; our own slices never ingest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 12:31:42 -05:00
DanSandClaude Opus 4.8 cc08854237 feat(chat): tier-3 larger messages — protocol + split + cross-tx reassembly core
Foundation for messages too large for a single transaction (tier 2's 16-output
ceiling, ~4000 chars). A long message is now split across SEVERAL transactions
that share a random message id; each carries a slice of the one ciphertext with
GLOBAL chunk indices, and the receiver reassembles across the siblings.

Protocol: the Message header gains an optional "mid" (message id); "n" is reused
as the TOTAL chunk count and its bound raised to kHushChatMaxMessageChunks (64 ⇒
up to ~16 KB / 4 txs, tunable, ≤256 for the 2-hex index). The grouper, when a
tx's header carries "mid", emits a PARTIAL (its global-indexed chunks + message
id + total) instead of reassembling in-place; HushChatTransactionMetadata
carries those partials through. Crypto is untouched — one secretstream session
for the whole message, chunking is transport-only.

Compose: splitOutgoingMessage() returns one OutgoingChatMemos per transaction
(one element = the classic tier-1/2 single tx, byte-identical; several = tier 3).
buildOutgoingMessage (single-tx) is unchanged. Old clients bound "n" to 16 so a
tier-3 header fails to parse and its memos are dropped — graceful degradation.

Unit-tested (testHushChatLargeMessageTier3): a ~9000-char message splits into 3
txs, every output shuffled, slices gathered across siblings, reassembled and
decrypted back to the original; short message stays single-tx (no mid); the
total-chunk cap is enforced. The stateful accumulator (persisted, confirm-gated),
sequential broadcast, and progressive/incomplete UI follow in later commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 12:20:11 -05:00
DanSandClaude Opus 4.8 1729f7dfad feat(chat): show the on-chain part count for a long message in the composer
A larger message (tier 2) is carried as one transaction split across up to 16
memo outputs, but the composer gave no sign of that. It now shows the part
count beside the byte gauge the moment the body spans more than one memo output
(computed the same way buildOutgoingMessage chunks it), and the gauge's hover
tooltip adds the count plus "sent as one transaction split into these memo
parts — it arrives as quickly as a normal message" (the honest answer: it's one
atomic 0-conf tx, not N transactions — multi-transaction splitting is the
unbuilt tier 3). New strings chat_parts_label / chat_parts_explain (English;
8 locales fall back).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 12:00:27 -05:00
DanSandClaude Opus 4.8 12c61bd796 fix(platform): correct wallet-size reporting for files over 2 GiB (Windows)
getFileSize used stat(), whose st_size is a 32-bit signed _off_t on mingw/
Windows, so a wallet.dat over 2 GiB overflowed negative and cast to a garbage
~2^64 value — the "Wallet Size: 17179869182.10 GB" (≈ 2^34 GiB) seen on a real
~2.1 GB shielded wallet, which also spuriously fired the "wallet is large"
banner. Switch to std::filesystem::file_size (portable 64-bit uintmax_t) with
the error_code overload, returning 0 on failure (missing / unreadable / a
Windows sharing violation while the daemon holds the file open) so the callers'
existing `> 0` guards fall back to "unknown". The size now reports correctly,
and the large-wallet warning fires on the real size (so a genuinely bloated
2+ GiB wallet is flagged, as intended).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 11:45:42 -05:00
DanSandClaude Opus 4.8 63c284684c test(chat): regtest harness verifying on-chain memo round-trip (larger messages + Control)
Adds a dev-only harness that closes the "needs a live daemon" gap for the new
chat wire paths, fund-safely on an isolated regtest chain:
- tools/chat_regtest_smoke.cpp (+ an EXCLUDE_FROM_ALL CMake target): a small
  driver linking the chat library — derive identity / compose a message or a
  Control memo / extract + decrypt harvested memos. Secret-safe.
- tools/chat_regtest.py: starts a throwaway regtest dragonxd (own datadir,
  -regtest, no network — nothing touches ~/.hush), funds a shielded note, then
  drives a full round-trip through the real daemon: compose -> z_sendmany the
  memo outputs -> mine -> z_listreceivedbyaddress -> feed back to the driver.

Verified on dragonxd v1.3.0: a chunked 1529-byte message (8 memo outputs in one
tx) and a self-addressed Control(del) memo both round-trip verbatim and
reassemble/extract correctly — i.e. the real node accepts a multi-output 0-value
chat tx and returns its memos intact. Paths are env-overridable
(DRAGONX_REGTEST_DAEMON_DIR / DRAGONX_CHAT_SMOKE / DRAGONX_REGTEST_RUN).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-24 14:03:45 -05:00
DanSandClaude Opus 4.8 2d63a72098 i18n(chat): translate chat_toast_too_long (8 languages)
The one remaining English-only chat string (the "message too long" toast from
the larger-messages composer cap) is now translated into de/es/fr/pt/ru/zh/ja/ko
(2503 -> 2504 keys each), consistent with each file's terminology and formality.
No new CJK glyphs — all covered by the existing subset font.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-24 12:48:06 -05:00
DanSandClaude Opus 4.8 743e7f8b65 feat(chat): raise the composer cap to the tier-2 larger-message capacity
The composer byte cap was 236 (one memo). With larger-messages tier 2 a body
can span up to 16 memo outputs in one tx, so kChatBodyMaxBytes rises to 4000
(a touch under the ~4063 protocol max so the composer never produces a body
compose would reject). The compose buffer grows to match, guarded by a
static_assert, and the emoji picker / byte gauge follow the single constant.
sendChatMessage now shows a specific "message too long" toast on a TooLong
compose instead of the generic compose-failed one (English string added; the
8 locales fall back until translated).

Broadcast is unchanged — both chatSendOutputs call sites already iterate the
N outputs. (Lite backend acceptance of >2 duplicate-address 0-value outputs is
the one bit needing a live smoke test, like the other lite broadcast paths.)

Larger-messages tiers 1-2 are now usable end to end on the full node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-24 11:19:45 -05:00
DanSandClaude Opus 4.8 6b93a8ab60 feat(chat): larger messages — multi-output chunking in one transaction (tier 2)
A message whose ciphertext exceeds one 512-byte memo is now split across up to
kHushChatMaxMessageParts (16) payload outputs in a SINGLE transaction, lifting
the ~239-byte cap to a few KB while staying atomic, instant (0-conf), and
one-fee. The crypto is untouched (one secretstream push/pull); chunking is a
transport concern:
- Header gains an optional part count `n` (emitted only when >1, so a 1-part
  message stays byte-identical on the wire); bounded in the parser.
- Each chunk is a 2-hex part-index prefix + its ciphertext slice, so the
  receiver reorders them after the daemon's output shuffle.
- OutgoingChatMemos carries a payload vector; chatSendOutputs emits header + N
  payloads (both broadcast call sites already iterate the result).
- The grouper gains a multi-part pre-scan: a lone Message header with n>1 →
  collect the N indexed chunks, sort, strip, concat → one pair; anything else
  falls through to the classic single-pair path (older clients mis-pair and
  drop — graceful). A body beyond the cap is refused as TooLong (tier 3 later).
Round-trip (shuffled outputs), 1-part byte-identical, and cap all unit-tested.

Larger-messages tier 2; tier 3 (multi-tx) + the denser raw-memo encoding remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-24 11:16:22 -05:00
DanSandClaude Opus 4.8 19e9b62632 chore(release): bump full-node to 2.1.0 and lite to 2.0.0
Full node 2.0.2 -> 2.1.0 (minor): a large additive feature + audit-fix release
since v2.0.2 with no breaking changes and only opt-in/user-initiated migrations.
Lite 1.1.1 -> 2.0.0 (major): inherits the shared feature work and carries a
deliberate on-disk/interop format change — newly-encrypted lite wallets now use
the salted Argon2id KDF (wallet v10) that older ObsidianDragonLite / SilentDragonXLite
cannot open — plus the lite line reaching functionally-complete status this cycle.

Single source of truth: project(VERSION) + DRAGONX_LITE_VERSION in CMakeLists.txt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-23 20:32:22 -05:00
DanSandClaude Opus 4.8 076bb3ff06 i18n(chat): translate the sensitive-conversation strings (8 languages)
Back-fills the 29 new chat_sensitive_* keys additively into de/es/fr/pt/ru/zh/
ja/ko (2474 -> 2503 each), matching each file's established terminology and —
critically — keeping the sensitive "passphrase" worded distinctly from the
"seed phrase" (e.g. de Passphrase vs Seed-Phrase, es contraseña vs frase de
recuperación, fr phrase secrète vs phrase de récupération), translated +
reviewed per language. Rebuilds NotoSansCJK-Subset.ttf (2107 -> 2111 glyphs)
for 4 new CJK glyphs (ja 論; ko 눌/똑/론) the new strings introduced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-23 18:49:44 -05:00
DanSandClaude Opus 4.8 f0be72ac36 feat(chat): sensitive-conversation UI (mark/unmark, setup/unlock, lock)
Wires sensitive mode into the chat tab:
- A per-conversation lock toggle in the header (next to delete) opens a dialog
  that, by vault state, sets up / unlocks the vault and marks or unmarks the
  conversation. The mark dialog shows the honest warning (lose the passphrase =
  gone forever; sent fully protected vs received still chain-recoverable; prior
  history stays recoverable).
- A "Lock sensitive" / "Unlock sensitive (N)" opener beside Blocked/Restore
  locks or unlocks the vault; unlocking surfaces the withheld sensitive threads.
- Passphrase fields use the password flag and are memset on close; marking
  requires the vault unlocked (the App layer migrates rows + broadcasts the
  on-chain signal).

~29 new English strings (fall back to English until translated). Phase B
complete (policy level); the cryptographic re-key level remains out of scope.
Needs a live smoke test (built unverified, like the delete UI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-23 18:25:12 -05:00
DanSandClaude Opus 4.8 6e285bd4a4 feat(chat): App vault lifecycle for sensitive conversations
Adds the App-level glue that makes sensitive mode operable:
- Owns ChatSensitiveVault (path <configDir>/chat_sensitive.vault); provisioning
  wires the service's local sensitive predicate to Settings::isChatSensitive and
  points the vault at its path (left LOCKED until the user enters its passphrase).
- chatSensitiveVaultState() (NotConfigured/Locked/Unlocked) + create / unlock /
  lock: unlock pushes the key to chat_db_ and pulls sensitive rows into the store
  (+ re-harvest); lock drops the key and reloads so sensitive threads vanish.
- markChatConversationSensitive / unmark: migrate the thread's existing rows
  to/from the vault key via upsert (so the whole thread hides when locked) and
  broadcast the on-chain "sensitive"/"unsensitive" signal via sendChatControl.
  (wallet lock already wipes the vault key via chat_db_.lock().)

Phase B, step 5a (App lifecycle). UI next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-23 17:47:57 -05:00
DanSandClaude Opus 4.8 fe3d2aa935 feat(chat): service-level sensitive gating + local sensitive marks
Wires the sensitive-conversation plumbing into ChatService + Settings:
- Settings gains a persisted local sensitive set (isChatSensitive /
  setChatSensitive / sensitiveChatConversations), mirroring the blocked set, for
  INSTANT effect the moment the user marks a conversation (before the on-chain
  signal is harvested back).
- ChatService::isConversationSensitive() OR-combines that local mark with the
  on-chain reduction, and setPersistence() wires it as the DB's per-row key
  selector (sensitive rows -> vault key).
- ingest() withholds a sensitive conversation's messages while the vault key is
  absent (fail-closed: not shown, not persisted); they re-import + persist under
  the vault key once it is unlocked and a scan runs. loadFromDatabase() already
  withholds via the DB's two-key try.
Service-tested end to end against a real ChatDatabase (ingest + reload).

Phase B, step 4 (service gating). App vault lifecycle + UI next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-23 17:42:31 -05:00
DanSandClaude Opus 4.8 6b9bda3ee4 feat(chat): per-conversation local-key encryption in ChatDatabase
ChatDatabase gains a second AEAD key — the local vault key (setSensitiveKey /
lockSensitive) — and a sensitivity predicate. Rows of conversations the
predicate marks sensitive are encrypted under the vault key instead of the
seed-derived key, and are persisted only while the vault is unlocked (never
stored under the seed key). load() tries the seed key then the vault key, so a
sensitive row is WITHHELD whenever the vault key is absent — including on a
fresh instance that holds only the seed (the "fresh seed import can't read
sensitive conversations" guarantee); a wrong vault key withholds too, never
yields garbage. encrypt/decrypt are now key-parametrized; lock() also wipes the
vault key. Unit-tested end to end.

Phase B, step 3 (DB dual-key).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-23 17:36:50 -05:00
DanSandClaude Opus 4.8 84ca1a5cbe feat(chat): passphrase-wrapped local key vault for sensitive conversations
Adds ChatSensitiveVault: a per-wallet random 32-byte key wrapped under a user
passphrase (Argon2id MODERATE KEK + XSalsa20-Poly1305) in an 89-byte local file.
This key encrypts the at-rest rows of "sensitive" conversations INDEPENDENTLY of
the seed-derived chat key, so those rows can't be read by someone who has the
seed but not this passphrase — and a wallet restored from the seed alone can't
read them at all. Losing the passphrase/file makes them unrecoverable by design.
create/unlock/changePassphrase/remove, atomic write + 0600, keyOut zeroed on any
failure. Unit-tested (round-trip, wrong-passphrase zeroing, rekey, no-clobber).

Phase B, step 2 (local vault key).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-23 17:31:30 -05:00
DanSandClaude Opus 4.8 669b8b7ca5 feat(chat): add an independent "sensitive" axis to the control reducer
Routes control ops to one of two independent per-conversation reductions: the
hide axis (del/res) and a new sensitive axis (sensitive/unsensitive), so a
conversation can be deleted AND sensitive without the two states clobbering
each other. Unknown future ops are ignored (not stored in either axis).
Adds conversationSensitiveOnChain() + onChainSensitiveConversations(); the
reduction stays monotonic by (compose-time, txid). Reducer-tested, including
axis independence and that marking sensitive neither hides nor erases a thread.

Phase B, step 1 (reducer) of per-conversation local-key encryption (policy level).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-23 17:25:36 -05:00
DanSandClaude Opus 4.8 796d639ba3 i18n(chat): translate the delete-everywhere / restore strings (8 languages)
Back-fills the 14 new chat delete/restore keys additively into de/es/fr/pt/ru/
zh/ja/ko (2460 -> 2474 each), matching each file's established terminology
(conversation, Delete, Restore, sync, seed phrase, shielded/z address, scan,
device, chain, fee) — translated + reviewed per language. Rebuilds
NotoSansCJK-Subset.ttf (2104 -> 2107 glyphs) to cover 3 new CJK glyphs (微, 曾,
伝) the new zh/ja strings introduced, so they don't render as tofu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-23 17:02:04 -05:00
DanSandClaude Opus 4.8 daae693d1b feat(chat): Restore dialog for on-chain-deleted conversations
Adds a "Restore" opener (shown when a delete is known from chain — so it works
on a wallet freshly restored from seed — or something is locally blocked) and a
Restore dialog that: lists chain-derived deleted conversations
(onChainDeletedConversations) each with a Restore button; accepts a pasted chat
(z) address, matched against the address each delete signal carries (else
treated as a raw cid); and offers Quick scan (noteWalletMutation → re-run the
change-gated scans → re-harvest control memos). Restoring clears any local
block/hide and broadcasts Control("res") so every device un-hides; the messages
re-import from chain on the next scan.

New English strings fall back cleanly until translated (chat_restore_*).
Phase A complete: protocol -> compose -> reducer -> broadcast -> delete+restore UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-20 08:23:35 -05:00
DanSandClaude Opus 4.8 2f1b666c20 feat(chat): "Delete everywhere" action in the delete dialog (on-chain signal)
Adds a distinct, sync-gated "Delete everywhere" button to the conversation
delete dialog: it blocks the thread locally (instant + persistent, so it can't
flicker back mid-sync) AND broadcasts a self-addressed Control(del) via
sendChatControl so every other device — and a future restore from the seed —
hides it too. Carries the peer z-address as the control label so Restore can
match by address. The button is disabled until the wallet is fully synced
(lite uses its own sync-complete signal, since it never sets tip_known), with a
note explaining the on-chain signal + fee. The two local-only modes (Delete /
Delete & block) are unchanged.

New English strings fall back cleanly until translated (chat_delete_everywhere*,
chat_delete_needs_sync). Phase A, step 5 (delete UI). Restore dialog next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-20 08:19:34 -05:00
DanSandClaude Opus 4.8 5aa5fa7594 feat(chat): broadcast self-addressed Control memos (App::sendChatControl)
Adds App::sendChatControl(cid, op, label): composes a self-addressed Control
memo and hands it to the existing chat send coordinator (enqueueChatSend →
pump → the 0-value kChatMinFeeDrgx z_sendmany / lite send) exactly as a message
does — no new fund-path semantics. It carries no UI echo bubble (empty
echoLocalId; resolveOutgoing("") is a no-op), and the memo harvested back to us
is routed to the reducer, never shown as a message.

Phase A, step 4 (broadcast) of on-chain chat delete/restore. UI wiring next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-20 00:26:59 -05:00
DanSandClaude Opus 4.8 f56c94bc91 feat(chat): reduce on-chain Control memos into per-conversation hide state
ingest() now routes Control memos (self-addressed, so processed ahead of the
"skip our own memos" guard) into a per-cid reducer that keeps the latest op by
(compose-time, txid) — monotonic, so a replayed older op never flips state. A
conversation whose latest op is "del" is hidden: its messages are dropped from
ingest and erased from the store view (even rows loaded from the DB before the
signal was reduced), on every device and on a fresh seed import, until a later
"res" restores it. The DB rows are left intact so restore (or a rescan) brings
the thread back. State is re-derived from chain each session; no local
persistence. Adds conversationHiddenOnChain() + onChainDeletedConversations()
(the restore picker's source), fully reducer-tested.

Phase A, step 3 (reducer) of on-chain chat delete/restore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-20 00:21:58 -05:00
DanSandClaude Opus 4.8 7320a98505 feat(chat): compose self-addressed Control memos (buildOutgoingControl)
Adds buildOutgoingControl: builds the two 0-value memo outputs for a control
signal (a "Ctrl" header carrying `op` + a plaintext label payload) addressed to
the sender's OWN z-address, so only this wallet's seed harvests it and it never
reaches the peer. buildHeaderMemo gains an optional `op` (emitted only for
Control; existing Memo/Cont headers stay byte-identical). Round-trip + guard
tested through the receive parser.

Phase A, step 2 (compose) of on-chain chat delete/restore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-20 00:16:23 -05:00
DanSandClaude Opus 4.8 529286777f feat(chat): add a Control memo header type for on-chain conversation signals
Introduces HushChatHeaderType::Control — a self-addressed control signal
(delete / restore / mark-sensitive) that rides a chat tx as a header memo
carrying an `op`, paired with a plaintext label payload so it groups like a
normal header/payload pair. The op VALUE is not restricted at parse time so a
future op reaches newer clients and is ignored by older ones; unknown header
types still fail-closed (grouped as an invalid header, never surfaced, never a
crash), preserving forward-compat.

Foundation for the on-chain chat delete/restore feature (Phase A).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-20 00:12:23 -05:00
DanSandClaude Opus 4.8 a3b4f534d5 feat(recovery): import keys from a legacy wallet backup file (z_importwallet)
Phase 3: for a wallet.dat the current daemon can't open, recover its keys from the OLD wallet's export file.
Settings → "Import From Backup File…" opens a dialog that takes the path to a z_exportwallet / dumpwallet
dump (or SilentDragon's "Export all private keys") and calls z_importwallet — the daemon reads every key
and does ONE rescan, far faster and simpler than importing keys one at a time. On success it nudges the user
to shield any mined coinbase from the Shield screen.

- New App::importWalletFile(path, cb): runs z_importwallet on the long-call lane (no client timeout — the
  rescan can take a long time), then refreshAddresses + markWalletMutated. Full-node only.
- Only the PATH touches GUI memory, never the keys themselves (the daemon reads the file) — less secret
  exposure than a pasted-key field.
- New renderImportWalletFileDialog (typed path + Paste/Clear, progress, result) + a Settings card.

Reuses the existing long-call + z_importwallet infra; the careful single-key import dialog is untouched.
No send/sweep/encrypt call changed. Build + ctest green; the import/rescan itself is a daemon RPC, not
pure-testable, and wasn't exercised end-to-end here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 22:43:49 -05:00
DanSandClaude Opus 4.8 293b633a24 feat(recovery): inline unlock in the migrate-to-seed sweep step
Phase 2 of legacy-wallet migration. The sweep (z_shieldcoinbase / z_mergetoaddress) needs an unlocked
wallet, so an encrypted wallet left the "Sweep all funds" button silently disabled with no way forward.
The Sweep step now, when the wallet is locked, shows a passphrase field + Unlock button that calls
unlockWallet(pass, 600); once it unlocks, the (already-present) canSweep gate re-enables the sweep. The
passphrase buffer is sodium_memzero'd immediately after submit.

No fund-path change: this only unlocks the wallet (the daemon's own walletpassphrase), and the sweep/adopt
logic is untouched. Batched-coinbase progress is already handled — the Confirming step shows the live
remaining balance and the existing "Sweep remaining" flow ("too many inputs for one transaction") drains it
across z_shieldcoinbase's 50-UTXO batches.

Runtime/UI change (build + ctest green; not pure-testable). Still to verify on a real degraded+encrypted
wallet end-to-end: unlock -> coinbase shield -> confirm -> adopt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 22:28:35 -05:00
DanSandClaude Opus 4.8 ae4012adb7 feat(recovery): offer legacy-fund recovery when a degraded wallet can't shield its transparent funds
Phase 1 of automating legacy wallet.dat migration: a persistent, actionable banner that detects a wallet
whose transparent funds are STUCK and offers the migrate-to-seed recovery flow (showSeedMigrationDialog).

Corrected trigger (the important part): it fires ONLY when wallet_degraded_ is set — i.e. z_getnewaddress
failed with "HD seed not found", so the wallet genuinely cannot create a shielded address to move its
funds — AND there is a transparent balance to move. It is NOT driven by "the wallet has no mnemonic": a
non-mnemonic but functional wallet (one that self-healed with a retrofit HD seed on load) has
wallet_degraded_ == false, can still shield/spend, and is deliberately never flagged — matching the
existing "legacy wallets are not nagged" policy. Full-node only.

Implemented in the already-unit-tested node banner: new NodeBannerReason::LegacyFundsStuck +
NodeBannerAction::RecoverLegacyFunds, gated on a new legacy_funds_stuck input (App computes it from
wallet_degraded_ + transparentBalance). NoPeers still takes priority (can't recover before the node sees
the chain); lite never shows it. Pure decision tests added: fires when stuck, never for a functional
wallet, yields to NoPeers, suppressed on lite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 22:05:11 -05:00
DanSandClaude Opus 4.8 6c2a277751 fix(wallet): surface the real z_getnewaddress error; sweep coinbase in migrate-to-seed
From a legacy-wallet migration support case (mined T-funds, 3y-old Silent Dragon wallet.dat): the balance
shows but new-address and migrate both fail with unhelpful/silent results. Two fixes:

1) Real address-creation error. createNewAddress() swallowed the daemon's z_getnewaddress error and the UI
   showed a generic "Failed to create shielded address". It now keeps the daemon's reason (friendly-mapped
   via friendlyRpcError) in last_address_error_, which the Receive tab and request-payment dialog show
   instead of the generic string — so a legacy/degraded wallet sees the actual "HD seed not found". And when
   that specific error appears it flips wallet_degraded_ + warns once, even for an EXTERNAL daemon
   (detectWalletDegraded() only scans the embedded daemon's log and would otherwise miss it).

2) Migrate-to-seed sweep skipped coinbase. beginSweepToSeedWallet()'s z_mergetoaddress ignores coinbase
   UTXOs, so a miner's coinbase-only balance produced "Could not find any funds to merge", the legacy
   balance never reached ~0, and the adopt gate stalled silently. The sweep now falls back to
   z_shieldcoinbase("*", dest, fee, 50) to the SAME shielded destination when the merge reports no funds.
   z_shieldcoinbase returns an opid just like the merge, so the single-opid adopt gate (opid -> txid ->
   confirm -> balance ~0 -> adopt) is unchanged, and a batch limit / mixed wallet drains via the existing
   "Sweep remaining" re-sweep.

Fund-path: the z_mergetoaddress call and its args are byte-identical; the fallback only ADDS a
z_shieldcoinbase to the same dest when merge finds nothing, and nothing is adopted until the swept funds
confirm and the legacy balance is ~0 (gate unchanged). Runtime-verified behavior (clean build + ctest; the
RPC paths are not pure-testable).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 19:47:38 -05:00
DanSandClaude Opus 4.8 a558a2a271 fix(explorer): commit the loadRangeCached/revision memoization the perf fix depends on
Repairs dev: 3ee9099 switched the Explorer call sites (and a test) to ExplorerBlockCache::loadRangeCached()
/ revision(), but those definitions were sitting uncommitted in the working tree (left over from the
explorer block-search feature, ebbeb84) and were not staged with that commit — so dev as pushed referenced
undefined members and would not compile on a clean checkout.

The infra: loadRangeCached() memoizes loadRange() per (min,max), invalidated by revision_, which is bumped
on every mutation that can change what loadRange returns (open/close/store/clear/prune/spill-reset); the
memo is capped at 16 ranges. My local builds passed only because the working tree already had these.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 19:15:19 -05:00
DanSandClaude Opus 4.8 3ee90993ed fix(resilience,perf): gate send on a known tip, surface dropped/stale txs, advance cached confs, memoize explorer
Backlog mediums (display/gating/caching only — no send/sweep/encrypt call or args changed):

Resilience:
- Gate Send/Review on a KNOWN peer-derived tip (SyncInfo::sendTipReady): a fresh reconnect or peerless node
  has tip_known=false, so its local height isn't a trustworthy network tip — sending is disabled with a
  clear tooltip until it is. Lite is exempt (its own sync gate). The z_sendmany call is untouched.
- History now shows "Dropped" for a conflicted tx (confirmations < 0) and "Expired?" for one unconfirmed
  > 1h, instead of "Pending" forever (new TransactionInfo::isConflicted/isStalePending; the tab inlines the
  check on its DisplayTx fields).
- A cached z_viewtransaction row's confirmations now advance by the blocks mined since it was sampled
  (observed_height), instead of freezing at the cached value; never goes backwards; pending (0) isn't
  extrapolated.

Performance:
- Explorer block list uses loadRangeCached() (memoized on the cache revision) instead of loadRange(), so it
  only hits SQLite after the cache changes, not every frame.

Tests: testResilienceMediums (tip gate, confirmations-advance, dropped/stale predicates) + loadRangeCached
memoization in testExplorerBlockCache (isolated on its own cache). Held: Overview/Balance per-frame list
cache, peers clippers, off-thread tx-history cache load (larger refactors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 19:12:32 -05:00
DanSandClaude Opus 4.8 b404bb2aff test(lite): set spendableShieldedBalance in the refresh fixture (realistic synced wallet)
Follow-up to 3a33e9c: testLiteRefreshModelAppliesToWalletState built its balance bundle without
spendableShieldedBalance (left 0), so the new "pending = total − spendable" unconfirmedBalance formula
reported the whole 2.0 shielded balance as pending (actual 2.0 vs expected 0.5). In real data
spendable_zbalance is a required parser field and a synced wallet reports all of it spendable; set the
fixture to match (= shieldedBalance), so unconfirmedBalance is the 0.5 mempool-incoming amount as intended.
No production code change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 19:02:21 -05:00
DanSandClaude Opus 4.8 3a33e9cae9 fix(lite): unconfirmedBalance uses full-node pending semantics; drop pending-spent t-UTXOs from spendable
Lite-parity audit mediums (display / spendable-gating only — never over-reports spendable; no send call):
- unconfirmedBalance was just the backend's mempool-incoming figure, but the shared UI (balance_tab "pending"
  chip, the fast-poll hint) reads it as the full-node "total minus spendable" portion. Set it to the larger
  of the two (the not-yet-spendable shielded part vs the mempool-incoming), so neither is hidden and a note
  counted in both isn't double-reported.
- transparentBalance/spendableTransparentBalance included t-UTXOs already spent by a pending tx (the backend
  tbalance sums every utxo with spent==None). Subtract unconfirmed-spent t-UTXOs from the SPENDABLE figure
  only (clamped; gross transparent display unchanged) so the Send picker can't offer funds a pending tx
  already consumed. Falls back to no-subtraction when the notes call failed that cycle (as before).

Test: testLiteUnconfirmedAndTransparentSpendable. Multi-recipient history rows held (TransactionInfo is
single-output, same as the full node).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 18:58:32 -05:00
DanSandClaude Opus 4.8 46de4cbfee fix(fund): Tier-B parser/aggregation hardening — range-check amounts, int-satoshi sums, strict balance parse
Fund-correctness audit, Tier-B (parse/validation/display only — no send/sweep/encrypt call touched; each
change can only reject bad input or remove drift, never over-report):
- lite_result_parsers: cap every backend-reported amount at the 21,000,000 DRGX supply (note value,
  outgoing output value + running sum, received amount, the net-amount fallback magnitude), and replace the
  two bare `return false` paths in the outgoing-metadata loop — which left ok=false with an EMPTY error and
  silently rejected the whole tx list — with fail() carrying a reason.
- network_refresh_service: accumulate per-address balances in integer satoshis (util::toSats) instead of
  summing doubles, so many notes can't drift by an ulp per addition (ten 0.1 notes now sum to exactly 1.0);
  skip non-finite amounts.
- readBalanceString: strict decimal parse (digits / one '.' / optional leading '-') instead of std::stod,
  which silently accepted "1.5abc", whitespace, hex, exponents and nan/inf.

Tests: testLiteParserAmountRangeChecks + integer-satoshi drift + strict-parse cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 18:55:02 -05:00
DanSandClaude Opus 4.8 d7fee801d3 fix(rpc): abort any in-flight call before a UI-thread disconnect, not just maintenance calls
Concurrency audit (finding #2, previously held for sign-off): abortMaintenanceCalls() — the choke point run
before every UI-thread rpc_->disconnect() (wallet switch/adopt/restore/rebuild, handleLostConnection,
shutdown) — only called requestAbort() when maint_calls_>0. An ordinary in-flight refresh/scan (not counted
as a maintenance call) holds curl_mutex_ for its whole curl_easy_perform, so disconnect() blocked the UI
for up to the call timeout (≈30s, or 120s for a wallet scan).

Abort unconditionally so disconnect() never waits on the curl lock. The abort flag is cleared by the next
tryConnect()/onConnected() before reconnecting. A user send that happens to be in flight during a teardown
is now aborted too, but that is handled as a lost reply — "outcome unknown, check History", never a
double-send (see the lost-reply classifier, 08a4427) — so no funds are at risk. The z_sendmany/broadcast
call and its args are unchanged.

Threading fix — clean build + ctest; the UI-block is runtime-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 18:48:22 -05:00
DanSandClaude Opus 4.8 4f4c6b7a16 fix(rpc): drop a stale connect result after teardown; don't let open() erase a pending abort
Concurrency audit, connect-lifecycle races (no fund-path call touched — connect-result gating + abort-flag
lifetime only):
- The connect task's MainCb ran onConnected() on any successful probe without re-checking that a wallet
  switch / adopt / restore / shutdown had already torn the connection down — so a probe that succeeded
  against the old daemon could start a refresh against one about to be stopped. Capture rpc_session_ when
  posting (main thread) and, in the MainCb, drop the result (requestAbort + disconnect, not a blocking bare
  disconnect) when the session changed or daemon_restarting_/shutting_down_ is set. Same guard on the
  cookie-auth retry post.
- RPCClient::open() called clearAbort(), so a requestAbort() issued while a connect was still queued was
  erased, stalling teardown/shutdown. open() no longer clears it; the two reconnect entry points
  (tryConnect for rpc_, onConnected for fast_rpc_) clear it on the main thread first, skipped while
  shutting_down_ so a shutdown abort survives. A fresh client defaults to not-aborted, so first connects
  are unaffected.

HELD (not in this commit): making abortMaintenanceCalls() abort unconditionally (the UI-freeze-on-disconnect
finding) — it would also abort an in-flight user send during teardown, a send-lifecycle behaviour change
that wants explicit sign-off (it is fund-safe via the lost-reply "outcome unknown" handling, but still a
behaviour change).

Threading fix — clean build + ctest; the races are runtime-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 18:41:21 -05:00
DanSandClaude Opus 4.8 5563becad2 fix(i18n): thread-safe translation for the daemon monitor thread (don't race loadLanguage)
Concurrency audit: I18n::translate() returns a const char* pointing INTO strings_, and loadLanguage()
clears/rebuilds strings_ on the main thread at a language switch. The EmbeddedDaemon state/monitor callback
builds its status strings via TR() off the main thread, so a concurrent language switch could free the
map node out from under that pointer — a use-after-free data race.

- Add a shared_mutex guarding strings_: loadLanguage() takes it exclusively for the whole clear+rebuild
  (its helpers loadBuiltinEnglish/overlayTranslations are called only from there and don't lock, so no
  recursion); a new translateCopy()/trCopy() takes a shared lock and returns an owned std::string.
- The lock-free translate()/TR() stays for main-thread UI (same thread as the writer, so never races).
- The four daemon-state setDaemonStatus(TR(...)) calls in the monitor callback now use util::trCopy(...).

Synchronization only; no fund-path call touched. Threading fix — clean build + ctest; the race is runtime-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 18:38:46 -05:00
DanSandClaude Opus 4.8 6301cd5984 fix(security): synchronize SecureVault::scope_ between the main thread and worker security ops
Concurrency audit: SecureVault::scope_ (std::string) is written by setWalletScope() on the main thread
(wallet switch) while getVaultPath() — reached from retrieve()/hasVault()/changePin()/store() — runs on
RPC worker threads during a send-time or startup unlock. A concurrent wallet switch and a worker vault op
is a data race on the string (and could resolve the wrong vault file).

Guard scope_ with a small mutex: setWalletScope() assigns under it; getVaultPath() copies it out under the
lock and uses the copy. The lock is held only for the string copy — never across Argon2id or file I/O —
and getVaultPath() never calls setWalletScope(), so no deadlock. Argon2id params, key derivation, AEAD,
the vault file format and retrieve/store/changePin are byte-identical. SecureVault is held via unique_ptr
and used by pointer, so the added (non-movable) mutex is fine.

Synchronization only. Threading fix — clean build + ctest; the race is runtime-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 18:34:59 -05:00
DanSandClaude Opus 4.8 9b6971095c fix(daemon): synchronize EmbeddedDaemon fields written across the monitor/start/stop threads
Concurrency audit: last_error_ was written unlocked at the five startProcess() error sites (while the
monitor thread could be in setState() writing it under error_mutex_) and read unlocked in start() — a
data race on a std::string. process_pid_ / stdout_fd_ (POSIX) and process_handle_ (Windows) are plain
scalars written by startProcess, the stop paths and the monitor (reap) and read by accessors.

- Route every startProcess() last_error_ write through a new locked setLastError(); read the start()
  detail via the already-locked getLastError().
- Make process_pid_ / stdout_fd_ / process_handle_ std::atomic (trivially atomic scalars/handles), so
  individual loads/stores no longer race. Process start/stop ordering and the shutdown hardening are
  unchanged — this only removes the data races, not the (already-serialized) sequencing.

Synchronization only; no fund-path call touched. Threading fix — verified by a clean build + ctest; the
race itself is runtime-only. (The Windows process_handle_ atomic is reasoning/compile-only on this Linux box.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 18:31:57 -05:00
DanSandClaude Opus 4.8 8a6d523062 fix(lite-backend): release the pending lock when a send expires; un-spend reorged-out UTXOs
Resilience audit, vendored SDXL core (post-hoc wallet bookkeeping only — the send/build/broadcast path,
note/UTXO selection, fees, recipients, memo and the post-broadcast pending-marking are byte-identical):

- cleanup_mempool never cleared unconfirmed_spent, so a send that was dropped / never mined / reorged out
  locked its notes and UTXOs until the app was reloaded ("insufficient funds" on a funded wallet). It now
  releases the lock once the send is provably dead. A send is built with Builder::new(tip) (expiry = tip+20,
  mineable through tip+20), so the lock is held until the chain reaches tip+21 (MEMPOOL_KEEP = DELTA+1) —
  one block past the old mempool-drop point, which would have released the notes while the tx could still be
  mined. A txid already in `txs` was mined (scan settled its inputs) and is skipped. mempool_txs is released
  before `txs` is taken, so the two locks are never nested.
- invalidate_block reset nd.spent on reorged-out spenders but never walked wtx.utxos, so a transparent UTXO
  whose spender was reorged out kept spent=Some(removed txid) and dropped out of the balance until a rescan.
  It now resets utxo.spent the same way (unconfirmed_spent left as-is, as for notes — the send may be re-mined
  on the new branch).
- Removed the stale "TODO remove unconfirmed_spent on expiry" in data.rs (now done).

Verified with the scratch-mirror rtest.sh (cargo 1.63, scratch HOME): cargo test --release --lib green,
129 passed / 0 failed / 12 ignored, including the oracle differential tests (updated in lockstep to clear
the sim's pending flag at the same expiry point) and test_z_mempool_expiry. The C-ABI wrapper and the
artifact rebuild / lite_smoke (needs a live server) were not run; they don't touch these functions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 18:13:33 -05:00
DanSandClaude Opus 4.8 16a1ca02e8 fix(lite): lock/unlock parity — shared attempt-lockout, immediate unlock state, no dead PIN, surfaced encrypt failure
Lite-parity audit, FIX C (gating/validation/presentation only — no encrypt/unlock/send backend call changed):
- Lite unlock went straight to the backend with no lockout. New App::unlockLiteWallet() runs the SAME
  escalating, persisted attempt-lockout as the full node (restoreLockoutState/applyUnlockFailure), wired into
  the Settings unlock, the send-time prompt and the lock screen. A wrong-passphrase failure counts; an
  already-unlocked/not-encrypted result does not.
- state_.locked is now cleared immediately on a successful unlock (the ~2s status poll lagged, so the UI
  stayed "locked" after a correct passphrase).
- PIN quick-unlock controls are hidden in lite Settings — lite has no PIN vault, so they were dead.
- A create/restore whose passphrase encryption FAILS now raises a one-shot visible error
  (takeEncryptionFailureNotice -> lite_encrypt_failed_unencrypted) instead of only a log line; the wallet is
  left open but unencrypted. Fixed the flag on BOTH the lifecycle and the direct create/restore encrypt paths.
- A background sync save is skipped while an encrypted wallet is unlocked (the backend save re-locks it),
  resuming once locked.

Compile- and unit-verified (testLiteWalletControllerLockUnlockParity covers the save-deferral + encrypt-failure
notice, run twice for the flaky guard); the lock-screen lockout/PIN-hiding UX itself is runtime/GUI-only and
not exercised here — worth a manual lite GUI pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 15:33:28 -05:00
DanSandClaude Opus 4.8 0cb1df3bfe fix(migrate): require a reorg-safe sweep depth + re-verify before adopt; make the wallet.dat swap crash-safe
Resilience audit, migrate-to-seed (the sweep RPC is byte-identical — see below):
- Adopt was enabled at >= 1 confirmation and never re-read state before stopping the daemon, so a 1-block
  reorg after the swap could un-mine the sweep and strand the funds in the .bak. Adopt now needs a
  reorg-safe depth (>= 10, matching the History "confirmed" threshold), and the button runs
  verifyThenAdoptSeedWallet(): a read-only gettransaction(confs) + z_gettotalbalance minconf 0 (includes
  unconfirmed, fixing the old minconf-1 "no funds" gate) immediately before the daemon stop, refusing while
  syncing or if any read fails. The remainder re-sweep still shows once mined (>=1).
- The wallet.dat swap was rename(legacy->bak) then non-atomic copy(new->wallet.dat): a hard kill left
  wallet.dat missing or half-written. New order: probe the new file is a BDB wallet; stage a size-verified
  copy beside wallet.dat; COPY (never overwrite) the legacy wallet to the timestamped .bak and verify it;
  then ONE atomic fs::rename of the staged file over wallet.dat. At every instant wallet.dat is the whole
  old or whole new wallet, and a verified .bak exists before the replace. On any failure only our scratch
  files are removed; the user's wallet is untouched.

Byte-identical: z_mergetoaddress (beginSweepToSeedWallet, ["ANY_TADDR","ANY_ZADDR"], amounts, fee, recipient,
memo) and beginAdoptSeedWallet's disconnect/vault-removal/stop/rescan/restart logic are unchanged; only the
adopt gating, two new read-only RPCs, presentation, and the file-move safety change. Pure tests:
sweepConfsAreReorgSafe + adoptReverifyPasses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 15:22:17 -05:00
DanSandClaude Opus 4.8 6f369376ec fix(lite): surface sync failures instead of a frozen "Syncing 99.9%"; don't report a lost server as "Synced"
Lite-parity audit, sync-progress semantics (state/presentation only — no send/sync backend call changed):
- A scan failure set scan->failed + lite_error, but no UI read lite_error, so the user saw "Syncing 99.9%"
  forever. The status bar now shows the error text on a lite scan/server failure.
- After the initial catch-up, a server loss on the cheap probe path (ProbeFailed) left the last "Synced"
  model on screen. The worker now re-publishes that model demoted (not complete, reason "server unreachable").
- refreshModelImpl's height-read "synced" override no longer fires while syncstatus says the backend is
  still scanning or is indeterminate, and a server-bound read failure in a full cycle demotes to not-synced.
- The syncstatus parser no longer treats a missing/unrecognised "syncing" field as fully synced: it reports
  an explicit "indeterminate" (not complete). Explicit "false" stays caught-up; the fake backend's
  syncing:true 1000/1000 behaviour is unchanged.

Tests: parser cases for missing/odd/idle "syncing" (testLiteSyncStatus*).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 15:16:36 -05:00
DanSandClaude Opus 4.8 02fa1a228a fix(lite): per-address spendable honours the backend spendable flag; flag imported view-only addresses
Lite-parity audit: per-address spendableBalance was set equal to the gross balance, ignoring the backend's
per-note 'spendable' flag (spending key present AND note at/below the anchor height), and has_spending_key
was effectively always true (mapAddress never set spendabilityKnown). So the Send auto-picker could select
an address that can't fund the send, and an imported view-only (zxviews) address looked spendable.

- Parser: carry the backend per-note 'spendable' flag when present (Sapling notes; transparent utxos keep
  the confirmed/unspent rule). It can only narrow spendability, never widen it.
- Mapper: derive per-address spending-key evidence from positive signals only — a spendable note proves a
  key (spendable), a confirmed long-mature (>=100 blocks) still-non-spendable note proves view-only
  (!spendable); fresh/immature/no notes stay unknown so nothing is mislabelled.
- Controller: info.balance stays the gross sum; info.spendableBalance now counts only backend-spendable
  outputs; a tolerated partial refresh keeps the last-known spendable balance and view-only verdict.

Display/selection only — gating can only get stricter for view-only/unanchored funds; no send/sweep/encrypt
call changes. Test: testLitePerAddressSpendableAndViewOnly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 15:11:47 -05:00
DanSandClaude Opus 4.8 099d5df3e1 fix(lite): refuse backend calls after a retired bridge is shut down; make the shutdown flag atomic
Concurrency audit (lite): LiteClientBridge::shutdownCalled_ was a plain bool, set by retire()/destruction
on one thread and read nowhere by the detached sync/console/broadcast threads. A retired controller's
in-flight execute() (send/save/sync) therefore still ran litelib_* against the PROCESS-GLOBAL client,
which may already belong to a NEW controller after a server switch.

- shutdownCalled_ is now std::atomic<bool>; shutdown() uses exchange() so exactly one caller tears the
  client down (idempotent) and the flag is published for the detached threads.
- execute() and initializeNew/NewFromPhrase/Existing refuse once the bridge is shut down (shutDownResult),
  so a stale command can't reach the global client.

Gating only — doSend/doShield, the JSON send payload, amounts/memo/recipients, persistAfterBroadcast,
encrypt/unlock/export and api_.execute argument passing are byte-identical when the bridge is live.
Test: testLiteClientBridgeRefusesAfterShutdown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 15:07:18 -05:00
DanSandClaude Opus 4.8 df98f98669 perf(history): don't rebuild the O(N) tx display cache on every block for a confirmation-only change
Performance audit: tx_revision bumps on every new block because each tx's confirmation count rises, so
the History tab's groupByTxid + merge + sort + deep-copy display cache fully rebuilt once per ~36s block
on the UI thread, even when nothing but the counts changed.

Add WalletState::tx_structure_revision, bumped alongside tx_revision EXCEPT when setTransactionsIfChanged
sees a pure confirmation-count delta (new sameTransactionLayout: every row equal ignoring the confirmation
COUNT, but the pending/confirmations==0 state — which drives the pending-first sort and shield merge — must
still match). Every other writer bumps both (conservative default). The History tab's groups cache and
display cache now key on tx_structure_revision; on a confirmation-only delta it patches dtx.confirmations
in place (rows, order and indices into state.transactions are unchanged) instead of rebuilding, and re-stamps
the filter snapshot so "export current filter" stays valid. Displayed confirmations still update every block.

No fund path touched; display/caching only. Test: testTxStructureRevision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 14:44:13 -05:00
DanSandClaude Opus 4.8 6d58802127 fix(lock): clear in-flight unlock/PIN/encrypt flags on disconnect so the lock screen can't wedge
Concurrency audit: RPCWorker::stop() discards any MainCb the fast worker pushed after the last drain,
and lock_unlock_in_progress_ / pin_in_progress_ / encrypt_in_progress_ are cleared only inside those
callbacks. So a disconnect (3-failure threshold, daemon restart) mid-unlock destroyed the callback that
would have cleared the flag, leaving the lock screen stuck on "Unlocking..." until the app was restarted.

Clear the three flags in onDisconnected() right after the fast worker is torn down (gated on it having
existed, since only fast-lane-posted callbacks can be lost this way). Chosen over draining the worker,
which would run stale callbacks after state_/rpc_session_ were already reset. The user can retry after
reconnect and the real encryption state is re-read on connect. No fund-path call is touched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 14:39:08 -05:00
DanSandClaude Opus 4.8 165675b131 fix(lite): render a lite send as a negative debit, not a green "+"; recover unauthored-send amounts
Lite-parity audit, two History-correctness findings (display/parse only — no send/broadcast call touched):
- A lite send carried a POSITIVE tx.amount, so History, the Details dialog and CSV export rendered an
  outgoing payment as a green "+" receipt (the full node uses a negative debit). The mapper already
  computes signedAmountZatoshis (negative for sends, positive for receives); applyLiteRefreshModelToWalletState
  now uses it for tx.amount, so the shared UI shows a red "-" and CSV a negative value. The summary cards
  use std::abs and are unaffected.
- A send this wallet did not author (seed restore / another device) has empty outgoing_metadata, so the
  parser summed it to 0 and showed amount 0 with a blank address, understating Sent totals. parseTransactionRecord
  now falls back to |record.amount| (the net debit, incl. fee) when the metadata sum is 0, tolerant of a
  missing/odd field; explicit outgoing_metadata still wins.

Tests: testLiteSendAmountIsNegative (send maps to <0, receive stays >0) and
testLiteForeignSendFallsBackToRecordAmount.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 14:37:23 -05:00
DanSandClaude Opus 4.8 08a4427e31 fix(send): treat a lost send reply as "outcome unknown", not a plain failure, to prevent a double-send
Resilience audit, highest-value finding: a z_sendmany (full node) or lite broadcast whose REPLY is
lost — a transport timeout / connection cut AFTER the request already left us — was reported as an
ordinary "Transaction failed". The user then retries and can broadcast the same payment twice, because
the daemon may already hold the operation (z_sendmany returns an opid and runs async).

Classify the ambiguous case and surface it distinctly, without touching the send itself:
- New pure helpers in rpc/rpc_failure.h: sendReplyMayBeLost() (an RpcTransportError that was requestSent
  with no HTTP status and a non-OK curl code — reply-timeout / got-nothing / our-own-abort; definitive
  rejections and any HTTP status are excluded), liteSendErrorMayBeInFlight() (a transport-style gRPC text
  heuristic, vetoing connect-phase and server-rejection text), and a readable-English marker tag.
- app_network.cpp: the z_sendmany catch records the flag; the failure branch (non-background only) tags
  the callback text and forces a wallet re-read so a committed send shows in History. onDisconnected()
  tags already-pending send callbacks the same way (they exist only after an opid was returned).
- Lite UserSend (app.cpp): a transport-style broadcast error is tagged identically.
- Send tab: a tagged outcome shows a non-success warning pointing at History and clears the compose form
  so any resend must be retyped deliberately; it is NOT presented as a retryable failure.

The actual rpc_->call("z_sendmany", {from, recipients, 1, fee}), the lite send/broadcast call, and all
amounts/fees/recipients/memo bytes are byte-identical; only result-handling and presentation change.
Pure classifier/marker regression tests added (testSendMaybeInFlight).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 14:30:16 -05:00
DanSandClaude Opus 4.8 f4460beddd fix(sweep,lite): don't misread a funded key as empty; guard lite send amount; use spendable balance
Fund-correctness audit, sweep + lite paths:
- Key-import sweep: balAt() swallowed every RPC error as 0.0, so a transient
  timeout/warmup after the import misclassified a funded key as "no funds". Let
  the failure propagate to the outer handler so the user sees a real error and
  can retry, instead of a false __NOFUNDS__.
- Lite send: the double→zatoshi conversion was an unchecked llround+cast — a
  negative/NaN amount would wrap to a huge uint64. Route it through
  util::coinsToZatoshis and reject invalid amounts before broadcast.
- Lite balance: the spend gate used the gross shielded total (including
  unverified/unconfirmed notes). Use the backend's spendable_zbalance (a required
  field) for spendablePrivateBalance/Total, clamped to the shielded total.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 13:10:55 -05:00
DanSandClaude Opus 4.8 70d5c233c0 fix(send): stop a fee-tier change from re-sending Max; compare affordability in satoshis
Fund-correctness audit, send path:
- Flagship wrong-send: s_send_max was only cleared on a successful send, so after
  Max → typing a smaller amount → changing the fee tier in the confirm dialog,
  the amount was silently overwritten back to the maximum and Confirm sent far
  more than the user typed. Clear s_send_max on any manual amount edit (DRGX and
  USD inputs), so a later fee bump never rewrites a hand-entered amount.
- A valid exact-maximum send was blocked by 1 ulp because amount+fee<=available
  was computed in IEEE doubles. New util::toSats/canAfford compare in int64
  satoshis; wired into the Review gate, the confirm-time re-check, the Send
  tooltip and the amount-error banner. This only unblocks a correct send; it
  changes no sent amount.
- Added util::coinsToZatoshis (reject non-finite / <=0 / rounds-to-0 / over-supply)
  and regression tests for formatAmountFixed, toSats/canAfford and coinsToZatoshis
  (incl. the exact failing inputs the audit found).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 13:10:55 -05:00
DanSandClaude Opus 4.8 c0d10e9664 security(hygiene): best-effort scrubbing of residual in-memory secret copies (Tier 4)
Defense-in-depth against a local attacker / core dump reading un-zeroed heap
copies of secrets (threat model is local, not remote). Best-effort by nature —
std::string SSO/reallocation and ImGui buffer growth can still strand copies —
so these shrink the exposure window rather than guarantee absence:
- rpc_client performCall: a scope guard now sodium_memzeroes the request body +
  payload tree (walletpassphrase / *_importkey args) after the call and detaches
  CURLOPT_POSTFIELDS. Request content sent to curl is unchanged.
- New ui::scrubImGuiInputTextState() zeroes ImGui's retained InputText buffers
  (live/revert/callback/deactivated) — the app memzeroed its own char[]s but
  ImGui kept copies. Called from wipeSecrets()/wipeDisplayedSecrets(). PINNED to
  ImGui 1.92.6 via a #error guard so a vendored bump forces a re-audit (the stb
  undo buffer is an incomplete type in the header, so it's left as a noted gap).
- Lite exportSeed/exportPrivateKeys wipe result.value + the parsed JSON tree.
- Seed-word grid (SplitSeedWords/RenderSeedWordGrid) zeroes its per-frame word
  and cell copies; the wizard word/chip vectors are zeroed before clearing.
- ChatStore::clear() + ResetChatTab() zero decrypted message bodies on
  lock/switch/teardown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 12:05:04 -05:00
DanSandClaude Opus 4.8 d16bbf3695 security: payment-URI amount validation, export filename sanitize, chat crash-guard, HTTPS-only, conf 0600, pool auto-lock
Six bounded defensive fixes:
- Payment-URI amount is now strictly validated (reject NaN/inf/sign/garbage,
  <=8 decimals, 0..21,000,000) at parse time, so a crafted QR/URI can't inject a
  bogus amount into the Send form. The z_sendmany call the user confirms is
  unchanged; this is input validation only.
- export-all-keys filename is sanitized (basename-only, no traversal) via a
  shared util::sanitizeExportFilename (factored out of the CSV export dialog).
- Chat append()/upsert() wrap the strict serialize in try/catch so a malformed
  (invalid-UTF-8, attacker-influenceable) body is skipped, not crashed on —
  matching appendBatch's existing drop behavior.
- http_download (used by the updaters + pool stats) restricts transfers AND
  redirects to HTTPS and bounds the download path's redirects, so a redirect
  can't downgrade to http:// or another scheme.
- The default DRGX.conf (rpcuser/rpcpassword) is written 0600 from the start via
  writeFileAtomically, closing the brief world-readable ofstream+chmod window.
- Auto-lock is suppressed only for SOLO mining (needs the wallet), not pool
  mining (pays an external address), which was holding the wallet open forever.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 11:39:38 -05:00
DanSandClaude Opus 4.8 83ce73f26b security(console): mask key-dump command RESULTS in the scrollback
The console redacted secret command ARGS but showed the OUTPUT of key-revealing
commands (dumpprivkey / z_exportkey / z_exportviewingkey / z_exportmnemonic /
dumpwallet / z_exportwallet, and the lite seed/export verbs) in full — visible
in the scrollback, Copy All, Ctrl+C and the filter. Now, when the submitted
command is key-revealing, its result is held ONLY in a member buffer (never in
the scrollback model, so Copy-All etc. can't leak it) and a masked placeholder
is shown. An explicit "Copy secret" button routes through the auto-clearing
clipboard; the buffer auto-wipes after 60s, on Clear, and on wallet lock
(via wipeDisplayedSecrets). The command string passed to the executor is
byte-identical. New pure helper ConsoleCommandRevealsSecret is unit-testable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 11:39:38 -05:00
DanSandClaude Opus 4.8 31e9c2ee2c security(exposure): owner-only ACLs on Windows, auto-clearing export copy, require SHA-256 for bootstrap
- writeFileAtomically honored restrictPermissions only on POSIX (0600); on
  Windows it was a no-op, so exported keys / seed backups / the vault / chat
  exports inherited the parent dir's ACL. It now builds a PROTECTED owner-only
  DACL (current-user SID, SE_DACL_PROTECTED) on the temp file before the atomic
  rename; best-effort, falling back to default ACLs on failure. (Verified it
  compiles under mingw-w64; the POSIX path is unchanged.)
- The key-export dialog's Copy wrote the private key to the plain, permanent
  clipboard; it now goes through copySecretToClipboard() (45s armed auto-wipe +
  its own "copied, auto-clears" notice), like the seed backup copy.
- Bootstrap archive verification dropped the collision-prone MD5 fallback: it now
  requires a published SHA-256 and fails closed otherwise. (Authenticity vs a
  malicious server still needs a detached ed25519 signature like the updaters —
  a separate release-coordinated change.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 10:34:47 -05:00
DanSandClaude Opus 4.8 6b0f2eda22 security: scrub on-screen secrets on lock; redact lite console secret args; no-shell wallet rebuild
- Locking the wallet (manual / auto / lite) now calls wipeDisplayedSecrets(),
  which scrubs + closes any revealed seed phrase, migration seed, or exported
  key — previously a locked wallet could leave the phrase/key on screen and
  resident. Runs synchronously, before the async lock round-trip.
- The console secret-arg redaction list only had full-node RPC names, so lite
  verbs (import / timport / encrypt / decrypt / unlock) echoed keys and
  passphrases into the scrollback and up-arrow history; added them.
- The offline wallet-rebuild helper was invoked via a shell string
  (popen "sh -c") built from paths including the wallet filename, so a crafted
  name could inject a command; it now refuses any path with a shell
  metacharacter (rc stays -1 and the existing verify-before-swap fails safe).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 09:28:25 -05:00
DanSandClaude Opus 4.8 dc1617a2c1 security(lite): harden the C-ABI string boundary + backend result parsing
- lite_owned_string: the destructor and move-assignment no longer copy the raw
  value before freeing (nothing reads the copy there, and a std::string copy can
  throw bad_alloc — from the noexcept destructor that meant std::terminate, and
  it could also double-attempt the copy after a throw). They now free only; the
  copy happens solely in intoResult(), which also moves (not copies) the value
  into its result so no extra plaintext lingers.
- lite_result_parsers: jsonToUnsigned rejected nothing on the string path, so
  std::stoull accepted "-1" and wrapped it to a huge balance; now requires a
  leading digit. jsonToSigned rejects leading whitespace/'+'. The outgoing-
  metadata sum rejects negative values and guards against signed overflow (UB),
  so a hostile/buggy backend can't forge or overflow a transaction amount.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 09:28:25 -05:00
DanSandClaude Opus 4.8 f213878a7c refactor(ui): semantic status text uses material tokens instead of hardcoded colors
Finishes the feedback-conventions hygiene thread: ~30 hardcoded ImVec4 green /
red / amber literals used for success / error / warning text now use
material::Success/Error/WarningVec4(), so semantic status colors follow the
active skin and stay legible in every theme (several hardcoded greens/reds read
poorly on the light/pastel skins). Covers validate-address, transaction-details,
key-export, export-all-keys, about, the Settings confirmations/lock state, and
the encryption/decryption dialogs. Brand/logo colors and muted white/grey
(disabled) text are left as-is; dynamic-alpha animated indicators are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 08:51:10 -05:00
DanSandClaude Opus 4.8 8d8e523a26 i18n(migrate,sweep): translate status/error strings + add a hygiene regression guard
- The import-key sweep and migrate-to-seed flows set several user-visible
  status/error strings (shown via sweep_status_ / seed_migration_status_ /
  rescan_status) as bare English; routed them through TR() with additive keys.
- Extended scripts/check-source-hygiene.sh with two low-false-positive content
  guards so this debt can't regrow: a bare-English migrate/sweep status
  assignment, and a Tactile button sized with a raw (unscaled) pixel literal.
  Passes clean on the tree (362 files); the dev sweep harness is allowlisted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 08:44:55 -05:00
DanSandClaude Opus 4.8 8b07f776e5 feat(ux): copy-to-clipboard confirmations + scale wizard buttons for HiDPI
Feedback-convention + HiDPI cleanup (from the audit's living-hygiene threads):
- Copy actions that were silent now toast a confirmation: transaction
  address/txid (context menu, click-to-copy, detail buttons), peer and banned-
  peer addresses, the Settings data-dir copy, the chat message "Copy", and the
  key-export result copy.
- The first-run wizard's Skip/Back buttons used raw ImVec2(120/80,0) widths that
  rendered tiny at HiDPI; they now multiply by ui::Layout::dpiScale().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 08:44:55 -05:00
DanSandClaude Opus 4.8 8b46ead72a feat(export): CSV export scope — All / current filter / date range
Finishes the transactions-export partial. The dialog now offers three scopes:
- All transactions (unchanged default; exported with no copy).
- Current History filter — the Transactions tab publishes a TxFilterSnapshot of
  the visible legs (a merged autoshield row contributes both its send+receive
  legs, so the CSV stays a per-leg record like an unfiltered export); the radio
  is disabled with an explanation when no filter is active, and a snapshot that
  predates the current tx list (revision mismatch) is refused rather than
  silently exporting everything.
- Date range (YYYY-MM-DD, inclusive end-of-day; pending/dateless rows excluded
  with a note; invalid/reversed dates blocked inline).
The live count reflects what will actually be exported. formatTransactionsCsv
and the atomic write are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 00:19:24 -05:00
DanSandClaude Opus 4.8 8961cbdfc9 feat(export): sanitize CSV filename + Open-folder button
The transactions CSV export now sanitizes the typed filename (basename only,
illegal chars → '_', no leading dots/trailing dots-or-spaces, length-capped,
always .csv) so it can't be pointed outside the config dir, and the success
screen gets an "Open folder" button. (All-vs-filter / date-range export scope is
left for a follow-up — it needs tx-tab filter-snapshot plumbing.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:51:07 -05:00
DanSandClaude Opus 4.8 794150b11b feat(chat,notifications): first-run chat intro card; clickable toast history + "+N" badge
- Chat: a dismissible first-run intro card (how chat works — encrypted, sent as
  shielded txs with a small fee, tied to the seed) with a "Copy my chat address"
  button, shown in the empty thread pane; dismissal persisted. The copy logic is
  shared with the existing sidebar button (moved to a helper, unchanged).
- Toasts: clicking a toast now opens the existing notification-history panel, and
  a "+N more" badge appears when several live toasts are queued (render only ever
  showed the newest). Badge opacity is a ui.toml token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:51:07 -05:00
DanSandClaude Opus 4.8 2e74a3ca69 feat(explorer,keys): distinguish lookup failures; key-export unlock affordance
- Explorer: a hash lookup now tells a transport/RPC failure apart from a genuine
  not-found (RPC -5/-8), showing "Lookup failed: <friendly error>" instead of
  "no block or transaction found"; block-detail errors routed through
  friendlyRpcError. getblock/getblockhash/getrawtransaction calls unchanged.
- Key-export: when the wallet is locked, the dialog now says "Unlock your wallet
  first" with an Unlock button (lite → requestLiteUnlock; full-node → close to
  reveal the lock screen) instead of a raw error, and the fetching state animates
  with LoadingDots. The export RPC/args are unchanged.

(i18n.cpp carries the additive keys for these and the chat/CSV commits below.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:51:06 -05:00
DanSandClaude Opus 4.8 e94d88e723 fix(console): confirmed destructive commands now run; gate z_exportmnemonic
Two gaps in the destructive-command confirm:
- Regression: a command confirmed via the command-reference dialog's "Run" was
  replayed through submitConsoleCommand, which re-armed the destructive gate and
  silently did nothing until the user retyped it. The dialog now pre-arms the
  exact-line confirm so the replay executes.
- z_exportmnemonic (full-node seed-phrase reveal) wasn't in the reference
  catalog, so a pasted z_exportmnemonic ran instantly. Added a small
  secret-revealing fallback set (z_exportmnemonic, z_exportviewingkey) to the
  destructive check. Command execution is otherwise unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:51:06 -05:00
DanSandClaude Opus 4.8 097b2ea014 feat(ux): wizard handoff note, daemon restart reminder, settings polish (code)
The code changes intended with the settings_window removal in a90aa29 (a bad
git-add pathspec aborted staging them there, leaving that commit with only the
file deletions and CMakeLists still referencing them):
- First-run handoff info toast at the completion point.
- Daemon-updater "restart pending" reminder (Settings → Daemon Binary) + the
  Done screen's warning-level "restart required" headline.
- Debug restart-note raised to a readable color with the restart prompt beneath
  it; DEBUG OPTIONS header shows a lock + explanatory caption (auth gate
  unchanged); a few hardcoded literals → material tokens / dpiScale.
- Drop the legacy settings_window from the build (CMakeLists + the dev-only
  references) to match the file deletions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:36:15 -05:00
DanSandClaude Opus 4.8 a43ac0156f feat(chat): make the key-verify lock clickable with a fingerprint dialog
The header key-verify lock was an unexplained hover-only icon. It's now
clickable, opening a dialog that shows the contact's identity-key fingerprint
and explains comparing it out-of-band, with a Copy-key button and a clearer
tooltip. The crypto/verification logic is unchanged (the wording is careful that
the lock means "encrypted to this key", since there is no trusted/verified flag).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:35:32 -05:00
DanSandClaude Opus 4.8 534120d3de feat(peers): success toasts for ban/unban/clear + confirm the bulk Clear-All
Peer ban actions only toasted on failure; success was silent. Added success
toasts for ban/unban/clear-all, and the bulk "Clear All Bans" now takes a
confirming second click (per-row unban stays one-click). The setban/clearbanned
RPC calls are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:35:32 -05:00
DanSandClaude Opus 4.8 a90aa29478 feat(ux): wizard handoff note, daemon restart reminder, settings polish; drop legacy settings_window
- First-run handoff: one info toast ("node starting, will keep syncing in the
  background") at the completion point, instead of dropping the user onto a busy
  blank window with no explanation.
- Daemon updater: a newer installed binary now sets a persistent "restart
  pending" reminder (surfaced in Settings → Daemon Binary with a Restart button)
  and the updater's Done screen leads with a warning-level "restart required"
  headline — easy to miss before.
- Settings: the debug-categories "restart required" note is raised from ~35%
  alpha to a readable color with the restart prompt moved directly beneath it;
  the DEBUG OPTIONS header shows a lock + explanatory caption so it's clearly a
  gated diagnostics section (the auth gate itself is unchanged); a couple of
  hardcoded color/size literals now use material tokens / dpiScale.
- Removed the legacy duplicate settings_window.cpp/.h (only reachable via a dev
  flag, never in production) so edits can't silently miss one of two settings UIs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:35:32 -05:00
DanSandClaude Opus 4.8 a30a083df0 feat(security): persist unlock lockout across restarts; keep unlock errors visible
- The failed-unlock error message now stays on screen until the next attempt /
  mode switch / success, instead of fading after ~3s (the reason was often gone
  before it could be read).
- The escalating failed-attempt lockout counter + expiry are now persisted
  (keyed to the active wallet file) and restored once per lock-screen session,
  so killing and relaunching the app can no longer reset the lockout curve — a
  brute-force window. A forward clock change is capped; the PIN/passphrase is
  never stored. The verification (walletpassphrase/decrypt) path is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:20:32 -05:00
DanSandClaude Opus 4.8 89d3975eb3 feat(migration): second backup acknowledgement + confirm Discard and Sweep
Migrate-to-seed is irreversible and fund-moving, so its gates are strengthened
(UI only — the sweep/adopt/wipe logic, z_mergetoaddress args, amounts and
destination are byte-identical):
- Continue now requires a second, consequence-stating acknowledgement checkbox
  (funds move and are recoverable only via the seed phrase), not just "I wrote
  it down".
- Discard, Sweep-all and Sweep-remaining each take a second confirming click
  (stating the amount being moved) instead of firing on one click; the confirm
  body runs verbatim only after confirmation, and arming never survives a
  close/reopen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:20:32 -05:00
DanSandClaude Opus 4.8 3f1838cfdd feat(chat): confirm before exporting decrypted chat history to plaintext
The chat header's export button wrote a decrypted plain-text file immediately on
click, warning only via a hover tooltip. It now opens a confirm dialog that
states the file is unencrypted and shows the destination path before writing.
The export format, path and write call are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:04:58 -05:00
DanSandClaude Opus 4.8 dcb8e6eb25 feat(mining): persistent payout error, solo-address explainer, i18n cleanup
- The pool payout-address rule is no longer hover-only: an invalid payout
  address now outlines the field in red and shows a short reason below it (a row
  is reserved so it doesn't overlap the card beneath). The longer tooltip stays.
- The solo "Mining Addr" row gains a hover explainer and a "None yet" empty-state
  (with a tooltip pointing to Receive → New mining address) instead of a blank.
- Replaced hardcoded English in the earnings/stats cards ("(%d txn)", uptime
  "N/A"/"%lldm %llds"/"%lldh %lldm") with TR() keys.

Also carries the chat-export confirm strings used by the next commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 23:04:58 -05:00
DanSandClaude Opus 4.8 3f127f9b62 feat(request,balance): Request-Payment create-address empty-state + tx empty states
- Request-Payment no longer dead-ends with no shielded address: it shows a hint
  and a "Create a shielded address" button (auto-selecting the new address), and
  labels address entries with their contact/wallet names.
- The balance "Recent transactions" empty area uses a proper empty-state and now
  distinguishes "still loading / syncing" from "no transactions yet" instead of a
  dim one-liner that also showed mid-sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 22:50:30 -05:00
DanSandClaude Opus 4.8 f2d9590058 feat(send): usable-while-syncing form, calmer validation, honest clear/memo/source
Send-tab UX pass (presentation/validation only — the z_sendmany submit path, its
amounts, fee, recipients and memo bytes are unchanged, and the Review button plus
the confirm-time live re-check still block any send until the wallet is synced):
- The form is now editable while syncing (so the recipient/amount/memo can be
  pre-filled); only Review/Send stays gated, with an "available once synced" note.
- The recipient's red "Invalid address" no longer flashes from the first
  keystroke — it waits until the field loses focus.
- The memo field stays visible (text preserved) with a "requires a shielded
  recipient" explainer for transparent recipients, instead of vanishing + silently
  dropping what was typed.
- "Cancel" (which only cleared the form) is relabeled "Clear form", disabled when
  empty, and drops its redundant confirm (the timed Undo already covers it).
- The source picker shows address labels, and warns when the chosen source is
  transparent (non-private on this chain).

Also adds the string keys used here and by the next commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 22:50:30 -05:00
DanSandClaude Opus 4.8 ee6e99965c feat(balance,notifications): shielded-card tooltip + clearer toast severity
- The Shielded balance card gains the explanatory hover tooltip the mining card
  already has (suppressed while the pending-chip tooltip is showing).
- Toasts get a severity-tinted background fill and a slightly thicker accent
  border, so Success/Warning/Error/Info differ by more than hue alone (the
  per-severity leading icon already existed). Tint opacity is a ui.toml token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 18:36:55 -05:00
DanSandClaude Opus 4.8 b3e09a3314 feat(onboarding): friendlier wizard bootstrap + Done screen starts the daemon
First-run bootstrap polish:
- Failed card wraps the (often long) error instead of clipping it, and offers a
  "Try mirror" button; Retry now keeps whichever source (main/mirror) was used.
- Bootstrap offer shows an honest size/time hint and relabels "Mirror" →
  "Use mirror" with a clearer tooltip.
- Daemon status dot/label is keyed off the EmbeddedDaemon::State enum instead of
  matching the English substring "Stopping" (which broke in every other
  language), and the faint ~55%-alpha label is raised to ~86%.
- Bootstrap Done screen: for the embedded daemon it now offers "Start Daemon"
  (and says the node will start) instead of telling the user to start it by hand.

Also carries the new string-table keys for these (and the shielded-card tooltip
used by the next commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 18:36:55 -05:00
DanSandClaude Opus 4.8 2fdf33f99b feat(chat): "Request" pill on unsolicited conversations in the list
The conversation list now marks a pending stranger request with a small
"Request" pill (bottom-right of the row), so it's distinguishable before you
open it; the preview text is clipped to the pill's left so they don't overlap.
Completes the request-affordance pair with the in-thread Accept/Block card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 18:27:44 -05:00
DanSandClaude Opus 4.8 092a802959 feat(balance): pending chip in the Donut, Shield and Timeline layouts too
Extends the labeled "Pending +X" chip (with its awaiting-confirmations tooltip)
to three more layouts via a shared DrawPendingChip() helper. It draws on the
ticker line (the USD value sits on the line below in these layouts, so there's no
overlap) and is skipped if it would overflow the card. The remaining draw-list
layouts with right-aligned content (Consolidated, Minimal, the grid variants)
are left for a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 18:27:44 -05:00
DanSandClaude Opus 4.8 e9eeaf00c3 feat(chat): Accept/Block card for unsolicited contact requests
An unsolicited first message from a stranger (an incoming ContactRequest, with
nothing sent by us and the peer not in contacts) now shows an Accept / Block
card above the thread instead of looking like a normal conversation. Accept adds
the peer to the address book (which clears the request state); Block deletes and
blocks the conversation (reversible from the Blocked manager). ConvSummary gains
a derived pendingRequest flag. (The list-row "Request" pill and a header Block
button were left out to avoid the fragile hand-drawn header-toolbar geometry.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 13:12:57 -05:00
DanSandClaude Opus 4.8 572b98cbcf feat(shield): pick the mining address from a dropdown + show a confirm preview
Shield-coinbase replaced its free-text "*"-wildcard address box with a dropdown
of the wallet's mining (transparent) addresses and their balances ("All mining
addresses" default), and the confirm step now previews "Shield up to X DRGX from
N mining address(es)… to <label>? Network fee: Y" instead of only relabeling the
button. Also blocks submitting from an empty source or one with no funds. The
z_shieldcoinbase call (args/fee/limit/destination) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 13:09:41 -05:00
DanSandClaude Opus 4.8 bcfc324426 feat(balance): labeled "Pending +X" chip with tooltip in more layouts
The unconfirmed balance was shown in only one layout as an unlabeled "+X" that
read as a mystery. It's now a labeled "Pending +X" chip with an "awaiting
confirmations" tooltip — the Classic card's badge is relabeled, and the shared
compact hero (Dashboard + Two-row layouts) now draws it too. (The remaining
hand-drawn layouts can reuse the same pattern as a follow-up.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 13:07:43 -05:00
DanSandClaude Opus 4.8 7cea9b7339 feat(bootstrap,validate): Retry keeps the mirror; Validate works offline
- Bootstrap: Retry now reuses the source the user actually started (was always
  the default), and a "Try mirror" button offers the alternate server on
  failure (s_lastUrl remembered on start, cleared on a fresh Confirm).
- Validate Address: classification + checksum are local, so the dialog no
  longer requires a node connection — offline/lite now validate and show the
  type, with ownership reported as "Unknown — node offline / not checkable in
  the lite wallet" instead of a dead "not connected" screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 13:05:40 -05:00
DanSandClaude Opus 4.8 f3f0b32a48 feat(ux): robust sync ETA, persist Explorer settings, gate export-all out of lite
- Sync ETA: a new pure util::SyncRateEstimator (App-owned) replaces the
  function-local statics that went stale after a rescan/restart (producing the
  "412h" ETA). It self-resets on a height drop and on connect/disconnect/rescan/
  restart (via an atomic flag, since onConnected can run off-thread), formats
  with days, and uses the Warning() token.
- Settings: the Explorer-tab URL fields + custom-fees/fetch-prices checkboxes
  now persist on edit — they were the only controls still relying on the
  About-only Save button that a user on this tab never sees.
- Export-all-keys: hidden in the lite build (which has its own key export),
  instead of dead-ending on "Not connected to the daemon".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 13:02:46 -05:00
DanSandClaude Opus 4.8 8fe20f6385 feat(mining): persistent start/stop error caption + timeout-aware pool status
- A mining start/stop/pool failure now sets a persistent App::mining_last_error_
  (cleared on the next attempt), rendered as a red caption with "Press Mine to
  try again" below the Mine button (plus a Dismiss, and a Windows-Defender
  "How to fix" on an AV block), so a crashed/blocked/failed miner is no longer
  just a button that "did nothing" once its toast faded. Two more hardcoded
  English strings (pool-payout-invalid, AV-blocked) moved to i18n.
- Pool connection status is now a persistent "Status" row in the pool stats
  card (PoolMiningState.running_since / last_error): it flips from "Connecting…"
  to "Can't reach pool — check the URL" after a 30s timeout instead of sitting
  on red forever, and no longer vanishes once logs arrive. The old transient
  banner in mining_tab.cpp was removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 12:09:49 -05:00
DanSandClaude Opus 4.8 15c1181aef feat(shield): colour shield/merge status by outcome + spinner; friendly errors
The shield/merge status line was amber only while in-flight and uncoloured for
every terminal state (success, failure, uncertain). It now tracks an OpOutcome
(display-only) and colours the terminal line Success/Error/Warning accordingly,
shows a LoadingDots spinner while the 120s scan runs, and maps the raw daemon
error through friendlyRpcError. The actual shield/merge submit logic is
unchanged (no "Try again" that could double-submit a fund move).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 11:55:40 -05:00
DanSandClaude Opus 4.8 0101cc707c feat(network): a clear auth-failed node banner with a fix-it action
When the node rejects the wallet's RPC login (bad rpcuser/rpcpassword), the
status bar showed developer jargon ("Auth failed — check rpcuser/rpcpassword")
and the only action was Reconnect, which just repeats the failure. Now a
dedicated AuthFailed banner reason surfaces "Wallet can't sign in to the node"
with a plain-language detail and a real fix: Restart node for the embedded
daemon (regenerates its cookie creds) or Open config folder for an external one
(to fix DRAGONX.conf). Driven by a new rpc_auth_failed_ flag (set on the auth
path, cleared on each connect attempt), with evaluator unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-18 11:53:29 -05:00