Hovering the Paste button in the import-key dialog read the OS clipboard
every frame to draw a ghost preview of the key, and the trimmed
plaintext was rebuilt each frame and never wiped. SDL's clipboard read
is a synchronous round-trip, and the pasted value is a secret private or
viewing key.
- The clipboard is now read only on hover entry and then at most twice a
second, and the classified preview is cached rather than recomputed
every frame.
- The cached preview is a dedicated secret buffer: it is wiped the moment
it stops being shown, wiped on secret wipe, and the transient trimmed
copy is zeroed right after it is classified.
- Trimming moves to a shared clipboard helper shared with the paste path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
While no lite wallet is open the startup auto-open check called
walletExists() every frame. That call crosses into the backend and
touches the filesystem (and prints), so it ran a disk stat on every
frame the app idled at the first-run prompt.
- walletExists() now caches its result for about two seconds on the main
thread, and the cache is invalidated whenever an open or lifecycle
result is consumed, since the wallet file may have changed.
- The auto-open condition is reordered so the cheap timer checks gate the
call, and the click-time overwrite decision uses a new
walletExistsFresh() that bypasses the cache.
Tests: the cache returns the first queried value within the TTL and the
fresh read re-queries the backend and refreshes the cache.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pool auto-balance step returned early whenever the pool selector was
in Manual mode, so a pending manual Refresh of the pool display stats was
dropped and the button did nothing unless Auto-balance was active.
- A pending refresh is now serviced in Manual mode too, re-querying the
pool display stats without auto-switching the pool or restarting xmrig.
- Auto-balance behaviour is unchanged; the manual path is display-only,
so it runs with no benchmark gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The global keyboard shortcuts tested keys without disabling key-repeat,
so holding one down fired it every frame: the theme cycle, balance and
market layout cycles, the effects and background toggles, the settings
and sidebar-collapse keys all re-triggered on a held key. Cycling themes
also stepped onto skins that had failed to load and wrote the broken
pick into settings.
- Every one-shot shortcut now checks for a fresh key press only, so a
held key fires once.
- Theme cycling skips over invalid skins and only applies, persists, and
shows the toast when a skin actually loads; if none can load, settings
are left untouched and no toast appears.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Once a shutdown was under way the app could still launch or restart the
full node and keep running the RPC connection state machine. That could
spawn a daemon process on the way out and surface a spurious connection
failure toast as the window tore down.
- Starting the embedded daemon now bails out immediately while a
shutdown is in progress, so the node is never (re)launched on exit.
- The connect attempt returns early during shutdown, so there is no
start/connect try and no stray failure toast on the exit screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The transaction export streamed rows straight into an ofstream. A write
that failed partway, on a full disk or removable media, left a truncated
CSV on disk behind a success toast. The CSV formatting also lived inline
in the dialog with no test coverage.
- The CSV is now built in memory and written atomically (temp file plus
rename), so a write failure is reported with an error toast and leaves
no partial file.
- Formatting moves into a reusable helper, and the timestamp conversions
use the thread-safe localtime variants and tolerate a conversion
failure rather than dereferencing a null result.
Tests: field escaping neutralises formula-injection prefixes and quotes
special characters, and the full CSV has the expected header and rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Renaming or adding a contact from the chat tab called the address-book
mutator and then saved again, even though the mutator already persists.
It also reported success unconditionally, so a failed rename still
showed a success toast.
- The redundant explicit save is removed; the mutator's own save stands.
- A rename now shows success only when the mutator succeeds and an error
toast when it does not.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pinning a lite server while a connection to it was already being
established tore the in-flight connect down and restarted it, slowing
the very connection the user was waiting on.
- Pinning is now skipped while a connect is already underway, so the
existing attempt is left to finish.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The request-payment dialog kept its generated payment URI across opens
and closes. A dialog opened for an address with no payable shielded
address could still show the previous request's URI, and the string
lingered after the dialog closed.
- The payment URI is cleared when the dialog opens, when it closes, and
on the close cleanup path.
- The URI field is shown only when there is both a URI and a real
address to pay.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The send tab read the OS clipboard every frame the paste button was
hovered, to preview a pasted address. SDL's clipboard read is a
synchronous round-trip that can stall, and the preview can only ever
latch when the recipient field is empty, so most of those reads were
wasted work.
- The clipboard is now read only while the button is hovered and the
recipient field is empty, polled once on hover entry and then at most
twice a second.
- Trimming the clipboard text moves to a shared helper that also bounds
the copy so a pathological multi-megabyte clipboard is ignored.
Tests: the trim helper handles null, surrounding whitespace, an
all-whitespace string, and the length bound.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The chart fetch and the Market tab both resolved the saved exchange and
pair names into the registry, with their own near-identical scans and
slightly different fallbacks. Keeping two copies risked the chart and
the chips disagreeing about which pair was selected.
- A single resolver maps the saved names to a registry index, matching
the exchange name (any exchange when none is saved) then the pair
display name, and falling back to the registry's first pair, the UI
default. The chart fetch and the Market tab both call it.
- The Market tab now derives its selection indices from the saved names
against the current registry every frame, so the indices stay correct
when the live exchange list replaces the fallback; a steady-state fast
path skips the scan once the indices already name the saved selection.
Tests: the resolver returns the right index for a matched exchange/pair,
an empty-exchange match, and the first-pair fallback on misses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The portfolio address picker's sort comparator looked up each address in
the selected-address set on every comparison, two hash lookups per call,
and the per-row draw repeated the lookup again. On a long address list
that is a lot of redundant hashing every frame.
- Each filtered candidate now carries its selected flag, looked up once
when the list is built. The comparator reads the stored flag, so the
sort produces the same ordering with no lookups of its own.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>