Commit Graph
1188 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 16d60a72f2 feat(wallet): one-click "switch to the built-in node and compact"
When connected to an external/standalone dragonxd (the wallet adopted a running
node, so daemon_controller_ is null), Compact couldn't run. Add a path that does
it in one click: compactViaEmbeddedSwitch() gates the connect loop, drops the RPC
connection, and on the worker gracefully stops the external node (RPC "stop" with
our credentials, via stopDaemonForWalletSwitch) and waits for the port; back on
the main thread it switches the wallet to its built-in node and starts it with a
one-shot compact_on_embedded_start_ flag that arms -salvagewallet, so the fresh
node rewrites wallet.dat (and rescans). On stop failure nothing is changed and
the external node stays up.

The Compact button is now enabled whenever the build can run the embedded node;
its confirmation branches — direct compact when the wallet already runs the
built-in node (canCompactWallet), else a clear message (confirm_compact_switch_
msg) that it will stop the external node, switch to the built-in one, and that
this changes which node binary runs. New strings confirm_compact_switch_msg /
compact_switching / compact_switch_stop_failed.

Mirrors restartDaemon's disconnect + background stop pattern; controller creation
stays on the main thread. Can't be runtime-verified here (no live daemon). Build
+ tests + hygiene green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-27 11:41:49 -05:00
DanSandClaude Opus 4.8 a5cd211e89 fix(wallet): correct Compact's behaviour with an external daemon
Compact rewrites wallet.dat by restarting the node with -salvagewallet, which the
wallet can only do for the EMBEDDED node it manages. When the wallet is connected
to a dragonxd the user started themselves (use-embedded off, or it attached to an
already-running node), daemon_controller_ is null — and compactWallet() fired the
misleading "unavailable in lite build" warning even on a full-node wallet.

Now: a precise App::canCompactWallet() (full-node AND embedded AND a live
controller) disables the Compact button for an external/standalone daemon with a
tooltip, and compactWallet() refuses with an accurate message (compact_needs_
embedded) that tells the user to enable "Use embedded dragonxd" or restart their
dragonxd with -salvagewallet themselves. The lite-build path keeps its own
message. New string compact_needs_embedded (English; 8 locales fall back).

Build + tests + hygiene green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-27 10:55:46 -05:00
DanSandClaude Opus 4.8 0ebd1ac5d5 feat(wallet): defer + retry a merge that hits a transient mempool conflict
A consolidation batch can be rejected by the daemon with
bad-txns-shielded-requirements-not-met (REJECT_DUPLICATE) when it clashes with an
earlier, still-unconfirmed batch — the batch outran its predecessors into the
mempool. That isn't a real failure: once the earlier batches confirm, the same
merge succeeds. Previously it failed the whole address.

Now a merge OP failure whose message is a mempool conflict (shielded- /
joinsplit-requirements-not-met, txn-mempool-conflict, inputs-spent — new pure
helper isDeferrableMergeOpError) HOLDS the address for kRunMempoolClearSec (2 min)
instead of failing it, then retries from where it left off; up to
kRunMaxDeferRetries consecutive defers (reset on any success), after which it is a
genuine failure. Deferrals don't feed the halt breaker, and since the address
outcome stays non-failed the run verdict reads Clean when the retry succeeds.
applyOpState takes `now` to stamp the hold; next() waits on a deferred address
until its window passes.

Unit-tested (testConsolidationDefer): the classifier; and a driven run where an
op reports the conflict → the address is deferred (not failed), held before the
window and retried after it. Build + tests + hygiene green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 18:26:28 -05:00
DanSandClaude Opus 4.8 94d41e2932 i18n(wallet): translate the Compact action + reworded consolidation strings
Translate the 7 new Compact strings (wallet_size_compact, tt_wallet_size_compact,
compact_started, confirm_compact_wallet_{title,msg,safe}, compact_wallet_confirm)
and re-translate the 4 corrected consolidation strings (consolidate_desc,
wallet_size_warn, wallet_size_auto_info, tt_wallet_size_warn) into de, es, fr, ja,
ko, pt, ru, zh (res/lang/*.json, 2512 → 2519 keys each), grounded per file and
keeping wallet.dat / Berkeley DB as-is. Rebuilt the CJK subset font
(2123 → 2127 glyphs) for the five new glyphs (肥 領 + ko 써 압 켤).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-26 17:29:33 -05:00
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