Commit Graph
1033 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 32ea4d0524 fix(mining): keep the thread benchmark running while the tab is hidden
The thread benchmark state machine was advanced from the Mining tab's
render path, so it only progressed while that tab was visible and the
window was on screen. Hiding the tab or minimizing the window froze the
benchmark mid-sweep.

- The benchmark now advances from the main update loop, ungated by
  connectivity, so it keeps running when the tab is hidden or the window
  is minimized.
- Its clock is driven from a steady wall-clock reading rather than the
  render delta, and each step is clamped like the other logic timers so
  a suspend/resume cannot leap a benchmark phase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 16:00:10 -05:00
DanSandClaude Opus 4.8 fae5ef05b9 fix(mining): restore the miner after a benchmark only if it was running
Cancelling a thread benchmark always stopped and restarted the pool
miner at its pre-benchmark thread count, even when the user had not been
mining before the benchmark began. That silently started the miner on a
plain cancel or a switch to solo mode.

- A cancel now always drops the candidate-thread miner, and restarts the
  pool miner only when the user was mining before the benchmark, read
  from a snapshot taken at the start rather than the live running state
  (the benchmark sweep transiently stops the miner between candidates,
  so a live check would drop the restore).
- Callers that must not restore the miner (leaving solo mode) pass the
  new restore flag as false.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 16:00:10 -05:00
DanSandClaude Opus 4.8 13989cf268 fix(notifications): prune expired toasts and skip invisible draws
The status-bar notifications only expired the front of the queue, so an
expired newer toast could sit in front of a still-live older one and
hide it from view. The liveness check that drives continuous repaints
also disagreed with what render() actually showed.

- Expired toasts are now pruned wherever they sit in the queue, not just
  at the front, so the newest remaining toast is always a live one.
- The visible toast is the newest live entry, skipping an expired newer
  one. The liveness check reports active while a toast is visible plus
  one trailing frame, so the final faded-out state is painted once and
  then repaints stop; it self-expires if render() is skipped.

Tests: pruning removes expired entries from anywhere in the queue, the
visible toast skips an expired newest entry, and liveness tracks it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 c5b325a752 perf(charts): fill chart areas in one linear pass
The filled sparklines and the market price chart built their area fill
with PathFillConcave, which runs a per-scanline pass over the whole
filled region. For a simple area under an x-monotone curve that work is
unnecessary.

- A new helper fills the area between an x-monotone polyline and a
  horizontal baseline directly as a triangle strip, in one pass over the
  points. The market sparklines, the market price chart and the mining
  hashrate sparkline all use it. The fill is flat and non-anti-aliased;
  the anti-aliased curve drawn on top hides the stepped top edge, and
  the faint side and bottom edges are imperceptible at these alphas.

Tests: the strip's vertex and index counts, index bounds, and that its
triangle area matches the trapezoid area under the polyline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 969718e03a perf(theme): drop the redundant acrylic grain under glass cards
Glass cards draw their own grain layer over the surface overlay, but the
acrylic primitive beneath them was also drawing its grain. That lower
layer sits under the overlay at well below one grey level, so it is
invisible and only costs an extra noise pass per card.

- The card now asks the acrylic primitive for zero grain and keeps its
  own visible grain layer, removing the redundant pass with no visible
  change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 7a9b29f358 fix(effects): stop retrying scroll-fade shader init after it fails
If the scroll-edge fade shader failed to compile or link, the init path
left its ready flag clear and tried again on the very next frame. On a
driver that rejects the shader that meant a fresh compile attempt every
frame, for the life of the program.

- A failed init is now latched and treated as final until destroy(), so
  the compile is attempted once, the failure is logged once, and the
  fades stay disabled without further retries. Destroying the shader
  clears the latch so a later re-init can try again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 968d6d3875 fix(effects): capture the acrylic backdrop at framebuffer size on HiDPI
The acrylic effect blits its backdrop from the default framebuffer using
the logical viewport size as the source rectangle. On a HiDPI display
the framebuffer is larger than the logical size, so the blur sampled
only the lower-left corner of the screen and the glass showed the wrong
region stretched across each panel.

- The capture blit source now uses the real framebuffer pixel size
  (logical size times ImGui's DisplayFramebufferScale, truncated the
  same way the GL backend does), while the capture and blur targets stay
  at logical size. The framebuffer size is tracked separately from the
  viewport size so quality or buffer-lifetime resets cannot lose it, and
  moving the window between a Retina and a non-Retina display redoes the
  capture. DX11 is unaffected; its scale is one.
- The debug screenshot sweep captures at framebuffer size too, so HiDPI
  captures are no longer cropped.

Tests: the framebuffer-extent helper matches the GL backend's
truncation and falls back to the logical size when the scale is unknown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 fdd95aa457 refactor(effects): drop the unused composite acrylic shader
The acrylic material carried a full composite shader — vertex and
fragment source, compile and link helpers, the program handle and seven
uniform locations — that nothing ever invoked. The acrylic is composited
on the ImGui draw list, not through this program.

- Remove the dead shader source, its compile/link helpers, the program
  and uniform fields, and the compile/link/cleanup steps in init and
  shutdown. No behaviour change; init no longer builds a program that is
  never bound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 75e0c5048e fix(theme): give the menu acrylic the card blur radius
Every panel class samples one shared blur texture, so a menu asking for
a different blur radius than cards could not actually get it. All it did
was force a second full-screen blur pass each frame as the radius
flipped between the card and menu values, and let whichever class drew
last decide the radius for everything.

- Setting the acrylic theme now copies the card blur radius onto the
  menu class, so menus and cards share one radius and the blur runs
  once per frame. The first-use default path goes through the same
  setter so it is covered too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 cd83038de5 perf(effects): cull off-screen card effects and sample border by arc
Glass cards scrolled out of view still ran their full effect path, and
the rainbow border always used a segment count derived from a fixed
per-pixel budget, so wide cards drew far more line segments than their
slowly changing gradient needed.

- A glass panel whose rectangle lies wholly outside the draw list's clip
  rect now returns early, so a scrolled-out card draws nothing (its
  border and ember passes are skipped along with the fill).
- The rainbow border is split into segments by perimeter arc length:
  straight edges get one segment per ~32 dp, corner arcs get a few
  chords each to stay round, and a fully transparent border is skipped
  outright. A wide card now draws tens of segments instead of a couple
  hundred, with no visible change.

Tests: the segment fractions are ascending and bounded, each corner arc
keeps at least three chords, and the rect-cull margin test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 fbc429f914 perf(effects): keep the theme animation clock in double precision
The theme effects stored their animation time in a 32-bit float taken
from the ImGui clock. After a few days of uptime a float can no longer
resolve a single frame step, so every oscillation, drift and sweep began
to stutter, snapping forward a frame at a time instead of moving
smoothly.

- The clock is now held as a double, and the phase/oscillator maths runs
  through a small set of shared helpers that reduce the argument in
  double before returning a float. A float clock at 30 days steps by a
  quarter second per ULP; the double clock stays frame-accurate.
- Hue cycling, the rainbow and gradient borders, shimmer, specular
  glare, glow pulse, edge trace, embers, the sandstorm and the viewport
  wash all use the shared helpers, so their timing no longer drifts with
  uptime.

Tests: the helpers advance smoothly after seven days, pin the legacy
float breakdown at 30 days, and stay in range for negative offsets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 944fafa1c7 perf(explorer): fetch each block with one RPC and back off on failure
Each recent block was fetched with two round trips (getblockhash then
getblock), and a height that failed, such as one briefly above the tip
during a reorg, was refetched on every frame.

- getblock is now called once with the height directly, falling back to
  the getblockhash plus getblock pair only when an older daemon answers
  "not found" for the short string; any other error is a real answer.
- A failed fetch backs off with a doubling wait (2 s up to 32 s) before
  any new fetch is scheduled, and a successful fetch resets the backoff.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 09:24:51 -05:00
DanSandClaude Opus 4.8 ebbeb846e8 feat(explorer): indexed block search with a bounded cache
The explorer block cache searched with a LIKE query that had to escape
wildcards, grew without any limit, and retried a failed open on every
frame, including a two-second busy wait on the UI thread.

- Search now uses an instr() substring test on a lowercased needle, and
  only checks the height text when the query is all digits, since a
  non-numeric needle can never appear inside a decimal height.
- The table is pruned to the highest 20000 blocks, once per open and
  periodically as blocks are stored, so the file stops growing at its
  high-water mark.
- A failed open is negative-cached (30 s by default) instead of retried
  every frame, and the busy timeout drops to 250 ms.
- When the cache database is unavailable or refuses a write, the block
  spills to a small bounded in-memory map that both the list and search
  read, so a spilled block never appears in one but not the other; the
  spill is cleared when the chain tip regresses.
- A batch of blocks is written under one savepoint instead of one commit
  per block.

Tests: case-insensitive hash and height search, the negative-cache and
spill paths with tip-regression eviction, and batch store plus prune.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 09:24:51 -05:00
DanSandClaude Opus 4.8 c3f981b52e fix(history): keep the open detail row across paging and filtering
The inline detail panel was tracked by the filtered list index. The
display list is rebuilt and re-sorted on every data change, sort switch
or filter edit, so a stored index pointed at a different transaction
after the rebuild, and a page reset was triggered by any change in the
row count, bouncing the view back to page 1.

- The open row is now tracked by a stable content key (txid, display
  type, address and the amount's exact bits) so the right row stays open
  across rebuilds.
- The page resets and the open row collapses only when a filter input
  changes (type, sort or the full search text). A new or removed row is
  data and now just clamps the current page.
- The open row is also cleared on page navigation and when the list
  empties, so a wallet switch does not resurrect an old expansion.

Adds pure helpers rowKey/filterInputsChanged to
transactions_tab_helpers.h.

Tests: rowKey distinguishes each field and separates +0.0 from -0.0, and
filterInputsChanged latches each input change but not the first frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 09:24:51 -05:00
DanSandClaude Opus 4.8 a5e4fd5c02 perf(history): group transaction rows by txid once per change
The History tab built a txid-to-indices map twice when its inputs
changed, once for the summary-card totals and once for the display list,
each a full pass over every transaction with string-keyed hashing.

- The rows are now grouped by txid a single time whenever the
  transaction list changes, into an index-only structure (first/next
  chains with no string copies), and both the summary pass and the
  display pass reuse it. A sort-combo change re-sorts without regrouping.

Adds a pure, unit-testable groupByTxid helper in
transactions_tab_helpers.h.

Tests: the chains match a reference map over a random id stream, and the
empty and single-row cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 09:24:51 -05:00
DanSandClaude Opus 4.8 01b59d24db perf(image-picker): cull off-screen thumbnail cells
The image picker laid out and drew every cell in the directory and image
grids, including the many cells scrolled out of view.

- A cell whose rectangle is not visible now reserves the same space with
  a dummy of the cell size, so the scroll extent is unchanged, and skips
  the hit-test and draw.

No visible change; only off-screen cells are skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 09:24:51 -05:00
DanSandClaude Opus 4.8 3ca7a5c90e perf(explorer): cull off-screen rows in the block detail list
The block detail modal drew every transaction row of a block, running
the hit-test, the draw and a hash truncation for each one, even when the
row sat far outside the scroll viewport.

- A row entirely outside the scroll clip now keeps its layout slot (so
  the scroll extent is unchanged) but skips the hit-test, draw and hash
  truncation. The expanded row is always laid out so its inline detail
  panel still appears.
- The old "showing first 100 of N" footer is dropped, since every row is
  now cheap to lay out regardless of how many a block has.

No visible change; only off-screen rows are skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 09:24:51 -05:00
DanSandClaude Opus 4.8 b8a4a11f90 fix(console): keep JSON folding active under channel filters
JSON folding in the console was bypassed in any filtered view, including
when only a channel toggle (daemon, RPC or app) was active. A collapsed
block silently expanded the moment a channel was toggled.

- Folding is now kept active unless the view is text-filtered or
  errors-only, which are flat so that every match stays reachable.
  Channel toggles keep folding, since JSON blocks are atomic per channel.
- The fold triangles and the collapsed " ... }" summary are drawn only
  in the fold-aware view, so the glyph and the rendered block can never
  disagree.

User-visible: JSON results stay foldable while channel filters are
applied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 08:23:30 -05:00
DanSandClaude Opus 4.8 c1b289cc27 perf(console): cull off-screen wrapped rows
A long wrapped console line could extend far past the viewport, yet
every sub-row was still measured and drawn.

- Sub-rows that fall entirely outside the clip rect are now skipped. The
  per-row ordinal still advances for a culled row, so scanline parity is
  unchanged.

No visible change; only off-screen rows are skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 08:23:30 -05:00
DanSandClaude Opus 4.8 f7ed1ae115 perf(console): tail-cap large RPC results without a full copy
A large RPC result was fully split into lines and copied before being
handed to the console, even though only the last few lines could survive
the model's line cap.

- The formatter now takes a line cap and keeps only the last N lines,
  located by a backward newline scan, so a huge result is never fully
  split or copied. JSON classification still keys off the first
  character of the whole result.
- It works over a string view and moves line text through, and fold
  spans are computed over the formatted lines directly instead of a
  separate vector of copies.
- getline semantics are preserved: a trailing newline adds no empty
  line and an empty result yields no lines.

Tests: getline parity for empty, trailing-newline and blank-line inputs,
the backward tail cap, and JSON classification and fold spans over a
capped result.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 08:23:30 -05:00
DanSandClaude Opus 4.8 116a997a3a fix(console): bound the pending queue by lines and bytes
The console's pending, not-yet-drained queue was unbounded, so a hidden
Console page that kept receiving verbose daemon-log chunks could grow
without limit.

- Pending lines are now bounded by both a line count and a byte budget.
  The oldest pending lines are dropped once either is exceeded, while
  the newest line is always kept so one huge line is never lost.
- A formatted RPC result is ingested as one block under a single lock,
  so lines from other producers can never interleave inside it.

The front-sequence accounting is also corrected: a pending line dropped
by the cap never entered the visible buffer, so it no longer advances
the front sequence. Only a front eviction or a clear does.

Tests: the line and byte caps keep the newest lines, a single oversized
line survives, a batch appends contiguously, and the front sequence
advances only on a front eviction or clear.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 08:23:30 -05:00
DanSandClaude Opus 4.8 70e96f75f0 perf(console): case-insensitive filtering without per-line copies
Console text filtering and match highlighting lowercased a full copy of
every line, and of every drawn segment, on each pass.

- A new alloc-free ASCII case-insensitive substring search folds both
  sides on the fly, so neither the filter test nor the highlight scan
  allocates a lowercased copy per line any more.
- Both call sites use it; the case-insensitive match behaviour is
  unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 08:23:30 -05:00
DanSandClaude Opus 4.8 6a24355c79 fix(console): release busy gate when the fast worker stops
When the daemon disconnects, teardown stops the fast RPC worker without
draining its queued main-thread callbacks. The console busy gate was
released inside that callback, so a command whose callback was discarded
left the input disabled for the rest of the session.

- The busy gate now drops from an RAII guard at the end of the worker
  job itself, so it is released however the job ends: success, error, or
  a worker torn down undrained.
- The result is published to the console before the gate drops, so a
  freed-up console always has the output queued.
- An unknown, non-std::exception failure is now caught and reported
  instead of escaping the job.

User-visible: the Console input can no longer stay stuck disabled after
a reconnect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 08:23:30 -05:00
DanSandClaude Opus 4.8 5376681af5 fix(schema): parse node-status banner and layout UI sections
The UI-schema loader walked a hardcoded list of flat TOML sections and
left [banners.*] and [layout] out, so those sections in ui.toml were
silently dropped. The node-status error strip at the top of the content
column could not be styled from the schema at all.

- The flat-section list is now one shared constant used by the file,
  string and overlay loaders, and it includes banners and layout.
- The node-status banner is themeable from ui.toml like every other
  section; a note in the toml records that these parse as flat sections.

No visible change unless ui.toml overrides those sections.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 08:23:30 -05:00
DanSandClaude Opus 4.8 8255923f33 fix(settings): scope portfolio groups by wallet identity
Market-tab portfolio groups were tied to a wallet by a hash of its
address set. That hash changed whenever an address was added, so a
group built for a wallet could silently drop out of view once the
wallet gained a new address.

- Groups are now scoped by the wallet's stable id, which is persisted
  and never derived from the address set, so adding an address can no
  longer orphan a group.
- An older build's address-hash scope fails open (the group stays
  visible) and is migrated once per wallet per session to the stable id.
  A legacy group is claimed when its old hash matches, when one of its
  own addresses belongs to this wallet, or when only one wallet is
  known; anything ambiguous is left legacy and stays visible.
- Scope filtering waits until the wallet's addresses are loaded, so a
  group never flashes an empty 0.0000 balance and Add stays disabled
  until the wallet is resolved. This works on Lite, which never stamps
  an address-update time.

Behaviour: an older build opened after this change hides groups tagged
with a stable "w:" scope (they are not deleted and return on the next
upgrade) - worth a release note.

Tests: group visibility across global, stable and legacy scopes with the
wallet resolved and not; the migration rules (hash match, address
overlap, single-wallet claim, ambiguous left alone, idempotent); and a
save/load round trip where an address-set change cannot orphan the scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 21:42:23 -05:00
DanSandClaude Opus 4.8 c3ffc97b31 fix(ui): finish a stranded bootstrap even when its dialog is closed
A Settings bootstrap was only wrapped up inside the dialog's own draw
path. If the dialog finished downloading, verifying or extracting while
the user had clicked away or minimized the window, completion was never
processed: the connect and reconcile gate stayed held and the
post-bootstrap rescan was never armed.

- Completion is now a pure decision driven from a pump() that both the
  per-frame update and the dialog's render call, so a finished bootstrap
  is handled every tick whether or not the dialog is drawn.
- With the dialog open the behaviour is unchanged: it advances to the
  Done or Failed screen and those screens still own the gate. With the
  dialog closed, pump() releases the gate itself and shows a success or
  failure toast; a user-cancelled run is released silently.

User-visible: a bootstrap started from Settings now completes, arms its
rescan and reports its result even if the dialog is closed or the window
is minimized when it finishes.

Tests: the completion decision for an unfinished worker, a completed run
with the dialog open and closed, a failed run, and a user-cancelled run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 21:42:23 -05:00
DanSandClaude Opus 4.8 d4339be94b fix(ui): keep a chosen send-from address from auto-select overwrite
The Send tab auto-selected the highest-balance address on first load.
When another tab had already pre-filled the source with "Send from this
address", that first-load pick overwrote the user's choice, and the
dropdown's selected index could drift out of step with the address that
would actually be debited when the address list changed.

- The send-from string is now the single source of truth. The one-shot
  auto-pick only fills an empty source and never replaces one the user
  or another tab set, and the list index is re-derived from the string
  each frame so it cannot desync.
- An explicit "Send from this address" choice latches the auto-pick off,
  so the first visit cannot undo it.
- A chosen source the dropdown does not list (zero balance, view-only,
  or the list still loading) now previews the address that will be
  debited instead of the "Select a source" placeholder.

User-visible: a send-from address chosen from another tab survives the
first load and later refresh ticks.

Tests: the source resolver keeps a pre-fill on first visit, fills an
empty source with the best spendable address, keeps a source missing
from a short list with index -1, latches nothing while disconnected or
with an empty list, and re-derives a stale index from the string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 21:42:23 -05:00
DanSandClaude Opus 4.8 579accd0c6 fix(ui): validate shielded addresses with the correct RPC
The Validate address dialog chose its RPC from the first character of
the input, then asked the transparent-only validateaddress for every
case. That RPC is Base58-only, so a valid Sapling (z) address came back
isvalid=false and was reported invalid.

- The input is now classified with the checksum-verified recognizers,
  and a shielded address is sent to z_validateaddress while a
  transparent (mining) address keeps validateaddress.
- Anything that is not an address - a typo, garbage, or a pasted
  viewing/spending key - is judged invalid on the spot and never sent to
  the node, so a key pasted into the field is not leaked to an RPC.
- On Lite there is no node to ask, so the dialog shows the checksum
  verdict alone and hides the "owned by this wallet" row.

Tests: validateRpcMethod maps shielded to z_validateaddress, transparent
to validateaddress, and a key or empty string to no RPC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 21:42:23 -05:00
DanSandClaude Opus 4.8 abc82136c4 fix(util): use the POSIX daemon binary name, scan versions off-thread
The installed-daemon lookup built the binary path from the Windows
resource key "dragonxd.exe", which never exists on Linux or macOS, so
the installed full node was never found there. The version scan also
read the whole binary, now around 200 MB, into memory on the UI thread.

- A new DAEMON_BINARY_NAME gives the daemon's file name on this platform
  ("dragonxd" on POSIX, "dragonxd.exe" on Windows); the old resource
  constant stays only as the embedded-table key. The installed daemon is
  now found on POSIX.
- The version-stamp scan streams the file in 4 MiB chunks with bounded
  memory, carrying bytes across each boundary so a stamp straddling it is
  neither cut short nor missed, and stops at the first match. The result
  is cached by path, size and mtime, so only a changed binary is
  rescanned.
- Settings stats the binary on the UI thread but runs the streamed scan
  on a worker, guarded so a new scan is not started while one is still
  running (the future blocks in its destructor), and the version fills
  in when it lands.

Tests: the POSIX path and binary name, a version stamp in a small file,
a stamp straddling the 4 MiB chunk boundary, and a file with no stamp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 21:42:23 -05:00
DanSandClaude Opus 4.8 21dc874988 fix(core): refresh transaction history on the Mining tab
The Mining tab shows earnings derived from transaction history, but it
was not treated as a wallet-data page, so newly mined or paid-out
earnings did not appear until the user left the tab and came back. A
coinbase row also changes category in place (immature becomes generate
at maturity, or orphan on a reorg), and the Recent merge kept both the
old and the new row, double-counting the earnings.

- The Mining tab now gets the cheap between-blocks Recent poll: one
  listtransactions per new block, bounded by the 15 s transaction-age
  timer, once history has loaded. It never runs a full routine scan and
  never pre-empts an owed dirty cycle; tracked sends still take their
  normal cycle on any page.
- The shielded-payout probe runs only while the pool miner is running,
  since pool payouts are shielded receives while a solo miner's earnings
  are transparent coinbase rows.
- The Recent merge drops the previous row of the other coinbase category
  when a coinbase transitions, so maturing or reorged rewards are no
  longer counted twice. A non-coinbase row sharing the txid is left
  alone.

Behaviour change: mining earnings update on the Mining tab per block
instead of only on tab re-entry, and maturing or reorged coinbase rows
no longer double-count.

Tests: the per-block poll gate (one poll per block, due only after
history loads, held while dirty or addresses are dirty), the Mining-page
cycle shaping, and the coinbase transition merge for immature to
generate, generate to orphan, and a shared-txid non-coinbase row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 21:42:23 -05:00
DanSandClaude Opus 4.8 fbe09c6a4c fix(security): follow the daemon's lock state on Set-PIN and expiry
Setting a PIN always unlocked the wallet with a 5 s probe and then
re-locked it, even when the wallet was already unlocked, so a PIN check
silently re-locked an unlocked wallet. And once the daemon's absolute
unlock window ran out the GUI still showed "unlocked", so the next send
failed with nothing on screen explaining it.

- The Set-PIN check now decides on the main thread: an unlocked wallet
  is verified with the time it has left (capped at seven days) and is
  not re-locked, so the GUI and the daemon keep agreeing; a locked
  wallet still uses the 5 s probe and re-locks. Either way the daemon's
  lock state is re-read afterwards, which self-heals any drift.
- When the daemon's unlock window has expired but the GUI still says
  "unlocked", a bounded reconcile (once per expiry, 30 s back-off)
  re-reads getwalletinfo so the lock screen and the transaction-cache
  lock follow the daemon. Lite wallets, which have no daemon timer, are
  left alone.

Behaviour change: with auto-lock disabled the daemon still re-locks at
its window, and the GUI now lands on the lock screen instead of
appearing unlocked until a send fails.

Tests: the Set-PIN verify plan (unlocked keeps its remaining time and
skips the re-lock, locked or near-expiry falls back to the 5 s probe and
re-lock, long windows are capped) and the unlock-expiry predicate across
encrypted, locked, no-deadline and not-yet-expired cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 21:42:23 -05:00
DanSandClaude Opus 4.8 2fce9a7860 fix(security): reconcile the encryption-pending flag on every connect
The wizard persists an "encryption requested" flag so a quit or crash
before the wallet is actually encrypted is not silent. That flag was
only reconciled on the slow-path getwalletinfo refresh, so it could be
left stale, and the clear-and-warn logic lived inline in two places.

- A single reconcileEncryptionPending now runs on every connect and on
  every encryption-state refresh, keyed on the parsed getwalletinfo
  reply. It clears the flag whenever the wallet is encrypted, however it
  got there (deferred request, Settings, another client), and warns once
  per session when encryption was requested but the wallet is not
  encrypted and nothing is pending or in flight.
- A failed or warm-up (-28) RPC decides nothing, so a 15 s timeout that
  forces encrypted=false can no longer mis-clear or mis-warn.
- The flag is also cleared at encryptwallet success and when the user
  removes encryption, so neither path leaves a stale flag that would
  warn on the next connect. No settings format change.

Tests: the pure decision helper over the flag, RPC-ok, encrypted,
deferred-pending, in-flight and already-warned combinations (clear when
encrypted, warn once when lost, nothing on a failed RPC or no flag).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 21:42:23 -05:00
DanSandClaude Opus 4.8 0aaf33ecf0 fix(core): drive auto-lock idle from real input, not widget focus
The auto-lock idle clock was reset whenever any ImGui item was active,
so a text field left focused (a half-filled send form, say) counted as
continuous activity and kept the wallet unlocked indefinitely, well past
the configured idle timeout.

- The idle clock is now fed by real input events (key, text, mouse,
  touch, pen, gamepad button) as they are ingested on the main thread,
  plus a still-held mouse button so a drag or scrollbar grab that emits
  no new event still counts. A focused but untouched widget no longer
  does.
- A new header-only IdleTracker owns the clock and never moves backwards,
  so an event stamped before the previous one cannot shorten the idle
  time.

Behaviour change: a form left focused and idle past the timeout now
auto-locks. The form's buffers are app state and survive the lock and
unlock, so nothing typed is lost.

Tests: a new IdleTracker case covers idle time growing with no events
(enough to trip a 900s lock), activity resetting it, the clock never
moving backwards, and a query before the last activity reading zero
rather than a negative value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 21:42:23 -05:00
DanSandClaude Opus 4.8 1ea584311e fix(lite): honor the backend sync result with retry and a failed state
A failed backend sync was silently swallowed: the reply was never
inspected, so a sync that errored out left the wallet showing a
"syncing" state that never completed and never reported a problem.

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 21:42:23 -05:00
DanSandClaude Opus 4.8 3f7af2d028 fix(lite-backend): fix t-scan rewind underflow and retry the owed scan
When a sync pass's transparent (t-address) scan failed, the backend
mishandled it three ways: the retry computed its start height as the
block height minus the batch size, an unsigned subtraction that wrapped
to a huge value for low heights; the block range left un-scanned was
not remembered, so a transient failure left a permanent hole in the
transparent history that nothing ever revisited; and the block times
needed to date the transactions in that range were discarded.

- The owed range and the block times seen so far are now kept and folded
  into the next pass, which resumes from the lower of its own first block
  and any owed range.
- The scan start never subtracts, so it can no longer underflow.
- A t-address scan owed by an earlier failed pass is paid on the next
  sync even when no new block is pending.

Behaviour change: a transparent scan that fails is retried in-process on
the next sync rather than being lost. The owed range lives only in
memory and is never written to the wallet file, so the file format is
unchanged and remains interchangeable with other builds. The debt is
lost if the process exits or the server is switched before a retry
completes; Re-download blocks (rescan) recovers it, as today.

Tests: library tests on the extracted t-scan seam cover a failed pass
keeping its range and block times, a later success clearing the debt,
and the scan start never exceeding the pass start across the batch-size
boundary (the old formula wrapped there).

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 21:42:22 -05:00
DanSandClaude Opus 5.5 e7f084e3eb fix(bootstrap): harden archive names, the cancel window and quit wait
The bootstrap preflight's protected names could be bypassed by names
that resolve onto them, and its cancel and quit paths had two timing
gaps.

- Entry names are judged as the file system resolves them: '\' is a
  separator, "." components are dropped and each component loses its
  trailing dots and spaces, as Win32 does ("./wallet.dat" and
  "DRAGONX.conf." name protected files). The whole archive is refused
  for an absolute or drive-rooted name, a ".." component, any ':'
  (drive letter or NTFS alternate data stream, "DRAGONX.conf::$DATA"),
  an 8.3 short name ("DRAGON~1.CON"), a component of only dots or
  spaces (other than "."), and any byte >= 0x80 or ASCII control
  character: macOS folds or ignores some Unicode (U+017F, U+212A,
  U+200C) onto a protected name, and the published bootstrap archive
  uses plain ASCII names. A refused name is echoed
  with \xNN escapes. A member is written at the path it was checked
  under, and the protected-name check runs on the resolved name.
- The user's Cancel (Settings dialog and first-run wizard) goes through
  requestCancel(). It is refused once the old chain data starts being
  replaced, and the start of that window re-checks a cancel under the
  same lock, so a click lands before the old chain is touched or not at
  all.
- Quit cancels the wizard's and the Settings bootstrap when the
  shutdown starts; the HEAD and checksum requests now end at once on
  cancel, and a cancelled HEAD keeps the partial download. A run
  cancelled before the window leaves the old chain as it was. A run
  inside it stops extracting and removes its partial chain while the
  shutdown screen shows "Cleaning up...", so it ends with no chain data
  (wallet.dat untouched; Retry reuses the zip, or the node resyncs).
  That wait is bounded: at most 2 minutes from the start of the
  shutdown, then 3 s in shutdown(), after which a worker still running
  is left to the process exit. Only a cleanup that overruns that cap
  can leave a partial chain, which dragonxd then answers with its
  reindex offer.

Tests: refusal of the Windows-resolved, non-ASCII and control-character
names with the escaped echo; the protected-name list incl. the resolved
forms and a planted -wallet file, conf and database/ that stay
byte-identical; the user-cancel window; the bounded quit wait and
safeToAbandon(); a cancel ends a HEAD request that never answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00
DanSandClaude Opus 5.5 a5185737e6 refactor(app): retire logo textures through the deferred-destroy queue
ensureLogoTexture() still freed the chat emoji, header logo and coin logo
textures it replaced at once, while every other theme-driven replacement
already goes through retireTexture() and is freed at the next preFrame().
It runs at the top of render(), so nothing referenced them yet; this is
hygiene, so that the same rule covers every replaced texture.

- ensureLogoTexture(): the three direct DestroyTexture() calls become
  retireTexture(). Cost: the old textures live one extra frame on a theme
  or accent change.
- setGradientTexture() (unused today) retires the texture it replaces
  instead of dropping it.
- No drain at shutdown: the process exits and the live logo and gradient
  textures are not freed at exit either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00
DanSandClaude Opus 5.5 a581f42a33 fix(settings): disable the unimplemented Tor option and tell its users
The "Use Tor" checkbox was saved but never read: no build routed any
traffic (node, RPC, price/update/bootstrap/pool HTTP or Lite gRPC)
through Tor, so users who ticked it believed they were anonymous.

- The checkbox is disabled and always shown unticked, on the Settings
  page and in the legacy Settings window. Its tooltip says it is not
  available and gives the alternatives: for the node, proxy=127.0.0.1:9050
  in DRAGONX.conf with Tor running; otherwise a system-wide VPN/Tor (Lite
  builds get the short version). The "Use Tor" label is kept.
- Behaviour change: a stored use_tor=true is cleared once at startup
  (Settings::consumeLegacyUseTor + a synchronous save before the async
  writer and any Settings page state), with a 20 s warning in the user's
  language. The cleared key is the "already told" flag, so the notice
  never repeats. No settings format change.
- 3 new strings (tt_tor_unavailable, tt_tor_unavailable_lite,
  tor_never_applied_notice) in all 8 languages; the tooltip wraps at
  tor-tooltip-wrap-width from ui.toml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00
DanSandClaude Opus 5.5 ae66b54694 fix(miner): keep an updated miner, don't re-extract the bundled one
Builds that embed xmrig (Windows release) re-extracted the bundled miner
whenever its size differed from the file on disk. An "Update miner..."
install always differs, so the next start silently put the bundled build
back while settings still showed the updated version.

- The bundled miner is now placed only when it is missing, like the node
  binaries (kXmrigRefreshOnSizeMismatch = false in the provision table).
  Params and asmap still follow the bundled size.
- Behaviour change: a wallet upgrade no longer refreshes a miner the user
  never updated; "Update miner..." is the way to get a newer build.
- The uncalled needsXmrigExtraction() is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00
DanSandClaude Opus 5.5 81733f5603 fix(rpc): run rescans and key imports without the 30 s timeout
A rescan or key import holds the node's cs_main for the whole wallet
scan. On the shared clients it hit the 30 s curl timeout, reported a
failure that wasn't one, abandoned a daemon RPC slot and flipped the app
into "node busy".

- One long wallet RPC at a time (rescan, importprivkey, importaddress,
  importwallet, z_importkey, z_importviewingkey, z_importwallet) runs on
  its own ungated client with no client timeout (rpc/long_call_lane.h).
  It can be aborted from any thread, and an abort is never lost, even
  one that lands before the client opens.
- The GUI rescan, key import and sweep use it. The sweep's fund-moving
  body is unchanged; only the client it calls differs. runtimeRescan()
  returns bool, and a refused post-bootstrap reconcile stays armed.
- The console routes rescan and the import commands through the lane;
  its input stays usable meanwhile, and a refusal is printed like any
  other result. RPCClient gains callRaw(.., timeoutSec).
- Restart, wallet switch, delete-chain, bootstrap (now also from
  Settings) and the other node-stopping flows are refused while it runs,
  with a message saying a key import or rescan is still running; quit
  asks for confirmation. The lane is aborted at quit.
- A second import or rescan is refused before the key is copied anywhere.
  An import cut off after the request reached the node (a lost reply,
  or a proxy's 502/504 while the node still imports) says the key may
  already be in the wallet and to run Settings -> Rescan. Only the
  node's own HTTP-layer refusals (401, 503, work queue full, unhandled
  request) say that the key was not imported. A pure
  classifyLongCallCutOff() makes that choice.
- New strings in all 8 languages; console results stay English.

Tests: the long-method list and the console's routing; one lane at a
time; an abort lands whether it comes before the client opens or during
the call; a call with no client timeout outlasts a 3 s scan, where a 1 s
gated client times out and marks the node busy; classifyLongCallCutOff()
for every failure kind with and without a sent request, and from raw
HTTP replies (502, 504, a plain 500 -> cut off; 401, 503, queue full,
unhandled -> refused).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00
DanSandClaude Opus 5.5 b3a57b07ad test(fake-daemon): add slow rescan and key-import methods
fake_dragonxd now answers rescan, importprivkey, z_importkey and
z_importviewingkey, so the GUI's long wallet RPCs can be reproduced
without a real node.

- Each one holds cs_main for FAKE_RESCAN_SECS (default 0). The lock-free
  getrescaninfo reports the scan while it runs, and, like v1.3.0, the
  scan stops early when a shutdown is requested.
- Argument checks, error codes and return values follow dragonxd
  (z_importkey returns null, importprivkey the t-address, the default
  "whenkeyisnew" skips a known key, z_importviewingkey needs its zaddr).
  Keys are only prefix-checked and never stored or logged; an imported
  spending key gets a made-up address that the address lists then show.
- FAKE_ENCRYPTED=1 / FAKE_WALLET_PASSPHRASE: an encrypted wallet that
  starts locked, with walletpassphrase (-14 on a wrong passphrase),
  walletpassphrasechange, walletlock and getwalletinfo unlocked_until;
  the imports answer -13 while it is locked. As in dragonxd, the right
  passphrase with a non-integer timeout unlocks first and only then
  fails (-1): the wallet stays unlocked with no relock timer of its
  own. A timer pending from an earlier unlock keeps running; otherwise
  only walletlock locks it again. Without FAKE_ENCRYPTED, those three
  methods answer -15 like dragonxd.
- FAKE_METHODS_ALL call events show keys and passphrases as <redacted>.

A plain run is unchanged: the same 63 GUI calls before and after, and
identical replies to every one of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00
DanSandClaude Opus 5.5 78e38570a8 fix(bootstrap): check archive and free space before deleting chain data
The bootstrap removed blocks/, chainstate/ and notarizations/ first and
only then opened the archive. An unsafe entry, a full disk or a corrupt
member therefore left no chain, or a half-written one for dragonxd to
start on.

- Preflight before anything is deleted: every archive entry must be
  safe (no absolute path, no "..") and readable, and the disk must hold
  the extracted size plus max(256 MiB, 5%), counting what removing the
  old chain frees (a symlinked chain dir counts 0). Free space counts as
  unknown only when the probe errors, the capacity is 0 or the
  available size is -1; a known disk with 0 bytes free is a refusal.
  The old chain is kept on every preflight failure.
- Protected members are skipped, compared case-insensitively: any path
  component starting with "wallet", any "database" component, a .cookie
  or *.conf file, and top-level *.dat files other than fee_estimates,
  peers and banlist. The number of non-critical member failures is
  logged.
- The removal of the old chain is verified (a locked file fails the run
  instead of mixing old and new chain files). If the extraction fails or
  is aborted, the partial chain is removed again before the failure is
  published; isDone() turns true only after the worker's last
  filesystem change.
- Cancel is disabled while the old chain data is being replaced, and
  quit aborts a running bootstrap.
- A complete zip from an earlier attempt is reused on Retry; its
  checksum is re-verified. A download that cannot fit fails early, and a
  disk-full write error says so.

Tests: the space rule (boundaries, unknown vs. full disk, saturation);
planArchive sizes, zip-slip, garbage and truncated zips; the
protected-name list; reclaimableBytes with symlinks; each preflight
refusal keeps the old chain and wallet.dat byte-identical; a corrupt
critical member removes the partial chain before the failure is
published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00
DanSandClaude Opus 5.5 70d8b0f687 fix(daemon): supervise a daemon restarted after a crash
After an unexpected exit the monitor thread returned on its own but
stayed joinable, and start() only spawned a monitor when none was
joinable. A node restarted after a crash (no stop() in between) then ran
unwatched: a second crash was never detected, its output was never
drained, and the dead run's stdout pipe leaked into the next fork.

- start() joins a monitor that returned on its own (only when joinable,
  at the top, after the "already running" early returns), closes the
  dead run's stdout pipe (Windows: process and pipe handles), and always
  starts a new monitor for the process it launched. crash_count_ is kept.
- New lifecycle_mutex_ serializes start(), beginStop(), stop()'s flag
  writes and joins, and endStop(), so two threads can never join the same
  monitor. The monitor thread and the state callback never take it; a
  beginStop() racing a start() waits for it to return (its longest wait
  is the ~300 ms datadir-lock poll).
- start() is refused (returns false, state unchanged) while a stop is in
  progress, i.e. between beginStop() and endStop().
- The daemon tests now start the stand-in only in isolation (temp
  -datadir override, port check skipped): they no longer write the real
  ~/.hush/DRAGONX/obsidiandragon-dragonxd.pid or tail the real debug.log.
  New tests: restart after a crash is supervised, a restarted run stops
  cleanly, no fd leak over crash+restart cycles (Linux), and start() is
  refused while stopping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00
DanSandClaude Opus 5.5 f6e5038043 fix(security): don't penalise a correct PIN after a passphrase change
A passphrase change left the PIN vault sealing the old passphrase. The
next PIN unlock opened the vault, walletpassphrase answered RPC -14, and
the generic catch ran the lockout curve on a correct PIN, while Settings
kept showing the PIN as active.

- Passphrase change: when the active wallet's vault exists at completion
  (same vault path as at dispatch, passphrase actually changed), the
  vault is marked out of date for the session and kept on disk: PIN entry
  is no longer offered, a warning toast says so, and Settings shows
  "PIN out of date" with a tooltip on how to fix it. The Change
  Passphrase dialog notes beforehand that a PIN will go out of date.
- PIN unlock: RPC -14 after the vault opened is treated as "PIN out of
  date": passphrase entry, a message, no attempt counted, no lockout, and
  the vault is not deleted (-14 is only the daemon's answer; the vault may
  hold the only copy of a passphrase that opens an older backup). After a
  restart Settings shows the PIN as active until the first PIN attempt.
- A malformed vault (zero-filled by a removeVault() whose unlink failed,
  truncated, unknown version) is treated as missing: passphrase entry,
  "PIN file unreadable", no penalty. New SecureVault::isWellFormed().
- Race guards: Set/Change/Remove PIN are disabled while a passphrase
  change runs, and Change Passphrase while a PIN operation runs.
- Setting or removing the PIN, or switching wallets, clears the mark;
  setting a PIN offers PIN entry again. Change PIN keeps the mark (the
  vault still seals the same passphrase).
- Pure decisions in data/pin_vault_policy.h, unit-tested. New strings in
  all 8 languages.

No vault, wallet or settings format change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00
DanSandClaude Opus 5.5 62a5577491 fix(sweep): keep the UI sweep away from real wallet data
The full UI sweep (dev builds only) ran its demo surfaces against the
real profile. Its teardown deleted wallet-{cold,trading,mining,donations,
2023,payroll}.dat from the real datadir, it created zero-filled wallet
files and picker-demo/extra-wallets folders, its demo wallet-index upsert
re-minted the active wallet's scopeId (hiding "w:"-scoped contacts), the
Contacts tab could re-attach legacy-scoped contacts to such a throwaway
id in addressbook.json, and it persisted the demo active wallet file and
Mining pool worker.

- Preflight: a full sweep is refused (toast + log line) while a real
  workflow is in flight: a pending seed_migration_* setting or open
  migrate dialog, a wallet switch, an encryption/import workflow, an
  encrypt/PIN/lock operation, the seed-backup dialog, or a bootstrap.
  The DRAGONX_FULL_SWEEP hook then exits as for an empty catalog.
- Persistence latches: AddressBook (snapshot on the first sweepBegin,
  save/load latched while a demo-contacts surface is shown), WalletIndex
  and Settings (active wallet file, contacts view mode, pool URL/worker)
  for the whole sweep. Settings::sweepEnd() is the last statement of
  clearDemoWalletData and flushes once only if a save was suppressed; a
  quit mid-sweep writes nothing to the profile (the 0700 sandbox, with
  zero-filled demo files only, may stay in the temp dir).
- Scope ids: during a full sweep activeWalletScopeId() returns the
  persisted id or "" and never mints one, so the Contacts tab cannot
  move legacy-scoped contacts to a throwaway id. The only address-book
  write a sweep can still cause is the one-time legacy re-attach to the
  wallet's persisted id, which opening Contacts does anyway.
- Sandbox: demo wallet files and folders go to a fresh random 0700 dir
  under the temp dir (exclusive create, a pre-existing path or symlink
  is refused), removed at sweep end. The teardown that deleted those six
  wallet files is gone.
- Cleanup: the migrate-to-seed state, wizard phase, demo chat
  conversations and a fabricated demo chat identity are reset at sweep
  end; the seed-backup surface always shows the demo phrase and a real
  z_exportmnemonic result is dropped while the sweep runs.
- i18n: one new key (screenshot_sweep_refused) in i18n.cpp and the 8
  language files.

Tests: the address book snapshots only on the first sweepBegin, keeps
addressbook.json byte-identical while latched and restores the real
book; the wallet index keeps wallets.json and the real scope id, also
with the background writer attached; Settings restores the latched
values and flushes once only when a save was suppressed; the sandbox
helper refuses a pre-existing path or symlink and removes only a
prefixed dir, never a symlink's target.

Release note: v2.0.x UI sweeps could delete wallet-{cold,trading,mining,
donations,2023,payroll}.dat from the data directory and re-scope
contacts; this cannot be undone automatically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00
DanSandClaude Opus 5.5 3ac46abe1a fix(security): compile the UI sweep only into dev builds
The screenshot sweep and the full UI sweep are dev tools. The full sweep
injects demo data into the live wallet state and the real data and
config dirs, and the tab sweep screenshots real balances and addresses.
Both were compiled into every build: the Settings buttons were
reachable in full-node releases, and the DRAGONX_FULL_SWEEP env hook
worked in every build, Lite releases included.

A default-OFF option alone would fail open: dev and release builds
share build/linux (and build/mac), so a release configured without
--clean would inherit a cached ON from the last dev configure.

- CMake: option(DRAGONX_ENABLE_UI_SWEEP ... OFF), passed to
  ObsidianDragon and ObsidianDragonTests as a compile definition.
- build.sh passes it explicitly: ON in the dev build, OFF in every
  release configure (Linux, Windows, macOS osxcross and native).
- Packaging guard: while the cache says ON, dragonx_release depends on
  dragonx_release_sweep_guard, which fails, so a hand-configured ON
  tree cannot package a release.
- startSweepImpl is a no-op unless the option is ON (an undefined macro
  counts as 0, so it fails closed). The enabled branch logs the
  sentinel "[Sweep] DRAGONX_UI_SWEEP_COMPILED_IN", so a release can be
  checked with `strings <bin> | grep -c DRAGONX_UI_SWEEP_COMPILED_IN`
  (must be 0).
- main.cpp: the DRAGONX_FULL_SWEEP hook is compiled out (autoSweep is
  false). It must be: with the sweep a no-op, the hook would quit the
  app once its warm-up frames ran out.
- Settings > Node > Debug options: both sweep buttons, open-dir, the
  seed-demo-chat button and the current-theme checkbox are compiled only
  into dev builds. The daemon debug categories are unchanged.

No i18n change: the existing strings stay in the tables.

Verified with an isolated release (DRAGONX_BUILD_ROOT/DRAGONX_RELEASE_ROOT
in a scratch dir) whose tree was first configured by hand with ON:
build.sh --linux-release without --clean leaves the cache OFF, and the
sentinel count is 0 in the built binary, the zip and the AppImage.
With the cache set back to ON by hand, --target dragonx_release fails
at the guard before compiling anything. Dev trees configured ON keep
the sweep (sentinel present; a one-surface DRAGONX_FULL_SWEEP run
against the fake daemon captured and exited cleanly).

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00
DanSandClaude Opus 5.5 b6cffdf942 fix(lite-backend): catch panics at the wallet-open C entry points
litelib_initialize_new, litelib_initialize_new_from_phrase,
litelib_initialize_existing and litelib_check_server_online had no
catch_unwind (only litelib_execute did), so any panic below them
unwound across the C ABI into the GUI. With rustc 1.63 that is
undefined behaviour; in practice the process aborts with "fatal runtime
error: failed to initiate panic, error 5". The simplest trigger is a
malformed user-entered server URL: get_server_or_default unwraps the
http::Uri parse before anything else runs.

- ffi_guard (next to error_cstring) runs a body under catch_unwind and
  turns a panic into "Error: Rust panic in <name>", which the C++ bridge
  already treats as a failed call. The three init entry points keep
  their extern signatures and #[no_mangle] and call ffi_guard; their old
  bodies are now private *_impl functions.
- litelib_check_server_online wraps its body the same way and returns
  false on a panic.
- litelib_initialize_existing opens the wallet through
  read_from_disk_reporting. After a recovery from the .bak it returns
  "OK: recovered from backup; damaged file kept as <name>" instead of
  "OK". The C++ side treats any non-empty, non-error string as a
  successful open, so this is ABI-compatible.

No ABI or symbol change: the exported symbol inventory
(lite-backend-symbols.txt) is identical. panic = "unwind" stays in the
release profile, and every guard depends on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:22:00 -05:00