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 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>
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>
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>
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>
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>
With any console filter on, each batch of new output rescanned all N
lines to rebuild the visible set. A text filter lowercase-copied the
needle and every line to test it: O(total characters) and 2N
allocations per batch.
When the filter is unchanged and the model only appended or evicted
lines since the last pass, the visible indices drop the evicted lines
and shift down, and only the new lines are tested. The result equals
a full rescan, since line text and channel never change and the
filtered view has no folds. A filter change, Clear, or a model window
that doesn't follow on from the last one still rescans.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each batch of new output re-wrapped all N visible lines glyph by glyph,
allocating a fresh segment vector per line; a line evicted at the
10,000-line cap shifted every index, so nothing could be kept. And on
every frame, idle or not, seeding the scanline shading summed the wrap
rows of every line above the view.
- The model numbers its lines and the view keeps one wrap per line,
aligned by that number, so an eviction drops a prefix instead of
forcing a rebuild. New output wraps only the new lines; the layout is
then a prefix sum over the N visible lines, with no measuring and no
per-line allocation.
- The wraps are kept while the wrap width, line height, font size, font
(by ImFont::FontId) and rasterizer density are exactly unchanged. A
line-spacing change only re-sums heights; filters and fold toggles
reuse the cached wraps. A resize drag still re-wraps every visible
line per frame, as before, but reuses their storage.
- The scanline seed is read from the prefix sum: O(1) per frame.
Heights, offsets and segments are bit-identical to the old rebuild. A
font reload at an unchanged size (the colour-emoji toggle) now re-wraps
the console at once; before, it waited for the next line of output.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Overview address list built all N rows on every frame (icons,
labels, balances, buttons, an InvisibleButton and a context-menu
check per address) and allocated a vector of N row positions, however
few rows fit on screen; the edge mask then walked all their vertices.
An ImGuiListClipper now submits only the rows that intersect the
list's clip rect, plus:
- one guard row on each side, so dividers and anti-aliased edges at
the clip boundary are drawn as before;
- the row being dragged and the row whose context menu is open, so
drag-to-reorder and the menu keep working when they scroll away;
- the rows keyboard navigation targets (kept by the clipper itself).
Before each row the cursor is put back exactly where the unclipped
loop left it, so the density-scaled spacing, which depends on the
cursor position, and every row's geometry are unchanged. Row tops are
computed from the index, so the per-frame vector is gone, and the
scroll extent is the same. The row model (sort, filter) is still
rebuilt over all addresses each frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Mining tab's memory bar read used system RAM and the wallet's own
memory on the UI thread every frame: synchronous OS queries, on Linux
an open and parse of /proc/meminfo and /proc/self/status per frame.
Both are now sampled at most once a second; the first frame after
more than a second idle resamples at once. Total RAM was already
cached and daemon RAM already comes from a background sampler.
The used-RAM and wallet-memory readings in the bar and its tooltip now
refresh at most once a second; what they show is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Mining tab's earnings card walked all N wallet transactions on
every frame to total Today / Yesterday / All time and pick the 4 most
recent rows: four string compares per transaction plus a memo search
on receives. It also ran localtime and mktime every frame to find
local midnight.
The pass is now a pure function behind a one-slot cache keyed on
WalletState::tx_revision, the transaction count, the filter pill, the
Solo/Pool mode, solo support and local midnight, with a 2 s maximum
age as a safety net. It reruns only when one of those changes or the
2 s age runs out, instead of every frame. The totals are bit-identical
(same loop, same summation order), and crossing local midnight still
moves Today into Yesterday at once.
Local midnight is recomputed only when the clock second changes,
through the shared util::localTime; a failed localtime now yields a
zeroed struct instead of an uninitialised one. The "x ago" labels and
Est. Daily are still computed per frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The copy button of a text selection in a chat bubble copied
body.substr(start, len) with byte offsets kept from earlier frames.
Those offsets can outlive a thread change (an older message inserted
above shifts the message indices, so the selection can land on a
shorter body), and a start past the end made substr throw
std::out_of_range out of the chat render; the main loop catches it,
but the rest of that frame's UI is dropped.
The copy now runs only when the start is not past the end of the body.
substr already clamps the length, so the copied text is the same in
every case that did not throw before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every frame the open chat thread handled all N messages: grouping flags
from two day keys each, a measuring layout of every body, then the
draw, which laid every body out a second time and ran the bubble, hover
and popup code, however few rows were on screen.
The thread now keeps a layout cache: grouping flags, each body's
wrapped size (keyed by message, so only new or re-wrapped bodies are
measured) and each row's height, with prefix offsets. A frame binary-
searches the visible range and submits those rows plus any failed send
(Retry stays reachable with Tab), the row being drag-selected and the
row whose context menu is open; one spacer stands in for each skipped
run, and scrolling to the newest message works as before.
- Rows are rebuilt when the chat store changes or when the wrap width,
fonts, font size, DPI scale, rasterizer density or compact mode
change. Fonts are keyed by FontId, so a font reload at the same size
(the colour-emoji toggle) re-measures. A rebuild is O(N), about
0.5-1 ms at 3000 messages.
- Grouping flags are recomputed on a data change and once a minute,
together with util::refreshTimezone(), so a system timezone change
reaches chat timestamps within a minute.
Rows above the visible range are now one spacer of their summed
heights, so the visible rows no longer move by a pixel each time a row
above them crosses the top of the app window. Debug builds log once if
a cached wrap width or row height ever disagrees with the live layout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The open chat thread called std::localtime at least twice per message
per frame (two day keys for grouping), and more for day separators and
time labels. On glibc every call re-checks the timezone with a stat of
/etc/localtime. The chat time helpers now use util::localTime() from
the new util/local_time.h: localtime_r (localtime_s on Windows), with
no per-call timezone check and no shared static result buffer.
glibc's localtime_r reads the timezone only once, so local_time.h also
adds util::refreshTimezone() (tzset) for caches to call when they
rebuild. Nothing calls it yet; its first caller is the chat thread's
grouping-flags cache in the next commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every frame the History tab:
- hashed the txid, amount, confirmations, time, type and address of
all N transactions (about 0.6-5 ms at 5k-20k) to decide whether its
totals and its merged, sorted row list were stale;
- copied every chat txid into a fresh set;
- ran the type and search filter over all N rows, with up to three
case-insensitive substring searches each.
The totals and the row list now key on tx_revision (the row list also
on the sort mode), the Message badge and the Chat filter use the chat
store's txid index, and the filtered rows are rebuilt only when the row
list, the type filter, the search text or, under the Chat filter, the
chat store changes. A frame is now a few compares plus one txid lookup
per row of the current page. An unchanged full-node refresh no longer
rebuilds the totals and the row list either: the old fingerprint folded
in the refresh time.
On lite, a change to only a memo or a sender address now shows at once.
The old fingerprint left those fields out and relied on a refresh time
that lite never sets, so such a row stayed stale until something else
in the list changed, such as the next block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The image picker decoded up to 6 full-size images per frame on the UI
thread to make its thumbnails (1.5-2.7 s per frame with 12 MP photos,
until the visible grid filled) and read every .gif/.webp a second time
just to see whether it animates. Thumbnails were freed only on navigate
or close, so a large folder and every hovered animation (up to ~20 MB
each) stayed resident.
Thumbnails now decode on util::StartImageDecode workers (at most 2 at
once, so the top of the visible grid fills first) and the UI thread
uploads up to 8 small textures per frame. The animation badge comes from
the same decode, with no second read. Hover previews get their own
2-slot lane, and the picker asks for a frame every 100 ms while anything
is pending; under idle pacing a deferred thumbnail used to wait for input
or the 2 s tick.
Behaviour changes:
- thumbnails and hover frames are LRU-capped at 64 MiB per folder;
evicted ones re-decode when they are needed again;
- leaving an animated thumbnail before its preview finishes decoding
cancels it (re-hover restarts it); a finished preview is kept;
- animated WebP files get a real thumbnail instead of the placeholder;
- images above the decoder's limits show the placeholder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Contacts tab read, decoded and downscaled custom avatars inside the
draw call, two per frame, rows scrolled out of view included: a 12 MP
photo froze the window for 0.25-0.45 s, a long GIF for up to 1.5 s, and
the avatar cache was never evicted. "Add image" decoded the picked file
and copied it inside the picker's confirm callback, freezing it again.
Avatars now decode on util::StartImageDecode workers (at most 2 at once).
The UI thread only uploads finished frames, at most about 4 MB per UI
frame, and asks for a frame every 100 ms while anything is pending;
under idle pacing a deferred avatar used to wait for input or the 2 s
tick.
Behaviour changes:
- an avatar scrolled out of view is not decoded until it scrolls in
(the badge shows for 0.1-0.3 s the first time);
- avatar textures are LRU-capped at 128 MiB; the least recently drawn
go first, never one drawn on-screen in the previous Contacts frame,
and they re-decode on scroll-back;
- an animated avatar starts playing once all its frames are uploaded;
- an import is validated and copied on a worker: the avatar appears
0.1-0.5 s after confirming and Save/Add stays disabled until it does;
a result only lands in the dialog session that started it;
- the copy goes to a temporary file renamed over the target (retried
briefly), so a crash or a failed copy never leaves a truncated avatar
or removes the existing library image;
- imports get the decoder's limits, and an animated WebP can now be
imported (it always failed with "couldn't be loaded").
Applying an import also no longer destroys an avatar texture the same
frame has already drawn (a use-after-free on DX11).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every open of the Wallets dialog, Refresh, folder add or remove, and Open
on a stale row walked each user-added folder recursively on the UI thread
before the dialog drew: up to 40,000 entries per folder, roughly
50-150 ms warm on Linux and seconds on a cold disk, a network share or
Windows.
The datadir listing (a few top-level files) stays synchronous, so a
wallet just created shows at once. The recursive walk runs on a detached
worker with the same depth, hit and entry limits; its rows are merged at
the start of a later frame and only the new rows are probed.
- The dialog opens at once with the datadir rows. The external rows
arrive when the walk ends, with "Scanning folders..." beside the
scanned-folders caption, and the card grows once.
- Reopening within 60 s with the same folders reuses the last walk
without touching the disk, so its rows and sizes can be up to a minute
stale: a wallet copied into a folder in that minute needs Refresh.
Refresh and Open on a stale row always walk again.
- While a walk or a wallet probe runs (up to 30 s) the dialog keeps
frames coming, so rows and badges appear without moving the mouse.
- The 200-row cap now holds across all folders; each extra folder could
add one row past it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Open on a wallet row whose file was moved or deleted after the list was
built rebuilt the list from inside the dialog's row loop. The loop still
held a reference to that row and walked a sort order sized for the old
list, so the click read freed memory and could index past the end. Open
now flags a rescan for the next frame; the vanished row drops out one
frame later, with no crash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clicking the Network-tab card of the server the wallet was already
pinned to and connected to tore the controller down and reopened the
wallet against the same server. That click is now a no-op. A wallet that
failed over to another server still reconnects, which retries the pinned
one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New address, Import key, Encrypt and Remove encryption called the
backend on the UI thread. Each ends in the backend's save, which waits on
the lock a scan holds for its whole duration and every refresh read holds
briefly, so during a scan the window froze until the scan ended. A create
or restore with a passphrase encrypted and saved just after its first
scan had started: for a restore, a freeze for the whole initial scan. A
server switch destroyed the old controller on the UI thread and joined
its refresh worker, which can sit for minutes in a read against a dead
server (each balance, notes or list read retries for ~62 s).
- The four ops run one at a time on a background lane and their results
are applied on the main thread; Settings shows "Working…" meanwhile.
Key and passphrase copies are wiped after the call, or when a queued
op is dropped.
- A create or restore encrypts and saves (an open unlocks) before the
first scan starts, so the save takes milliseconds and the scan runs on
the encrypted file.
- Import, Encrypt, Remove encryption and Receive's New address are
disabled while a scan runs ("Wait for blockchain to sync"); Redownload
blocks is disabled while an op is queued.
- A switch retires the old controller without blocking (queued ops
dropped, pending ones reported failed, its client shut down) and
destroys it on a background thread. It no longer saves on the way out:
the new client re-scans the blocks since the last save, as after a
restart.
- A switch waits for an op in flight, at most 15 s, so the op can't
finish and save on the old client while the new one owns the wallet
file. The Network tab already shows the new selection meanwhile.
- The refresh worker stops between backend commands, skips the remaining
server reads once one fails, and then retries after 4, 8, then 15 s
instead of every 2 s.
- Quit waits up to 2.5 s for an op in flight, e.g. Encrypt's save.
Encrypt and Remove encryption now report a failed save: the backend
returns it as a JSON error that the old check never saw, so the failure
was reported as success. The trimmed copy of a key typed into Import is
now wiped too, and stopping the refresh worker can no longer miss its
wakeup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The daemon's RSS was read on the UI thread: a scan of every /proc entry
on Linux (1-7 ms), a forked ps on macOS (up to ~30 ms). The mining poll
probed every 1-5 s on every tab, and the Mining tab's memory bar probed
again every 1.5 s.
util::BackgroundSampler now runs the probe on one lazily started thread
and caches the result. getDaemonMemoryUsageMB() returns the last sample
and asks for a new one at most every 3 s, and only while the Mining tab
memory bar or the Peers tab Memory cell is drawn: nothing on other tabs
or while minimized. EmbeddedDaemon::memoryProbe() hands the sampler a
pid snapshot (on Windows the value read from the owned process handle),
so nothing off-thread touches daemon state. The mining poll does no I/O
any more; it carries the last sample. Both shutdown paths stop the
sampler.
Fixed along the way: an unreadable /proc made the directory iterator
throw on the UI thread; the scan now just stops.
User-visible:
- the Peers tab Memory cell keeps updating during sync and shows an
external daemon's memory (it showed the mining poll's value, which
pauses during sync and covers only the embedded daemon);
- the Mining tab memory bar refreshes every ~3 s (was 1.5 s).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The thread grid is inert during a benchmark, but the -/+ buttons and the
count field still took input: a click changed the shown and saved count
while the benchmark owned the miner's (before the previous commit it
also restarted xmrig at that count and spoiled the measurement). The
buttons and the field are now disabled until the benchmark ends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every thread-count change on the Mining tab saved settings.json and,
with a miner running, stopped and restarted it: xmrig re-inits its
RandomX dataset and reconnects to the pool, seconds of lost hashing
each time. A drag across the grid did this once per tile crossed (up to
~24), with a "Starting pool miner" toast each; every -/+ click or Enter
did it again.
The tab now hands each pick to the App, which puts the newest in
settings at once and applies it when the input settles: on drag
release, or 0.5 s after the last click, even if the Mining tab was left
in the meantime. The header, RAM estimate and tile fill still follow
the drag live.
- Pool: the restart is decided on the mining-control thread, after every
earlier start or stop has run. It never starts a miner the user
stopped, is a no-op when xmrig already runs at that count, and is no
longer silently skipped when the pick lands in another restart's stop
window (which left xmrig on the old count).
- Solo: one setgenerate per gesture. A pick made while one is in flight
now waits for it instead of being dropped (another stale count).
- Clicking the tile that is already selected does nothing; it used to
save settings and restart the miner.
- With idle thread scaling on, the grid no longer snaps back to the old
count while a pick is on its way to the miner. A pool auto-balance
restart keeps a count still queued.
- Idle thread scaling changes the pool count through the same retarget.
Its correction after a start compares with the count last requested,
not the xmrig API's lagging report, so it comes about 1 s in instead
of after the first API report (3-5 s).
- A retarget whose pool config no longer validates shows the same error
and stops the miner, as the old stop-then-start did.
- While a benchmark runs a pick is saved, but the miner change is
dropped: the benchmark owns the count until it ends.
A crash within 0.5 s of a pick, or mid-drag, loses that pick; every
clean exit saves it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Six sliders (noise opacity, UI opacity, window opacity, chat font size,
chat poll rate, contacts list scale) rewrote settings.json, with its two
fsyncs, on every frame the value changed: 60-300 durable writes per
drag, at 1-4 ms each on NVMe, 3-20 ms on a Windows SSD, 20-60 ms on a
HDD and seconds while dragonxd is busy on the disk.
The value still applies live and is kept in Settings for the per-frame
readers; settings.json is written once, when the slider is released, as
the blur and font-scale sliders already did. A drag cut off before its
release (a dialog closed mid-drag) is persisted by the next save or at
exit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Keep in each address" posted one task that called z_mergetoaddress for
every batch of every address back to back; each call is a whole-wallet
scan under cs_main, and the daemon runs the ops one at a time anyway.
It is now an App-owned services::ConsolidationRun:
- one RPC at a time; the next merge only after the previous batch left the
queue and a gap as long as the last call took (2-120 s);
- pauses while the node syncs, the wallet is locked, a send of ours is in
progress, or another wallet scan is running;
- failures classified: nothing that may have queued is ever resent; a
breaker stops after 3 dead addresses or 3 failed ops in a row;
- each merge is charged to the scan budget; one re-read and one forced
balance at the end;
- it keeps running when the dialog closes: a status-bar chip, a run view
with progress, the wait reason and a Stop button.
The "Into one" merge can no longer be confirmed twice while its call is
in flight. fake_dragonxd gains FAKE_SCAN_SECS, FAKE_MERGE_SERIAL and
FAKE_MERGE_FAIL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The balance, address scan, history and probe were throttled one by one at
10% each, which added up to 30-40% of dragonxd's cs_main with all of them
due. services::WalletScanGovernor runs one wallet scan at a time across
both RPC lanes and keeps one shared quiet clock at 15% of wall-clock (cut
when the node is behind). Waiters are served first in, first out, with a
head start for what the page shows and a starvation floor of max(30 s
visible / 900 s hidden, 15x the stream's cost). Explicit work (a dirty
set, the user's send, a first load) is never refused but is charged.
Routine scans run only when the tip, the mempool size (getmempoolinfo:
no cs_main) or a wallet-mutation epoch changed since their last good scan,
or a safety sweep is due. Pages that don't show the balance poll it at
most every max(10 min, 50x its cost) (2 min with client-side auto-shield);
coming back into view with an old value fetches it at once. The staleness
badge counts from the last poll that confirmed nothing changed, and hides
while the budget holds the balance within its guarantee.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Core poll ran z_gettotalbalance twice (minconf 0 and 1), each a whole-
wallet scan under dragonxd's cs_main. The spendable (minconf=1) call now
runs only after the display call succeeded, and only when something is
unconfirmed or pending or the exact total moved: about 95% of polls make
one scan instead of two. A forced post-send balance can no longer be lost
to a refused enqueue, and pending-send debits aren't re-applied each tick.
z_getoperationstatus is filtered to our opids with its real argument form
([[id,...]]; the old ["id"] was rejected), falling back to the unfiltered
call if a daemon refuses the filter. The Shield/Merge dialog's own polls
are filtered and session-guarded too, and finally reach Done; the dead
RPCClient wrappers are deleted.
The Merge/Consolidate scope scan becomes a tested state machine: one scan
at a time, failures latch behind a Retry button, only "node not ready"
retries by itself (10/30/60 s, at most 3), Lite never scans. Its 120 s
calls are maintenance calls: every main-thread disconnect aborts one in
flight first, so a lifecycle action no longer freezes the UI behind it.
An aborted merge that was already sent says it may still go through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Market tab calls the exchange-list, price-history and candle
fetches every frame, so a failed fetch (offline, rate-limited, a
venue's API down) was retried on the very next frame, ~20 requests a
second.
- util::FetchBackoff: 30 s doubling to a 10 min cap, +/-20% jitter.
The exchange list, the price history (a partial result counts as a
failure) and the per-pair candles each back off on failure.
- Candle backoff is per pair. Switching to another pair still loads at
once, a failed refresh keeps the on-screen pair's last candles, and
switching back to a pair that is backing off shows the aggregate
chart rather than the previous pair's candles.
- The Market tab's refresh button retries at once (at most every 5 s).
A price fetch (60 s timer, no backoff) that succeeds after failing
means the network is back, so it lifts all three backoffs.
- A partial price history no longer marks the chart loaded: with no
fetch timestamp, the 30-minute reload gate measured system uptime
and blocked retries for the first 30 minutes after boot.
- The exchange list goes through httpGetString (TLS verified, size
capped) with short timeouts (5 s connect, 12 s total, as it runs on
the shared RPC worker) instead of its own curl code. A failed fetch
is retried; it used to be tried only once per session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Explorer: the tip check runs every frame the Explorer tab is open,
and it read both tip rows and rewrote them each time (two autocommit
WAL writes per frame). A steady tip is now kept in memory and skips
SQLite. A new tip writes both rows in one savepoint, so a crash
between them can't pair a new height with the old hash. A test
checks that 100 steady checks write nothing.
- Console: while the daemon was in the Error state the Console tab
added its error line on every poll, i.e. every frame, re-laying out
the whole buffer each time. It prints once per error now: on entering
Error, when the message changes, or after another crash
(EmbeddedDaemon::getUnexpectedExitCount(), never reset). setState
writes the message before the state, so a reader that sees Error sees
that error's message. The stray " ===" suffix is gone.
- perf.log: the profiler appended a block every 300 frames on every
run and never rotated the file (tens of MB a day for a wallet left
open). It is now off unless the app starts with DRAGONX_PERFLOG=1;
otherwise its zone hooks are near no-ops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restoring or un-maximizing the window called refreshNow(), which wipes
the history-scan progress and re-scans every z-address (a
z_listreceivedbyaddress per address, each holding cs_main). The Peers
tab's refresh button did the same, and holding F5 auto-repeated it.
- Window restore no longer forces a refresh; the regular timers catch
up (skipping the daemon-output backlog stays).
- The Peers refresh button refreshes peers only.
- F5 ignores key repeat. An explicit F5 or History refresh still does
the full re-scan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While theme effects were on (the default) the main loop drew every
frame it could. Where vsync is off (offscreen, some drivers, remote
sessions) an idle wallet spun at ~3,000 fps, 88% of a core; with vsync
it still drew every refresh. Frames are now paced by what needs them:
- full rate while there is input, a held key, a window appearing or
settling, a smooth scroll or the UI sweep;
- ambient (30 fps focused, 10 fps unfocused) for theme effects, the
acrylic backdrop, avatar animations and notifications;
- progress (10 fps focused, 4 fps unfocused) for UI that changes
without input. The updater and bootstrap downloads, console
commands, lite-wallet operations, seed migration, the loading
overlay and the lock-screen countdowns ask for it with
material::RequestProgressFrame();
- otherwise idle, as before: wait for events, with a housekeeping wake
every 2 s (5 s unfocused).
A software frame cap (0.9x the display refresh) bounds the loop where
vsync is off, and hidden, occluded or minimized windows get no ambient
frames. Reduce Motion freezes the theme-effect clock
(ThemeEffects::setMotionFrozen), so it needs no ambient frames either.
The displayed-balance roll goes through one clamped StepDisplayLerp
(balance_lerp.h). An idle-length frame pushed dt*speed past 1, so the
roll overshot, and under Reduce Motion's speed of 999 it diverged to
inf/NaN. The classic layout had its own copy of the old code.
Idle CPU, Lite build, offscreen (no vsync): 88% -> 5% of a core with
effects on, and 91% -> 3% with effects saved off (with the previous
commit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Logic timers advanced by io.DeltaTime, which update() clamps to 0.25 s
so animations don't jump after a pause. That matched real time only
while the main loop rendered continuously: with frames seconds apart, a
timer fed the clamped delta runs up to 8x slow. Affected: the refresh
scheduler, the seed-migration poll, the chat fast scan, the encryption
check, the loading and shutdown timers, the lock-screen countdowns, the
Settings debug gate and the mining benchmark.
- App::logicDeltaTime(): steady-clock seconds since the previous
update(), capped at 6 s so a sleep/resume fires each timer once
instead of replaying the gap. Visual interpolation keeps the clamped
io.DeltaTime.
- The refresh timers tick before the RPC results are drained, so a
hold that a callback arms (the connect-retry backoff) starts fresh
instead of being charged the gap before it existed.
- The shutdown timer is measured from shutdown_start_time_:
beginShutdown runs from SDL event handling, so an accumulated delta
would count the idle gap before the close request.
- The lock-screen error and lockout countdowns subtract only the time
since they were armed: a failed unlock arms them from an RPC result,
which can land on the frame that ends an idle gap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Users who keep funds apart under different labelled addresses can now
shed notes (smaller wallet, faster scans/witness rebuilds) without the
funds being merged into a single address.
- Merge/Consolidate dialog: "Where should the funds go?" - Into one
address (unchanged) or Keep in each address. The latter lists shielded
addresses with 2+ spendable notes (label, note count, amount;
too-small ones disabled) and merges each address's notes back into
that address with z_mergetoaddress [addr] addr. Addresses with more
notes than the batch size get several batches (the daemon locks a
batch's notes before returning, so calls take disjoint notes); the
dialog suggests a second run after those confirm. Per-address
progress; ops keep running if the dialog is closed. The Consolidate
(wallet-size) preset opens in this mode; Merge keeps "Into one".
Shielded only: a transparent address can't take a merge's output.
- services/note_consolidation: groupSpendableNotes, worthConsolidating,
estimateBatches, consolidateInPlace (pure; driven by a merge-call
function), with unit tests against a stand-in following dragonxd's
rules.
- fake_dragonxd: optional note wallet (FAKE_WALLET_NOTES,
FAKE_MERGE_SECS) with z_mergetoaddress and its operations.
- i18n: 17 new keys in all 8 languages; CJK subset rebuilt.
- UI sweep: modal-consolidate target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to 5246898. A transparent address only ever receives mining
rewards on DragonX (private chain), so:
- One explainer ("Receives solo-mining rewards only - the network rejects
payments to it. Rewards are shielded automatically once they mature.")
on the Overview row hover, the Receive picker hover, the Receive card
and the Validate Address result (now typed "Mining address
(transparent)").
- Receive: a selected mining address shows the explainer and a "Use a
shielded address" button (creates one if none exists) instead of the
payment request, and a "Not for payments" panel instead of a QR code.
The default selection prefers a shielded address; the "All" filter
hides empty mining addresses unless solo mining or marked as a mining
address; the filter reads All / Shielded / Mining.
- Request Payment: no transparent section, never prefilled with one.
- Overview: no "Mining - Mining" tag on a marked transparent row.
- FAQ: address answers rewritten; the send and memo answers no longer
imply you can pay a transparent address. All 8 languages.
i18n: 4 new keys, 6 FAQ answers updated in place, 2 dead keys removed,
CJK subset rebuilt. UI sweep: receive-mining-address and
modal-validate-mining targets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DragonX is a private chain (-ac_private=1): consensus rejects any
non-coinbase output to a transparent address, so a t-address can only
ever receive mining rewards, and its coins can only move to a shielded
address. The wallet treated them like ordinary recipients.
Block paying one:
- Send: a transparent recipient shows "Mining address - can't be paid"
and disables Review Send (tooltip explains; ask for a zs... address).
Typing, paste, contacts and payment URIs all go through this field.
- Overview drag-to-transfer: a mining-address row is never a transfer
target (it only reorders).
- Pool payout must be shielded (pools pay with ordinary transactions):
field tooltip, disabled Start button and the start check say why.
- isValidRecipientAddress() is now shielded-only; new isDragonXAddress()
and classifyRecipient() (Shielded / MiningOnly / Invalid).
Call them mining addresses:
- Overview: "Mining rewards" card (amber "Not yet shielded" when there
is a balance; hover explains; click opens the Shield dialog), "New
mining address" button with a tooltip, rows typed "Mining".
- QR / label dialogs, portfolio filter, contact badge ("Mining only"),
Receive notifications.
i18n: 17 new keys in all 8 languages, 12 dead keys removed, CJK subset
rebuilt. Tests: recipient classification, translation completeness.
UI sweep: new "send-mining-recipient" target.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump DRAGONX_LITE_VERSION 1.0.0 -> 1.1.0: since lite-1.0.0 the Lite variant
gained the whole 2.0.x shared UI/UX + diagnostics + i18n + perf work and this
session's chat delete/block — a minor bump (new features, no breaking change).
Make the FAQ variant-aware (walletFaq(fullNode)) so the Lite build reads
correctly: "What is ObsidianDragonLite?", encryption in the Wallet tab (not
Node & Security), migrate-to-seed hidden, node-specific answers reworded
neutrally ("the wallet syncs"), a new "Lite Wallet" subcategory (server model +
privacy tradeoff) standing in for the hidden Daemon group, and the redundant
single "Wallet" group tab dropped when there's no Daemon group to switch to.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Auto-shield status: the node's z_autoshieldstatus disabled_reason was echoed
verbatim ("HD seed origin is not known-recoverable; back the seed up and pass
-autoshield=1") and drawn INSIDE the Wallet OPTIONS checkbox grid, wedging a
full-width line between the checkboxes. Move it to a full-width note BELOW the
grid, and replace the raw daemon text with friendly, actionable wording keyed
on the seed_recoverable flag (back up your seed to enable it); the raw daemon
reason is kept on hover. Adds App::daemonAutoShieldSeedRecoverable().
- FAQ: add a Chat & Contacts entry ("How do I hide, delete, or block a
conversation?") covering the hide / delete-revive / delete-&-block actions and
the local-only caveat — data + i18n only, no UI code change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Immediate-mode render functions re-run every frame; these rebuilt O(N)
state each time even when nothing changed. From a 6-lens perf audit, each
finding verified on a hot-path basis:
- Console: ConsoleModel gains revision(); the full-model filter scan and the
glyph-by-glyph text-layout pass (BuildConsoleLayout) rebuild only when the
model / filter / wrap-width / zoom change — previously it re-shaped up to
10,000 lines every frame even when idle/scrolled. clear() force-invalidates
the memo mid-render (no OOB on the just-emptied visible set).
- Transactions: the summary-card totals memoize behind the tab's existing
FNV-1a fingerprint (also folds away a now-duplicate O(N) display-key pass).
- Send / Receive recent lists: early-exit the prefix scan (state.transactions
is kept newest-first) instead of filtering the whole tx history every frame.
- network_refresh_service: O(new x total) txid find-and-replace -> hash map.
- Sidebar unconfirmed-tx badge cached on last_tx_update + tx count; the
daemon-memory probe (/proc scan on Linux, popen on macOS) throttled to ~1.5s.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a per-conversation "delete" with two modes, and removes the per-frame
rescans of the chat history that shared these files.
Delete conversation (header trash icon → confirm dialog):
- Delete (revive-on-new-message): clears local history and tombstones the
messages (new chat_deleted table, keyed dedup hashes) so the every-few-
seconds memo re-scan can't re-import them; a genuinely NEW message (new
txid) revives the thread.
- Delete & block: removes history WITHOUT a tombstone and records the cid as
blocked (settings); ChatService::ingest drops that conversation's messages
— old and future — until unblocked from the "Blocked" manager, which then
re-imports the conversation from chain.
- Local-only (messages remain on-chain; the peer keeps their copy).
deleteConversation() deletes the DB rows FIRST and only then mutates the
store, so a failed write can't leave the two diverged.
Unit-tested (revive / tombstone-survives-reload / block / unblock) and
adversarially reviewed (store/DB divergence, half-open DB, revive-unread).
Performance (chat + badge hot paths, from the perf audit):
- ChatStore gains revision(); the Chat unread badge is now a single O(N)
no-alloc pass cached on it (was O(conversations x messages) copy+sort every
frame). The conversation list and open thread are memoized on revision()
(+ show-hidden and AddressBook::revision() for peer-name resolution).
- AddressBook gains revision() so an in-place contact rename invalidates the
chat memo (an edit keeps entries().size() constant).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bundles the session's UI work (the touched files carry several of these
changes together, so they are committed as one coherent UI batch):
- FAQ: new RenderFaqDialog + data-driven faq_content, opened from a
status-bar "?" (and the Windows title bar), styled like the Wallets
modal with search, Wallet/Daemon tabs, and smooth scroll.
- DPI/font-scale audit: multiply hand-drawn absolute geometry by
Layout::dpiScale() across ~30 files so nothing renders native-size at
HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x).
- Mining: chart now fills the horizontal space; thread stepper +/- buttons
match the input-box height; move the stratum-host toggle into
Node & Security (v1.3.0+).
- Settings: fix the auto-shield status text overlapping the grid.
- Sidebar: drop the peer-count badge on the Network button.
- i18n: wrap 193 hardcoded literals with TR() (keys/translations added in
the preceding i18n commit), so the security/PIN/lock flow, seed-backup
wizard, and witness-rebuild dialog localize.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make Merge to Address actually serve wallet-bloat consolidation and be far less
opaque. New ShieldDialog::showConsolidate() preset (used by the large-wallet
Settings banner + alert action) frames it as "Consolidate funds" and targets
shielded notes — the bloat the nudge warns about.
- Source selector: consolidate shielded notes (ANY_SAPLING), transparent
(ANY_TADDR), or both (*) — previously hardcoded to ANY_TADDR, which never
reduced the shielded-witness bloat. Batch limit now applies to the right side.
- Scope: on open, count spendable UTXOs + notes (listunspent / z_listunspent)
and show "N transparent + M shielded · ~X DRGX"; warn "repeat to finish" when
the set exceeds one batch.
- Destination auto-selects the best spendable z-address (button enabled by
default); empty wallets get an inline "Create shielded address" (z_getnewaddress).
- Advanced disclosure hides Fee + "Max inputs per batch" (renamed from the "UTXO
Limit" jargon) with sane defaults.
- Inline confirm step before the fund-moving call (amount + input count + dest).
- Live progress: self-polls z_getoperationstatus to show Consolidating… →
Done/Failed, replacing the raw opid + manual "Check status" button.
All three merge entry points now use the typed showMerge()/showConsolidate()
(no stale-static leaks from direct show(MergeToAddress)). Shield-coinbase mode
keeps working. New i18n keys fall back to English.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Settings tabs brought closer to the approved mockup:
- ActionButton/renderCardButton retune (settings-scoped): 7px radius, 9px
padX, Primary → accent-outline chip, Secondary/card buttons more defined.
- Daemon-binary card: compact status right-aligned on the DAEMON BINARY
heading (Up to date / Version differs / Not installed), filled/rounded
status box, neutral danger divider (was alarming red), roomier spacing.
- RPC Connection: two-row column-aligned layout (Host | Port, then
Username | Password) so the password no longer clips off the card edge.
- Chat settings tab: live conversation preview below the Appearance /
Messaging cards; "Focus input on open" checkbox reflowed onto the console
color-toggle row.
- Debug Options: "Current theme only" toggle restricts either screenshot
sweep to the active theme instead of cycling every skin.
- Tabs fill the full content width (content-max-width cap disabled) and the
sidebar nav panel centers within the true visible area.
- i18n: new keys for the above (untranslated keys fall back to English).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Send and Receive are now consistent in layout, and the Send recipient row's buttons
render correctly.
Card envelope (Send ⇄ Receive consistency):
- Add Layout::mainComposeCardBox(availW) — a single shared source for the compose card's
width + centering (fill the available column up to content-max-width, then center). Both
tabs derive their card from it, so they can't drift again. Previously Send capped at
760dp and Receive at 860dp, so the Send card rendered ~150px narrower on any window wider
than ~860dp; now they fill available width identically.
Receive:
- Justify the footer buttons edge-to-edge (equal shares over the live count) instead of
left-clustering with dead space, matching Send's full-width footer rhythm.
- Build the address-dropdown preview to the combo's real pixel width so the trailing
balance ("— 12.00000000 DRGX") no longer hard-clips at 150% (was char-count truncation).
Send recipient row (input | Paste | contacts-icon):
- Pin the contacts icon button to the frame height so the larger iconMed font doesn't
auto-size it taller than Paste/the input.
- Reserve the real ItemSpacing.x gaps (not the smaller spacingSm token) so the row no
longer overshoots the card and clips the icon's right border.
draw_helpers (root cause, app-wide):
- TactileButton's icon path measured/drew the label INCLUDING the "##id" suffix (which
CalcTextSizeA/AddText don't strip the way ImGui's text render does), shoving the glyph
off-center-left. Strip at "##" before measuring/drawing. Corrects any icon button that
passes an explicit size and a "##id" label; no-op for labels without "##".
Verified via headless sweeps at 1.0x and 1.5x, plus a real 3800px-wide render (both cards
byte-identical at L=1174/R=2773). ctest 1/1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the layout-improvement suggestions from the layout audit (visual arrangement
only; no functionality added or removed):
- Send/Receive: the recent-activity list now grows to fill the space below the fixed
compose/receive card (more history visible), with a centered empty-state when there is
none — instead of leaving dead canvas.
- Shield/Merge: pair the Fee and UTXO-Limit fields on one row to tighten vertical rhythm.
- Market: extend + frame the portfolio group-list as one contained panel (with a bottom
edge) and center its empty-state, closing the previously un-anchored gap.
- Overlay dialogs: raise the card glass fill/border alpha (35/50 -> 60/90 of 255) so the
dialog card reads as a distinct surface over busy backdrops (global, all overlays).
- Wallets: size the list height to the actual wallet count instead of always reserving 7
rows, removing the large gap before the scan/create prompts (still scrolls when many).
- Contacts: width-aware address truncation shows more of the address on wide rows.
- Transfer Funds: give the "sends the full balance" warning a warning icon + color so the
stakes stand out from the neutral result-preview lines.
- First-run wizard: collapse a completed Step 1 (Appearance) to the compact pill like
Step 2, so a finished step is no longer taller than the active one.
- Explorer: distribute the Chain card's two stats to match the density of the sibling
metrics grid.
- Validate Address: a "Results will appear here" caption fills the pre-interaction blank.
- Change Passphrase: add the warning banner its sibling security dialogs have.
- Migration ShowSeed: box the 24-word mnemonic grid (a GlassSectionScope behind the
existing RenderSeedWordGrid) so the critical secret reads as a distinct artifact —
purely visual, no seed/logic/state change.
Verified at 1280 across full-node + Lite + Windows (ctest green) and an adversarial diff
review (clean). New i18n key backfilled into all 8 languages.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses the cross-screen inconsistencies from the layout audit by routing screens onto
the design system's own (previously under-used) shared helpers:
- Dialog footers: migrate ~9 overlay dialogs off hand-rolled placement onto the shared
helpers — DialogActionFooter (primary+Close), DialogConfirmFooter, or
BeginOverlayDialogFooter for custom/multi-button rows — so footers share one centered
treatment. All footer/action buttons now use TactileButton (glass press) instead of the
bare StyledButton some dialogs used.
- Empty states: add a shared material::DrawEmptyState(icon, title, hint) (centered icon +
title + wrapped hint) and adopt it in Peers, Transactions, and Market-portfolio, which
previously showed a bare left-aligned caption.
- Security dialogs: add the missing Cancel to Change Passphrase and Set PIN so the whole
security family shares a two-button footer (Cancel dismisses without applying).
- Transactions pager: shared TactileButton helpers (matching Explorer).
- Frosted-pane rounding: Contacts/Chat use Layout::glassRounding() instead of hardcoded
12/10/8px literals, matching Peers.
- "Set Label..." title loses its stray trailing ellipsis.
Preserves every button's label and action; the transfer footer's order becomes
[Confirm][Cancel] to match the shared helper's primary-first convention. Verified at 1280
across full-node + Lite + Windows (ctest green) and an adversarial diff review (clean).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes the cut-off/clipping bugs from the layout audit (all visible at the default
1280/1024 window sizes):
- Receive "Recent Received" rows: the amount collided with the relative-time
("+15.7500 DRGX14 days ago") and the type label touched the address at narrow
widths. Use the shared short time format (formatTimeAgoShort, matching Overview),
chain the amount's right edge off the measured time width, and start the address
after the measured type-label width — so neither pair can collide.
- Daemon & xmrig updater verify-note: drawn unwrapped and clipped at the card's right
edge; wrap it (PushTextWrapPos) within the already-reserved height.
- Request Payment: the three footer buttons shared one fixed width (clipping "Copy
Full Address"); size each to its own label. The Payment URI overflowed a plain
field; render it in a bordered read-only box (bounded, un-chunked).
- Console: the filter input shrank below its own placeholder (gone entirely at 1024);
give it a min width >= the placeholder and drop the "N lines" count when the row
can't fit both.
- Address-label "Choose Icon" grid: had NoScrollbar hiding most of the catalog with
no cue; give it a real scrollbar.
- Overview "Recent Transactions": drop the 4th row at 1024 (it clipped off-screen) by
capping to rows that fully fit the reserved height.
- Sidebar: reserve the unread-badge width in the nav-label centering so History/Chat
labels no longer collide with their badge.
Verified at 1024 and 1280 across full-node + Lite + Windows (ctest green) and an
adversarial diff review (clean). Skipped the legacy settings_window overlay footer
(dead code / removal candidate).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wide/ultrawide (1440-3440px) responsiveness was unhealthy: no page/content-level
max-width cap existed, and every card/form/table derived width from raw
GetContentRegionAvail().x with floor-only clamps, so surfaces stretched edge-to-edge
(2000-3000px inputs, ballooning cards, giant grid cells, 2000px+ dead row-voids).
Root cause: adopt the (previously dead-code) clamp helpers.
- New Layout::kContentMaxWidth() (~1600dp, tunable via ui.toml [layout]
content-max-width; <=0 disables). Cap ##ContentArea to it and center the column in
wider windows — every tab derives from this child, so one change tames the app at
wide widths. No-op below the cap (fills as before), so 1080p/1440p are unaffected.
Per-surface upper-clamps (std::min(cap*dp, expr), floors preserved) where a single
element is still too wide even within the capped column:
- Settings: Theme/Layout/Language combos, the font-scale slider (~3000px -> 360dp),
the effect sliders, Explorer URL and RPC credential fields.
- Send / Receive: cap the compose / receive cards to a readable form width and center
them (Indent(pad+offset) so the auto-layout fields align with the hand-drawn card);
the recent-tx lists below keep the full column width.
- Chat message bubbles + composer, mining pool URL/payout inputs + stats left/right
split, contacts search, and the lite-network add-server row / server cards / status
panel (capped + centered).
- Wizard: vertically center the cards when they fit (was top-anchored, leaving a void
on tall monitors), compensating the content-height measurement so it can't oscillate.
The 1600 cap also subsumes the fixed-4-column balance grids (~400px cards) and the
right-anchored row dead-gaps (voids shrink from ~2700px to ~800px), so those are left
to the cap rather than blind column/row redesigns.
Verified at 1024/1280 (and via a temporary 900dp cap to exercise the cap+center path,
since the test display clamps to 1280) across full-node + Lite + Windows (ctest green)
and an adversarial diff review (clean). The true wide/ultrawide look and the 1600dp cap
value still want eyes on a real wide monitor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tail of the DPI/font-scale/responsiveness audit — ~26 remaining findings.
Container / recent-list (Theme-1 leftovers):
- Send: drop NoScrollbar|NoScrollWithMouse on ##SendFormScroll so Recent Sends is
reachable at font_scale 1.5 (parity with receive).
- Receive: cap the QR/form card via std::min(mainCardTargetH, availH - recentReserve)
so RECENT RECEIVED stays on-screen (identity at 1.0x).
- Wallets dialog: size the capped-mode list to whole rows so it no longer clips a
partial row / crowds "Create a new wallet".
Narrow-width (1024px) reflow:
- Console toolbar reserves space for ALL trailing controls (both icon toggles + zoom
buttons) so the +/- zoom no longer runs off-window.
- History sort combo sized to its measured widest localized label ("Newest first").
- Settings Theme/Layout/Language row: scale the wide→stacked breakpoint by dpiScale so
it drops to full-width stacked combos at 1.5x (Consolidated Card no longer clips).
- Recent-tx type label: derive the address column X from the measured label width so it
can't collide at narrow widths.
- Mining Recent Pool Payouts: floor the panel height to fit the empty-state caption.
Cosmetic ×dpiScale() on absolute geometry (no-ops at 1.0x): mining SOLO|POOL toggle &
idle combos, market pair-chips, password/PIN strength bars, receive/send currency
toggles, explorer search bar/rows/rounding, About-card logo, chat empty-state wrap,
recent-list address/time offsets, address-toolbar & two-row action buttons, console
line-gap/status-dot/pane rounding.
Verified at font_scale 1.5 and at 1024px across full-node + Lite + Windows (ctest
green) and an adversarial diff review (one over-reserve regression fixed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>