The contacts list carried its own lowercase-and-compare sort, its own
lowercase substring search, and an inline linear middle-truncation loop
for fitting an address to a column width. These duplicated logic that
belongs in the shared text helpers.
- A new allocation-free case-insensitive compare replaces the per-sort
lowercasing; it lowercases ASCII per byte and leaves bytes >= 0x80
alone, so UTF-8 labels sort exactly as before.
- Search now uses the shared case-insensitive contains helper.
- Middle-truncation to a pixel width moves to a shared template that
binary-searches the fit (about log2(n) width measurements instead of
up to n) while producing the same result as the old linear shrink.
Tests: the compare matches lowercase-then-compare across ASCII, UTF-8
bytes and length ties, and the fit helper matches the old linear loop
across lengths and widths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>